From bec13978d67d794b2f5cdc71279d67e43dbb9bfb Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 1 Nov 2014 16:16:44 -0300 Subject: [PATCH 01/16] v1 --- util/swipeWallet.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/util/swipeWallet.js b/util/swipeWallet.js index 6185dcc18..de526ef9b 100755 --- a/util/swipeWallet.js +++ b/util/swipeWallet.js @@ -130,7 +130,12 @@ firstWallet.updateIndexes(function() { var amount = balance - DFLT_FEE; firstWallet.createTx(destAddr, amount, '', {}, function(err, ntxid) { console.log('\n\t### Tx Proposal Created...\n\tWith copayer 0 signature.'); - if (requiredCopayers === 1) { + if (!ntxid) + throw new Error('Counld not create tx' + err); + + console.log('\n\t### Tx Proposal Created... With copayer 0 signature.'); + + if (requiredCopayers ===1) { firstWallet.sendTx(ntxid, function(txid) { console.log('\t ####### SENT TXID:', txid); process.exit(1); From 2564cb22d7a5f19a6c1d300d429cc2fe81a28f54 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 1 Nov 2014 16:29:37 -0300 Subject: [PATCH 02/16] . --- js/models/Wallet.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index ac3f1981d..8570d3b65 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2367,17 +2367,17 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var b; - try { +// try { b = new Builder(opts) .setUnspent(utxos) .setOutputs([{ address: toAddress.data, amountSatStr: amountSatStr, }]); - } catch (e) { - log.debug(e.message); - return; - }; +// } catch (e) { +// log.debug(e.message); +// return; +// }; var selectedUtxos = b.getSelectedUnspent(); var inputChainPaths = selectedUtxos.map(function(utxo) { From c6c6b459b6d932c0040c8bddb088695e73ac2fef Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 1 Nov 2014 16:37:15 -0300 Subject: [PATCH 03/16] . --- js/models/Wallet.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 8570d3b65..b47783cab 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2365,19 +2365,14 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos opts[k] = Wallet.builderOpts[k]; } - var b; + var b = new Builder(opts); -// try { - b = new Builder(opts) - .setUnspent(utxos) - .setOutputs([{ - address: toAddress.data, - amountSatStr: amountSatStr, - }]); -// } catch (e) { -// log.debug(e.message); -// return; -// }; + b.setUnspent(utxos); + + b.setOutputs([{ + address: toAddress.data, + amountSatStr: amountSatStr, + }]); var selectedUtxos = b.getSelectedUnspent(); var inputChainPaths = selectedUtxos.map(function(utxo) { From a778cc71a1ac13375983abc5863ae90fce13c0a4 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 2 Nov 2014 17:17:03 -0300 Subject: [PATCH 04/16] add console.log to utxos --- js/models/Wallet.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index b47783cab..464732d8b 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2369,6 +2369,8 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos b.setUnspent(utxos); +console.log('[Wallet.js.2370:utxos:]',utxos); //TODO + b.setOutputs([{ address: toAddress.data, amountSatStr: amountSatStr, From 78b84fc4665df6e71cca9d20333c778d3e77ce0a Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 2 Nov 2014 19:07:43 -0300 Subject: [PATCH 05/16] filter unspent --- js/models/Wallet.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 464732d8b..5d038277e 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2172,11 +2172,17 @@ Wallet.prototype.getBalance = function(cb) { var balanceByAddr = {}; var COIN = coinUtil.COIN; - this.getUnspent(function(err, safeUnspent, unspent) { + this.getUnspent(function(err, safeUnspent, unspentRaw) { if (err) { return cb(err); } + // This filter out possible broken unspent, as reported on + // https://github.com/bitpay/copay/issues/1585 + // and later gitter conversation. + + var unspent = _.filter(unspentRaw, 'scriptPubKey'); + for (var i = 0; i < unspent.length; i++) { var u = unspent[i]; var amt = u.amount * COIN; From 0567714628fa7a4bdd8bda66c449de51026bda36 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 2 Nov 2014 19:17:55 -0300 Subject: [PATCH 06/16] mv filter to Insight --- js/models/Insight.js | 11 +++++++++-- js/models/Wallet.js | 36 ++++++++++-------------------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/js/models/Insight.js b/js/models/Insight.js index 2a07a56d7..091ccfb47 100644 --- a/js/models/Insight.js +++ b/js/models/Insight.js @@ -5,6 +5,7 @@ var async = require('async'); var request = require('request'); var bitcore = require('bitcore'); var io = require('socket.io-client'); +var _ = require('lodash'); var log = require('../log'); var EventEmitter = require('events').EventEmitter; @@ -310,9 +311,15 @@ Insight.prototype.getUnspent = function(addresses, cb) { this.requestPost('/api/addrs/utxo', { addrs: addresses.join(',') - }, function(err, res, body) { + }, function(err, res, unspentRaw) { if (err || res.statusCode != 200) return cb(err || res); - cb(null, body); + + // This filter out possible broken unspent, as reported on + // https://github.com/bitpay/copay/issues/1585 + // and later gitter conversation. + + var unspent = _.filter(unspentRaw, 'scriptPubKey'); + cb(null, unspent); }); }; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 5d038277e..37bb4cb42 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1879,15 +1879,9 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) merchantData.total = merchantData.total.toString(10); - var b; - try { - b = new Builder(opts) - .setUnspent(unspent) - .setOutputs(outs); - } catch (e) { - log.debug(e.message); - return; - }; + var b = new Builder(opts) + .setUnspent(unspent) + .setOutputs(outs); merchantData.pr.pd.outputs.forEach(function(output, i) { var script = { @@ -2172,17 +2166,11 @@ Wallet.prototype.getBalance = function(cb) { var balanceByAddr = {}; var COIN = coinUtil.COIN; - this.getUnspent(function(err, safeUnspent, unspentRaw) { + this.getUnspent(function(err, safeUnspent, unspent) { if (err) { return cb(err); } - // This filter out possible broken unspent, as reported on - // https://github.com/bitpay/copay/issues/1585 - // and later gitter conversation. - - var unspent = _.filter(unspentRaw, 'scriptPubKey'); - for (var i = 0; i < unspent.length; i++) { var u = unspent[i]; var amt = u.amount * COIN; @@ -2371,16 +2359,12 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos opts[k] = Wallet.builderOpts[k]; } - var b = new Builder(opts); - - b.setUnspent(utxos); - -console.log('[Wallet.js.2370:utxos:]',utxos); //TODO - - b.setOutputs([{ - address: toAddress.data, - amountSatStr: amountSatStr, - }]); + var b = new Builder(opts) + .setUnspent(utxos) + .setOutputs([{ + address: toAddress.data, + amountSatStr: amountSatStr, + }]); var selectedUtxos = b.getSelectedUnspent(); var inputChainPaths = selectedUtxos.map(function(utxo) { From f26f2d04538da30ef6d1ed64437a1c5ef25e27d6 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 2 Nov 2014 20:32:12 -0300 Subject: [PATCH 07/16] add log to raw tx --- util/swipeWallet.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/util/swipeWallet.js b/util/swipeWallet.js index de526ef9b..81118b028 100755 --- a/util/swipeWallet.js +++ b/util/swipeWallet.js @@ -1,6 +1,8 @@ #!/usr/bin/env node 'use strict'; + + var copay = require('../copay'); var program = require('commander'); var _ = require('lodash'); @@ -11,6 +13,11 @@ var bitcore = require('bitcore'); var readline = require('readline'); var async = require('async'); +// Fee to asign to the tx. Please put a bigger number if you get 'unsufficient unspent' +var FEE = 0.0001; + + + var rl = readline.createInterface({ input: process.stdin, output: process.stdout @@ -18,7 +25,6 @@ var rl = readline.createInterface({ var args = process.argv; var destAddr = args[2]; -var DFLT_FEE = 0.0001 * bitcore.util.COIN; if (!args[4]) { console.log('\n\tusage: swipeWallet.js [ ...]'); @@ -119,23 +125,23 @@ firstWallet.updateIndexes(function() { console.log('Balance per address:', balanceByAddr); //TODO if (!balance) { - console.log('Could not find any balance from the generated wallet'); //TODO + console.log('Could not find any coins in the generated wallet'); //TODO process.exit(1); } - rl.question("\n\tShould I swipe the wallet (destination address" + destAddr + ")?\n\t(`yes` to continue)\n\t", function(answer) { + rl.question("\n\tShould I swipe the wallet (destination address is:" + destAddr + ")?\n\t(`yes` to continue)\n\t", function(answer) { if (answer !== 'yes') process.exit(1); - var amount = balance - DFLT_FEE; + var amount = balance - FEE * bitcore.util.COIN; firstWallet.createTx(destAddr, amount, '', {}, function(err, ntxid) { - console.log('\n\t### Tx Proposal Created...\n\tWith copayer 0 signature.'); + console.log('\n\t### Tx Proposal Created...\n\tWith copayer 0 signature.'); if (!ntxid) - throw new Error('Counld not create tx' + err); + throw new Error('Counld not create tx' + err + '. Try a bigger FEE by editing the head lines of this script.'); console.log('\n\t### Tx Proposal Created... With copayer 0 signature.'); - if (requiredCopayers ===1) { + if (requiredCopayers === 1) { firstWallet.sendTx(ntxid, function(txid) { console.log('\t ####### SENT TXID:', txid); process.exit(1); @@ -166,6 +172,9 @@ firstWallet.updateIndexes(function() { var p = firstWallet.txProposals.getTxProposal(ntxid); if (p.builder.isFullySigned()) { console.log('\t FULLY SIGNED. BROADCASTING NOW....'); + var tx = p.builder.build(); + var txHex = tx.serialize().toString('hex'); + console.log('\t RAW TX: ', txHex); firstWallet.sendTx(ntxid, function(txid) { console.log('\t ####### SENT TXID:', txid); process.exit(1); From 22ef1bb3f33bf2f8b76109a8d31737c697339adc Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 2 Nov 2014 22:41:04 -0300 Subject: [PATCH 08/16] fixes handling wrong txs --- js/models/Insight.js | 2 +- js/models/Wallet.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/js/models/Insight.js b/js/models/Insight.js index 091ccfb47..439aced7f 100644 --- a/js/models/Insight.js +++ b/js/models/Insight.js @@ -256,7 +256,7 @@ Insight.prototype.broadcast = function(rawtx, cb) { this.requestPost('/api/tx/send', { rawtx: rawtx }, function(err, res, body) { - if (err || res.status != 200) cb(err || res); + if (err || res.statusCode != 200) cb(err || body); cb(null, body ? body.txid : null); }); }; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 37bb4cb42..d52dc8bc5 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1381,14 +1381,17 @@ Wallet.prototype.sendTx = function(ntxid, cb) { var self = this; this.blockchain.broadcast(txHex, function(err, txid) { - log.debug('Wallet:' + self.id + ' BITCOIND txid:', txid); + if (err) + log.error('Error sending TX:',err); + if (txid) { + log.debug('Wallet:' + self.getName() + ' Broadcasted TX. BITCOIND txid:', txid); self.txProposals.get(ntxid).setSent(txid); self.sendTxProposal(ntxid); self.emitAndKeepAlive('txProposalsUpdated'); return cb(txid); } else { - log.debug('Wallet:' + self.id + ' Sent failed. Checking if the TX was sent already'); + log.info('Wallet:' + self.getName() + '. Sent failed. Checking if the TX was sent already'); self._checkSentTx(ntxid, function(txid) { if (txid) self.emitAndKeepAlive('txProposalsUpdated'); From 3108969b7cdd249b888b31151c1a0cb952255852 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 2 Nov 2014 23:53:47 -0300 Subject: [PATCH 09/16] add commander to params --- util/swipeWallet.js | 61 +++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/util/swipeWallet.js b/util/swipeWallet.js index 81118b028..c446d74d2 100755 --- a/util/swipeWallet.js +++ b/util/swipeWallet.js @@ -1,10 +1,8 @@ #!/usr/bin/env node - 'use strict'; var copay = require('../copay'); -var program = require('commander'); var _ = require('lodash'); var config = require('../config'); var version = require('../version').version; @@ -12,11 +10,23 @@ var sinon = require('sinon'); var bitcore = require('bitcore'); var readline = require('readline'); var async = require('async'); +var program = require('commander'); + +function list(val) { + return val.split(','); +} + +program + .version('0.0.1') + .usage('-d n2kMqQ8Si9GndzQ6FrJxcwHMKacK2rCEpK -n 2 -k tprv8ZgxMBicQKsPem5BuuDT6xY9etUC2RohpUoyzoa1MEkkZyAHhszaHPZTmgDheN31hSP1r6bRwpj2JC66r1CPpftwaRrhz') + .option('-d, --destination ', 'Destination Address') + .option('-n, --required ', 'Required number of signatures', parseInt) + .option('-k, --keys ', 'master private keys', list) + .option('-f, --fee [n]', 'Set fee in BTC (default 0.0001 BTC)', parseFloat) + .parse(process.argv); // Fee to asign to the tx. Please put a bigger number if you get 'unsufficient unspent' -var FEE = 0.0001; - - +var fee = parseFloat(program.fee) || 0.0001; var rl = readline.createInterface({ input: process.stdin, @@ -24,25 +34,30 @@ var rl = readline.createInterface({ }); var args = process.argv; -var destAddr = args[2]; +var requiredCopayers = program.required; +var extPrivKeys = program.keys; +var totalCopayers = extPrivKeys.length; +var destAddr = program.destination; -if (!args[4]) { - console.log('\n\tusage: swipeWallet.js [ ...]'); - console.log('\t e.g.: ./swipeWallet.js mxBVchwitGLXBHtT4Vah7DdP8J9M23ftE6 2 tprv8ZgxMBicQKsPejj9Xpky8M7NFv7szxqszBR2VvZTEkBTCCXZLtJfQwRxhUycNCu4sqyZepx8AfT1vuJr949np1gxYbZaJK3R9qekYPCZiJz tprv8ZgxMBicQKsPdWe14mn5SPY4zjG7fJnrmhkVZgTHQfYp91Kf1Lxof38KBQJiis4xv2zvZ2pVHgLn4GFRDUd8kR2HkMxDqLDNWTmnKqp95mZ tprv8ZgxMBicQKsPdzoFwT72Lwhr6n48ZyPahTAhPNaoAP4srVA1mcfPon7GWQaiwfAWesWACHm3aCBLYNGNPVKSU3E9vr1cLiBoMkayZiARywe'); +if (!requiredCopayers || !extPrivKeys || !extPrivKeys.length || !destAddr){ + program.outputHelp(); process.exit(1); } -var requiredCopayers = parseInt(args[3]); -var extPrivKeys = args.slice(4); -var totalCopayers = extPrivKeys.length; var addr = new bitcore.Address(destAddr); if (!addr.isValid()) { console.log('\tBad destination address'); //TODO process.exit(1); + } + + + var networkName = addr.network().name; -console.log('\tNetwork: %s\n\tDestination Address:%s\n\tRequired copayers: %d\n\tTotal copayers: %d\n\tKeys:', networkName, destAddr, requiredCopayers, totalCopayers, extPrivKeys); //TODO +console.log('\tNetwork: %s\n\tDestination Address:%s\n\tRequired copayers: %d\n\tTotal copayers: %d\n\tFee: %d\n\tKeys:', + networkName, destAddr, requiredCopayers, + totalCopayers, fee, extPrivKeys); //TODO console.log('\n ----------------------------'); if (requiredCopayers > totalCopayers) @@ -129,17 +144,19 @@ firstWallet.updateIndexes(function() { process.exit(1); } - rl.question("\n\tShould I swipe the wallet (destination address is:" + destAddr + ")?\n\t(`yes` to continue)\n\t", function(answer) { - if (answer !== 'yes') - process.exit(1); + // rl.question("\n\tShould I swipe the wallet (destination address is:" + destAddr + ")?\n\t(`yes` to continue)\n\t", function(answer) { + // if (answer !== 'yes') + // process.exit(1); - var amount = balance - FEE * bitcore.util.COIN; + var amount = balance - fee * bitcore.util.COIN; + +console.log('[swipeWallet.js.136]'); //TODO firstWallet.createTx(destAddr, amount, '', {}, function(err, ntxid) { - console.log('\n\t### Tx Proposal Created...\n\tWith copayer 0 signature.'); - if (!ntxid) - throw new Error('Counld not create tx' + err + '. Try a bigger FEE by editing the head lines of this script.'); + if (err || !ntxid) { + throw new Error('Could not create tx' + err + '. Try a bigger fee (--fee).'); + } - console.log('\n\t### Tx Proposal Created... With copayer 0 signature.'); + console.log('\n\t### Tx Proposal Created...\n\tWith copayer 0 signature.'); if (requiredCopayers === 1) { firstWallet.sendTx(ntxid, function(txid) { @@ -185,6 +202,6 @@ firstWallet.updateIndexes(function() { } ) }); - }); + // }); }); }); From abc50231079ba83d5f9d6480b8a06006dad6a523 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sun, 2 Nov 2014 23:53:53 -0300 Subject: [PATCH 10/16] add limits to TXs --- js/models/Wallet.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index d52dc8bc5..4bf9e5272 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -30,6 +30,8 @@ var Async = require('./Async'); var Insight = module.exports.Insight = require('./Insight'); var copayConfig = require('../../config'); +var TX_MAX_SIZE_KB = 60; +var TX_MAX_INS = 100; /** * @desc * Wallet manages a private key for Copay, network, storage of the wallet for @@ -2317,7 +2319,13 @@ Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) this.getUnspent(function(err, safeUnspent) { if (err) return cb(new Error('Could not get list of UTXOs')); - var ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts); + var ntxid; + try { + ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts); + } catch (e) { + return cb(e); + } + if (!ntxid) { return cb(new Error('Error creating the transaction')); } @@ -2369,7 +2377,13 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos amountSatStr: amountSatStr, }]); + var selectedUtxos = b.getSelectedUnspent(); + + if (selectedUtxos.size > TX_MAX_INS) + throw new Error('Resulting TX is TOO big:' + selectedUtxos.size + ' inputs. Aborting'); + + var inputChainPaths = selectedUtxos.map(function(utxo) { return pkr.pathForAddress(utxo.address); }); @@ -2386,6 +2400,12 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos if (!tx.countInputSignatures(0)) throw new Error('Could not sign generated tx'); + var txSize = tx.getSize(); + + if (txSize/1024 > TX_MAX_SIZE_KB) + throw new Error('Resulting TX is TOO big ' + txSize + ' bytes. Aborting'); + + var me = {}; me[myId] = now; From 8b4e86647283d6d10d002f44814f657a3308f33c Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 3 Nov 2014 08:40:53 -0300 Subject: [PATCH 11/16] add amount param --- js/models/Wallet.js | 4 ++-- util/swipeWallet.js | 30 +++++++++++++++++++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 4bf9e5272..8c4d6bce2 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2381,7 +2381,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var selectedUtxos = b.getSelectedUnspent(); if (selectedUtxos.size > TX_MAX_INS) - throw new Error('Resulting TX is TOO big:' + selectedUtxos.size + ' inputs. Aborting'); + throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.size + ' inputs. Aborting'); var inputChainPaths = selectedUtxos.map(function(utxo) { @@ -2403,7 +2403,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var txSize = tx.getSize(); if (txSize/1024 > TX_MAX_SIZE_KB) - throw new Error('Resulting TX is TOO big ' + txSize + ' bytes. Aborting'); + throw new Error('BIG: Resulting TX is too big ' + txSize + ' bytes. Aborting'); var me = {}; diff --git a/util/swipeWallet.js b/util/swipeWallet.js index c446d74d2..448fc5bdb 100755 --- a/util/swipeWallet.js +++ b/util/swipeWallet.js @@ -22,7 +22,8 @@ program .option('-d, --destination ', 'Destination Address') .option('-n, --required ', 'Required number of signatures', parseInt) .option('-k, --keys ', 'master private keys', list) - .option('-f, --fee [n]', 'Set fee in BTC (default 0.0001 BTC)', parseFloat) + .option('-a, --amount ', 'Optional, amount to transfer, in Satoshis', parseInt) + .option('-f, --fee [n]', 'Optional, fee in BTC (default 0.0001 BTC)', parseFloat) .parse(process.argv); // Fee to asign to the tx. Please put a bigger number if you get 'unsufficient unspent' @@ -38,6 +39,7 @@ var requiredCopayers = program.required; var extPrivKeys = program.keys; var totalCopayers = extPrivKeys.length; var destAddr = program.destination; +var amount = program.amount; if (!requiredCopayers || !extPrivKeys || !extPrivKeys.length || !destAddr){ program.outputHelp(); @@ -140,20 +142,26 @@ firstWallet.updateIndexes(function() { console.log('Balance per address:', balanceByAddr); //TODO if (!balance) { - console.log('Could not find any coins in the generated wallet'); //TODO - process.exit(1); + throw ('Could not find any coins in the generated wallet'); } - // rl.question("\n\tShould I swipe the wallet (destination address is:" + destAddr + ")?\n\t(`yes` to continue)\n\t", function(answer) { - // if (answer !== 'yes') - // process.exit(1); - var amount = balance - fee * bitcore.util.COIN; + if (amount && amount >= balance) + throw ('Not enought balance fund to fullfill ' + amount + ' satoshis'); + + rl.question("\n\tShould I swipe the wallet (destination address is:" + destAddr + " Amount: "+ amount + "Satoshis )?\n\t(`yes` to continue)\n\t", function(answer) { + if (answer !== 'yes') + process.exit(1); + + amount = amount || balance - fee * bitcore.util.COIN; -console.log('[swipeWallet.js.136]'); //TODO firstWallet.createTx(destAddr, amount, '', {}, function(err, ntxid) { if (err || !ntxid) { - throw new Error('Could not create tx' + err + '. Try a bigger fee (--fee).'); + if (err && err.toString().match('BIG')) { + throw new Error('Could not create tx' + err ); + } else { + throw new Error('Could not create tx' + err + '. Try a bigger fee (--fee).'); + } } console.log('\n\t### Tx Proposal Created...\n\tWith copayer 0 signature.'); @@ -191,7 +199,7 @@ console.log('[swipeWallet.js.136]'); //TODO console.log('\t FULLY SIGNED. BROADCASTING NOW....'); var tx = p.builder.build(); var txHex = tx.serialize().toString('hex'); - console.log('\t RAW TX: ', txHex); + //console.log('\t RAW TX: ', txHex); firstWallet.sendTx(ntxid, function(txid) { console.log('\t ####### SENT TXID:', txid); process.exit(1); @@ -202,6 +210,6 @@ console.log('[swipeWallet.js.136]'); //TODO } ) }); - // }); + }); }); }); From 7453042ccd1c33dd1bc5a53d68ee66155426e8f9 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 3 Nov 2014 08:45:38 -0300 Subject: [PATCH 12/16] help --- util/swipeWallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/swipeWallet.js b/util/swipeWallet.js index 448fc5bdb..743904002 100755 --- a/util/swipeWallet.js +++ b/util/swipeWallet.js @@ -37,7 +37,6 @@ var args = process.argv; var requiredCopayers = program.required; var extPrivKeys = program.keys; -var totalCopayers = extPrivKeys.length; var destAddr = program.destination; var amount = program.amount; @@ -47,6 +46,7 @@ if (!requiredCopayers || !extPrivKeys || !extPrivKeys.length || !destAddr){ } +var totalCopayers = extPrivKeys.length; var addr = new bitcore.Address(destAddr); if (!addr.isValid()) { console.log('\tBad destination address'); //TODO From e01cf88a90d536ad5031cc1ad396b8809f6a2d92 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 3 Nov 2014 09:55:44 -0300 Subject: [PATCH 13/16] better arg parsing --- util/swipeWallet.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/util/swipeWallet.js b/util/swipeWallet.js index 743904002..98237497b 100755 --- a/util/swipeWallet.js +++ b/util/swipeWallet.js @@ -22,12 +22,10 @@ program .option('-d, --destination ', 'Destination Address') .option('-n, --required ', 'Required number of signatures', parseInt) .option('-k, --keys ', 'master private keys', list) - .option('-a, --amount ', 'Optional, amount to transfer, in Satoshis', parseInt) - .option('-f, --fee [n]', 'Optional, fee in BTC (default 0.0001 BTC)', parseFloat) + .option('-a, --amount ', 'Optional, amount to transfer, in Satoshis. If not provided, will wipe all funds', parseInt) + .option('-f, --fee [n]', 'Optional, fee in BTC (default 0.0001 BTC), only if amount is not provided', parseFloat) .parse(process.argv); -// Fee to asign to the tx. Please put a bigger number if you get 'unsufficient unspent' -var fee = parseFloat(program.fee) || 0.0001; var rl = readline.createInterface({ input: process.stdin, @@ -40,11 +38,19 @@ var extPrivKeys = program.keys; var destAddr = program.destination; var amount = program.amount; +if (amount && program.fee) { + console.log('If amount if given, fee will be automatically calculated'); + process.exit(1); +} + if (!requiredCopayers || !extPrivKeys || !extPrivKeys.length || !destAddr){ program.outputHelp(); process.exit(1); } +// Fee to asign to the tx. Please put a bigger number if you get 'unsufficient unspent' +var fee = program.fee || 0.0001; + var totalCopayers = extPrivKeys.length; var addr = new bitcore.Address(destAddr); From 1f9b9c8dcaf284b191788c919aa8b6eda41a1a78 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 3 Nov 2014 15:47:35 -0300 Subject: [PATCH 14/16] variable fee for "send all funds" --- js/controllers/send.js | 63 +++++++++++++++++++--------------- js/models/Wallet.js | 21 ++++++++++-- js/services/controllerUtils.js | 27 +++++---------- test/Wallet.js | 12 +++++++ test/blockchain.Insight.js | 2 +- util/swipeWallet.js | 2 +- views/send.html | 4 +-- 7 files changed, 78 insertions(+), 53 deletions(-) diff --git a/js/controllers/send.js b/js/controllers/send.js index e4f378c1c..eb6c7b25b 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -4,32 +4,6 @@ var preconditions = require('preconditions').singleton(); angular.module('copayApp.controllers').controller('SendController', function($scope, $rootScope, $window, $timeout, $anchorScroll, $modal, isMobile, notification, controllerUtils, rateService) { - controllerUtils.redirIfNotComplete(); - - var w = $rootScope.wallet; - preconditions.checkState(w); - preconditions.checkState(w.settings.unitToSatoshi); - - $rootScope.title = 'Send'; - $scope.loading = false; - var satToUnit = 1 / w.settings.unitToSatoshi; - $scope.defaultFee = bitcore.TransactionBuilder.FEE_PER_1000B_SAT * satToUnit; - $scope.unitToBtc = w.settings.unitToSatoshi / bitcore.util.COIN; - $scope.unitToSatoshi = w.settings.unitToSatoshi; - - $scope.alternativeName = w.settings.alternativeName; - $scope.alternativeIsoCode = w.settings.alternativeIsoCode; - - $scope.isRateAvailable = false; - $scope.rateService = rateService; - - - - rateService.whenAvailable(function() { - $scope.isRateAvailable = true; - $scope.$digest(); - }); - /** * Setting the two related amounts as properties prevents an infinite * recursion for watches while preserving the original angular updates @@ -396,13 +370,19 @@ angular.module('copayApp.controllers').controller('SendController', }; $scope.getAvailableAmount = function() { - var amount = ((($rootScope.availableBalance * w.settings.unitToSatoshi).toFixed(0) - bitcore.TransactionBuilder.FEE_PER_1000B_SAT) / w.settings.unitToSatoshi); + if (!$rootScope.safeUnspentCount) return null; + + // Each signature takes + var estimatedFee = copay.Wallet.estimatedFee($rootScope.safeUnspentCount); +console.log('[send.js.376:estimatedFee:]',estimatedFee); //TODO + var amount = ((($rootScope.availableBalance * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi); + +console.log('[send.js.402:amount:]',amount); //TODO return amount > 0 ? amount : 0; }; $scope.topAmount = function(form) { $scope.amount = $scope.getAvailableAmount(); - form.amount.$pristine = false; }; @@ -615,4 +595,31 @@ angular.module('copayApp.controllers').controller('SendController', }); }; + controllerUtils.redirIfNotComplete(); + + var w = $rootScope.wallet; + preconditions.checkState(w); + preconditions.checkState(w.settings.unitToSatoshi); + + $rootScope.title = 'Send'; + $scope.loading = false; + var satToUnit = 1 / w.settings.unitToSatoshi; + $scope.defaultFee = bitcore.TransactionBuilder.FEE_PER_1000B_SAT * satToUnit; + $scope.unitToBtc = w.settings.unitToSatoshi / bitcore.util.COIN; + $scope.unitToSatoshi = w.settings.unitToSatoshi; + + $scope.alternativeName = w.settings.alternativeName; + $scope.alternativeIsoCode = w.settings.alternativeIsoCode; + + $scope.isRateAvailable = false; + $scope.rateService = rateService; + $scope.availableBalance = $scope.getAvailableAmount(); + + rateService.whenAvailable(function() { + $scope.isRateAvailable = true; + $scope.$digest(); + }); + + + }); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 8c4d6bce2..83ef55582 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2154,12 +2154,23 @@ Wallet.prototype.addressIsOwn = function(addrStr, opts) { }; +/* + * Estimate a tx fee in satoshis given its input count + * only for spending all wallet funds + */ +Wallet.estimatedFee = function(unspentCount) { + preconditions.checkArgument(_.isNumber(unspentCount)); + var estimatedSizeKb = Math.ceil( ( 500 + unspentCount * 250) / 1024 ); + return parseInt( estimatedSizeKb * bitcore.TransactionBuilder.FEE_PER_1000B_SAT); +}; + /** * @callback {getBalanceCallback} * @param {string=} err - an error, if any * @param {number} balance - total number of satoshis for all addresses * @param {Object} balanceByAddr - maps string addresses to satoshis * @param {number} safeBalance - total number of satoshis in UTXOs that are not part of any TxProposal + * @param {number} safeUnspentCount - total number of safe unspent Outputs that make this balance. */ /** * @desc Returns the balances for all addresses in Satoshis @@ -2190,14 +2201,16 @@ Wallet.prototype.getBalance = function(cb) { balance = parseInt(balance.toFixed(0), 10); - for (var i = 0; i < safeUnspent.length; i++) { + var safeUnspentCount = safeUnspent.length; + + for (var i = 0; i < safeUnspentCount; i++) { var u = safeUnspent[i]; var amt = u.amount * COIN; safeBalance += amt; } safeBalance = parseInt(safeBalance.toFixed(0), 10); - return cb(null, balance, balanceByAddr, safeBalance); + return cb(null, balance, balanceByAddr, safeBalance, safeUnspentCount); }); }; @@ -2322,6 +2335,7 @@ Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) var ntxid; try { ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts); + log.debub('TX Created: ntxid', ntxid); //TODO } catch (e) { return cb(e); } @@ -2370,6 +2384,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos opts[k] = Wallet.builderOpts[k]; } +console.log('[Wallet.js.2386]'); //TODO var b = new Builder(opts) .setUnspent(utxos) .setOutputs([{ @@ -2377,8 +2392,10 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos amountSatStr: amountSatStr, }]); + log.debug('Creating TX: Builder ready'); var selectedUtxos = b.getSelectedUnspent(); +console.log('[Wallet.js.2397:selectedUtxos:]',selectedUtxos); //TODO if (selectedUtxos.size > TX_MAX_INS) throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.size + ' inputs. Aborting'); diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 94e672817..a106f8d1f 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -241,18 +241,19 @@ angular.module('copayApp.services') }; - root._computeBalance = function(w, cb) { + root._fetchBalance = function(w, cb) { cb = cb || function() {}; var satToUnit = 1 / w.settings.unitToSatoshi; var COIN = bitcore.util.COIN; - w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat) { + w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat, safeUnspentCount) { if (err) return cb(err); var r = {}; r.totalBalance = balanceSat * satToUnit; r.totalBalanceBTC = (balanceSat / COIN); r.availableBalance = safeBalanceSat * satToUnit; + r.safeUnspentCount = safeUnspentCount; r.availableBalanceBTC = (safeBalanceSat / COIN); r.lockedBalance = (balanceSat - safeBalanceSat) * satToUnit; @@ -276,22 +277,10 @@ angular.module('copayApp.services') }); }; - root._updateScope = function(w, data, $scope, cb) { - $scope.totalBalance = data.totalBalance; - $scope.totalBalanceBTC = data.totalBalanceBTC; - $scope.availableBalance = data.availableBalance; - $scope.availableBalanceBTC = data.availableBalanceBTC; - - $scope.lockedBalance = data.lockedBalance; - $scope.lockedBalanceBTC = data.lockedBalanceBTC; - - $scope.balanceByAddr = data.balanceByAddr; - - $scope.totalBalanceAlternative = data.totalBalanceAlternative; - $scope.alternativeIsoCode = data.alternativeIsoCode; - $scope.lockedBalanceAlternative = data.lockedBalanceAlternative; - $scope.alternativeConversionRate = data.alternativeConversionRate; - + root._updateScope = function(w, data, scope, cb) { + _.each(data, function(v, k) { + scope[k] = data[k]; + }) if (cb) return cb(); }; @@ -320,7 +309,7 @@ angular.module('copayApp.services') scope.updatingBalance = true; } - root._computeBalance(w, function(err, res) { + root._fetchBalance(w, function(err, res) { if (err) throw err; _balanceCache[wid] = res; root._updateScope(w, _balanceCache[wid], scope, function() { diff --git a/test/Wallet.js b/test/Wallet.js index 7090943ac..cdfa81822 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -1005,6 +1005,18 @@ describe('Wallet model', function() { }); }); + describe('#estimatedFee', function() { + it('should calculate estimated fee', function() { + var COIN = 100000000; + Wallet.estimatedFee(1).should.equal(0.0001 * COIN); + Wallet.estimatedFee(2).should.equal(0.0001 * COIN); + Wallet.estimatedFee(3).should.equal(0.0002 * COIN); + Wallet.estimatedFee(1000).should.equal(0.0245 * COIN); + }); + }); + + + describe('#send', function() { it('should call this.network.send', function() { var w = cachedCreateW2(); diff --git a/test/blockchain.Insight.js b/test/blockchain.Insight.js index 244bde8ee..386e16bbf 100644 --- a/test/blockchain.Insight.js +++ b/test/blockchain.Insight.js @@ -128,7 +128,7 @@ describe('Insight model', function() { sinon.stub(blockchain, "requestPost", function(url, data, cb) { url.should.be.equal('/api/tx/send'); - var res = {status: 200}; + var res = {statusCode: 200}; var body = {txid: 1234}; setTimeout(function() { cb(null, res, body); diff --git a/util/swipeWallet.js b/util/swipeWallet.js index 98237497b..94186193f 100755 --- a/util/swipeWallet.js +++ b/util/swipeWallet.js @@ -21,7 +21,7 @@ program .usage('-d n2kMqQ8Si9GndzQ6FrJxcwHMKacK2rCEpK -n 2 -k tprv8ZgxMBicQKsPem5BuuDT6xY9etUC2RohpUoyzoa1MEkkZyAHhszaHPZTmgDheN31hSP1r6bRwpj2JC66r1CPpftwaRrhz') .option('-d, --destination ', 'Destination Address') .option('-n, --required ', 'Required number of signatures', parseInt) - .option('-k, --keys ', 'master private keys', list) + .option('-k, --keys ', 'master private keys, separated by , ', list) .option('-a, --amount ', 'Optional, amount to transfer, in Satoshis. If not provided, will wipe all funds', parseInt) .option('-f, --fee [n]', 'Optional, fee in BTC (default 0.0001 BTC), only if amount is not provided', parseFloat) .parse(process.argv); diff --git a/views/send.html b/views/send.html index 7707ce9dd..539e68905 100644 --- a/views/send.html +++ b/views/send.html @@ -80,9 +80,9 @@ - Use all funds ({{getAvailableAmount()}} {{$root.wallet.settings.unitName}}) + Use all funds ({{availableBalance}} {{$root.wallet.settings.unitName}})
From 4821567b1def807b906aa7b8319ed8e65eabd347 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 3 Nov 2014 16:06:17 -0300 Subject: [PATCH 15/16] inform the user of "too big" tx --- js/controllers/send.js | 22 +++++++++++++--------- js/models/Wallet.js | 11 +++++------ views/send.html | 6 ++++++ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/js/controllers/send.js b/js/controllers/send.js index eb6c7b25b..dd3df7441 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -77,8 +77,7 @@ angular.module('copayApp.controllers').controller('SendController', $scope.submitForm = function(form) { if (form.$invalid) { - var message = 'Unable to send transaction proposal'; - notification.error('Error', message); + $scope.error = 'Unable to send transaction proposal'; return; } @@ -90,8 +89,15 @@ angular.module('copayApp.controllers').controller('SendController', function done(err, ntxid, merchantData) { if (err) { - var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + ' could not be created'; - notification.error('Error', message); + copay.logger.error(err); + + var msg = err.toString(); + + if (msg.match('BIG')) + msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts.' + + var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + ' could not be created: ' + msg; + $scope.error = message; $scope.loading = false; $scope.loadTxs(); return; @@ -135,7 +141,7 @@ angular.module('copayApp.controllers').controller('SendController', } notification.success('Transaction broadcasted', message); } else { - notification.error('Error', 'There was an error sending the transaction'); + $scope.error = 'There was an error sending the transaction'; } $scope.loading = false; $scope.loadTxs(); @@ -374,10 +380,8 @@ angular.module('copayApp.controllers').controller('SendController', // Each signature takes var estimatedFee = copay.Wallet.estimatedFee($rootScope.safeUnspentCount); -console.log('[send.js.376:estimatedFee:]',estimatedFee); //TODO var amount = ((($rootScope.availableBalance * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi); -console.log('[send.js.402:amount:]',amount); //TODO return amount > 0 ? amount : 0; }; @@ -391,7 +395,7 @@ console.log('[send.js.402:amount:]',amount); //TODO $rootScope.txAlertCount = 0; w.sendTx(ntxid, function(txid, merchantData) { if (!txid) { - notification.error('Error', 'There was an error sending the transaction'); + $scope.error = 'There was an error sending the transaction'; } else { if (!merchantData) { notification.success('Transaction broadcasted', 'Transaction id: ' + txid); @@ -415,7 +419,7 @@ console.log('[send.js.402:amount:]',amount); //TODO $scope.loading = true; w.sign(ntxid, function(ret) { if (!ret) { - notification.error('Error', 'There was an error signing the transaction'); + $scope.error = 'There was an error signing the transaction'; $scope.loadTxs(); } else { var p = w.txProposals.getTxProposal(ntxid); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 83ef55582..15c378742 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -30,8 +30,8 @@ var Async = require('./Async'); var Insight = module.exports.Insight = require('./Insight'); var copayConfig = require('../../config'); -var TX_MAX_SIZE_KB = 60; -var TX_MAX_INS = 100; +var TX_MAX_SIZE_KB = 50; +var TX_MAX_INS = 70; /** * @desc * Wallet manages a private key for Copay, network, storage of the wallet for @@ -2337,6 +2337,7 @@ Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts); log.debub('TX Created: ntxid', ntxid); //TODO } catch (e) { +console.log('[Wallet.js.2340]', e); //TODO return cb(e); } @@ -2384,7 +2385,6 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos opts[k] = Wallet.builderOpts[k]; } -console.log('[Wallet.js.2386]'); //TODO var b = new Builder(opts) .setUnspent(utxos) .setOutputs([{ @@ -2395,10 +2395,9 @@ console.log('[Wallet.js.2386]'); //TODO log.debug('Creating TX: Builder ready'); var selectedUtxos = b.getSelectedUnspent(); -console.log('[Wallet.js.2397:selectedUtxos:]',selectedUtxos); //TODO - if (selectedUtxos.size > TX_MAX_INS) - throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.size + ' inputs. Aborting'); + if (selectedUtxos.length > TX_MAX_INS) + throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.length + ' inputs. Aborting'); var inputChainPaths = selectedUtxos.map(function(utxo) { diff --git a/views/send.html b/views/send.html index 539e68905..5aac3faf1 100644 --- a/views/send.html +++ b/views/send.html @@ -11,6 +11,12 @@
+

+ + {{error|translate}} +

+
From 7a610c1b5b40fcfe236d25aebadfd019e6f8f238 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 3 Nov 2014 17:28:36 -0300 Subject: [PATCH 16/16] fix type --- js/controllers/send.js | 1 - js/models/Wallet.js | 3 +-- test/unit/controllers/controllersSpec.js | 2 ++ views/send.html | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/controllers/send.js b/js/controllers/send.js index dd3df7441..d170dbae2 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -378,7 +378,6 @@ angular.module('copayApp.controllers').controller('SendController', $scope.getAvailableAmount = function() { if (!$rootScope.safeUnspentCount) return null; - // Each signature takes var estimatedFee = copay.Wallet.estimatedFee($rootScope.safeUnspentCount); var amount = ((($rootScope.availableBalance * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi); diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 15c378742..f7d68e65c 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -2335,9 +2335,8 @@ Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) var ntxid; try { ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts); - log.debub('TX Created: ntxid', ntxid); //TODO + log.debug('TX Created: ntxid', ntxid); //TODO } catch (e) { -console.log('[Wallet.js.2340]', e); //TODO return cb(e); } diff --git a/test/unit/controllers/controllersSpec.js b/test/unit/controllers/controllersSpec.js index 654484485..dfc539e55 100644 --- a/test/unit/controllers/controllersSpec.js +++ b/test/unit/controllers/controllersSpec.js @@ -42,6 +42,7 @@ describe("Unit: Controllers", function() { beforeEach(inject(function($controller, $rootScope) { scope = $rootScope.$new(); $rootScope.iden = sinon.stub(); + $rootScope.safeUnspentCount = 1; var w = {}; w.isReady = sinon.stub().returns(true); @@ -467,6 +468,7 @@ describe("Unit: Controllers", function() { expect(form.amount.$pristine).to.equal(false); }); it('should return available amount', function() { + form.amount.$setViewValue(123356); var amount = scope.getAvailableAmount(); expect(amount).to.equal(123356); }); diff --git a/views/send.html b/views/send.html index 5aac3faf1..b5a2499d3 100644 --- a/views/send.html +++ b/views/send.html @@ -11,9 +11,9 @@
-

- + {{error|translate}}