Ivan Socolsky
10 years ago
18 changed files with 820 additions and 449 deletions
@ -0,0 +1,25 @@ |
|||||
|
|
||||
|
var _ = require('lodash'); |
||||
|
|
||||
|
var Bitcore = require('bitcore'); |
||||
|
var BitcoreAddress = Bitcore.Address; |
||||
|
|
||||
|
function BitcoinUtils () {}; |
||||
|
|
||||
|
BitcoinUtils.deriveAddress = function(publicKeyRing, path, m, network) { |
||||
|
|
||||
|
var publicKeys = _.map(publicKeyRing, function(xPubKey) { |
||||
|
var xpub = new Bitcore.HDPublicKey(xPubKey); |
||||
|
return xpub.derive(path).publicKey; |
||||
|
}); |
||||
|
|
||||
|
var bitcoreAddress = BitcoreAddress.createMultisig(publicKeys, m, network); |
||||
|
|
||||
|
return { |
||||
|
address: bitcoreAddress.toString(), |
||||
|
path: path, |
||||
|
publicKeys: _.invoke(publicKeys, 'toString'), |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
module.exports = BitcoinUtils; |
@ -1,382 +0,0 @@ |
|||||
'use strict'; |
|
||||
|
|
||||
var _ = require('lodash'); |
|
||||
var util = require('util'); |
|
||||
var async = require('async'); |
|
||||
var log = require('npmlog'); |
|
||||
var request = require('request') |
|
||||
log.debug = log.verbose; |
|
||||
|
|
||||
var Bitcore = require('bitcore') |
|
||||
var SignUtils = require('../signutils'); |
|
||||
|
|
||||
var BASE_URL = 'http://localhost:3001/copay/api'; |
|
||||
|
|
||||
function _createProposalOpts(opts, signingKey) { |
|
||||
var msg = opts.toAddress + '|' + opts.amount + '|' + opts.message; |
|
||||
opts.proposalSignature = SignUtils.sign(msg, signingKey); |
|
||||
return opts; |
|
||||
}; |
|
||||
|
|
||||
function _getUrl(path) { |
|
||||
return BASE_URL + path; |
|
||||
}; |
|
||||
|
|
||||
function _parseError(body) { |
|
||||
if (_.isString(body)) { |
|
||||
try { |
|
||||
body = JSON.parse(body); |
|
||||
} catch (e) { |
|
||||
body = { |
|
||||
error: body |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
var code = body.code || 'ERROR'; |
|
||||
var message = body.error || 'There was an unknown error processing the request'; |
|
||||
log.error(code, message); |
|
||||
}; |
|
||||
|
|
||||
function _signRequest(method, url, args, privKey) { |
|
||||
var message = method.toLowerCase() + '|' + url + '|' + JSON.stringify(args); |
|
||||
return SignUtils.sign(message, privKey); |
|
||||
}; |
|
||||
|
|
||||
function _createXPrivKey(network) { |
|
||||
return new Bitcore.HDPrivateKey(network).toString(); |
|
||||
}; |
|
||||
|
|
||||
function API(opts) { |
|
||||
if (!opts.storage) { |
|
||||
throw new Error('Must provide storage option'); |
|
||||
} |
|
||||
this.storage = opts.storage; |
|
||||
this.verbose = !!opts.verbose; |
|
||||
if (this.verbose) { |
|
||||
log.level = 'debug'; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
API.prototype._loadAndCheck = function() { |
|
||||
var data = this.storage.load(); |
|
||||
if (!data) { |
|
||||
log.error('Wallet file not found.'); |
|
||||
process.exit(1); |
|
||||
} |
|
||||
|
|
||||
if (data.verified == 'corrupt') { |
|
||||
log.error('The wallet is tagged as corrupt. Some of the copayers cannot be verified to have known the wallet secret.'); |
|
||||
process.exit(1); |
|
||||
} |
|
||||
if (data.n > 1) { |
|
||||
var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; |
|
||||
if (!pkrComplete) { |
|
||||
log.warn('The file ' + this.filename + ' is incomplete. It will allow you to operate with the wallet but it should not be trusted as a backup. Please wait for all copayers to join the wallet and run the tool with -export flag.') |
|
||||
} |
|
||||
} |
|
||||
return data; |
|
||||
}; |
|
||||
|
|
||||
API.prototype._doRequest = function(method, url, args, data, cb) { |
|
||||
var reqSignature = _signRequest(method, url, args, data.signingPrivKey); |
|
||||
var absUrl = _getUrl(url); |
|
||||
var args = { |
|
||||
headers: { |
|
||||
'x-identity': data.copayerId, |
|
||||
'x-signature': reqSignature, |
|
||||
}, |
|
||||
method: method, |
|
||||
url: absUrl, |
|
||||
body: args, |
|
||||
json: true, |
|
||||
}; |
|
||||
log.verbose('Request Args', util.inspect(args)); |
|
||||
request(args, function(err, res, body) { |
|
||||
log.verbose('Response:', err, body); |
|
||||
|
|
||||
if (err) return cb(err); |
|
||||
if (res.statusCode != 200) { |
|
||||
_parseError(body); |
|
||||
return cb('Request error'); |
|
||||
} |
|
||||
return cb(null, body); |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
API.prototype._doPostRequest = function(url, args, data, cb) { |
|
||||
return this._doRequest('post', url, args, data, cb); |
|
||||
}; |
|
||||
|
|
||||
API.prototype._doGetRequest = function(url, data, cb) { |
|
||||
return this._doRequest('get', url, {}, data, cb); |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
|
|
||||
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'); |
|
||||
|
|
||||
var data = this.storage.load(); |
|
||||
if (data) return cb('File ' + this.filename + ' already contains a wallet'); |
|
||||
|
|
||||
// Generate wallet key pair to verify copayers
|
|
||||
var privKey = new Bitcore.PrivateKey(null, network); |
|
||||
var pubKey = privKey.toPublicKey(); |
|
||||
|
|
||||
data = { |
|
||||
m: m, |
|
||||
n: n, |
|
||||
walletPrivKey: privKey.toString(), |
|
||||
}; |
|
||||
|
|
||||
var args = { |
|
||||
name: walletName, |
|
||||
m: m, |
|
||||
n: n, |
|
||||
pubKey: pubKey.toString(), |
|
||||
network: network, |
|
||||
}; |
|
||||
var url = '/v1/wallets/'; |
|
||||
|
|
||||
this._doPostRequest(url, args, data, function(err, body) { |
|
||||
if (err) return cb(err); |
|
||||
|
|
||||
var walletId = body.walletId; |
|
||||
var secret = walletId + ':' + privKey.toString() + ':' + (network == 'testnet' ? 'T' : 'L'); |
|
||||
var ret; |
|
||||
|
|
||||
if (n > 1) |
|
||||
ret = data.secret = secret; |
|
||||
|
|
||||
self.storage.save(data); |
|
||||
self._joinWallet(data, secret, copayerName, function(err) { |
|
||||
if (err) return cb(err); |
|
||||
|
|
||||
return cb(null, ret); |
|
||||
}); |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
API.prototype._joinWallet = function(data, secret, copayerName, cb) { |
|
||||
var self = this; |
|
||||
data = data || {}; |
|
||||
|
|
||||
var secretSplit = secret.split(':'); |
|
||||
var walletId = secretSplit[0]; |
|
||||
|
|
||||
var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]); |
|
||||
var network = secretSplit[2] == 'T' ? 'testnet' : 'livenet'; |
|
||||
data.xPrivKey = _createXPrivKey(network); |
|
||||
|
|
||||
var xPubKey = new Bitcore.HDPublicKey(data.xPrivKey); |
|
||||
var xPubKeySignature = SignUtils.sign(xPubKey.toString(), walletPrivKey); |
|
||||
|
|
||||
var signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey; |
|
||||
var args = { |
|
||||
walletId: walletId, |
|
||||
name: copayerName, |
|
||||
xPubKey: xPubKey.toString(), |
|
||||
xPubKeySignature: xPubKeySignature, |
|
||||
}; |
|
||||
var url = '/v1/wallets/' + walletId + '/copayers'; |
|
||||
|
|
||||
this._doPostRequest(url, args, data, function(err, body) { |
|
||||
var wallet = body.wallet; |
|
||||
data.copayerId = body.copayerId; |
|
||||
data.walletPrivKey = walletPrivKey; |
|
||||
data.signingPrivKey = signingPrivKey.toString(); |
|
||||
data.m = wallet.m; |
|
||||
data.n = wallet.n; |
|
||||
data.publicKeyRing = wallet.publicKeyRing; |
|
||||
self.storage.save(data); |
|
||||
|
|
||||
return cb(); |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
API.prototype.joinWallet = function(secret, copayerName, cb) { |
|
||||
var self = this; |
|
||||
|
|
||||
var data = this.storage.load(); |
|
||||
if (data) return cb('File ' + this.filename + ' already contains a wallet'); |
|
||||
|
|
||||
self._joinWallet(data, secret, copayerName, cb); |
|
||||
}; |
|
||||
|
|
||||
API.prototype.getStatus = function(cb) { |
|
||||
var self = this; |
|
||||
|
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/wallets/'; |
|
||||
this._doGetRequest(url, data, function(err, body) { |
|
||||
if (err) return cb(err); |
|
||||
|
|
||||
var wallet = body; |
|
||||
if (wallet.n > 0 && wallet.status === 'complete' && !data.verified) { |
|
||||
var pubKey = Bitcore.PrivateKey.fromString(data.walletPrivKey).toPublicKey().toString(); |
|
||||
var fake = []; |
|
||||
_.each(wallet.copayers, function(copayer) { |
|
||||
|
|
||||
|
|
||||
console.log('[clilib.js.224]', copayer.xPubKey, copayer.xPubKeySignature, pubKey); //TODO
|
|
||||
if (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, pubKey)) { |
|
||||
|
|
||||
console.log('[clilib.js.227] FAKE'); //TODO
|
|
||||
fake.push(copayer); |
|
||||
} |
|
||||
}); |
|
||||
if (fake.length > 0) { |
|
||||
log.error('Some copayers in the wallet could not be verified to have known the wallet secret'); |
|
||||
data.verified = 'corrupt'; |
|
||||
} else { |
|
||||
data.verified = 'ok'; |
|
||||
} |
|
||||
self.storage.save(data); |
|
||||
} |
|
||||
|
|
||||
return cb(null, wallet); |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
/** |
|
||||
* send |
|
||||
* |
|
||||
* @param inArgs |
|
||||
* @param inArgs.toAddress |
|
||||
* @param inArgs.amount |
|
||||
* @param inArgs.message |
|
||||
*/ |
|
||||
API.prototype.sendTxProposal = function(inArgs, cb) { |
|
||||
var self = this; |
|
||||
|
|
||||
var data = this._loadAndCheck(); |
|
||||
var args = _createProposalOpts(inArgs, data.signingPrivKey); |
|
||||
|
|
||||
var url = '/v1/txproposals/'; |
|
||||
this._doPostRequest(url, args, data, cb); |
|
||||
}; |
|
||||
|
|
||||
// Get addresses
|
|
||||
API.prototype.getAddresses = function(cb) { |
|
||||
var self = this; |
|
||||
|
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/addresses/'; |
|
||||
this._doGetRequest(url, data, cb); |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
// Creates a new address
|
|
||||
// TODO: verify derivation!!
|
|
||||
API.prototype.createAddress = function(cb) { |
|
||||
var self = this; |
|
||||
|
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/addresses/'; |
|
||||
this._doPostRequest(url, {}, data, cb); |
|
||||
}; |
|
||||
|
|
||||
API.prototype.history = function(limit, cb) { |
|
||||
|
|
||||
}; |
|
||||
|
|
||||
API.prototype.getBalance = function(cb) { |
|
||||
var self = this; |
|
||||
|
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/balance/'; |
|
||||
this._doGetRequest(url, data, cb); |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
API.prototype.getTxProposals = function(opts, cb) { |
|
||||
var self = this; |
|
||||
|
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/txproposals/'; |
|
||||
this._doGetRequest(url, data, cb); |
|
||||
}; |
|
||||
|
|
||||
API.prototype.signTxProposal = function(txp, cb) { |
|
||||
var self = this; |
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
|
|
||||
//Derive proper key to sign, for each input
|
|
||||
var privs = [], |
|
||||
derived = {}; |
|
||||
|
|
||||
var network = new Bitcore.Address(txp.toAddress).network.name; |
|
||||
var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network); |
|
||||
|
|
||||
_.each(txp.inputs, function(i) { |
|
||||
if (!derived[i.path]) { |
|
||||
derived[i.path] = xpriv.derive(i.path).privateKey; |
|
||||
} |
|
||||
privs.push(derived[i.path]); |
|
||||
}); |
|
||||
|
|
||||
var t = new Bitcore.Transaction(); |
|
||||
_.each(txp.inputs, function(i) { |
|
||||
t.from(i, i.publicKeys, txp.requiredSignatures); |
|
||||
}); |
|
||||
|
|
||||
t.to(txp.toAddress, txp.amount) |
|
||||
.change(txp.changeAddress) |
|
||||
.sign(privs); |
|
||||
|
|
||||
var signatures = []; |
|
||||
_.each(privs, function(p) { |
|
||||
var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); |
|
||||
signatures.push(s); |
|
||||
}); |
|
||||
|
|
||||
var url = '/v1/txproposals/' + txp.id + '/signatures/'; |
|
||||
var args = { |
|
||||
signatures: signatures |
|
||||
}; |
|
||||
|
|
||||
this._doPostRequest(url, args, data, cb); |
|
||||
}; |
|
||||
|
|
||||
API.prototype.rejectTxProposal = function(txp, reason, cb) { |
|
||||
var self = this; |
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/txproposals/' + txp.id + '/rejections/'; |
|
||||
var args = { |
|
||||
reason: reason || '', |
|
||||
}; |
|
||||
this._doPostRequest(url, args, data, cb); |
|
||||
}; |
|
||||
|
|
||||
API.prototype.broadcastTxProposal = function(txp, cb) { |
|
||||
var self = this; |
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/txproposals/' + txp.id + '/broadcast/'; |
|
||||
this._doPostRequest(url, {}, data, cb); |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
|
|
||||
API.prototype.removeTxProposal = function(txp, cb) { |
|
||||
var self = this; |
|
||||
var data = this._loadAndCheck(); |
|
||||
|
|
||||
var url = '/v1/txproposals/' + txp.id; |
|
||||
|
|
||||
this._doRequest('delete', url, {}, data, cb); |
|
||||
}; |
|
||||
|
|
||||
module.exports = API; |
|
@ -1,24 +0,0 @@ |
|||||
|
|
||||
var fs = require('fs') |
|
||||
|
|
||||
function FileStorage(opts) { |
|
||||
if (!opts.filename) { |
|
||||
throw new Error('Please set the config filename'); |
|
||||
} |
|
||||
this.filename = opts.filename; |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
FileStorage.prototype.save = function(data) { |
|
||||
fs.writeFileSync(this.filename, JSON.stringify(data)); |
|
||||
}; |
|
||||
|
|
||||
FileStorage.prototype.load = function() { |
|
||||
try { |
|
||||
return JSON.parse(fs.readFileSync(this.filename)); |
|
||||
} catch (ex) {} |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
module.exports = FileStorage; |
|
||||
|
|
@ -0,0 +1,59 @@ |
|||||
|
var $ = require('preconditions').singleton(); |
||||
|
var _ = require('lodash'); |
||||
|
var log = require('npmlog'); |
||||
|
|
||||
|
var Bitcore = require('bitcore'); |
||||
|
var BitcoinUtils = require('../bitcoinutils') |
||||
|
var SignUtils = require('../signutils'); |
||||
|
|
||||
|
/* |
||||
|
* Checks data given by the server |
||||
|
*/ |
||||
|
|
||||
|
function Verifier(opts) {}; |
||||
|
|
||||
|
Verifier.checkAddress = function(data, address) { |
||||
|
var local = BitcoinUtils.deriveAddress(data.publicKeyRing, address.path, data.m, data.network); |
||||
|
return (local.address == address.address && JSON.stringify(local.publicKeys) == JSON.stringify(address.publicKeys)); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//
|
||||
|
|
||||
|
Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) { |
||||
|
|
||||
|
var walletPubKey = Bitcore.PrivateKey.fromString(walletPrivKey).toPublicKey().toString(); |
||||
|
|
||||
|
if (copayers.length != 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 (!SignUtils.verify(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) { |
||||
|
log.error('Invalid signatures in server response'); |
||||
|
error = true; |
||||
|
} |
||||
|
}); |
||||
|
if (error) |
||||
|
return false; |
||||
|
|
||||
|
var myXPubKey = new Bitcore.HDPublicKey(myXPrivKey).toString(); |
||||
|
if (!_.contains(_.pluck(copayers, 'xPubKey'), myXPubKey)) { |
||||
|
log.error('Server response does not contains our public keys') |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
module.exports = Verifier; |
@ -0,0 +1,410 @@ |
|||||
|
'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') |
||||
|
log.debug = log.verbose; |
||||
|
|
||||
|
var Bitcore = require('bitcore') |
||||
|
var SignUtils = require('../signutils'); |
||||
|
var Verifier = require('./verifier'); |
||||
|
var ServerCompromisedError = require('./servercompromisederror') |
||||
|
|
||||
|
var BASE_URL = 'http://localhost:3001/copay/api'; |
||||
|
|
||||
|
function _createProposalOpts(opts, signingKey) { |
||||
|
var msg = opts.toAddress + '|' + opts.amount + '|' + opts.message; |
||||
|
opts.proposalSignature = SignUtils.sign(msg, signingKey); |
||||
|
return opts; |
||||
|
}; |
||||
|
|
||||
|
function _getUrl(path) { |
||||
|
return BASE_URL + path; |
||||
|
}; |
||||
|
|
||||
|
function _parseError(body) { |
||||
|
if (_.isString(body)) { |
||||
|
try { |
||||
|
body = JSON.parse(body); |
||||
|
} catch (e) { |
||||
|
body = { |
||||
|
error: body |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
var code = body.code || 'ERROR'; |
||||
|
var message = body.error || 'There was an unknown error processing the request'; |
||||
|
log.error(code, message); |
||||
|
}; |
||||
|
|
||||
|
function _signRequest(method, url, args, privKey) { |
||||
|
var message = method.toLowerCase() + '|' + url + '|' + JSON.stringify(args); |
||||
|
return SignUtils.sign(message, privKey); |
||||
|
}; |
||||
|
|
||||
|
function _createXPrivKey(network) { |
||||
|
return new Bitcore.HDPrivateKey(network).toString(); |
||||
|
}; |
||||
|
|
||||
|
function API(opts) { |
||||
|
if (!opts.storage) { |
||||
|
throw new Error('Must provide storage option'); |
||||
|
} |
||||
|
this.storage = opts.storage; |
||||
|
this.verbose = !!opts.verbose; |
||||
|
this.request = request || opts.request; |
||||
|
if (this.verbose) { |
||||
|
log.level = 'debug'; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
|
||||
|
API.prototype._tryToComplete = function(data, cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
var url = '/v1/wallets/'; |
||||
|
self._doGetRequest(url, data, function(err, ret) { |
||||
|
if (err) return cb(err); |
||||
|
var wallet = ret.wallet; |
||||
|
|
||||
|
if (wallet.status != 'complete') |
||||
|
return cb('Wallet Incomplete'); |
||||
|
|
||||
|
if (!Verifier.checkCopayers(wallet.copayers, data.walletPrivKey, data.xPrivKey, data.n)) |
||||
|
return cb('Some copayers in the wallet could not be verified to have known the wallet secret'); |
||||
|
|
||||
|
data.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') |
||||
|
|
||||
|
self.storage.save(data, function(err) { |
||||
|
return cb(err, data); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
API.prototype._loadAndCheck = function(cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this.storage.load(function(err, data) { |
||||
|
if (err || !data) { |
||||
|
return cb(err || 'Wallet file not found.'); |
||||
|
} |
||||
|
if (data.n > 1) { |
||||
|
var pkrComplete = data.publicKeyRing && data.m && data.publicKeyRing.length === data.n; |
||||
|
|
||||
|
if (!pkrComplete) { |
||||
|
return self._tryToComplete(data, cb); |
||||
|
} |
||||
|
} |
||||
|
return cb(null, data); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype._doRequest = function(method, url, args, data, cb) { |
||||
|
var reqSignature = _signRequest(method, url, args, data.signingPrivKey); |
||||
|
var absUrl = _getUrl(url); |
||||
|
var args = { |
||||
|
headers: { |
||||
|
'x-identity': data.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) { |
||||
|
_parseError(body); |
||||
|
return cb('Request error'); |
||||
|
} |
||||
|
|
||||
|
return cb(null, body); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
API.prototype._doPostRequest = function(url, args, data, cb) { |
||||
|
return this._doRequest('post', url, args, data, cb); |
||||
|
}; |
||||
|
|
||||
|
API.prototype._doGetRequest = function(url, data, cb) { |
||||
|
return this._doRequest('get', url, {}, data, cb); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
|
||||
|
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'); |
||||
|
|
||||
|
this.storage.load(function(err, data) { |
||||
|
if (data) |
||||
|
return cb('Storage already contains a wallet'); |
||||
|
|
||||
|
console.log('[API.js.132]'); //TODO
|
||||
|
// Generate wallet key pair to verify copayers
|
||||
|
var privKey = new Bitcore.PrivateKey(null, network); |
||||
|
var pubKey = privKey.toPublicKey(); |
||||
|
|
||||
|
data = { |
||||
|
m: m, |
||||
|
n: n, |
||||
|
walletPrivKey: privKey.toWIF(), |
||||
|
network: network, |
||||
|
}; |
||||
|
|
||||
|
var args = { |
||||
|
name: walletName, |
||||
|
m: m, |
||||
|
n: n, |
||||
|
pubKey: pubKey.toString(), |
||||
|
network: network, |
||||
|
}; |
||||
|
var url = '/v1/wallets/'; |
||||
|
|
||||
|
self._doPostRequest(url, args, data, function(err, body) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
var walletId = body.walletId; |
||||
|
var secret = walletId + ':' + privKey.toString() + ':' + (network == 'testnet' ? 'T' : 'L'); |
||||
|
var ret; |
||||
|
|
||||
|
if (n > 1) |
||||
|
ret = data.secret = secret; |
||||
|
|
||||
|
self.storage.save(data, function(err) { |
||||
|
if (err) return cb(err); |
||||
|
self._joinWallet(data, secret, copayerName, function(err) { |
||||
|
return cb(err, ret); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype._joinWallet = function(data, secret, copayerName, cb) { |
||||
|
var self = this; |
||||
|
data = data || {}; |
||||
|
|
||||
|
var secretSplit = secret.split(':'); |
||||
|
var walletId = secretSplit[0]; |
||||
|
|
||||
|
var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]); |
||||
|
var network = secretSplit[2] == 'T' ? 'testnet' : 'livenet'; |
||||
|
data.xPrivKey = _createXPrivKey(network); |
||||
|
|
||||
|
var xPubKey = new Bitcore.HDPublicKey(data.xPrivKey); |
||||
|
var xPubKeySignature = SignUtils.sign(xPubKey.toString(), walletPrivKey); |
||||
|
|
||||
|
var signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey; |
||||
|
var args = { |
||||
|
walletId: walletId, |
||||
|
name: copayerName, |
||||
|
xPubKey: xPubKey.toString(), |
||||
|
xPubKeySignature: xPubKeySignature, |
||||
|
}; |
||||
|
var url = '/v1/wallets/' + walletId + '/copayers'; |
||||
|
|
||||
|
this._doPostRequest(url, args, data, function(err, body) { |
||||
|
var wallet = body.wallet; |
||||
|
data.copayerId = body.copayerId; |
||||
|
data.walletPrivKey = walletPrivKey.toWIF(); |
||||
|
data.signingPrivKey = signingPrivKey.toString(); |
||||
|
data.m = wallet.m; |
||||
|
data.n = wallet.n; |
||||
|
data.publicKeyRing = wallet.publicKeyRing; |
||||
|
data.network = wallet.network, |
||||
|
self.storage.save(data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype.joinWallet = function(secret, copayerName, cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this.storage.load(function(err, data) { |
||||
|
if (data) |
||||
|
return cb('Storage already contains a wallet'); |
||||
|
|
||||
|
self._joinWallet(data, secret, copayerName, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype.getStatus = function(cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck(function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
var url = '/v1/wallets/'; |
||||
|
self._doGetRequest(url, data, function(err, body) { |
||||
|
return cb(err, body); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* send |
||||
|
* |
||||
|
* @param inArgs |
||||
|
* @param inArgs.toAddress |
||||
|
* @param inArgs.amount |
||||
|
* @param inArgs.message |
||||
|
*/ |
||||
|
API.prototype.sendTxProposal = function(inArgs, cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck(function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
var args = _createProposalOpts(inArgs, data.signingPrivKey); |
||||
|
|
||||
|
var url = '/v1/txproposals/'; |
||||
|
self._doPostRequest(url, args, data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype.createAddress = function(cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck(function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
var url = '/v1/addresses/'; |
||||
|
self._doPostRequest(url, {}, data, function(err, address) { |
||||
|
if (err) return cb(err); |
||||
|
if (!Verifier.checkAddress(data, address)) { |
||||
|
return cb(new ServerCompromisedError('Server sent fake address')); |
||||
|
} |
||||
|
|
||||
|
return cb(null, address); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype.history = function(limit, cb) { |
||||
|
|
||||
|
}; |
||||
|
|
||||
|
API.prototype.getBalance = function(cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck(function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
var url = '/v1/balance/'; |
||||
|
self._doGetRequest(url, data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
API.prototype.getTxProposals = function(opts, cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck( |
||||
|
function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
var url = '/v1/txproposals/'; |
||||
|
self._doGetRequest(url, data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype.signTxProposal = function(txp, cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck( |
||||
|
function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
|
||||
|
//Derive proper key to sign, for each input
|
||||
|
var privs = [], |
||||
|
derived = {}; |
||||
|
|
||||
|
var network = new Bitcore.Address(txp.toAddress).network.name; |
||||
|
var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network); |
||||
|
|
||||
|
_.each(txp.inputs, function(i) { |
||||
|
if (!derived[i.path]) { |
||||
|
derived[i.path] = xpriv.derive(i.path).privateKey; |
||||
|
} |
||||
|
privs.push(derived[i.path]); |
||||
|
}); |
||||
|
|
||||
|
var t = new Bitcore.Transaction(); |
||||
|
_.each(txp.inputs, function(i) { |
||||
|
t.from(i, i.publicKeys, txp.requiredSignatures); |
||||
|
}); |
||||
|
|
||||
|
t.to(txp.toAddress, txp.amount) |
||||
|
.change(txp.changeAddress) |
||||
|
.sign(privs); |
||||
|
|
||||
|
var signatures = []; |
||||
|
_.each(privs, function(p) { |
||||
|
var s = t.getSignatures(p)[0].signature.toDER().toString('hex'); |
||||
|
signatures.push(s); |
||||
|
}); |
||||
|
|
||||
|
var url = '/v1/txproposals/' + txp.id + '/signatures/'; |
||||
|
var args = { |
||||
|
signatures: signatures |
||||
|
}; |
||||
|
|
||||
|
self._doPostRequest(url, args, data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype.rejectTxProposal = function(txp, reason, cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck( |
||||
|
function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
var url = '/v1/txproposals/' + txp.id + '/rejections/'; |
||||
|
var args = { |
||||
|
reason: reason || '', |
||||
|
}; |
||||
|
self._doPostRequest(url, args, data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
API.prototype.broadcastTxProposal = function(txp, cb) { |
||||
|
var self = this; |
||||
|
|
||||
|
this._loadAndCheck( |
||||
|
function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
var url = '/v1/txproposals/' + txp.id + '/broadcast/'; |
||||
|
self._doPostRequest(url, {}, data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
|
||||
|
API.prototype.removeTxProposal = function(txp, cb) { |
||||
|
var self = this; |
||||
|
this._loadAndCheck( |
||||
|
function(err, data) { |
||||
|
if (err) return cb(err); |
||||
|
var url = '/v1/txproposals/' + txp.id; |
||||
|
self._doRequest('delete', url, {}, data, cb); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
module.exports = API; |
@ -0,0 +1,30 @@ |
|||||
|
|
||||
|
var fs = require('fs') |
||||
|
|
||||
|
function FileStorage(opts) { |
||||
|
if (!opts.filename) { |
||||
|
throw new Error('Please set the config filename'); |
||||
|
} |
||||
|
this.filename = opts.filename; |
||||
|
this.fs = opts.fs || fs; |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
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,8 +1,5 @@ |
|||||
//var client = ;
|
//var client = ;
|
||||
|
|
||||
var client = module.exports = require('./API'); |
var client = module.exports = require('./api'); |
||||
client.FileStorage = require('./FileStorage'); |
client.FileStorage = require('./filestorage'); |
||||
|
client.Verifier = require('./verifier'); |
||||
|
|
||||
// TODO
|
|
||||
//module.exports.storage = require('./storage');
|
|
||||
|
@ -0,0 +1,6 @@ |
|||||
|
function ServerCompromisedError(message) { |
||||
|
this.code = 'SERVERCOMPROMISED'; |
||||
|
this.message = message; |
||||
|
}; |
||||
|
|
||||
|
module.exports = ServerCompromisedError; |
@ -0,0 +1,153 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
var _ = require('lodash'); |
||||
|
var chai = require('chai'); |
||||
|
var sinon = require('sinon'); |
||||
|
var should = chai.should(); |
||||
|
var Client = require('../../lib/client'); |
||||
|
var API = Client.API; |
||||
|
var Bitcore = require('bitcore'); |
||||
|
var TestData = require('./clienttestdata'); |
||||
|
|
||||
|
describe(' client API ', function() { |
||||
|
|
||||
|
var client; |
||||
|
|
||||
|
beforeEach(function() { |
||||
|
|
||||
|
var fsmock = {};; |
||||
|
fsmock.readFile = sinon.mock().yields(null, JSON.stringify(TestData.storage.wallet11)); |
||||
|
fsmock.writeFile = sinon.mock().yields(); |
||||
|
var storage = new Client.FileStorage({ |
||||
|
filename: 'dummy', |
||||
|
fs: fsmock, |
||||
|
}); |
||||
|
client = new Client({ |
||||
|
storage: storage |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe(' _tryToComplete ', function() { |
||||
|
it('should complete a wallet ', function(done) { |
||||
|
var request = sinon.stub(); |
||||
|
|
||||
|
// Wallet request
|
||||
|
request.onCall(0).yields(null, { |
||||
|
statusCode: 200, |
||||
|
}, TestData.serverResponse.completeWallet); |
||||
|
request.onCall(1).yields(null, { |
||||
|
statusCode: 200, |
||||
|
}, "pepe"); |
||||
|
|
||||
|
client.request = request; |
||||
|
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); |
||||
|
client.getBalance(function(err, x) { |
||||
|
should.not.exist(err); |
||||
|
done(); |
||||
|
}); |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
it('should handle incomple wallets', function(done) { |
||||
|
var request = sinon.stub(); |
||||
|
|
||||
|
// Wallet request
|
||||
|
request.onCall(0).yields(null, { |
||||
|
statusCode: 200, |
||||
|
}, TestData.serverResponse.incompleteWallet); |
||||
|
|
||||
|
client.request = request; |
||||
|
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); |
||||
|
client.createAddress(function(err, x) { |
||||
|
err.should.contain('Incomplete'); |
||||
|
done(); |
||||
|
}); |
||||
|
}) |
||||
|
|
||||
|
it('should reject wallets with bad signatures', function(done) { |
||||
|
var request = sinon.stub(); |
||||
|
// Wallet request
|
||||
|
request.onCall(0).yields(null, { |
||||
|
statusCode: 200, |
||||
|
}, TestData.serverResponse.corruptWallet22); |
||||
|
|
||||
|
client.request = request; |
||||
|
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); |
||||
|
client.createAddress(function(err, x) { |
||||
|
err.should.contain('verified'); |
||||
|
done(); |
||||
|
}); |
||||
|
}) |
||||
|
it('should reject wallets with missing signatures ', function(done) { |
||||
|
var request = sinon.stub(); |
||||
|
// Wallet request
|
||||
|
request.onCall(0).yields(null, { |
||||
|
statusCode: 200, |
||||
|
}, TestData.serverResponse.corruptWallet222); |
||||
|
|
||||
|
client.request = request; |
||||
|
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); |
||||
|
client.createAddress(function(err, x) { |
||||
|
err.should.contain('verified'); |
||||
|
done(); |
||||
|
}); |
||||
|
}) |
||||
|
|
||||
|
it('should reject wallets missing caller"s pubkey', function(done) { |
||||
|
var request = sinon.stub(); |
||||
|
// Wallet request
|
||||
|
request.onCall(0).yields(null, { |
||||
|
statusCode: 200, |
||||
|
}, TestData.serverResponse.missingMyPubKey); |
||||
|
|
||||
|
client.request = request; |
||||
|
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22)); |
||||
|
client.createAddress(function(err, x) { |
||||
|
err.should.contain('verified'); |
||||
|
done(); |
||||
|
}); |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
describe(' createAddress ', function() { |
||||
|
it(' should check address ', function(done) { |
||||
|
|
||||
|
var response = { |
||||
|
createdOn: 1424105995, |
||||
|
address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', |
||||
|
path: 'm/2147483647/0/7', |
||||
|
publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] |
||||
|
}; |
||||
|
var request = sinon.mock().yields(null, { |
||||
|
statusCode: 200 |
||||
|
}, response); |
||||
|
client.request = request; |
||||
|
|
||||
|
|
||||
|
client.createAddress(function(err, x) { |
||||
|
should.not.exist(err); |
||||
|
x.address.should.equal('2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq'); |
||||
|
done(); |
||||
|
}); |
||||
|
}) |
||||
|
it(' should detect fake addresses ', function(done) { |
||||
|
var response = { |
||||
|
createdOn: 1424105995, |
||||
|
address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', |
||||
|
path: 'm/2147483647/0/8', |
||||
|
publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] |
||||
|
}; |
||||
|
var request = sinon.mock().yields(null, { |
||||
|
statusCode: 200 |
||||
|
}, response); |
||||
|
client.request = request; |
||||
|
client.createAddress(function(err, x) { |
||||
|
err.code.should.equal('SERVERCOMPROMISED'); |
||||
|
err.message.should.contain('fake address'); |
||||
|
done(); |
||||
|
}); |
||||
|
}) |
||||
|
}) |
||||
|
}); |
@ -0,0 +1,116 @@ |
|||||
|
var storage = { |
||||
|
wallet11: { |
||||
|
"m": 1, |
||||
|
"n": 1, |
||||
|
"walletPrivKey": "{\"bn\":\"6b862ffbfc90a37a2fedbbcfea91c6a4e49f49b6aaa322b6e16c46bfdbe71a38\",\"compressed\":true,\"network\":\"livenet\"}", |
||||
|
"network": "testnet", |
||||
|
"xPrivKey": "tprv8ZgxMBicQKsPeisyNJteQXZnb7CnhYc4TVAyxxicXuxMjK1rmaqVq1xnXtbSTPxUKKL9h5xJhUvw1AKfDD3i98A82eJWSYRWYjmPksewFKR", |
||||
|
"copayerId": "a84daa08-17b5-45ad-84cd-e275f3b07123", |
||||
|
"signingPrivKey": "42798f82c4ed9ace4d66335165071edf180e70bc0fc08dacb3e35185a2141d5b", |
||||
|
"publicKeyRing": ["tpubD6NzVbkrYhZ4YBumFxZEowDuA8iirsny2nmmFUkuxBkkZoGdPyf61Waei3tDYvVa1yqW82Xhmmd6oiibeDyM1MS3zTiky7Yg75UEV9oQhFJ"] |
||||
|
}, |
||||
|
|
||||
|
incompleteWallet22: { |
||||
|
"m": 2, |
||||
|
"n": 2, |
||||
|
"walletPrivKey":"L2Fu6TM1AqSNBaQcjgjvYjGf3EzS3MVSTwEeTw3bvy52x7ZkffWj", |
||||
|
"network": "testnet", |
||||
|
"secret": "b6f57154-0df8-4845-a61d-47ecd648c2d4:eab5a55d9214845ee8d13ea1033e42ec8d7f780ae6e521d830252a80433e91a5:T", |
||||
|
"xPrivKey": "tprv8ZgxMBicQKsPfFVXegcKyJjy2Y5DSrHNrtGBHG1f9pPX75QQdHwHGjWUtR7cCUXV7QcCCDon4cieHWTYscy8M7oXwF3qd3ssfBiV9M68bPB", |
||||
|
"copayerId": "3fc03e7a-6ebc-409b-a4b7-45b14d5a8199", |
||||
|
"signingPrivKey": "0d3c796fb12e387c4b5a5c566312b2b22fa0553ca041d859e3f0987215ca3a4f", |
||||
|
"publicKeyRing": [] |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var serverResponse = { |
||||
|
completeWallet: { |
||||
|
wallet: { |
||||
|
m: 2, |
||||
|
n: 2, |
||||
|
status: 'complete', |
||||
|
publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' |
||||
|
], |
||||
|
addressIndex: 0, |
||||
|
copayers: [{ |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', |
||||
|
}, { |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4YiXKYLGvNiQ5bZb9cBUHSBrxZn3xa6BuwZfBFgksTE8M4ZFBLWVJ4PLnAJs2JKhkpJVqsrJEAkGpb62rx62Bk4o4N5Lz8dQ', |
||||
|
xPubKeySignature: '3045022100e03b069db333428153c306c9bf66ebc7f25e7d7f3d087e1ca7234fbbb1a47efa02207421fb375d0dd7a7f2116301f2cdf1bce88554a6c88a82d4ec9fb37fb6680ae8', |
||||
|
}], |
||||
|
pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', |
||||
|
network: 'testnet', |
||||
|
}}, |
||||
|
|
||||
|
missingMyPubKey: { |
||||
|
wallet: { |
||||
|
m: 2, |
||||
|
n: 2, |
||||
|
status: 'complete', |
||||
|
publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' |
||||
|
], |
||||
|
addressIndex: 0, |
||||
|
copayers: [{ |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', |
||||
|
}, { |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV', |
||||
|
xPubKeySignature: '3044022025c93b418ebdbb66a0f2b21af709420e8ae769bf054f29aaa252cb5417c46a2302205e0c8b931324736b7eea4971a48039614e19abe26e13ab0ef1547aef92b55aab', |
||||
|
}], |
||||
|
pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', |
||||
|
network: 'testnet', |
||||
|
}}, |
||||
|
|
||||
|
|
||||
|
incompleteWallet: { |
||||
|
wallet: { |
||||
|
m: 2, |
||||
|
n: 2, |
||||
|
status: 'pending', |
||||
|
publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' |
||||
|
], |
||||
|
addressIndex: 0, |
||||
|
copayers: [{ |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', |
||||
|
}], |
||||
|
pubKey: ' { "x": "b2903ab878ed1316f82b859e9807e23bab3d579175563e1068d2ed9c9e37873c", "y": "5f30165915557394223a58329c1527dfa0f34f483d8aed02e0638f9124dbddef", "compressed": true }', |
||||
|
network: 'testnet', |
||||
|
}}, |
||||
|
|
||||
|
corruptWallet22: { |
||||
|
wallet: { |
||||
|
m: 2, |
||||
|
n: 2, |
||||
|
status: 'complete', |
||||
|
publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' |
||||
|
], |
||||
|
copayers: [{ |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
xPubKeySignature: '3045022100ef86122060bbb7681db05486f8b1ee1579c5800e8da78182a87384f05542a4cc0220215ce7ef8c484b64178779414efdf2b7033d25ed752eebf4eb3241f9fa8e6b67', |
||||
|
}, { |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
xPubKeySignature: 'bababa', |
||||
|
}], |
||||
|
}}, |
||||
|
corruptWallet222: { |
||||
|
wallet: { |
||||
|
m: 2, |
||||
|
n: 2, |
||||
|
status: 'complete', |
||||
|
publicKeyRing: ['tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
'tpubD6NzVbkrYhZ4WSuBBLyubi8DHMipbFQcZoLJHjb21gEtznCEJMJhwkvaSshHVLtq8C1uNMKD4GtADVYY6WZt1cyT218JUm3PiNKYVkMATWV' |
||||
|
], |
||||
|
copayers: [{ |
||||
|
xPubKey: 'tpubD6NzVbkrYhZ4Y1CGuCZ88eZvhDSTjAqjotZWGXC7e4GEoyXq3SQgZK9iRz4qC2h8MrzqrYBndCMQDiaaLdqpY8ihYmJC9Msvns83jGopb3E', |
||||
|
}, ], |
||||
|
}}, |
||||
|
}; |
||||
|
|
||||
|
module.exports.serverResponse = serverResponse; |
||||
|
module.exports.storage = storage; |
@ -0,0 +1 @@ |
|||||
|
--recursive |
Loading…
Reference in new issue