34 changed files with 8 additions and 3044 deletions
@ -1,13 +0,0 @@ |
|||
ClientLib |
|||
- check derive address |
|||
- check change address |
|||
- remove storage from clientlib |
|||
- check prposal signature |
|||
- check xpriv keys correspond to wallet's network |
|||
- check secret format in join |
|||
- test raw tx have signatures |
|||
- add broadcast API to server |
|||
- enhance 'no network (internet)' error |
|||
- /Users/ematiu/devel/bitcore-wallet-service/node_modules/bitcore/lib/transaction/transaction.js:137 |
|||
throw new errors.Transaction.DustOutputs(); |
|||
and others TX errores now appear at broadcast time. |
@ -1,7 +0,0 @@ |
|||
- Check length < 100 for both wallet.name & copayer.name |
|||
- Proposal with spent input should be tagged as invalid or removed |
|||
- Cron job to broadcast accepted txps that failed to broadcast (we may need to track broadcast attempts for this). |
|||
- Payment protocol |
|||
- Automatically create ./db directory |
|||
|
|||
- check parameters for KEY at storage |
@ -1,29 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var program = require('commander'); |
|||
|
|||
program |
|||
.version('0.0.1') |
|||
.command('create <walletName> <m-n> [username]', 'creates a wallet') |
|||
.command('join <secret> [username]', 'join a wallet') |
|||
.command('status', 'get wallet status') |
|||
.command('address', 'create a new address from server') |
|||
.command('addresses', 'list addresses') |
|||
.command('balance', 'wallet balance') |
|||
.command('send <address> <amount> [note]', 'send bitcoins') |
|||
.command('sign <txpId>', 'sign a transaction proposal') |
|||
.command('reject <txpId> [reason]', 'reject a transaction proposal') |
|||
.command('broadcast <txpId>', 'broadcast a transaction proposal to the Bitcoin network') |
|||
.command('rm <txpId>', 'remove a transaction proposal') |
|||
.command('history', 'list of past incoming and outgoing transactions') |
|||
.command('export', 'export wallet critical data') |
|||
.command('import', 'import wallet critical data') |
|||
.command('confirm', 'show copayer\'s data for confirmation') |
|||
.command('recreate', 'recreate a wallet on a remove server given local infomation') |
|||
.command('txproposals', 'list transactions proposals') |
|||
.command('genkey', 'generates extended private key for later wallet usage') |
|||
.parse(process.argv); |
|||
|
|||
if (!program.args.length || !program.runningCommand) |
|||
program.help(); |
|||
|
@ -1,17 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
utils.getClient(program, function (client) { |
|||
client.createAddress(function(err, x) { |
|||
utils.die(err); |
|||
console.log('* New Address %s ', x.address); |
|||
}); |
|||
}); |
@ -1,29 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var utils = require('./cli-utils'); |
|||
var Client = require('../lib/client'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getMainAddresses({ |
|||
doNotVerify: true |
|||
}, function(err, x) { |
|||
utils.die(err); |
|||
|
|||
if (x.length > 0) { |
|||
console.log('* Addresses:'); |
|||
_.each(x, function(a) { |
|||
console.log(' ', a.address); |
|||
}); |
|||
} else { |
|||
console.log('* No addresses.'); |
|||
} |
|||
}); |
|||
}); |
@ -1,18 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getBalance(function(err, x) { |
|||
utils.die(err); |
|||
console.log('* Wallet balance %s (Locked %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount) ); |
|||
}); |
|||
}); |
@ -1,26 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.usage('[options] <txpid>') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
var txpid = args[0] || ''; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getTxProposals({}, function(err, txps) { |
|||
utils.die(err); |
|||
|
|||
var txp = utils.findOneTxProposal(txps, txpid); |
|||
client.broadcastTxProposal(txp, function(err, txp) { |
|||
utils.die(err); |
|||
console.log('Transaction Broadcasted: TXID: ' + txp.txid); |
|||
}); |
|||
}); |
|||
}); |
@ -1,35 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.parse(process.argv); |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getStatus(function(err, x) { |
|||
utils.die(err); |
|||
|
|||
if (x.wallet.n == 1) { |
|||
console.log('Confirmations only work on shared wallets'); |
|||
process.exit(1); |
|||
} |
|||
console.log('\n To be sure that no copayer has joined this wallet more than once, you can asked them for their confirmation number. They can get theirs by running the bit-confirm command.'); |
|||
console.log('\n * Copayer confirmation IDs:'); |
|||
|
|||
var myConfirmationId; |
|||
_.each(x.wallet.copayers, function(x) { |
|||
var confirmationId = utils.confirmationId(x); |
|||
if (x.id != client.credentials.copayerId) |
|||
console.log('\t\t* %s : %s', x.name, confirmationId); |
|||
else |
|||
myConfirmationId = confirmationId; |
|||
}); |
|||
|
|||
console.log('\t\t---'); |
|||
console.log('\t\tYour confirmation ID: %s', myConfirmationId); |
|||
}); |
|||
}); |
@ -1,39 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var ClientLib = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.option('-t, --testnet', 'Create a Testnet Wallet') |
|||
.usage('[options] <walletName> <m-n> [copayerName]') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
if (!args[0]) |
|||
program.help(); |
|||
|
|||
var walletName = args[0]; |
|||
var copayerName = args[2] || process.env.USER; |
|||
var network = program.testnet ? 'testnet' : 'livenet'; |
|||
|
|||
var mn; |
|||
try { |
|||
mn = utils.parseMN(args[1]); |
|||
} catch (ex) { |
|||
utils.die(ex); |
|||
} |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.createWallet(walletName, copayerName, mn[0], mn[1], network, function(err, secret) { |
|||
utils.die(err); |
|||
console.log(' * ' + _.capitalize(network) + ' Wallet Created.'); |
|||
utils.saveClient(program, client, function () { |
|||
if (secret) { |
|||
console.log(' - Secret to share:\n\t' + secret); |
|||
} |
|||
}); |
|||
}); |
|||
}); |
@ -1,57 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var program = require('commander'); |
|||
var qr = require('qr-image'); |
|||
var fs = require('fs'); |
|||
var _ = require('lodash'); |
|||
|
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.option('-a, --access [level]', 'access privileges for exported data (full, readwrite, readonly)', 'full') |
|||
.option('-q, --qr', 'export a QR code') |
|||
.option('-o, --output [filename]', 'output file'); |
|||
|
|||
program.on('--help', function() { |
|||
console.log(' Access Levels:'); |
|||
console.log(''); |
|||
console.log(' readonly : allows to read wallet data: balance, tx proposals '); |
|||
console.log(' readwrite: + allows to create addresses and unsigned tx prposals '); |
|||
console.log(' full : + allows sign tx prposals '); |
|||
console.log(''); |
|||
}); |
|||
|
|||
program |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
var client = utils.getClient(program); |
|||
|
|||
if (!_.contains(['full', 'readwrite', 'readonly'], program.access)) { |
|||
program.help(); |
|||
} |
|||
|
|||
var msg = ' Access Level: ' + program.access; |
|||
|
|||
client.export({ |
|||
access: program.access |
|||
}, function(err, x) { |
|||
utils.die(err); |
|||
if (program.qr) { |
|||
var filename = program.file + '.svg'; |
|||
var qr_svg = qr.image(x, { |
|||
type: 'svg' |
|||
}); |
|||
qr_svg.pipe(fs.createWriteStream(filename)); |
|||
console.log('Wallet Critical Data: exported to %s. %s\n',filename, msg); |
|||
} else { |
|||
if (program.output) { |
|||
fs.writeFileSync(program.output, x); |
|||
console.log('Wallet Critical Data saved at %s. %s\n', program.output, msg); |
|||
} else { |
|||
console.log('Wallet Critical Data (%s)\n%s', msg, x); |
|||
} |
|||
} |
|||
}); |
@ -1,21 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.option('-t, --testnet', 'Create a Testnet Extended Private Key') |
|||
.option('-n, --nopasswd [level]', 'Set access for no password usage: none(default), readonly, readwrite, full', 'none') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
var client = utils.getClient(program); |
|||
var network = program.testnet ? 'testnet' : 'livenet'; |
|||
|
|||
client.generateKey(network, function(err) { |
|||
utils.die(err); |
|||
console.log(' * ' + _.capitalize(network) + ' Extended Private Key Created.'); |
|||
}); |
@ -1,42 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var fs = require('fs'); |
|||
var moment = require('moment'); |
|||
var program = require('commander'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getTxHistory({}, function (err, txs) { |
|||
if (_.isEmpty(txs)) |
|||
return; |
|||
|
|||
console.log("* TX History:") |
|||
|
|||
_.each(txs, function(tx) { |
|||
var time = moment(tx.time * 1000).fromNow(); |
|||
var amount = utils.renderAmount(tx.amount); |
|||
var confirmations = tx.confirmations || 0; |
|||
var proposal = tx.proposalId ? '["' + tx.message + '" by ' + tx.creatorName + '] ' : ''; |
|||
switch (tx.action) { |
|||
case 'received': |
|||
direction = '<='; |
|||
break; |
|||
case 'moved': |
|||
direction = '=='; |
|||
break; |
|||
case 'sent': |
|||
direction = '=>'; |
|||
break; |
|||
} |
|||
|
|||
console.log("\t%s: %s %s %s %s(%s confirmations)", time, direction, tx.action, amount, proposal, confirmations); |
|||
}); |
|||
}); |
|||
}); |
@ -1,27 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
var program = require('commander'); |
|||
|
|||
var Client = require('../lib/client'); |
|||
var fs = require('fs'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
|
|||
program |
|||
.version('0.0.1') |
|||
.option('-n, --nopasswd [level]', 'Set access for no password usage: none(default), readonly, readwrite, full', 'none') |
|||
.usage('import [options] <file>') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
|
|||
if (!args[0]) |
|||
program.help(); |
|||
|
|||
var client = utils.getClient(program); |
|||
var str = fs.readFileSync(args[0]); |
|||
|
|||
client.import(str, function(err, x) { |
|||
utils.die(err); |
|||
console.log('Wallet Imported. Access level:' + x); |
|||
}); |
@ -1,25 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.usage('[options] <secret> [copayerName]') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
if (!args[0]) |
|||
program.help(); |
|||
|
|||
var secret = args[0]; |
|||
var copayerName = args[1] || process.env.USER; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.joinWallet(secret, copayerName, function(err, wallet) { |
|||
utils.die(err); |
|||
console.log(' * Wallet Joined.', wallet.name); |
|||
utils.saveClient(program, client, function () {}); |
|||
}); |
|||
}); |
@ -1,23 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var ClientLib = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.usage('[options] walletname') |
|||
.description('Creates a wallet on the remove server given the local information') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
if (!args[0]) |
|||
program.help(); |
|||
|
|||
var walletName = args[0]; |
|||
var client = utils.getClient(program); |
|||
client.reCreateWallet(walletName, function(err) { |
|||
utils.die(err); |
|||
console.log(' * Wallet Created.'); |
|||
}); |
@ -1,30 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.usage('[options] <txpid> [reason]') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
var txpid = args[0] || ''; |
|||
var reason = args[1] || ''; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getTxProposals({}, function(err, txps) { |
|||
utils.die(err); |
|||
|
|||
var txp = utils.findOneTxProposal(txps, txpid); |
|||
client.rejectTxProposal(txp, reason, function(err, tx) { |
|||
utils.die(err); |
|||
if (tx.status == 'rejected') |
|||
console.log('Transaction finally rejected.'); |
|||
else |
|||
console.log('Transaction rejected by you.'); |
|||
}); |
|||
}); |
|||
}); |
@ -1,31 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.usage('[options] <txpid>') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
var txpid = args[0] || ''; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getTxProposals({}, function(err, txps) { |
|||
utils.die(err); |
|||
|
|||
if (program.verbose) |
|||
console.log('* Raw Server Response:\n', txps); //TODO |
|||
|
|||
var txp = utils.findOneTxProposal(txps, txpid); |
|||
client.removeTxProposal(txp, function(err) { |
|||
utils.die(err); |
|||
|
|||
console.log('Transaction removed.'); |
|||
}); |
|||
}); |
|||
}); |
@ -1,44 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var program = require('commander'); |
|||
var Client = require('../lib/client'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.usage('[options] <address> <amount> [note]') |
|||
.description('Create a proposal for sending bitcoins to a destination address.\n The amount can be specified in bit, btc or sat (the default).'); |
|||
|
|||
program.on('--help', function(){ |
|||
console.log(' Examples:'); |
|||
console.log(''); |
|||
console.log(' $ bit-send n2HRFgtoihgAhx1qAEXcdBMjoMvAx7AcDc 500bit'); |
|||
console.log(' $ bit-send mgWeRvUC6d1LRPKtdDbvYEpaUEmApS4XrY 0.2btc "dinner with friends"'); |
|||
console.log(''); |
|||
}); |
|||
program.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
if (!args[0] || !args[1]) |
|||
program.help(); |
|||
|
|||
var address = args[0]; |
|||
var amount; |
|||
try { |
|||
amount = utils.parseAmount(args[1]); |
|||
} catch (ex) { |
|||
utils.die(ex); |
|||
} |
|||
var note = args[2]; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.sendTxProposal({ |
|||
toAddress: address, |
|||
amount: amount, |
|||
message: note |
|||
}, function(err, x) { |
|||
utils.die(err); |
|||
console.log(' * Tx created: ID %s [%s] RequiredSignatures:', |
|||
x.id, x.status, x.requiredSignatures); |
|||
}); |
|||
}); |
@ -1,63 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var fs = require('fs'); |
|||
var program = require('commander'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.usage('[options] <txpid>') |
|||
.option('-i, --input [filename]', 'use signatures from file') |
|||
.option('-o, --output [filename]', 'write signatures to file') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
var txpid = args[0] || ''; |
|||
|
|||
function end(client, txp) { |
|||
if (program.output) { |
|||
client.getSignatures(txp, function(err, signatures) { |
|||
utils.die(err); |
|||
var out = { |
|||
id: txp.id, |
|||
signatures: signatures, |
|||
}; |
|||
fs.writeFileSync(program.output, JSON.stringify(out)); |
|||
console.log('Signatures written to file.'); |
|||
}); |
|||
} else { |
|||
|
|||
if (program.input) { |
|||
|
|||
var infile = JSON.parse(fs.readFileSync(program.input)); |
|||
if (infile.id != txp.id) |
|||
utils.die('Signatures does not match Transaction') |
|||
|
|||
txp.signatures = infile.signatures; |
|||
} |
|||
|
|||
client.signTxProposal(txp, function(err, tx) { |
|||
utils.die(err); |
|||
if (tx.status == 'broadcasted') |
|||
console.log('Transaction Broadcasted: TXID: ' + tx.txid); |
|||
else |
|||
console.log('Transaction signed by you.'); |
|||
}); |
|||
} |
|||
|
|||
}; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
if (program.input && program.output) { |
|||
var inFile = JSON.parse(fs.readFileSync(program.input)); |
|||
end(client, inFile.txps[0]); |
|||
} else { |
|||
client.getTxProposals({}, function(err, txps) { |
|||
utils.die(err); |
|||
var txp = utils.findOneTxProposal(txps, txpid); |
|||
utils.die(err); |
|||
end(client, txp); |
|||
}); |
|||
} |
|||
}); |
@ -1,28 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var program = require('commander'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
utils.getClient(program, function (client) { |
|||
client.getStatus(function(err, res) { |
|||
utils.die(err); |
|||
|
|||
var x = res.wallet; |
|||
console.log('* Wallet %s [%s]: %d-of-%d %s ', x.name, x.network, x.m, x.n, x.status); |
|||
|
|||
if (x.status != 'complete') |
|||
console.log(' Missing copayers:', x.n - x.copayers.length); |
|||
console.log('* Copayers:', _.pluck(x.copayers,'name').join(', ')); |
|||
|
|||
var x = res.balance; |
|||
console.log('* Balance %s (Locked: %s)', utils.renderAmount(x.totalAmount), utils.renderAmount(x.lockedAmount)); |
|||
|
|||
utils.renderTxProposals(res.pendingTxps); |
|||
}); |
|||
}); |
@ -1,26 +0,0 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
var _ = require('lodash'); |
|||
var fs = require('fs'); |
|||
var program = require('commander'); |
|||
var utils = require('./cli-utils'); |
|||
program = utils.configureCommander(program); |
|||
|
|||
program |
|||
.option('-o, --output [filename]', 'write tx to output file for offline signing') |
|||
.parse(process.argv); |
|||
|
|||
var args = program.args; |
|||
|
|||
utils.getClient(program, function (client) { |
|||
client.getTxProposals({forAirGapped: !!program.output}, function (err, res) { |
|||
utils.die(err); |
|||
|
|||
if (program.output) { |
|||
fs.writeFileSync(program.output, JSON.stringify(res)); |
|||
console.log(' * Tx proposals saved to: %s\n', program.output); |
|||
} else { |
|||
utils.renderTxProposals(res); |
|||
} |
|||
}); |
|||
}); |
@ -1,207 +0,0 @@ |
|||
var _ = require('lodash'); |
|||
var Client = require('../lib/client'); |
|||
var FileStorage = require('./filestorage'); |
|||
var read = require('read') |
|||
var log = require('npmlog'); |
|||
|
|||
var Utils = function() {}; |
|||
|
|||
var die = Utils.die = function(err) { |
|||
if (err) { |
|||
if (err.code && err.code == 'ECONNREFUSED') { |
|||
console.error('Could not connect to Bicore Wallet Service'); |
|||
} else { |
|||
console.error(err); |
|||
} |
|||
process.exit(1); |
|||
} |
|||
}; |
|||
|
|||
Utils.parseMN = function(text) { |
|||
if (!text) throw new Error('No m-n parameter'); |
|||
|
|||
var regex = /^(\d+)(-|of|-of-)?(\d+)$/i; |
|||
var match = regex.exec(text.trim()); |
|||
|
|||
if (!match || match.length === 0) throw new Error('Invalid m-n parameter'); |
|||
|
|||
var m = parseInt(match[1]); |
|||
var n = parseInt(match[3]); |
|||
if (m > n) throw new Error('Invalid m-n parameter'); |
|||
|
|||
return [m, n]; |
|||
}; |
|||
|
|||
|
|||
Utils.shortID = function(id) { |
|||
return id.substr(id.length - 4); |
|||
}; |
|||
|
|||
Utils.confirmationId = function(copayer) { |
|||
return parseInt(copayer.xPubKeySignature.substr(-4), 16).toString().substr(-4); |
|||
} |
|||
|
|||
Utils.getClient = function(args, cb) { |
|||
var storage = new FileStorage({ |
|||
filename: args.file || process.env['BIT_FILE'], |
|||
}); |
|||
var client = new Client({ |
|||
baseUrl: args.host || process.env['BIT_HOST'], |
|||
verbose: args.verbose, |
|||
}); |
|||
storage.load(function(err, walletData) { |
|||
if (err && err.code != 'ENOENT') die(err); |
|||
if (!walletData) return cb(client); |
|||
|
|||
client.import(walletData); |
|||
client.openWallet(function(err, justCompleted) { |
|||
if (client.isComplete() && justCompleted) { |
|||
Utils.saveClient(args, client, function() { |
|||
log.info('Your wallet has just been completed. Please backup your wallet file or use the export command.'); |
|||
return cb(client); |
|||
}); |
|||
} else { |
|||
return cb(client); |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
Utils.saveClient = function(args, client, cb) { |
|||
var storage = new FileStorage({ |
|||
filename: args.file || process.env['BIT_FILE'], |
|||
}); |
|||
var str = client.export(); |
|||
storage.save(str, function(err) { |
|||
die(err); |
|||
return cb(); |
|||
}); |
|||
}; |
|||
|
|||
// var setPassword;
|
|||
// c.on('needPassword', function(cb) {
|
|||
// if (args.password) {
|
|||
// return cb(args.password);
|
|||
// } else {
|
|||
// if (setPassword)
|
|||
// return cb(setPassword);
|
|||
|
|||
// read({
|
|||
// prompt: 'Password for ' + args.file + ' : ',
|
|||
// silent: true
|
|||
// }, function(er, password) {
|
|||
// setPassword = password;
|
|||
// return cb(password);
|
|||
// })
|
|||
// }
|
|||
// });
|
|||
|
|||
// c.on('needNewPassword', function(cb) {
|
|||
// if (args.password) {
|
|||
// return cb(args.password);
|
|||
// } else {
|
|||
// read({
|
|||
// prompt: 'New Password: ',
|
|||
// silent: true
|
|||
// }, function(er, password) {
|
|||
// return cb(password);
|
|||
// })
|
|||
// }
|
|||
// });
|
|||
|
|||
|
|||
|
|||
Utils.findOneTxProposal = function(txps, id) { |
|||
var matches = _.filter(txps, function(tx) { |
|||
return _.endsWith(Utils.shortID(tx.id), id); |
|||
}); |
|||
|
|||
if (!matches.length) |
|||
Utils.die('Could not find TX Proposal:' + id); |
|||
|
|||
if (matches.length > 1) |
|||
Utils.die('More than one TX Proposals match:' + id + ' : ' + _.map(matches, function(tx) { |
|||
return tx.id; |
|||
}).join(' '));; |
|||
|
|||
return matches[0]; |
|||
}; |
|||
|
|||
Utils.UNITS = { |
|||
'btc': 100000000, |
|||
'bit': 100, |
|||
'sat': 1, |
|||
}; |
|||
|
|||
Utils.parseAmount = function(text) { |
|||
if (!_.isString(text)) |
|||
text = text.toString(); |
|||
|
|||
var regex = '^(\\d*(\\.\\d{0,8})?)\\s*(' + _.keys(Utils.UNITS).join('|') + ')?$'; |
|||
var match = new RegExp(regex, 'i').exec(text.trim()); |
|||
|
|||
if (!match || match.length === 0) throw new Error('Invalid amount'); |
|||
|
|||
var amount = parseFloat(match[1]); |
|||
if (!_.isNumber(amount) || _.isNaN(amount)) throw new Error('Invalid amount'); |
|||
|
|||
var unit = (match[3] || 'sat').toLowerCase(); |
|||
var rate = Utils.UNITS[unit]; |
|||
if (!rate) throw new Error('Invalid unit') |
|||
|
|||
var amountSat = parseFloat((amount * rate).toPrecision(12)); |
|||
if (amountSat != Math.round(amountSat)) throw new Error('Invalid amount'); |
|||
|
|||
return amountSat; |
|||
}; |
|||
|
|||
Utils.configureCommander = function(program) { |
|||
program |
|||
.version('0.0.1') |
|||
.option('-f, --file [filename]', 'Wallet file', process.env['HOME'] + '/.bit.dat') |
|||
.option('-h, --host [host]', 'Bitcore Wallet Service URL (eg: http://localhost:3001/copay/api') |
|||
.option('-v, --verbose', 'be verbose') |
|||
|
|||
return program; |
|||
}; |
|||
|
|||
Utils.renderAmount = function(amount) { |
|||
var unit = process.env.BIT_UNIT || 'bit'; |
|||
if (unit === 'SAT') { |
|||
// Do nothing
|
|||
} else if (process.env.BIT_UNIT === 'btc') { |
|||
amount = amount / 1e8; |
|||
} else { |
|||
amount = amount / 100; |
|||
} |
|||
amount = (parseFloat(amount.toPrecision(12))); |
|||
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ' + unit; |
|||
}; |
|||
|
|||
Utils.renderTxProposals = function(txps) { |
|||
if (_.isEmpty(txps)) |
|||
return; |
|||
|
|||
console.log("* TX Proposals:") |
|||
|
|||
_.each(txps, function(x) { |
|||
var missingSignatures = x.requiredSignatures - _.filter(_.values(x.actions), function(a) { |
|||
return a.type == 'accept'; |
|||
}).length; |
|||
console.log("\t%s [\"%s\" by %s] %s => %s", Utils.shortID(x.id), x.message, x.creatorName, Utils.renderAmount(x.amount), x.toAddress); |
|||
|
|||
if (!_.isEmpty(x.actions)) { |
|||
console.log('\t\tActions: ', _.map(x.actions, function(a) { |
|||
return a.copayerName + ' ' + (a.type == 'accept' ? '✓' : '✗') + (a.comment ? ' (' + a.comment + ')' : ''); |
|||
}).join('. ')); |
|||
} |
|||
if (missingSignatures > 0) { |
|||
console.log('\t\tMissing signatures: ' + missingSignatures); |
|||
} else { |
|||
console.log('\t\tReady to broadcast'); |
|||
} |
|||
}); |
|||
|
|||
}; |
|||
|
|||
module.exports = Utils; |
@ -1,30 +0,0 @@ |
|||
var fs = require('fs') |
|||
|
|||
function FileStorage(opts) { |
|||
if (!opts.filename) { |
|||
throw new Error('Please set wallet filename'); |
|||
} |
|||
this.filename = opts.filename; |
|||
this.fs = opts.fs || fs; |
|||
}; |
|||
|
|||
FileStorage.prototype.getName = function() { |
|||
return this.filename; |
|||
}; |
|||
|
|||
FileStorage.prototype.save = function(data, cb) { |
|||
this.fs.writeFile(this.filename, JSON.stringify(data), cb); |
|||
}; |
|||
|
|||
FileStorage.prototype.load = function(cb) { |
|||
this.fs.readFile(this.filename, 'utf8', function(err, data) { |
|||
if (err) return cb(err); |
|||
try { |
|||
data = JSON.parse(data); |
|||
} catch (e) {} |
|||
return cb(null, data); |
|||
}); |
|||
}; |
|||
|
|||
|
|||
module.exports = FileStorage; |
@ -1,114 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var chai = require('chai'); |
|||
var sinon = require('sinon'); |
|||
var should = chai.should(); |
|||
var CliUtils = require('../cli-utils'); |
|||
|
|||
describe('CliUtils', function() { |
|||
describe('#parseMN', function() { |
|||
it('should successfully parse m & n', function() { |
|||
var texts = { |
|||
'1-1': [1, 1], |
|||
'1-of-1': [1, 1], |
|||
'1of1': [1, 1], |
|||
'1-OF-2': [1, 2], |
|||
'1OF2': [1, 2], |
|||
' 2-2': [2, 2], |
|||
'2-3 ': [2, 3], |
|||
'10-10': [10, 10], |
|||
'10-of-10': [10, 10], |
|||
}; |
|||
_.each(texts, function(expected, text) { |
|||
var result = CliUtils.parseMN(text); |
|||
result.should.deep.equal(expected); |
|||
}); |
|||
}); |
|||
it('should fail to parse incorrect m & n', function() { |
|||
var texts = [ |
|||
'', |
|||
' ', |
|||
'1', |
|||
'x-1', |
|||
'1-x', |
|||
'of-1-1', |
|||
'2-2-of', |
|||
'1-1-1', |
|||
' 1_1 ', |
|||
'2-1', |
|||
'2-of-1', |
|||
'-1-2', |
|||
'1--2', |
|||
'x-of-2', |
|||
]; |
|||
_.each(texts, function(text) { |
|||
var valid = true; |
|||
try { |
|||
CliUtils.parseMN(text); |
|||
} catch (e) { |
|||
valid = false; |
|||
} |
|||
valid.should.be.false; |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('#parseAmount', function() { |
|||
it('should successfully parse amounts', function() { |
|||
var texts = { |
|||
'1': 1, |
|||
'0': 0, |
|||
'1.': 1, |
|||
'000000.0000': 0, |
|||
'123': 123, |
|||
'123sat': 123, |
|||
'123 sat': 123, |
|||
'00123 sat': 123, |
|||
'1.23bit': 123, |
|||
'1.23 bit': 123, |
|||
'0 bit': 0, |
|||
'.45bit': 45, |
|||
'1btc': 100000000, |
|||
' 1btc': 100000000, |
|||
'9999btc': 999900000000, |
|||
'0.00000001btc': 1, |
|||
'00000.00000001BTC': 1, |
|||
'0.00000001 BTC': 1, |
|||
'0.123btc': 12300000, |
|||
'0.123 bTc': 12300000, |
|||
}; |
|||
_.each(texts, function(satoshi, text) { |
|||
var amount = CliUtils.parseAmount(text); |
|||
amount.should.equal(satoshi); |
|||
}); |
|||
}); |
|||
it('should fail to parse incorrect amounts', function() { |
|||
var texts = [ |
|||
'', |
|||
' ', |
|||
'btc', |
|||
'1satoshi', |
|||
'no-number', |
|||
'-3', |
|||
'1 b t c', |
|||
'btc1', |
|||
'btc 1', |
|||
'1,234', |
|||
'0.000000001btc', |
|||
'0.1sat', |
|||
'0.123bit', |
|||
'2.000000009btc', |
|||
]; |
|||
_.each(texts, function(text) { |
|||
var valid = true; |
|||
try { |
|||
CliUtils.parseAmount(text); |
|||
} catch (e) { |
|||
valid = false; |
|||
} |
|||
valid.should.be.false; |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,7 @@ |
|||
var BWS = {}; |
|||
|
|||
BWS.ExpressApp = require('./lib/expressapp'); |
|||
BWS.Storage = require('./lib/storage'); |
|||
|
|||
|
|||
module.exports = BWS; |
@ -1,62 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var $ = require('preconditions').singleton(); |
|||
var util = require('util'); |
|||
var async = require('async'); |
|||
var log = require('npmlog'); |
|||
var events = require('events'); |
|||
log.debug = log.verbose; |
|||
var Bitcore = require('bitcore') |
|||
|
|||
var Credentials = require('./credentials'); |
|||
var WalletUtils = require('../walletutils'); |
|||
var Verifier = require('./verifier'); |
|||
var ServerCompromisedError = require('./servercompromisederror'); |
|||
var ClientError = require('../clienterror'); |
|||
|
|||
function AirGapped(opts) { |
|||
this.verbose = !!opts.verbose; |
|||
if (this.verbose) { |
|||
log.level = 'debug'; |
|||
} else { |
|||
log.level = 'info'; |
|||
} |
|||
this.credentials = Credentials.create(opts.network || 'livenet'); |
|||
}; |
|||
|
|||
util.inherits(AirGapped, events.EventEmitter); |
|||
|
|||
AirGapped.prototype.getSeed = function() { |
|||
return { |
|||
xPubKey: this.credentials.xPubKey, |
|||
requestPrivKey: this.credentials.requestPrivKey, |
|||
}; |
|||
}; |
|||
|
|||
AirGapped.prototype.signTxProposal = function(txp, encryptedPkr, m, n) { |
|||
var self = this; |
|||
|
|||
var publicKeyRing; |
|||
try { |
|||
publicKeyRing = JSON.parse(WalletUtils.decryptMessage(encryptedPkr, self.credentials.personalEncryptingKey)); |
|||
} catch (ex) { |
|||
console.log(ex); |
|||
throw new Error('Could not decrypt public key ring'); |
|||
} |
|||
|
|||
if (!_.isArray(publicKeyRing) || publicKeyRing.length != n) { |
|||
throw new Error('Invalid public key ring'); |
|||
} |
|||
|
|||
self.credentials.m = m; |
|||
self.credentials.n = n; |
|||
self.credentials.addPublicKeyRing(publicKeyRing); |
|||
|
|||
if (!Verifier.checkTxProposal(self.credentials, txp)) { |
|||
throw new Error('Fake transaction proposal'); |
|||
} |
|||
return WalletUtils.signTxp(txp, self.credentials.xPrivKey); |
|||
}; |
|||
|
|||
module.exports = AirGapped; |
@ -1,565 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var $ = require('preconditions').singleton(); |
|||
var util = require('util'); |
|||
var async = require('async'); |
|||
var log = require('npmlog'); |
|||
var request = require('request') |
|||
var events = require('events'); |
|||
log.debug = log.verbose; |
|||
var Bitcore = require('bitcore') |
|||
var sjcl = require('sjcl'); |
|||
|
|||
var Credentials = require('./credentials'); |
|||
var WalletUtils = require('../walletutils'); |
|||
var Verifier = require('./verifier'); |
|||
var ServerCompromisedError = require('./servercompromisederror'); |
|||
var ClientError = require('../clienterror'); |
|||
|
|||
var BASE_URL = 'http://localhost:3001/copay/api'; |
|||
var WALLET_ENCRYPTION_OPTS = { |
|||
iter: 5000 |
|||
}; |
|||
|
|||
function _encryptMessage(message, encryptingKey) { |
|||
if (!message) return null; |
|||
return WalletUtils.encryptMessage(message, encryptingKey); |
|||
}; |
|||
|
|||
function _decryptMessage(message, encryptingKey) { |
|||
if (!message) return ''; |
|||
try { |
|||
return WalletUtils.decryptMessage(message, encryptingKey); |
|||
} catch (ex) { |
|||
return '<ECANNOTDECRYPT>'; |
|||
} |
|||
}; |
|||
|
|||
function _processTxps(txps, encryptingKey) { |
|||
if (!txps) return; |
|||
_.each([].concat(txps), function(txp) { |
|||
txp.encryptedMessage = txp.message; |
|||
txp.message = _decryptMessage(txp.message, encryptingKey); |
|||
_.each(txp.actions, function(action) { |
|||
action.comment = _decryptMessage(action.comment, encryptingKey); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
function _parseError(body) { |
|||
if (_.isString(body)) { |
|||
try { |
|||
body = JSON.parse(body); |
|||
} catch (e) { |
|||
body = { |
|||
error: body |
|||
}; |
|||
} |
|||
} |
|||
var ret; |
|||
if (body.code) { |
|||
ret = new ClientError(body.code, body.message); |
|||
} else { |
|||
ret = { |
|||
code: 'ERROR', |
|||
error: body.error || 'There was an unknown error processing the request', |
|||
}; |
|||
} |
|||
log.error(ret); |
|||
return ret; |
|||
}; |
|||
|
|||
function _signRequest(method, url, args, privKey) { |
|||
var message = method.toLowerCase() + '|' + url + '|' + JSON.stringify(args); |
|||
return WalletUtils.signMessage(message, privKey); |
|||
}; |
|||
|
|||
function API(opts) { |
|||
opts = opts || {}; |
|||
|
|||
this.verbose = !!opts.verbose; |
|||
this.request = opts.request || request; |
|||
this.baseUrl = opts.baseUrl || BASE_URL; |
|||
this.basePath = this.baseUrl.replace(/http.?:\/\/[a-zA-Z0-9:-]*\//, '/'); |
|||
if (this.verbose) { |
|||
log.level = 'debug'; |
|||
} else { |
|||
log.level = 'info'; |
|||
} |
|||
}; |
|||
|
|||
util.inherits(API, events.EventEmitter); |
|||
|
|||
API.prototype.seedFromExtendedPrivateKey = function(xPrivKey) { |
|||
this.credentials = Credentials.fromExtendedPrivateKey(xPrivKey); |
|||
}; |
|||
|
|||
API.prototype.seedFromAirGapped = function(seed) { |
|||
this.credentials = Credentials.fromExtendedPublicKey(seed.xPubKey, seed.requestPrivKey); |
|||
}; |
|||
|
|||
/** |
|||
* export |
|||
* |
|||
* @param opts |
|||
* @param opts.compressed |
|||
* @param opts.password |
|||
*/ |
|||
API.prototype.export = function(opts) { |
|||
$.checkState(this.credentials); |
|||
|
|||
opts = opts || {}; |
|||
|
|||
var output; |
|||
if (opts.compressed) { |
|||
output = this.credentials.exportCompressed(); |
|||
} else { |
|||
output = JSON.stringify(this.credentials.toObj()); |
|||
} |
|||
|
|||
if (opts.password) { |
|||
output = sjcl.encrypt(opts.password, output, WALLET_ENCRYPTION_OPTS); |
|||
} |
|||
|
|||
return output; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* export |
|||
* |
|||
* @param opts |
|||
* @param opts.compressed |
|||
* @param opts.password |
|||
*/ |
|||
API.prototype.import = function(str, opts) { |
|||
opts = opts || {}; |
|||
|
|||
var input = str; |
|||
if (opts.password) { |
|||
try { |
|||
input = sjcl.decrypt(opts.password, input); |
|||
} catch (ex) { |
|||
throw new Error('Incorrect password'); |
|||
} |
|||
} |
|||
|
|||
var credentials; |
|||
try { |
|||
if (opts.compressed) { |
|||
credentials = Credentials.importCompressed(input); |
|||
// TODO: complete missing fields that live on the server only such as: walletId, walletName, copayerName
|
|||
} else { |
|||
credentials = Credentials.fromObj(JSON.parse(input)); |
|||
} |
|||
} catch (ex) { |
|||
throw new Error('Error importing from source'); |
|||
} |
|||
this.credentials = credentials; |
|||
}; |
|||
|
|||
API.prototype.toString = function(password) { |
|||
$.checkState(this.credentials); |
|||
return this.credentials.toObject(); |
|||
}; |
|||
|
|||
API.prototype.fromString = function(str) { |
|||
this.credentials = Credentials.fromObject(str); |
|||
}; |
|||
|
|||
API.prototype._doRequest = function(method, url, args, cb) { |
|||
$.checkState(this.credentials); |
|||
|
|||
var reqSignature; |
|||
|
|||
if (this.credentials.requestPrivKey) { |
|||
reqSignature = _signRequest(method, url, args, this.credentials.requestPrivKey); |
|||
} |
|||
|
|||
var absUrl = this.baseUrl + url; |
|||
var args = { |
|||
// relUrl: only for testing with `supertest`
|
|||
relUrl: this.basePath + url, |
|||
headers: { |
|||
'x-identity': this.credentials.copayerId, |
|||
'x-signature': reqSignature, |
|||
}, |
|||
method: method, |
|||
url: absUrl, |
|||
body: args, |
|||
json: true, |
|||
}; |
|||
|
|||
log.verbose('Request Args', util.inspect(args, { |
|||
depth: 10 |
|||
})); |
|||
this.request(args, function(err, res, body) { |
|||
log.verbose(util.inspect(body, { |
|||
depth: 10 |
|||
})); |
|||
if (err) return cb(err); |
|||
|
|||
if (res.statusCode != 200) { |
|||
return cb(_parseError(body)); |
|||
} |
|||
|
|||
return cb(null, body, res.header); |
|||
}); |
|||
}; |
|||
|
|||
|
|||
API.prototype._doPostRequest = function(url, args, cb) { |
|||
return this._doRequest('post', url, args, cb); |
|||
}; |
|||
|
|||
API.prototype._doGetRequest = function(url, cb) { |
|||
return this._doRequest('get', url, {}, cb); |
|||
}; |
|||
|
|||
API.prototype._doDeleteRequest = function(url, cb) { |
|||
return this._doRequest('delete', url, {}, cb); |
|||
}; |
|||
|
|||
API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) { |
|||
var args = { |
|||
walletId: walletId, |
|||
name: copayerName, |
|||
xPubKey: xPubKey, |
|||
xPubKeySignature: WalletUtils.signMessage(xPubKey, walletPrivKey), |
|||
}; |
|||
var url = '/v1/wallets/' + walletId + '/copayers'; |
|||
this._doPostRequest(url, args, function(err, body) { |
|||
if (err) return cb(err); |
|||
return cb(null, body.wallet); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.isComplete = function() { |
|||
return this.credentials && this.credentials.isComplete(); |
|||
}; |
|||
|
|||
/** |
|||
* Opens a wallet and tries to complete the public key ring. |
|||
* @param {Function} cb - Returns an error and a flag indicating that the wallet has just been completed and needs to be persisted |
|||
*/ |
|||
API.prototype.openWallet = function(cb) { |
|||
$.checkState(this.credentials); |
|||
|
|||
var self = this; |
|||
|
|||
if (self.credentials.isComplete()) return cb(null, false); |
|||
|
|||
self._doGetRequest('/v1/wallets/', function(err, ret) { |
|||
if (err) return cb(err); |
|||
var wallet = ret.wallet; |
|||
|
|||
if (wallet.status != 'complete') return cb('Wallet Incomplete'); |
|||
|
|||
if (!!self.credentials.walletPrivKey) { |
|||
if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) { |
|||
return cb(new ServerCompromisedError( |
|||
'Copayers in the wallet could not be verified to have known the wallet secret')); |
|||
} |
|||
} else { |
|||
log.warn('Could not perform verification of other copayers in the wallet'); |
|||
} |
|||
|
|||
self.credentials.addPublicKeyRing(_.pluck(wallet.copayers, 'xPubKey')); |
|||
if (!self.credentials.hasWalletInfo()) { |
|||
var me = _.find(wallet.copayers, { |
|||
id: self.credentials.copayerId |
|||
}); |
|||
self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, null, me.name); |
|||
} |
|||
|
|||
return cb(null, true); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) { |
|||
var self = this; |
|||
|
|||
network = network || 'livenet'; |
|||
if (!_.contains(['testnet', 'livenet'], network)) return cb('Invalid network'); |
|||
|
|||
if (!self.credentials) { |
|||
log.info('Generating new keys'); |
|||
self.credentials = Credentials.create(network); |
|||
} else { |
|||
log.info('Using existing keys'); |
|||
} |
|||
|
|||
$.checkState(network == self.credentials.network); |
|||
|
|||
var walletPrivKey = new Bitcore.PrivateKey(); |
|||
var args = { |
|||
name: walletName, |
|||
m: m, |
|||
n: n, |
|||
pubKey: walletPrivKey.toPublicKey().toString(), |
|||
network: network, |
|||
}; |
|||
|
|||
self._doPostRequest('/v1/wallets/', args, function(err, body) { |
|||
if (err) return cb(err); |
|||
|
|||
var walletId = body.walletId; |
|||
|
|||
var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); |
|||
self.credentials.addWalletInfo(walletId, walletName, m, n, walletPrivKey.toString(), copayerName); |
|||
|
|||
self._doJoinWallet(walletId, walletPrivKey, self.credentials.xPubKey, copayerName, |
|||
function(err, wallet) { |
|||
if (err) return cb(err); |
|||
return cb(null, n > 1 ? secret : null); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.joinWallet = function(secret, copayerName, cb) { |
|||
var self = this; |
|||
|
|||
try { |
|||
var secretData = WalletUtils.fromSecret(secret); |
|||
} catch (ex) { |
|||
return cb(ex); |
|||
} |
|||
|
|||
if (!self.credentials) { |
|||
self.credentials = Credentials.create(secretData.network); |
|||
} |
|||
|
|||
self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, self.credentials.xPubKey, copayerName, |
|||
function(err, wallet) { |
|||
if (err) return cb(err); |
|||
self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, secretData.walletPrivKey, copayerName); |
|||
return cb(null, wallet); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.getStatus = function(cb) { |
|||
$.checkState(this.credentials); |
|||
var self = this; |
|||
|
|||
self._doGetRequest('/v1/wallets/', function(err, result) { |
|||
_processTxps(result.pendingTxps, self.credentials.sharedEncryptingKey); |
|||
return cb(err, result); |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* send |
|||
* |
|||
* @param opts |
|||
* @param opts.toAddress |
|||
* @param opts.amount |
|||
* @param opts.message |
|||
*/ |
|||
API.prototype.sendTxProposal = function(opts, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
$.checkArgument(opts); |
|||
$.shouldBeNumber(opts.amount); |
|||
|
|||
var self = this; |
|||
|
|||
var args = { |
|||
toAddress: opts.toAddress, |
|||
amount: opts.amount, |
|||
message: _encryptMessage(opts.message, self.credentials.sharedEncryptingKey), |
|||
}; |
|||
var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); |
|||
args.proposalSignature = WalletUtils.signMessage(hash, self.credentials.requestPrivKey); |
|||
log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); |
|||
|
|||
self._doPostRequest('/v1/txproposals/', args, function(err, txp) { |
|||
if (err) return cb(err); |
|||
return cb(null, txp); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.createAddress = function(cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
|
|||
var self = this; |
|||
|
|||
self._doPostRequest('/v1/addresses/', {}, function(err, address) { |
|||
if (err) return cb(err); |
|||
if (!Verifier.checkAddress(self.credentials, address)) { |
|||
return cb(new ServerCompromisedError('Server sent fake address')); |
|||
} |
|||
|
|||
return cb(null, address); |
|||
}); |
|||
}; |
|||
|
|||
/* |
|||
* opts.doNotVerify |
|||
*/ |
|||
|
|||
API.prototype.getMainAddresses = function(opts, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
|
|||
var self = this; |
|||
|
|||
self._doGetRequest('/v1/addresses/', function(err, addresses) { |
|||
if (err) return cb(err); |
|||
|
|||
if (!opts.doNotVerify) { |
|||
var fake = _.any(addresses, function(address) { |
|||
return !Verifier.checkAddress(self.credentials, address); |
|||
}); |
|||
if (fake) |
|||
return cb(new ServerCompromisedError('Server sent fake address')); |
|||
} |
|||
return cb(null, addresses); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.getBalance = function(cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
var self = this; |
|||
|
|||
self._doGetRequest('/v1/balance/', cb); |
|||
}; |
|||
|
|||
|
|||
/** |
|||
* |
|||
* opts.doNotVerify |
|||
* opts.forAirGapped |
|||
* @return {undefined} |
|||
*/ |
|||
|
|||
API.prototype.getTxProposals = function(opts, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
|
|||
var self = this; |
|||
|
|||
self._doGetRequest('/v1/txproposals/', function(err, txps) { |
|||
if (err) return cb(err); |
|||
|
|||
_processTxps(txps, self.credentials.sharedEncryptingKey); |
|||
|
|||
var fake = _.any(txps, function(txp) { |
|||
return (!opts.doNotVerify && !Verifier.checkTxProposal(self.credentials, txp)); |
|||
}); |
|||
|
|||
if (fake) |
|||
return cb(new ServerCompromisedError('Server sent fake transaction proposal')); |
|||
|
|||
var result; |
|||
if (opts.forAirGapped) { |
|||
result = { |
|||
txps: JSON.parse(JSON.stringify(txps)), |
|||
publicKeyRing: WalletUtils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey), |
|||
m: self.credentials.m, |
|||
n: self.credentials.n, |
|||
}; |
|||
} else { |
|||
result = txps; |
|||
} |
|||
|
|||
return cb(null, result); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.getSignatures = function(txp, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
$.checkArgument(txp.creatorId); |
|||
|
|||
var self = this; |
|||
|
|||
if (!self.credentials.canSign()) |
|||
return cb('You do not have the required keys to sign transactions'); |
|||
|
|||
if (!Verifier.checkTxProposal(self.credentials, txp)) { |
|||
return cb(new ServerCompromisedError('Transaction proposal is invalid')); |
|||
} |
|||
|
|||
return cb(null, WalletUtils.signTxp(txp, self.credentials.xPrivKey)); |
|||
}; |
|||
|
|||
API.prototype.signTxProposal = function(txp, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
$.checkArgument(txp.creatorId); |
|||
|
|||
var self = this; |
|||
|
|||
if (!self.credentials.canSign() && !txp.signatures) |
|||
return cb(new Error('You do not have the required keys to sign transactions')); |
|||
|
|||
if (!Verifier.checkTxProposal(self.credentials, txp)) { |
|||
return cb(new ServerCompromisedError('Server sent fake transaction proposal')); |
|||
} |
|||
|
|||
var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey); |
|||
|
|||
var url = '/v1/txproposals/' + txp.id + '/signatures/'; |
|||
var args = { |
|||
signatures: signatures |
|||
}; |
|||
|
|||
self._doPostRequest(url, args, function(err, txp) { |
|||
if (err) return cb(err); |
|||
return cb(null, txp); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.rejectTxProposal = function(txp, reason, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
$.checkArgument(cb); |
|||
|
|||
var self = this; |
|||
|
|||
var url = '/v1/txproposals/' + txp.id + '/rejections/'; |
|||
var args = { |
|||
reason: _encryptMessage(reason, self.credentials.sharedEncryptingKey) || '', |
|||
}; |
|||
self._doPostRequest(url, args, function(err, txp) { |
|||
if (err) return cb(err); |
|||
return cb(null, txp); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.broadcastTxProposal = function(txp, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
|
|||
var self = this; |
|||
|
|||
var url = '/v1/txproposals/' + txp.id + '/broadcast/'; |
|||
self._doPostRequest(url, {}, function(err, txp) { |
|||
if (err) return cb(err); |
|||
return cb(null, txp); |
|||
}); |
|||
}; |
|||
|
|||
|
|||
|
|||
API.prototype.removeTxProposal = function(txp, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
|
|||
var self = this; |
|||
|
|||
var url = '/v1/txproposals/' + txp.id; |
|||
self._doDeleteRequest(url, function(err) { |
|||
if (err) return cb(err); |
|||
return cb(); |
|||
}); |
|||
}; |
|||
|
|||
API.prototype.getTxHistory = function(opts, cb) { |
|||
$.checkState(this.credentials && this.credentials.isComplete()); |
|||
|
|||
var self = this; |
|||
|
|||
self._doGetRequest('/v1/txhistory/', function(err, txs) { |
|||
if (err) return cb(err); |
|||
|
|||
_processTxps(txs, self.credentials.sharedEncryptingKey); |
|||
|
|||
return cb(null, txs); |
|||
}); |
|||
}; |
|||
|
|||
module.exports = API; |
@ -1,169 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
var $ = require('preconditions').singleton(); |
|||
var _ = require('lodash'); |
|||
var Bitcore = require('bitcore'); |
|||
var WalletUtils = require('../walletutils'); |
|||
|
|||
var FIELDS = [ |
|||
'network', |
|||
'xPrivKey', |
|||
'xPubKey', |
|||
'requestPrivKey', |
|||
'copayerId', |
|||
'publicKeyRing', |
|||
'walletId', |
|||
'walletName', |
|||
'm', |
|||
'n', |
|||
'walletPrivKey', |
|||
'personalEncryptingKey', |
|||
'sharedEncryptingKey', |
|||
'copayerName', |
|||
]; |
|||
|
|||
var EXPORTABLE_FIELDS = [ |
|||
'xPrivKey', |
|||
'requestPrivKey', |
|||
'xPubKey', |
|||
'm', |
|||
'n', |
|||
'publicKeyRing', |
|||
'sharedEncryptingKey', |
|||
]; |
|||
|
|||
function Credentials() { |
|||
this.version = '1.0.0'; |
|||
}; |
|||
|
|||
Credentials.create = function(network) { |
|||
var x = new Credentials(); |
|||
|
|||
x.network = network; |
|||
x.xPrivKey = (new Bitcore.HDPrivateKey(network)).toString(); |
|||
x._expand(); |
|||
return x; |
|||
}; |
|||
|
|||
Credentials.fromExtendedPrivateKey = function(xPrivKey) { |
|||
var x = new Credentials(); |
|||
x.xPrivKey = xPrivKey; |
|||
x._expand(); |
|||
return x; |
|||
}; |
|||
|
|||
Credentials.fromExtendedPublicKey = function(xPubKey, requestPrivKey) { |
|||
var x = new Credentials(); |
|||
x.xPubKey = xPubKey; |
|||
x.requestPrivKey = requestPrivKey; |
|||
x._expand(); |
|||
return x; |
|||
}; |
|||
|
|||
Credentials.prototype._expand = function() { |
|||
$.checkState(this.xPrivKey || this.xPubKey); |
|||
|
|||
if (this.xPrivKey) { |
|||
var xPrivKey = new Bitcore.HDPrivateKey.fromString(this.xPrivKey); |
|||
this.xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); |
|||
this.requestPrivKey = xPrivKey.derive('m/1/1').privateKey.toString(); |
|||
} |
|||
var network = WalletUtils.getNetworkFromXPubKey(this.xPubKey); |
|||
if (this.network) { |
|||
$.checkState(this.network == network); |
|||
} else { |
|||
this.network = network; |
|||
} |
|||
|
|||
this.personalEncryptingKey = WalletUtils.privateKeyToAESKey(this.requestPrivKey); |
|||
this.copayerId = WalletUtils.xPubToCopayerId(this.xPubKey); |
|||
}; |
|||
|
|||
Credentials.fromObj = function(obj) { |
|||
var x = new Credentials(); |
|||
|
|||
_.each(FIELDS, function(k) { |
|||
x[k] = obj[k]; |
|||
}); |
|||
|
|||
return x; |
|||
}; |
|||
|
|||
Credentials.prototype.toObj = function() { |
|||
return this; |
|||
}; |
|||
|
|||
Credentials.prototype.addWalletInfo = function(walletId, walletName, m, n, walletPrivKey, copayerName) { |
|||
this.walletId = walletId; |
|||
this.walletName = walletName; |
|||
this.m = m; |
|||
this.n = n; |
|||
this.walletPrivKey = walletPrivKey; |
|||
this.sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); |
|||
this.copayerName = copayerName; |
|||
if (n == 1) { |
|||
this.addPublicKeyRing([this.xPubKey]); |
|||
} |
|||
}; |
|||
|
|||
Credentials.prototype.hasWalletInfo = function() { |
|||
return !!this.walletId; |
|||
}; |
|||
|
|||
Credentials.prototype.addPublicKeyRing = function(publicKeyRing) { |
|||
this.publicKeyRing = _.clone(publicKeyRing); |
|||
}; |
|||
|
|||
Credentials.prototype.canSign = function() { |
|||
return !!this.xPrivKey; |
|||
}; |
|||
|
|||
Credentials.prototype.isComplete = function() { |
|||
if (!this.m || !this.n) return false; |
|||
if (!this.publicKeyRing || this.publicKeyRing.length != this.n) return false; |
|||
return true; |
|||
}; |
|||
|
|||
Credentials.prototype.exportCompressed = function() { |
|||
var self = this; |
|||
|
|||
var values = _.map(EXPORTABLE_FIELDS, function(field) { |
|||
if ((field == 'xPubKey' || field == 'requestPrivKey') && self.canSign()) return ''; |
|||
if (field == 'requestPrivKey') { |
|||
return Bitcore.PrivateKey.fromString(self.requestPrivKey).toWIF(); |
|||
} |
|||
if (field == 'publicKeyRing') { |
|||
return _.without(self.publicKeyRing, self.xPubKey); |
|||
} |
|||
return self[field]; |
|||
}); |
|||
values.unshift(self.version); |
|||
|
|||
return JSON.stringify(values); |
|||
}; |
|||
|
|||
Credentials.importCompressed = function(compressed) { |
|||
var list; |
|||
try { |
|||
list = JSON.parse(compressed); |
|||
} catch (ex) { |
|||
throw new Error('Invalid compressed format'); |
|||
} |
|||
|
|||
var x = new Credentials(); |
|||
|
|||
// Remove version
|
|||
var version = list[0]; |
|||
list = _.rest(list); |
|||
|
|||
_.each(EXPORTABLE_FIELDS, function(field, i) { |
|||
x[field] = list[i]; |
|||
}); |
|||
x._expand(); |
|||
|
|||
x.network = WalletUtils.getNetworkFromXPubKey(x.xPubKey); |
|||
x.publicKeyRing.push(x.xPubKey); |
|||
return x; |
|||
}; |
|||
|
|||
module.exports = Credentials; |
@ -1,3 +0,0 @@ |
|||
var client = module.exports = require('./api'); |
|||
client.Verifier = require('./verifier'); |
|||
client.AirGapped = require('./airgapped'); |
@ -1,6 +0,0 @@ |
|||
function ServerCompromisedError(message) { |
|||
this.code = 'SERVERCOMPROMISED'; |
|||
this.message = message; |
|||
}; |
|||
|
|||
module.exports = ServerCompromisedError; |
@ -1,78 +0,0 @@ |
|||
var $ = require('preconditions').singleton(); |
|||
var _ = require('lodash'); |
|||
var log = require('npmlog'); |
|||
|
|||
var Bitcore = require('bitcore'); |
|||
var WalletUtils = require('../walletutils') |
|||
|
|||
/* |
|||
* Checks data given by the server |
|||
*/ |
|||
|
|||
function Verifier(opts) {}; |
|||
|
|||
Verifier.checkAddress = function(credentials, address) { |
|||
$.checkState(credentials.isComplete()); |
|||
var local = WalletUtils.deriveAddress(credentials.publicKeyRing, address.path, credentials.m, credentials.network); |
|||
return (local.address == address.address && JSON.stringify(local.publicKeys) == JSON.stringify(address.publicKeys)); |
|||
}; |
|||
|
|||
Verifier.checkCopayers = function(credentials, copayers) { |
|||
$.checkState(credentials.walletPrivKey); |
|||
var walletPubKey = Bitcore.PrivateKey.fromString(credentials.walletPrivKey).toPublicKey().toString(); |
|||
|
|||
if (copayers.length != credentials.n) { |
|||
log.error('Missing public keys in server response'); |
|||
return false; |
|||
} |
|||
|
|||
// Repeated xpub kes?
|
|||
var uniq = []; |
|||
var error; |
|||
_.each(copayers, function(copayer) { |
|||
if (uniq[copayers.xPubKey]++) { |
|||
log.error('Repeated public keys in server response'); |
|||
error = true; |
|||
} |
|||
|
|||
// Not signed pub keys
|
|||
if (!copayer.xPubKey || !copayer.xPubKeySignature || |
|||
!WalletUtils.verifyMessage(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) { |
|||
log.error('Invalid signatures in server response'); |
|||
error = true; |
|||
} |
|||
}); |
|||
if (error) |
|||
return false; |
|||
|
|||
if (!_.contains(_.pluck(copayers, 'xPubKey'), credentials.xPubKey)) { |
|||
log.error('Server response does not contains our public keys') |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
|
|||
Verifier.checkTxProposal = function(credentials, txp) { |
|||
$.checkArgument(txp.creatorId); |
|||
$.checkState(credentials.isComplete()); |
|||
|
|||
var creatorXPubKey = _.find(credentials.publicKeyRing, function(xPubKey) { |
|||
if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true; |
|||
}); |
|||
|
|||
if (!creatorXPubKey) return false; |
|||
|
|||
var creatorSigningPubKey = (new Bitcore.HDPublicKey(creatorXPubKey)).derive('m/1/1').publicKey.toString(); |
|||
|
|||
var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.encryptedMessage || txp.message); |
|||
log.debug('Regenerating & verifying tx proposal hash -> Hash: ', hash, ' Signature: ', txp.proposalSignature); |
|||
|
|||
|
|||
if (!WalletUtils.verifyMessage(hash, txp.proposalSignature, creatorSigningPubKey)) |
|||
return false; |
|||
|
|||
return Verifier.checkAddress(credentials, txp.changeAddress); |
|||
}; |
|||
|
|||
module.exports = Verifier; |
File diff suppressed because it is too large
@ -1,96 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
var _ = require('lodash'); |
|||
var Uuid = require('uuid'); |
|||
var chai = require('chai'); |
|||
var sinon = require('sinon'); |
|||
var should = chai.should(); |
|||
var Bitcore = require('bitcore'); |
|||
var WalletUtils = require('../lib/walletutils'); |
|||
|
|||
var aText = 'hola'; |
|||
var aPubKey = '03bec86ad4a8a91fe7c11ec06af27246ec55094db3d86098b7d8b2f12afe47627f'; |
|||
var aPrivKey = '09458c090a69a38368975fb68115df2f4b0ab7d1bc463fc60c67aa1730641d6c'; |
|||
var aSignature = '3045022100d6186930e4cd9984e3168e15535e2297988555838ad10126d6c20d4ac0e74eb502201095a6319ea0a0de1f1e5fb50f7bf10b8069de10e0083e23dbbf8de9b8e02785'; |
|||
|
|||
var otherPubKey = '02555a2d45e309c00cc8c5090b6ec533c6880ab2d3bc970b3943def989b3373f16'; |
|||
|
|||
describe('WalletUtils', function() { |
|||
|
|||
describe('#hashMessage', function() { |
|||
it('should create a hash', function() { |
|||
var res = WalletUtils.hashMessage(aText); |
|||
res.toString('hex').should.equal('4102b8a140ec642feaa1c645345f714bc7132d4fd2f7f6202db8db305a96172f'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#signMessage', function() { |
|||
it('should sign a message', function() { |
|||
var sig = WalletUtils.signMessage(aText, aPrivKey); |
|||
should.exist(sig); |
|||
sig.should.equal(aSignature); |
|||
}); |
|||
it('should fail to sign with wrong args', function() { |
|||
(function() { |
|||
WalletUtils.signMessage(aText, aPubKey); |
|||
}).should.throw('Number'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#verifyMessage', function() { |
|||
it('should fail to verify a malformed signature', function() { |
|||
var res = WalletUtils.verifyMessage(aText, 'badsignature', otherPubKey); |
|||
should.exist(res); |
|||
res.should.equal(false); |
|||
}); |
|||
it('should fail to verify a null signature', function() { |
|||
var res = WalletUtils.verifyMessage(aText, null, otherPubKey); |
|||
should.exist(res); |
|||
res.should.equal(false); |
|||
}); |
|||
it('should fail to verify with wrong pubkey', function() { |
|||
var res = WalletUtils.verifyMessage(aText, aSignature, otherPubKey); |
|||
should.exist(res); |
|||
res.should.equal(false); |
|||
}); |
|||
it('should verify', function() { |
|||
var res = WalletUtils.verifyMessage(aText, aSignature, aPubKey); |
|||
should.exist(res); |
|||
res.should.equal(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('#signMessage #verifyMessage round trip', function() { |
|||
it('should sign and verify', function() { |
|||
var aLongerText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; |
|||
var sig = WalletUtils.signMessage(aLongerText, aPrivKey); |
|||
WalletUtils.verifyMessage(aLongerText, sig, aPubKey).should.equal(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('#encryptMessage #decryptMessage round trip', function() { |
|||
it('should encrypt and decrypt', function() { |
|||
var pwd = "ezDRS2NRchMJLf1IWtjL5A=="; |
|||
var ct = WalletUtils.encryptMessage('hello world', pwd); |
|||
var msg = WalletUtils.decryptMessage(ct, pwd); |
|||
msg.should.equal('hello world'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toSecret #fromSecret round trip', function() { |
|||
it('should create secret and parse secret', function() { |
|||
var i = 0; |
|||
while (i++ < 100) { |
|||
var walletId = Uuid.v4(); |
|||
var walletPrivKey = new Bitcore.PrivateKey(); |
|||
var network = 'testnet'; |
|||
var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); |
|||
var result = WalletUtils.fromSecret(secret); |
|||
result.walletId.should.equal(walletId); |
|||
result.walletPrivKey.toString().should.equal(walletPrivKey.toString()); |
|||
result.network.should.equal(network); |
|||
}; |
|||
}); |
|||
}); |
|||
|
|||
}); |
Loading…
Reference in new issue