'use strict';
const saito = require('./saito');
const Big = require('big.js');
const AbstractCryptoModule = require('../templates/abstractcryptomodule')
const ModalSelectCrypto = require('./ui/modal-select-crypto/modal-select-crypto');
/**
* A Saito-lite wallet.
* @param {*} app
*/
class Wallet {
constructor(app) {
if (!(this instanceof Wallet)) {
return new Wallet(app);
}
this.app = app || {};
this.wallet = {};
this.wallet.balance = "0";
this.wallet.publickey = "";
this.wallet.privatekey = "";
this.wallet.inputs = [];
this.wallet.outputs = [];
this.wallet.spends = []; // spent but still around
this.wallet.default_fee = 2;
this.wallet.version = 3.480;
this.wallet.preferred_crypto = "SAITO";
this.wallet.preferred_txs = [];
this.inputs_hmap = [];
this.inputs_hmap_counter = 0;
this.inputs_hmap_counter_limit = 10000;
this.outputs_hmap = [];
this.outputs_hmap_counter = 0;
this.outputs_hmap_counter_limit = 10000;
this.outputs_prune_limit = 100;
this.recreate_pending_transactions = 0;
// SaitoCrypto is an AbstractCryptoModule just like the others so we
// don't have to treat Saito as a special case.
class SaitoCrypto extends AbstractCryptoModule {
constructor(app) {
super(app, "SAITO");
this.name = "Saito";
this.description = "Saito";
}
async returnBalance() {
return parseFloat(this.app.wallet.returnBalance());
}
returnAddress() {
return this.app.wallet.returnPublicKey();
}
returnPrivateKey() {
return this.app.wallet.returnPrivateKey();
}
async transfer(howMuch, to) {
this.app.wallet.transferTo(howMuch, to);
}
async hasPayment(howMuch, from, to, timestamp) {
// This doens't account for the from field in case when the user is "to"
// and doesn't account for "to" field when the user is "from".
let from_from = 0;
let to_to = 0;
if (to == this.app.wallet.returnPublicKey()) {
for (let i = 0; i < this.app.wallet.wallet.inputs.length; i++) {
if (this.app.wallet.wallet.inputs[i].amt === howMuch) {
if (parseInt(this.app.wallet.wallet.inputs[i].ts) >= parseInt(timestamp)) {
if (this.app.wallet.wallet.inputs[i].add == to) {
return true;
}
}
}
}
for (let i = 0; i < this.app.wallet.wallet.outputs.length; i++) {
if (this.app.wallet.wallet.outputs[i].amt === howMuch) {
if (parseInt(this.app.wallet.wallet.outputs[i].ts) >= parseInt(timestamp)) {
if (this.app.wallet.wallet.outputs[i].add == to) {
return true;
}
}
}
}
return false;
} else {
if (from == this.app.wallet.returnPublicKey()) {
for (let i = 0; i < this.app.wallet.wallet.outputs.length; i++) {
//console.log("OUTPUT");
//console.log(this.app.wallet.wallet.outputs[i]);
if (this.app.wallet.wallet.outputs[i].amt === howMuch) {
if (parseInt(this.app.wallet.wallet.outputs[i].ts) >= parseInt(timestamp)) {
if (this.app.wallet.wallet.outputs[i].add == to) {
return true;
}
}
}
}
}
return false;
}
}
returnIsActivated() {
return true;
}
onIsActivated() {
return new Promise((resolve, reject) => {
resolve();
});
}
}
this.saitoCrypo = new SaitoCrypto(app);
}
addInput(x) {
//////////////
// add slip //
//////////////
//
// we keep our slip array sorted according to block_id
// so that we can (1) spend the earliest slips first,
// and (2) simplify deleting expired slips
//
let pos = this.wallet.inputs.length;
while (pos > 0 && this.wallet.inputs[pos-1].bid > x.bid) { pos--; }
if (pos == -1) { pos = 0; }
this.wallet.inputs.splice(pos, 0, x);
this.wallet.spends.splice(pos, 0, 0);
let hmi = x.returnSignatureSource(x);
this.inputs_hmap[hmi] = 1;
this.inputs_hmap_counter++;
////////////////////////
// regenerate hashmap //
////////////////////////
//
// we want to periodically re-generate our hashmaps
// that help us check if inputs and outputs are already
// in our wallet for memory-management reasons and
// to maintain reasonable accuracy.
//
if (this.inputs_hmap_counter > this.inputs_hmap_counter_limit) {
this.inputs_hmap = [];
this.outputs_hmap = [];
this.inputs_hmap_counter = 0;
this.outputs_hmap_counter = 0;
for (let i = 0; i < this.wallet.inputs.length; i++) {
let hmi = this.wallet.inputs[i].returnSignatureSource();
this.inputs_hmap[hmi] = 1;
}
for (let i = 0; i < this.wallet.outputs.length; i++) {
let hmi = this.wallet.outputs[i].returnSignatureSource();
this.outputs_hmap[hmi] = 1;
}
}
return;
}
addOutput(x) {
//////////////
// add slip //
//////////////
this.wallet.outputs.push(x);
let hmi = x.returnSignatureSource();
this.outputs_hmap[hmi] = 1;
this.outputs_hmap_counter++;
///////////////////////
// purge old outputs //
///////////////////////
if (this.wallet.outputs.length > this.outputs_prune_limit) {
console.log("Deleting Excessive outputs from heavy-spend wallet...");
let outputs_excess_amount = this.wallet.outputs.length - this.outputs_prune_limit;
outputs_excess_amount += 10;
this.wallet.outputs.splice(0, outputs_excess_amount);
this.outputs_hmap_counter = 0;
}
return;
}
containsInput(s) {
let hmi = s.returnSignatureSource();
if (this.inputs_hmap[hmi] == 1) { return true; }
return false;
}
containsOutput(s) {
let hmi = s.returnSignatureSource();
if (this.outputs_hmap[hmi] == 1) { return true; }
return false;
}
transferTo(amount, toAddress) {
let newtx = this.app.wallet.returnBalance() > 0 ?
this.app.wallet.createUnsignedTransactionWithDefaultFee(toAddress, amount) :
this.app.wallet.createUnsignedTransaction(toAddress, amount, 0.0);
newtx = this.app.wallet.signAndEncryptTransaction(newtx);
this.app.network.propagateTransaction(newtx);
console.log("wallet transfer to");
console.log(newtx);
//return newtx;
}
addTransactionToPending(tx) {
let txjson = JSON.stringify(tx.transaction);
// do not put large TXS in pending - 100 kb
if (txjson.length > 100000) { return; }
if (! this.wallet.pending.includes(txjson)) {
this.wallet.pending.push(txjson);
this.saveWallet();
} else {
//alert("DOUBLEADD to PENDING: " + JSON.stringify(tx.msg));
}
}
doesSlipInPendingTransactionsSpendBlockHash(bsh="") {
for (let i = 0; i < this.wallet.pending.length; i++) {
let ptx = new saito.transaction(JSON.parse(this.wallet.pending[i]));
for (let k = 0; k < ptx.transaction.from.length; k++) {
if (ptx.transaction.from[k].bsh == bsh) {
return true;
}
}
}
return false;
}
/**
* Initialize the Saito-Lite wallet
* @param {Object} app - Saito-Lite Application Context
*/
initialize(app) {
if (this.wallet.privatekey == "") {
if (this.app.options.wallet != null) {
/////////////
// upgrade //
/////////////
if (this.app.options.wallet.version < this.wallet.version) {
if (this.app.BROWSER == 1) {
let tmpprivkey = this.app.options.wallet.privatekey;
let tmppubkey = this.app.options.wallet.publickey;
// specify before reset to avoid archives reset problem
this.wallet.publickey = tmppubkey;
this.wallet.privatekey = tmpprivkey;
// let modules purge stuff
this.app.modules.onWalletReset();
// reset and save
this.app.storage.resetOptions();
this.app.storage.saveOptions();
// re-specify after reset
this.wallet.publickey = tmppubkey;
this.wallet.privatekey = tmpprivkey;
this.app.options.wallet = this.wallet;
// reset blockchain
this.app.options.blockchain.last_bid = "";
this.app.options.blockchain.last_hash = "";
this.app.options.blockchain.last_ts = "";
// delete inputs and outputs
this.app.options.wallet.inputs = [];
this.app.options.wallet.outputs = [];
this.app.options.wallet.pending = [];
this.app.options.wallet.spends = [];
this.app.options.wallet.balance = "0.0";
this.app.options.wallet.version = this.wallet.version;
this.saveWallet();
salert("Saito Upgrade: Wallet Reset");
} else {
//
// purge old slips
//
this.app.options.wallet.version = this.wallet.version;
this.app.options.wallet.inputs = [];
this.app.options.wallet.outputs = [];
this.app.options.wallet.spends = [];
this.app.options.wallet.pending = [];
this.app.options.wallet.balance = "0.0";
this.app.storage.saveOptions();
}
}
this.wallet = Object.assign(this.wallet, this.app.options.wallet);
}
////////////////
// new wallet //
////////////////
if (this.wallet.privatekey == "") {
this.resetWallet();
}
}
//////////////////
// import slips //
//////////////////
this.wallet.spends = []
if (this.app.options.wallet != null) {
if (this.app.options.wallet.inputs != null) {
for (let i = 0; i < this.app.options.wallet.inputs.length; i++) {
let {add,amt,type,bid,tid,sid,bsh,lc,rn} = this.app.options.wallet.inputs[i];
this.wallet.inputs[i] = new saito.slip(add,amt,type,bid,tid,sid,bsh,lc,rn);
if (this.app.options.wallet.inputs[i].ts) { this.wallet.inputs[i].ts = this.app.options.wallet.inputs[i].ts; }
this.wallet.spends.push(0);
////////////////////
// update hashmap //
////////////////////
let hmi = this.wallet.inputs[i].returnSignatureSource();
this.inputs_hmap[hmi] = 1;
this.inputs_hmap_counter++;
}
}
if (this.app.options.wallet.outputs != null) {
for (let i = 0; i < this.app.options.wallet.outputs.length; i++) {
let {add,amt,type,bid,tid,sid,bsh,lc,rn} = this.app.options.wallet.outputs[i];
this.wallet.outputs[i] = new saito.slip(add,amt,type,bid,tid,sid,bsh,lc,rn);
////////////////////
// update hashmap //
////////////////////
let hmi = this.wallet.outputs[i].returnSignatureSource();
this.outputs_hmap[hmi] = 1;
this.outputs_hmap_counter++;
}
}
}
//
// check pending transactions and update spent slips
//
for (let z = 0; z < this.wallet.pending.length; z++) {
let ptx = new saito.transaction(JSON.parse(this.wallet.pending[z]));
for (let y = 0; y < ptx.transaction.from.length; y++) {
let spent_slip = ptx.transaction.from[y];
let ptx_bsh = spent_slip.bsh;
let ptx_bid = spent_slip.bid;
let ptx_tid = spent_slip.tid;
let ptx_sid = spent_slip.sid;
for (let x = 0; x < this.wallet.inputs.length; x++) {
if (this.wallet.inputs[x].bid == ptx_bid) {
if (this.wallet.inputs[x].tid == ptx_tid) {
if (this.wallet.inputs[x].sid == ptx_sid) {
if (this.wallet.inputs[x].bsh == ptx_bsh) {
this.wallet.spends[x] = 1;
x = this.wallet.inputs.length;
}
}
}
}
}
}
}
//
// listen to network conditions
//
this.app.connection.on('connection_up', (peer) => {
this.rebroadcastPendingTransactions(peer);
});
this.purgeExpiredSlips();
this.updateBalance();
this.saveWallet();
}
isSlipInPendingTransactions(slip=null) {
if (slip == null) { return false; }
let slipidx = slip.returnSignatureSource();
for (let i = 0; i < this.wallet.pending.length; i++) {
let ptx = new saito.transaction(JSON.parse(this.wallet.pending[i]));
for (let k = 0; k < ptx.transaction.from.length; k++) {
let fslip = ptx.transaction.from[k];
if (fslip.returnSignatureSource() === slipidx) {
return true;
}
}
}
return false;
}
//
// if peer is not null, rebroadcast to that peer, else everyone
//
rebroadcastPendingTransactions(peer=null) {
let loop_length = this.wallet.pending.length;
for (let i = 0; i < loop_length; i++) {
let tx = new saito.transaction(JSON.parse(this.wallet.pending[i]));
if (!tx.isFrom(this.returnPublicKey())) {
this.wallet.pending.splice(i, 1);
i--; loop_length--;
} else {
if (tx.transaction.type == 0) {
if (peer == null) {
this.app.network.propagateTransaction(tx);
} else {
this.app.network.propagateTransaction(tx);
}
} else {
//
// remove golden tickets and other unnecessary slips from pending
//
this.app.wallet.wallet.pending.splice(i, 1);
this.app.wallet.unspendInputSlips(tx);
this.app.wallet.saveWallet();
i--; loop_length--;
}
}
}
}
unspendInputSlips(tmptx=null) {
if (tmptx == null) { return; }
for (let i = 0; i < tmptx.transaction.from.length; i++) {
let fsidx = tmptx.transaction.from[i].returnSignatureSource();
for (let z = 0; z < this.wallet.inputs.length; z++) {
if (fsidx == this.wallet.inputs[z].returnSignatureSource()) {
this.wallet.spends[z] = 0;
}
}
}
}
onChainReorganization(bid, bsh, lc, pos) {
if (lc == 1) {
this.purgeExpiredSlips();
this.resetSpentInputs();
//
// recreate pending slips
//
let bad_pending_tx_indexes = [];
if (this.recreate_pending_transactions == 1) {
for (let i = 0; i < this.wallet.pending.length; i++) {
let ptx = new saito.transaction(JSON.parse(this.wallet.pending[i]));
let newtx = this.createReplacementTransaction(ptx);
if (newtx != null) {
newtx = this.signTransaction(newtx);
if (newtx != null) {
this.wallet.pending[i] = JSON.stringify(newtx);
}
} else {
bad_pending_tx_indexes.push(i);
console.log("pending transaction could not be recreated, purging");
}
}
for(let i = 0; i < bad_pending_tx_indexes.length; i++) {
this.wallet.pending.splice(i, 1);
}
this.recreate_pending_transactions = 0;
}
} else {
if (this.doesSlipInPendingTransactionsSpendBlockHash(bsh)) {
console.log("doesSlipInPendingTransactionsSpendBlockHash true, recreate_pending_transactions");
this.recreate_pending_transactions = 1;
} else {
console.log("doesn't doesSlipInPendingTransactionsSpendBlockHash, recreate_pending_transactions not set");
}
}
this.resetExistingSlips(bid, bsh, lc);
}
processPayments(blk, lc=0) {
for (let i = 0; i < blk.transactions.length; i++) {
let tx = blk.transactions[i];
let slips = tx.returnSlipsToAndFrom(this.returnPublicKey());
let to_slips = [];
let from_slips = [];
for (let m = 0; m < slips.to.length; m++) { to_slips.push(slips.to[m].cloneSlip()); }
for (let m = 0; m < slips.from.length; m++) { from_slips.push(slips.from[m].cloneSlip()); }
//
// update slips prior to insert
//
for (let ii = 0; ii < to_slips.length; ii++) {
to_slips[ii].bid = blk.block.id;
to_slips[ii].bsh = blk.returnHash();
to_slips[ii].tid = tx.transaction.id;
to_slips[ii].lc = lc;
to_slips[ii].ts = blk.block.ts; // set ts according to block
to_slips[ii].from = tx.transaction.from; // set from slips
}
for (let ii = 0; ii < from_slips.length; ii++) {
from_slips[ii].ts = blk.block.ts; // set ts according to block
}
//
// any txs in pending should be checked to see if
// we can remove them now that we have received
// a transaction that might be it....
//
let removed_pending_slips = 0;
if (this.wallet.pending.length > 0) {
for (let i = 0; i < this.wallet.pending.length; i++) {
let ptx = new saito.transaction(JSON.parse(this.wallet.pending[i]));
if (this.wallet.pending[i].indexOf(tx.transaction.sig) > 0) {
this.wallet.pending.splice(i, 1);
i--;
removed_pending_slips = 1;
} else {
if (ptx.transaction.type == 1) {
this.wallet.pending.splice(i, 1);
this.unspendInputSlips(ptx);
i--;
removed_pending_slips = 1;
} else {
//
// 10% chance of deletion
//
if (Math.random() <= 0.1) {
let ptx_ts = ptx.transaction.ts;
let blk_ts = blk.block.ts;
if ((ptx_ts + 12000000) < blk_ts) {
this.wallet.pending.splice(i, 1);
this.unspendInputSlips(ptx);
removed_pending_slips = 1;
i--;
}
}
}
}
}
}
if (removed_pending_slips == 1) {
this.saveWallet();
}
//
// inbound payments
//
if (to_slips.length > 0) {
for (let m = 0; m < to_slips.length; m++) {
if (to_slips[m].amt > 0) {
if (this.containsInput(to_slips[m]) == 0) {
if (this.containsOutput(to_slips[m]) == 0) {
if (to_slips[m].type != 4) {
this.addInput(to_slips[m]);
}
}
} else {
if (lc == 1) {
let our_index = to_slips[m].returnSignatureSource();
for (let n = this.wallet.inputs.length-1; n >= 0; n--) {
if (this.wallet.inputs[n].returnSignatureSource() === our_index) {
this.wallet.inputs[n].lc = lc;
}
}
}
}
}
}
}
//
// outbound payments
//
if (from_slips.length > 0) {
for (var m = 0; m < from_slips.length; m++) {
var s = from_slips[m];
for (var c = 0; c < this.wallet.inputs.length; c++) {
var qs = this.wallet.inputs[c];
if (
s.bid == qs.bid &&
s.tid == qs.tid &&
s.sid == qs.sid &&
s.bsh == qs.bsh &&
s.amt == qs.amt &&
s.add == qs.add
) {
if (this.containsOutput(s) == 0) {
this.addOutput(s);
//this.addOutput(this.wallet.inputs[c]);
}
this.wallet.inputs.splice(c, 1);
this.wallet.spends.splice(c, 1);
c = this.wallet.inputs.length+2;
}
}
}
}
}
/***** MARCH 11
//
// if we have way too many slips, merge some
//
if (this.wallet.inputs.length > 20) {
console.log("---------------------------------------");
console.log("Merging Wallet Slips on Proess Payment!");
console.log("---------------------------------------");
let mtx = this.createUnsignedTransaction(this.returnPublicKey(), 0.0, 0.0, 8);
mtx = this.signTransaction(mtx);
this.app.network.propagateTransaction(mtx);
}
******/
//
// save wallet
//
this.updateBalance();
this.app.options.wallet = this.wallet;
this.app.storage.saveOptions();
}
purgeExpiredSlips() {
let gid = this.app.blockchain.genesis_bid;
for (let m = this.wallet.inputs.length-1; m >= 0; m--) {
if (this.wallet.inputs[m].bid < gid) {
this.wallet.inputs.splice(m, 1);
this.wallet.spends.splice(m, 1);
}
}
for (let m = this.wallet.outputs.length-1; m >= 0; m--) {
if (this.wallet.outputs[m].bid < gid) {
this.wallet.outputs.splice(m, 1);
}
}
}
resetExistingSlips(bid, bsh, lc) {
for (let m = this.wallet.inputs.length-1; m >= 0; m--) {
if (this.wallet.inputs[m].bid == bid && this.wallet.inputs[m].bsh === bsh) {
this.wallet.inputs[m].lc = lc;
} else {
if (this.wallet.inputs[m].bid < bid) {
return;
}
}
}
}
resetSpentInputs(bid=0) {
if (bid == 0) {
for (let i = 0; i < this.wallet.inputs.length; i++) {
if (this.isSlipInPendingTransactions(this.wallet.inputs[i]) == false) {
this.wallet.spends[i] = 0;
}
}
} else {
let target_bid = this.app.blockchain.returnLatestBlockId() - bid;
for (let i = 0; i < this.wallet.inputs.length; i++) {
if (this.wallet.inputs[i].bid <= target_bid) {
if (this.isSlipInPendingTransactions(this.wallet.inputs[i]) == false) {
this.wallet.spends[i] = 0;
}
}
}
}
}
returnAdequateInputs(amt) {
var utxiset = [];
var value = Big(0.0);
var bigamt = Big(amt);
this.purgeExpiredSlips();
//
// this adds a 1 block buffer so that inputs are valid in the future block included
//
var lowest_block = this.app.blockchain.last_bid - this.app.blockchain.genesis_period + 2;
//
// check pending txs to avoid slip reuse if necessary
//
if (this.wallet.pending.length > 0) {
for (let i = 0; i < this.wallet.pending.length; i++) {
let pendingtx = new saito.transaction(JSON.parse(this.wallet.pending[i]));
for (let k = 0; k < pendingtx.transaction.from.length; k++) {
let slipIndex = pendingtx.transaction.from[k].returnSignatureSource();
for (let m = 0; m < this.wallet.inputs; m++) {
let thisSlipIndex = this.wallet.inputs[m].returnSignatureSource();
// if the input in the wallet is already in a pending tx...
// then set spends[m] to 1
if (thisSlipIndex === slipIndex) {
while (this.wallet.spends.length < m) {
this.wallet.spends.push(0);
}
this.wallet.spends[m] = 1;
}
}
}
}
}
let hasAdequateInputs = false;
let slipIndexes = [];
for (let i = 0; i < this.wallet.inputs.length; i++) {
if (this.wallet.spends[i] == 0 || i >= this.wallet.spends.length) {
var slip = this.wallet.inputs[i];
if (slip.lc == 1 && slip.bid >= lowest_block) {
if (this.app.mempool.transactions_inputs_hmap[slip.returnSignatureSource()] != 1) {
slipIndexes.push(i);
utxiset.push(slip);
value = value.plus(Big(slip.amt));
if (value.gt(bigamt) || value.eq(bigamt)) {
hasAdequateInputs = true;
break
}
}
}
}
}
if(hasAdequateInputs) {
for(let i = 0; i < slipIndexes.length; i++) {
this.wallet.spends[slipIndexes[i]] = 1;
}
return utxiset;
} else {
return null;
}
}
/**
* Calculates balance from slips in the local storage wallet
* @return {Big}
*/
calculateBalance() {
let bal = Big(0);
this.wallet.inputs.forEach((input, index )=> {
if (this.isSlipValid(input, index)) {
bal = bal.plus(input.amt);
}
});
return bal;
}
calculateDisplayBalance() {
var s = this.calculateBalance();
this.wallet.pending.forEach(tx => {
tx.to.forEach(slip => {
Big(s).plus(Big(slip.amt));
});
});
}
isSlipValid(slip, index) {
let isSlipSpent = this.wallet.spends[index];
let isSlipLC = slip.lc == 1;
let isSlipGtLVB = slip.bid >= this.app.blockchain.returnLowestValidBlock();
let isSlipinTX = this.app.mempool.transactions_inputs_hmap[slip.returnSignatureSource()] != 1;
let valid = !isSlipSpent && isSlipLC && isSlipGtLVB && isSlipinTX;
return valid;
}
returnBalance() {
return this.wallet.balance.replace(/0+$/,'').replace(/\.$/,'\.0');
}
/**
* Returns Private key
* @return {String}
*/
returnPrivateKey() {
return this.wallet.privatekey;
}
/**
* Returns Public key
* @return {String}
*/
returnPublicKey() {
return this.wallet.publickey;
}
/**
* Serialized the user's wallet to JSON and downloads it to their local machine
*/
async backupWallet() {
try {
if (this.app.BROWSER == 1) {
let content = JSON.stringify(this.app.options);
var pom = document.createElement('a');
pom.setAttribute('type', "hidden");
pom.setAttribute('href', 'data:application/json;utf-8,' + encodeURIComponent(content));
pom.setAttribute('download', "saito.wallet.json");
document.body.appendChild(pom);
pom.click();
pom.remove();
}
} catch (err) {
console.log("Error backing-up wallet: " + err);
}
}
/**
* Restores the user's wallet from uploaded JSON
*/
async restoreWallet(file) {
// let password_prompt = "";
// let confirm_password = await sconfirm("Did you encrypt this backup with a password. Click cancel if not:");
// if (confirm_password) {
//
// password_prompt = await sprompt("Please provide the password you used to encrypt this backup:");
// if (!password_prompt) {
// salert("Wallet Restore Cancelled");
// return;
// }
// } else {
// password_prompt = "";
// }
//
// password_prompt = await sprompt("Enter encryption password (blank for no password):");
//
// let groo = password_prompt;
//
// if (password_prompt === false) {
// salert("Wallet Restore Cancelled");
// return;
// }
var wallet_reader = new FileReader();
wallet_reader.readAsBinaryString(file);
wallet_reader.onloadend = () => {
let decryption_secret = "";
let decrypted_wallet = "";
// if (password_prompt != "") {
// decryption_secret = app.crypto.hash(password_prompt + "SAITO-PASSWORD-HASHING-SALT");
// try {
// decrypted_wallet = app.crypto.aesDecrypt(wallet_reader.result, decryption_secret);
// } catch (err) {
// salert(err);
// }
// } else {
decrypted_wallet = wallet_reader.result;
//}
try {
let wobj = JSON.parse(decrypted_wallet);
wobj.wallet.version = this.wallet.version;
wobj.wallet.inputs = [];
wobj.wallet.outputs = [];
wobj.wallet.spends = [];
wobj.games = [];
wobj.gameprefs = {};
this.app.options = wobj;
this.app.blockchain.resetBlockchain();
this.app.modules.returnModule('Arcade').onResetWallet();
this.app.storage.saveOptions();
salert("Restoration Complete ... click to reload Saito");
window.location.reload();
} catch (err) {
if(err.name == "SyntaxError") {
salert("Error reading wallet file. Did you upload the correct file?");
} else if(false) {// put this back when we support encrypting wallet backups again...
salert("Error decrypting wallet file. Password incorrect");
} else {
salert("Unknown error<br/>" + err);
}
}
};
}
/**
* Generates a new keypair for the user, resets all stored wallet info, and saves
* the new wallet to local storage.
*/
async resetWallet() {
//
// we do not do this because of referrals and bundles stored in options file
// reset and save
//await this.app.storage.resetOptions();
//this.app.storage.saveOptions();
this.wallet.privatekey = this.app.crypto.generateKeys();
this.wallet.publickey = this.app.crypto.returnPublicKey(this.wallet.privatekey);
// blockchain
if (this.app.options.blockchain != undefined) {
this.app.blockchain.resetBlockchainOptions();
}
// keychain
if (this.app.options.keys != undefined) {
this.app.options.keys = [];
}
this.wallet.inputs = [];
this.wallet.outputs = [];
this.wallet.spends = [];
this.wallet.pending = [];
this.saveWallet();
if (this.app.browser.browser_active == 1) {
window.location.reload();
}
}
/**
* Saves the current wallet state to local storage.
*/
saveWallet() {
this.app.options.wallet = this.wallet;
this.app.storage.saveOptions();
}
/**
* Sign an arbitrary message with wallet keypair
*/
signMessage(msg) {
return this.app.crypto.signMessage(msg, this.returnPrivateKey());
}
/**
* If the to field of the transaction contains a pubkey which has previously negotiated a diffie-hellman
* key exchange, encrypt the message part of message, attach it to the transaction, and resign the transaction
* @param {Transaction}
* @return {Transaction}
*/
signAndEncryptTransaction(tx) {
if (tx == null) { return null; }
for (var i = 0; i < tx.transaction.to.length; i++) { tx.transaction.to[i].sid = i; }
//
// convert tx.msg to base64 tx.transaction.ms
//
// if the transaction is of excessive length, we cut the message and
// continue blank. so be careful kids as there are some hardcoded
// limits in NodeJS!
//
try {
if (this.app.keys.hasSharedSecret(tx.transaction.to[0].add)) {
tx.msg = this.app.keys.encryptMessage(tx.transaction.to[0].add, tx.msg);
}
tx.transaction.m = this.app.crypto.stringToBase64(JSON.stringify(tx.msg));
tx.transaction.sig = tx.returnSignature(this.app, 1); // force clean sig as encrypted
} catch (err) {
console.log("####################");
console.log("### OVERSIZED TX ###");
console.log("### -revert- ###");
console.log("####################");
console.log(err);
tx.msg = {};
return tx;
}
return tx;
}
/**
* Sign a transactions and attach the sig to the transation
* @param {Transaction}
* @return {Transaction}
*/
signTransaction(tx) {
if (tx == null) { return null; }
for (let i = 0; i < tx.transaction.to.length; i++) { tx.transaction.to[i].sid = i; }
// this causes issues with transactions not validating, but eliminates recursive
//for (let i = 0; i < tx.transaction.to.length; i++) { try { delete tx.transaction.to[i].from; } catch (err) {} }
//for (let i = 0; i < tx.transaction.from.length; i++) { try { delete tx.transaction.from[i].from; } catch (err) {} };
//
// convert tx.msg to base64 tx.transaction.ms
//
// if the transaction is of excessive length, we cut the message and
// continue blank. so be careful kids as there are some hardcoded
// limits in NodeJS!
//
try {
tx.transaction.m = this.app.crypto.stringToBase64(JSON.stringify(tx.msg));
tx.transaction.sig = tx.returnSignature(this.app); // 1 not second arg = no force refresh
} catch (err) {
console.error("####################");
console.error("### OVERSIZED TX ###");
console.error("### -revert- ###");
console.error("####################");
console.error(err);
tx.msg = {};
return tx;
}
return tx;
}
updateBalance() {
let existing_balance = this.wallet.balance;
this.wallet.balance = this.calculateBalance().toFixed(8);
if (this.wallet.balance != existing_balance) {
this.app.connection.emit("update_balance", this);
}
}
returnDisplayBalance() {
return this.calculateDisplayBalance();
}
createSlip(addr) {
return new saito.slip(addr);
}
createRawTransaction(txobj) {
return new saito.transaction(txobj);
}
createUnsignedTransactionWithDefaultFee(publickey="", amt=0.0, force_merge=0) {
if (publickey === "") { publickey = this.app.wallet.returnPublicKey(); }
return this.createUnsignedTransaction(publickey, amt, this.wallet.default_fee);
}
createUnsignedTransaction(publickey="", amt=0.0, fee=0.0, force_merge=0) {
if (publickey == "") { publickey = this.returnPublicKey(); }
var wallet_avail = this.calculateBalance();
if (Big(fee).gt(wallet_avail)) {
//console.log("Inadequate funds in wallet for fee, creating with 0.0 fee instead.");
fee = 0.0;
}
var tx = new saito.transaction();
var total_fees = Big(amt).plus(Big(fee));
var wallet_avail = this.calculateBalance();
//
// check to-address is ok -- this just keeps a server
// that receives an invalid address from forking off
// the main chain because it creates its own invalid
// transaction.
//
// this is not strictly necessary, but useful for the demo
// server during the early stages, which produces a majority of
// blocks.
//
if (!this.app.crypto.isPublicKey(publickey)) {
throw "Invalid address " + publickey;
console.log("trying to send message to invalid address");
return null;
}
if (total_fees.gt(wallet_avail)) {
amt = 0.0;
fee = 0.0;
//console.log("Inadequate funds in wallet to create transaction. total: " + total_fees);
return null;
}
//
// zero-fee transactions have fake inputs
//
if (total_fees.eq(0.0)) {
tx.transaction.from = [];
tx.transaction.from.push(new saito.slip(this.returnPublicKey()));
} else {
tx.transaction.from = this.returnAdequateInputs(total_fees);
}
tx.transaction.ts = new Date().getTime();
tx.transaction.to.push(new saito.slip(publickey, amt));
// specify that this is a normal transaction
tx.transaction.to[tx.transaction.to.length-1].type = 0;
if (tx.transaction.from == null) {
//
// take a hail-mary pass and try to send this as a free transaction
//
tx.transaction.from = [];
tx.transaction.from.push(new saito.slip(this.returnPublicKey(), 0.0));
//return null;
}
if (tx.transaction.to == null) {
//
// take a hail-mary pass and try to send this as a free transaction
//
tx.transaction.to = [];
tx.transaction.to.push(new saito.slip(publickey, 0.0));
//return null;
}
// add change input
var total_inputs = Big(0.0);
for (let ii = 0; ii < tx.transaction.from.length; ii++) {
total_inputs = total_inputs.plus(Big(tx.transaction.from[ii].amt));
}
//
// generate change address(es)
//
var change_amount = total_inputs.minus(total_fees);
if (Big(change_amount).gt(0)) {
//
// if we do not have many slips left, generate a few extra inputs
//
if (this.wallet.inputs.length < 8) {
let change1 = change_amount.div(2).toFixed(8);
let change2 = change_amount.minus(Big(change1)).toFixed(8);
//
// split change address
//
// this prevents some usability issues with APPS
// by making sure there are usually at least 3
// utxo available for spending.
//
tx.transaction.to.push(new saito.slip(this.returnPublicKey(), change1));
tx.transaction.to[tx.transaction.to.length-1].type = 0;
tx.transaction.to.push(new saito.slip(this.returnPublicKey(), change2));
tx.transaction.to[tx.transaction.to.length-1].type = 0;
} else {
//
// single change address
//
tx.transaction.to.push(new saito.slip(this.returnPublicKey(), change_amount.toFixed(8)));
tx.transaction.to[tx.transaction.to.length-1].type = 0;
}
}
//
// if our wallet is filling up with slips, merge a few
//
//if (this.wallet.inputs.length > 200 || force_merge > 0) {
if (this.wallet.inputs.length > 30 || force_merge > 0) {
console.log("---------------------");
console.log("Merging Wallet Slips!");
console.log("---------------------");
let slips_to_merge = 7;
if (force_merge > 7) { slips_to_merge = force_merge; }
let slips_merged = 0;
let output_amount = Big(0);
let lowest_block = this.app.blockchain.last_bid - this.app.blockchain.genesis_period + 2;
//
// check pending txs to avoid slip reuse
//
if (this.wallet.pending.length > 0) {
for (let i = 0; i < this.wallet.pending.length; i++) {
let ptx = new saito.transaction(JSON.parse(this.wallet.pending[i]));
for (let k = 0; k < ptx.transaction.from.length; k++) {
let slipIndex = ptx.transaction.from[k].returnSignatureSource();
for (let m = 0; m < this.wallet.inputs; m++) {
let thisSlipIndex = this.wallet.inputs[m].returnSignatureSource();
if (thisSlipIndex === slipIndex) {
while (this.wallet.spends.length < m) {
this.wallet.spends.push(0);
}
this.wallet.spends[m] = 1;
}
}
}
}
}
for (let i = 0; slips_merged < slips_to_merge && i < this.wallet.inputs.length; i++) {
if (this.wallet.spends[i] == 0 || i >= this.wallet.spends.length) {
var slip = this.wallet.inputs[i];
if (slip.lc == 1 && slip.bid >= lowest_block) {
if (this.app.mempool.transactions_inputs_hmap[slip.returnSignatureSource()] != 1) {
this.wallet.spends[i] = 1;
slips_merged++;
output_amount = output_amount.plus(Big(slip.amt));
tx.transaction.from.push(slip);
}
}
}
}
// add new output
tx.transaction.to.push(new saito.slip(this.returnPublicKey(), output_amount.toFixed(8)));
tx.transaction.to[tx.transaction.to.length-1].type = 0;
}
//
// we save here so that we don't create another transaction
// with the same inputs after broadcasting on reload
//
this.saveWallet();
return tx;
}
createToSlips(num, address, amount, change_amount) {
var amt_per_slip = 1;
if(num > amount) {
amt_per_slip = 1;
num = Math.floor(amount);
} else {
amt_per_slip = Math.floor(amount / num);
}
var remainder = amount % amt_per_slip;
var to_slips = [];
for (let i = 0; i < num; i++) {
to_slips.push(new saito.slip(address, Big(amt_per_slip)));
to_slips[to_slips.length - 1].type = 0;
}
if (Big(remainder).gt(0)) {
to_slips.push(new saito.slip(address, Big(remainder)));
to_slips[to_slips.length - 1].type = 0;
}
if (Big(change_amount).gt(0)) {
to_slips.push(new saito.slip(this.app.wallet.returnPublicKey(), change_amount.toFixed(8)));
to_slips[to_slips.length - 1].type = 0;
}
return to_slips;
}
createReplacementTransaction(oldtx) {
let inputs_amt = Big(0.0);
for (let z = 0; z < oldtx.transaction.from.length; z++) {
inputs_amt = inputs_amt.plus(Big(oldtx.transaction.from[z].amt));
}
let newtx = new saito.transaction(oldtx.transaction);
let inputs = this.returnAdequateInputs(inputs_amt);
if(inputs) {
newtx.transaction.from = this.returnAdequateInputs(inputs_amt);
this.saveWallet();
return newtx;
} else {
return null;
}
}
///////////////////////
// PREFERRED CRYPTOS //
///////////////////////
/**
* Returns a list of any modules installed in this Saito-Lite node which extend AbstractCryptoModule.
* AbstractCryptoModules represent various cryptocurrencies like DOT, Kusama, ETH, etc.
* @return {Array} Array of modules
*/
returnInstalledCryptos() {
let cryptoModules = this.app.modules.returnModulesBySubType(AbstractCryptoModule);
cryptoModules.push(this.saitoCrypo);
return cryptoModules;
}
/**
* Returns a list of any modules installed in this Saito-Lite node which extend AbstractCryptoModule.
* AbstractCryptoModules represent various cryptocurrencies like DOT, Kusama, ETH, etc.
* @return {Array} Array of modules
*/
returnActivatedCryptos() {
let allMods = this.returnInstalledCryptos();
let activeMods = [];
for (let i = 0; i < allMods.length; i++) {
if (allMods[i].returnIsActivated()) {
activeMods.push(allMods[i]);
}
}
return activeMods;
}
/**
* Gets an installed AbstractCryptoModule by ticker
* @param {String} ticker - Ticker of install crypto module
* @return {Module}
*/
returnCryptoModuleByTicker(ticker) {
let mods = this.returnInstalledCryptos();
for (let i = 0; i < mods.length; i++) {
if (mods[i].ticker === ticker) { return mods[i]; }
}
throw "Module Not Found: " + ticker;
}
/**
* Set user's preferred crypto module by ticker
* @param {String} ticker - Ticker of install crypto module
*/
setPreferredCrypto(ticker, show_overlay=0) {
let can_we_do_this = 0;
let mods = this.returnInstalledCryptos();
let cryptomod = null;
for (let i = 0; i < mods.length; i++) {
if (mods[i].ticker === ticker) { cryptomod = mods[i]; can_we_do_this = 1; }
}
if (ticker == "SAITO") { can_we_do_this = 1; }
if (can_we_do_this == 1) {
this.wallet.preferred_crypto = ticker;
this.saveWallet();
this.app.connection.emit("set_preferred_crypto", ticker);
}
if (cryptomod != null && show_overlay == 1) {
if (cryptomod.renderModalSelectCrypto() != null) {
let modal_select_crypto = new ModalSelectCrypto(this.app, cryptomod);
modal_select_crypto.render(this.app, cryptomod);
modal_select_crypto.attachEvents(this.app, cryptomod);
}
}
return;
}
/**
* A user can set their preferred crypto within the Saito Lite environment. This will be stored
* in their local storage and can be retrieved by other modules here for any purpose.
* @return {Module}
*/
returnPreferredCrypto() {
try {
return this.returnCryptoModuleByTicker(this.wallet.preferred_crypto);
} catch(err) {
if (err.startsWith("Module Not Found:")) {
this.setPreferredCrypto("SAITO");
return this.returnCryptoModuleByTicker(this.wallet.preferred_crypto);
} else {
throw err;
}
}
}
returnPreferredCryptoTicker() {
try {
let pc = this.returnPreferredCrypto();
if (pc != null && pc != undefined) {
return pc.ticker;
}
} catch (err) {
return "";
}
}
returnCryptoAddressByTicker(ticker="SAITO") {
try {
if (ticker === "SAITO") {
return this.returnPublicKey();
} else {
let cmod = this.returnCryptoModuleByTicker(ticker);
return cmod.returnAddress();
}
} catch (err) {
}
return "";
}
/**
* A user can set their preferred crypto within the Saito Lite environment. This will be stored
* in their local storage and can be retrieved by other modules here for any purpose.
* @param {String} address - How much of the token to transfer
* @param {String} to - Pubkey/address to send to
* @abstract
* @return {Int} Bool as int
*/
isOurPreferredCryptoAddress(address, ticker) {
if (address == this.returnPublicKey()) { return 1; }
return 0;
}
/**
* Get's balance from user's preferred crypto module.
* @param {Array} addresses - Array of addresses
* @param {Function} mycallback - (Array of {address: {String}, balance: {Int}}) -> {...}
* @param {String} ticker - Ticker of install crypto module
* @return {Array} Array of {address: {String}, balance: {Int}}
*/
async returnPreferredCryptoBalances(addresses=[], mycallback=null, ticker="") {
if (ticker == "") { ticker = this.wallet.preferred_crypto; }
let cryptomod = this.returnCryptoModuleByTicker(ticker);
let returnObj = [];
let balancePromises = [];
for (let i = 0; i < addresses.length; i++) {
balancePromises.push(cryptomod.returnBalance(addresses[i]));
}
let balances = await Promise.all(balancePromises);
for (let i = 0; i < addresses.length; i++) {
returnObj.push({address: addresses[i], balance: balances[i]});
}
if (mycallback != null) { mycallback(returnObj); }
return returnObj;
}
/*** courtesy function to simplify balance checks for a single address w/ ticker ***/
async checkBalance(address, ticker) {
let robj = await this.returnPreferredCryptoBalances([address], null, ticker);
if (robj.length < 1) { return 0; }
if (robj[0].balance) { return robj[0].balance; }
return 0;
}
async returnPreferredCryptoBalance() {
let cryptomod = this.returnPreferredCrypto();
return await this.checkBalance(cryptomod.returnAddress(), cryptomod.ticker);
}
/**
* Sends payments to the addresses provided if this user is the corresponding
* sender. Will not send if similar payment was found after the given timestamp.
* @param {Array} senders - Array of addresses
* @param {Array} receivers - Array of addresses
* @param {Array} amounts - Array of amounts to send
* @param {Int} timestamp - Timestamp of time after which payment should be made
* @param {Function} mycallback - ({hash: {String}}) -> {...}
* @param {String} ticker - Ticker of install crypto module
*/
async sendPayment(senders=[], receivers=[], amounts=[], timestamp, mycallback, ticker) {
// validate inputs
if (senders.length != receivers.length || senders.length != amounts.length) {
console.log("Lengths of senders, receivers, and amounts must be the same")
//mycallback({err: "Lengths of senders, receivers, and amounts must be the same"});
}
if (senders.length !== 1) {
// We have no code which exercises multiple senders/receivers so can't implement it yet.
console.log('sendPayment ERROR: Only supports one transaction')
//mycallback({err: "Only supports one transaction"});
}
// only send if hasn't been sent before
if (!this.doesPreferredCryptoTransactionExist(senders, receivers, amounts, timestamp, ticker)) {
let cryptomod = this.returnCryptoModuleByTicker(ticker);
for (let i = 0; i < senders.length; i++) {
if (senders[i] === cryptomod.returnAddress()) {
// Need to save before we await, otherwise there is a race condition
this.savePreferredCryptoTransaction(senders, receivers, amounts, timestamp, ticker);
try {
let hash = await cryptomod.transfer(amounts[i], receivers[i]);
// execute callback if exists
mycallback({hash: hash});
break;
} catch(err) {
// it failed, delete the transaction
console.log("sendPayment ERROR: payment failed....\n" + err);
this.deletePreferredCryptoTransaction(senders, receivers, amounts, timestamp, ticker);
//mycallback({err: err});
break;
}
}
}
} else {
console.log("sendPayment ERROR: already sent");
//mycallback({err: "already sent"});
}
};
/**
* Checks that a payment has been received if the current user is the receiver.
* @param {Array} senders - Array of addresses
* @param {Array} receivers - Array of addresses
* @param {Array} amounts - Array of amounts to send
* @param {Int} timestamp - Timestamp of time after which payment should be made
* @param {Function} mycallback - (Array of {address: {String}, balance: {Int}}) -> {...}
* @param {String} ticker - Ticker of install crypto module
* @param {Int} tries - (default: 36) Number of tries to query the underlying crypto API before giving up. Sending -1 will cause infinite retries.
* @param {Int} pollWaitTime - (default: 5000) Amount of time to wait between tries
* @return {Array} Array of {address: {String}, balance: {Int}}
*/
async receivePayment(senders=[], receivers=[], amounts=[], timestamp, mycallback, ticker, tries = 36, pollWaitTime = 5000) {
// original design of this interface was to use async/await by returning a promise.
// Instead there was an insistence on using mycallback. The consumer of this interface does not use
// async/await or .then(...) so this is being abandoned(i.e. i'm removing the promise) but leaving it
// here in case someone wants to refactor this or wonders why this interface is this way.
//return new Promise(async(resolve, reject) => {
if (senders.length != receivers.length || senders.length != amounts.length) {
// There is no way to handle errors with the interface of receivePayment as it's been designed.
// We will swallow this error and log it to the console and return.
// Do not delete this console.log, at least maybe the engineer who is maintaining this needs
// some hope of figuring out why the game isn't progressing.
console.log("receivePayment ERROR. Lengths of senders, receivers, and amounts must be the same");
return;
// mycallback({err: "Lengths of senders, receivers, and amounts must be the same"});
}
if (senders.length !== 1) {
// There is no way to handle errors with the interface of receivePayment as it's been designed.
// We will swallow this error and log it to the console and return.
// Do not delete this console.log, at least maybe the engineer who is maintaining this needs
// some hope of figuring out why the game isn't progressing.
console.log("receivePayment ERROR. Only supports one transaction");
return;
//mycallback({err: "Only supports one transaction"});
}
//
// if payment already received, return
//
if (this.doesPreferredCryptoTransactionExist(senders, receivers, amounts, timestamp, ticker)) {
mycallback();
return;
}
let cryptomod = this.returnCryptoModuleByTicker(ticker);
await cryptomod.onIsActivated();
//
// create a function we can loop through to check if the payment has come in....
//
let check_payment_function = async() => {
return await cryptomod.hasPayment(amounts[0], senders[0], receivers[0], timestamp - 3); // subtract 3 seconds in case system time is slightly off
}
let poll_check_payment_function = async() => {
console.log("poll_check_payment_function remaining tries: " + tries);
let result = null;
try {
result = await check_payment_function();
} catch(err) {
// if check_payment_function throws an error, we want to bail out,
// there's no point trying another 100 times.
// There is no way to handle errors with the interface of receivePayment as it's been designed.
// We will swallow this error and log it to the console and return.
// Do not delete this console.log, at least maybe the engineer who is maintaining this needs
// some hope of figuring out why the game isn't progressing.
console.log("receivePayment ERROR." + err);
return;
//mycallback({err: err});
}
did_complete_payment(result);
};
let did_complete_payment = (result) => {
if (result) {
// The transaction was found, we're done.
console.log("TRANSACTION FOUND");
this.savePreferredCryptoTransaction(senders, receivers, amounts, timestamp, ticker);
mycallback(result);
} else {
// The transaction was not found.
tries--;
// This is === rather than < because sending -1 is a way to do infinite polling
if(tries != 0) {
setTimeout(() => {
poll_check_payment_function();
}, pollWaitTime);
} else {
// There is no way to handle errors with the interface of receivePayment as it's been designed.
// We will swallow this error and log it to the console and return.
// Do not delete this console.log, at least maybe the engineer who is maintaining this needs
// some hope of figuring out why the game isn't progressing.
console.log("Did not receive payment after " + ((pollWaitTime * tries)/1000) + " seconds");
return;
// mycallback({err: "Did not receive payment after " + ((pollWaitTime * tries)/1000) + " seconds"});
}
}
}
poll_check_payment_function();
//});
}
savePreferredCryptoTransaction(senders=[], receivers=[], amounts, timestamp, ticker) {
let sig = this.app.crypto.hash(JSON.stringify(senders) + JSON.stringify(receivers) + JSON.stringify(amounts) + timestamp + ticker);
this.wallet.preferred_txs.push({
sig : sig,
ts : (new Date().getTime())
})
for (let i = this.wallet.preferred_txs.length-1; i >= 0; i--) {
// delete references after ~30 hours
if (this.wallet.ts < ((new Date().getTime()) - 100000000)) {
this.wallet.preferred_txs.splice(i, 1);
}
}
this.saveWallet();
return 1;
}
doesPreferredCryptoTransactionExist(senders=[], receivers=[], amounts, timestamp, ticker) {
let sig = this.app.crypto.hash(JSON.stringify(senders) + JSON.stringify(receivers) + JSON.stringify(amounts) + timestamp + ticker);
for (let i = 0; i < this.wallet.preferred_txs.length; i++) {
if (this.wallet.preferred_txs[i].sig === sig) {
return 1;
}
}
return 0;
}
deletePreferredCryptoTransaction(senders=[], receivers=[], amounts, timestamp, ticker) {
let sig = this.app.crypto.hash(JSON.stringify(senders) + JSON.stringify(receivers) + JSON.stringify(amounts) + timestamp + ticker);
for (let i = 0; i < this.wallet.preferred_txs.length; i++) {
if (this.wallet.preferred_txs[i].sig === sig) {
this.wallet.preferred_txs.splice(i, 1);
}
}
}
///////////////////////////
// END PREFERRED CRYPTOS //
///////////////////////////
}
module.exports = Wallet;