diff --git a/README.md b/README.md index a2eee2ae3..f58f03b9a 100644 --- a/README.md +++ b/README.md @@ -196,4 +196,17 @@ Copay uses the Javascript library Bitcore for Bitcoin related functions. Bitcore var cmd = `node util/build_bitcore.js` cd node $cmd + +Payment Protocol +---------------- + +Copay support BIP70 (Payment Protocol), with the following current limitations: + + * Only one output is allowed. Payment requests is more that one output are not supported. + * Only standard Pay-to-pubkeyhash and Pay-to-scripthash scripts are supported (on payment requests). Other script types will cause the entire payment request to be rejected. + * Memos from the custormer to the server are not supported (i.e. there is no place to write messages to the server in the current UX) + + + + ``` diff --git a/copay.js b/copay.js index bdb5c759b..79a70f8b0 100644 --- a/copay.js +++ b/copay.js @@ -20,6 +20,3 @@ module.exports.Compatibility = require('./js/models/Compatibility'); module.exports.PluginManager = require('./js/models/PluginManager'); module.exports.version = require('./version').version; module.exports.commitHash = require('./version').commitHash; - -// test hack :s, will fix -module.exports.FakePayProServer = require('./test/mocks/FakePayProServer'); diff --git a/js/controllers/history.js b/js/controllers/history.js index 770bae7c5..3f47fb95c 100644 --- a/js/controllers/history.js +++ b/js/controllers/history.js @@ -144,6 +144,8 @@ angular.module('copayApp.controllers').controller('HistoryController', _.each(items, function(r) { r.ts = r.minedTs || r.sentTs; }); + + $scope.blockchain_txs = w.cached_txs = items; $scope.nbPages = res.nbPages; $scope.totalItems = res.nbItems; diff --git a/js/controllers/send.js b/js/controllers/send.js index 88a67ef58..a46fec5d1 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -24,6 +24,7 @@ angular.module('copayApp.controllers').controller('SendController', $scope.isRateAvailable = false; $scope.rateService = rateService; + $scope.showScanner = false; rateService.whenAvailable(function() { @@ -100,6 +101,24 @@ angular.module('copayApp.controllers').controller('SendController', if (!window.cordova && !navigator.getUserMedia) $scope.disableScanner = 1; + $scope._showError = function(err) { + copay.logger.error(err); + + var msg = err.toString(); + if (msg.match('BIG')) + msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts' + + if (msg.match('totalNeededAmount')) + msg = 'Not enough funds' + + var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + + ' could not be created: ' + msg; + + $scope.error = message; + $scope.loading = false; + $scope.loadTxs(); + }; + $scope.submitForm = function(form) { if (form.$invalid) { $scope.error = 'Unable to send transaction proposal'; @@ -112,92 +131,37 @@ angular.module('copayApp.controllers').controller('SendController', var amount = parseInt((form.amount.$modelValue * w.settings.unitToSatoshi).toFixed(0)); var commentText = form.comment.$modelValue; - function done(err, ntxid, merchantData) { - if (err) { - copay.logger.error(err); - var msg = err.toString(); - - if (msg.match('BIG')) - msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts.' - - if (msg.match('totalNeededAmount')) - msg = 'Not enough funds.' - - var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + ' could not be created: ' + msg; - $scope.error = message; - $scope.loading = false; - $scope.loadTxs(); - return; - } - - // If user is granted the privilege of choosing - // their own amount, add it to the tx. - if (merchantData && +merchantData.total === 0) { - var txp = w.txProposals.get(ntxid); - var tx = txp.builder.tx = txp.builder.tx || txp.builder.build(); - tx.outs[0].v = bitcore.Bignum(amount + '', 10).toBuffer({ - // XXX This may not work in node due - // to the bignum only-big endian bug: - endian: 'little', - size: 1 - }); - } - - if (w.requiresMultipleSignatures()) { - $scope.loading = false; - notification.success('Success', 'The transaction proposal created'); - $scope.loadTxs(); - } else { - w.sendTx(ntxid, function(txid, merchantData) { - if (txid) { - var message = 'Transaction id: ' + txid; - if (merchantData) { - if (merchantData.pr.ca) { - message += ' This payment protocol transaction' + ' has been verified through ' + merchantData.pr.ca + '.'; - } - message += merchantData.pr.pd.memo; - message += ' Merchant: ' + merchantData.pr.pd.payment_url; - } - $scope.success = 'Transaction broadcasted' + message; - } else { - $scope.error = 'There was an error sending the transaction'; - } - $scope.loading = false; - $scope.loadTxs(); - }); - } - $rootScope.pendingPayment = null; - } - - var uri; + var payInfo; if (address.indexOf('bitcoin:') === 0) { - uri = new bitcore.BIP21(address).data; + payInfo = (new bitcore.BIP21(address)).data; } else if (/^https?:\/\//.test(address)) { - uri = { + payInfo = { merchant: address }; } // If we're setting the domain, ignore the change. if ($rootScope.merchant && $rootScope.merchant.domain && address === $rootScope.merchant.domain) { - uri = { + payInfo = { merchant: $rootScope.merchant.request_url }; } + w.spend({ + toAddress: address, + amountSat: amount, + comment: commentText, + url: (payInfo && payInfo.merchant) ? payInfo.merchant : null, + }, function(err, txid, status) { + // reset fields + $scope.address = $scope.amount = $scope.commentText = null; + form.address.$pristine = form.amount.$pristine = true; + $rootScope.pendingPayment = null; + if (err) return $scope._showError(err); - if (uri && uri.merchant) { - w.createPaymentTx({ - uri: uri.merchant, - memo: commentText - }, done); - } else { - w.createTx(address, amount, commentText, done); - } - - // reset fields - $scope.address = $scope.amount = $scope.commentText = null; - form.address.$pristine = form.amount.$pristine = true; + $scope.notifyStatus(status); + $scope.loadTxs(); + }); }; // QR code Scanner @@ -294,7 +258,6 @@ angular.module('copayApp.controllers').controller('SendController', $scope.openScanner = function() { if (window.cordova) return $scope.scannerIntent(); - $scope.showScanner = true; // Wait a moment until the canvas shows @@ -399,28 +362,24 @@ angular.module('copayApp.controllers').controller('SendController', $scope.amount = $rootScope.topAmount; }; + $scope.notifyStatus = function(status) { + if (status == copay.Wallet.TX_BROADCASTED) + notification.success('Success', 'Transaction broadcasted!'); + else if (status == copay.Wallet.TX_PROPOSAL_SENT) + notification.success('Success', 'Transaction proposal created'); + else if (status == copay.Wallet.TX_SIGNED) + notification.success('Success', 'Transaction proposal was signed'); + else + notification.error('Error', 'Unknown error occured'); + }; + $scope.send = function(ntxid, cb) { $scope.error = $scope.success = null; $scope.loading = true; $rootScope.txAlertCount = 0; - w.sendTx(ntxid, function(txid, merchantData) { - if (!txid) { - notification.error('Error', 'There was an error sending the transaction'); - } else { - if (!merchantData) { - notification.success('Success', 'Transaction broadcasted!'); - } else { - var message = 'Transaction ID: ' + txid; - if (merchantData.pr.ca) { - message += ' This payment protocol transaction' + ' has been verified through ' + merchantData.pr.ca + '.'; - } - message += ' Message from server: ' + merchantData.ack.memo; - message += ' For merchant: ' + merchantData.pr.pd.payment_url; - notification.success('Success', 'Transaction sent' + message); - } - } - + w.broadcastTx(ntxid, function(err, txid, status) { + $scope.notifyStatus(status); if (cb) return cb(); else $scope.loadTxs(); }); @@ -430,22 +389,10 @@ angular.module('copayApp.controllers').controller('SendController', $scope.loading = true; $scope.error = $scope.success = null; - try { - w.sign(ntxid); - } catch (e) { - notification.error('Error','There was an error signing the transaction'); + w.signAndSend(ntxid, function(err, id, status) { + $scope.notifyStatus(status); $scope.loadTxs(); - return; - } - - var p = w.txProposals.getTxProposal(ntxid); - if (p.builder.isFullySigned()) { - $scope.send(ntxid, function() { - $scope.loadTxs(); - }); - } else { - $scope.loadTxs(); - } + }); }; $scope.reject = function(ntxid) { @@ -453,7 +400,6 @@ angular.module('copayApp.controllers').controller('SendController', $rootScope.txAlertCount = 0; w.reject(ntxid); notification.warning('Transaction rejected', 'You rejected the transaction successfully'); - $scope.loading = false; $scope.loadTxs(); }; @@ -546,7 +492,9 @@ angular.module('copayApp.controllers').controller('SendController', }, 10 * 1000); // Payment Protocol URI (BIP-72) - $scope.wallet.fetchPaymentTx(uri.merchant, function(err, merchantData) { + $scope.wallet.fetchPaymentRequest({ + url: uri.merchant + }, function(err, merchantData) { if (!timeout) return; clearTimeout(timeout); diff --git a/js/models/Async.js b/js/models/Async.js index 0e9d0c522..b7b9bca33 100644 --- a/js/models/Async.js +++ b/js/models/Async.js @@ -207,7 +207,6 @@ Network.prototype._onMessage = function(enc) { log.debug('Ignoring trailing message. Ts:', enc.ts); return; } - log.debug('Async: receiving ' + JSON.stringify(payload)); var self = this; switch (payload.type) { @@ -239,6 +238,7 @@ Network.prototype._onMessage = function(enc) { Network.prototype._setupConnectionHandlers = function(opts, cb) { preconditions.checkState(this.socket); + log.debug('setting up connection', opts); var self = this; self.socket.on('connect_error', function(m) { @@ -260,7 +260,7 @@ Network.prototype._setupConnectionHandlers = function(opts, cb) { if (fromTs) { self.ignoreMessageFromTs = fromTs; } - log.info('Async: synchronizing from: ',fromTs); + log.info('Async: synchronizing from: ', fromTs); self.socket.emit('sync', fromTs); self.started = true; }); @@ -398,8 +398,6 @@ Network.prototype.send = function(dest, payload, cb) { if (to == this.copayerId) continue; - log.debug('SEND to: ' + to, this.copayerId, JSON.stringify(payload)); - var message = this.encode(to, payload); this.socket.emit('message', message); } diff --git a/js/models/Identity.js b/js/models/Identity.js index 2503a9c62..fbb5c8783 100644 --- a/js/models/Identity.js +++ b/js/models/Identity.js @@ -197,7 +197,8 @@ Identity.prototype.storeWallet = function(wallet, cb) { this.storage.setItem(key, val, function(err) { if (err) { - log.debug('Wallet:' + wallet.getName() + ' couldnt be stored:', err); + log.error('Wallet:' + wallet.getName() + ' couldnt be stored:', err); + log.error('Wallet:' + wallet.getName() + ' Size:', JSON.stringify(wallet.sizes())); } if (cb) return cb(err); @@ -402,6 +403,9 @@ Identity.prototype.bindWallet = function(w) { w.on('txProposalsUpdated', function() { Identity.storeWalletDebounced(self, w); }); + w.on('paymentAck', function() { + Identity.storeWalletDebounced(self, w); + }); w.on('newAddresses', function() { Identity.storeWalletDebounced(self, w); }); diff --git a/js/models/Insight.js b/js/models/Insight.js index 2f3e8f90c..a040c7fec 100644 --- a/js/models/Insight.js +++ b/js/models/Insight.js @@ -224,9 +224,7 @@ Insight.prototype.subscribe = function(addresses) { s.emit('subscribe', address); s.on(address, handler); - } else { - log.debug('Already subcribed to: ', address); - } + } }); }; diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index fba658556..32afe115b 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -11,8 +11,9 @@ var Key = bitcore.Key; var buffertools = bitcore.buffertools; var preconditions = require('preconditions').instance(); +var TX_MAX_SIZE_KB = 50; var VERSION = 1; -var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment', 'paymentProtocolURL']; +var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment', 'paymentProtocolURL', 'paymentAckMemo']; function TxProposal(opts) { @@ -37,10 +38,72 @@ function TxProposal(opts) { this.comment = opts.comment || null; this.readonly = opts.readonly || null; this.merchant = opts.merchant || null; + this.paymentAckMemo = null; this.paymentProtocolURL = opts.paymentProtocolURL || null; + + if (opts.creator) { + var now = Date.now(); + var me = {}; + me[opts.creator] = now; + + this.seenBy = me; + this.signedBy = {}; + this.creator = opts.creator; + this.createdTs = now; + if (opts.signWith) { + if (!this.sign(opts.signWith, opts.creator)) + throw new Error('Could not sign generated tx'); + } + } + this._sync(); } +TxProposal.prototype._checkPayPro = function() { + if (!this.merchant) return; + + if (this.paymentProtocolURL !== this.merchant.request_url) + throw new Error('PayPro: Mismatch on Payment URLs'); + + if (!this.merchant.outs || this.merchant.outs.length !== 1) + throw new Error('PayPro: Unsopported number of outputs'); + + if (this.merchant.expires < (this.getSent() || Date.now() / 1000.)) + throw new Error('PayPro: Request expired'); + + if (!this.merchant.total || !this.merchant.outs[0].amountSatStr || !this.merchant.outs[0].address) + throw new Error('PayPro: Missing amount'); + + var outs = JSON.parse(this.builder.vanilla.outs); + if (_.size(outs) != 1) + throw new Error('PayPro: Wrong outs in Tx'); + + var ppOut = this.merchant.outs[0]; + var txOut = outs[0]; + + if (ppOut.address !== txOut.address) + throw new Error('PayPro: Wrong out address in Tx'); + + if (ppOut.amountSatStr !== txOut.amountSatStr + '') + throw new Error('PayPro: Wrong amount in Tx'); + +}; + + +TxProposal.prototype.isFullySigned = function() { + return this.builder && this.builder.isFullySigned(); +}; + +TxProposal.prototype.sign = function(keys, signerId) { + var before = this.countSignatures(); + this.builder.sign(keys); + + var signaturesAdded = this.countSignatures() > before; + if (signaturesAdded){ + this.signedBy[signerId] = Date.now(); + } + return signaturesAdded; +}; TxProposal.prototype._check = function() { @@ -49,6 +112,11 @@ TxProposal.prototype._check = function() { } var tx = this.builder.build(); + + var txSize = tx.getSize(); + if (txSize / 1024 > TX_MAX_SIZE_KB) + throw new Error('BIG: Invalid TX proposal. Too big: ' + txSize + ' bytes'); + if (!tx.ins.length) throw new Error('Invalid tx proposal: no ins'); @@ -61,6 +129,28 @@ TxProposal.prototype._check = function() { if (hashType && hashType !== Transaction.SIGHASH_ALL) throw new Error('Invalid tx proposal: bad signatures'); }); + this._checkPayPro(); +}; + + +TxProposal.prototype.trimForStorage = function() { + // TODO (remove builder / builderObj. utxos, etc) + // + return this; +}; + +TxProposal.prototype.addMerchantData = function(merchantData) { + preconditions.checkArgument(merchantData.pr); + preconditions.checkArgument(merchantData.request_url); + var m = _.clone(merchantData); + + if (!this.paymentProtocolURL) + this.paymentProtocolURL = m.request_url; + + // remove unneeded data + m.raw = m.pr.pki_data = m.pr.signature = undefined; + this.merchant = m; + this._checkPayPro(); }; TxProposal.prototype.rejectCount = function() { @@ -101,7 +191,6 @@ TxProposal.prototype._sync = function() { return this; } - TxProposal.prototype.getId = function() { preconditions.checkState(this.builder); return this.builder.build().getNormalizedHash().toString('hex'); @@ -221,17 +310,6 @@ TxProposal._infoFromRedeemScript = function(s) { }; }; -TxProposal.prototype.mergeBuilder = function(incoming) { - var b0 = this.builder; - var b1 = incoming.builder; - - var before = JSON.stringify(b0.toObj()); - b0.merge(b1); - var after = JSON.stringify(b0.toObj()); - return after !== before; -}; - - TxProposal.prototype.getSeen = function(copayerId) { return this.seenBy[copayerId]; }; @@ -248,11 +326,14 @@ TxProposal.prototype.setRejected = function(copayerId) { if (!this.rejectedBy[copayerId]) this.rejectedBy[copayerId] = Date.now(); + + return this; }; TxProposal.prototype.setSent = function(sentTxid) { this.sentTxid = sentTxid; this.sentTs = Date.now(); + return this; }; TxProposal.prototype.getSent = function() { @@ -338,9 +419,17 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { // merge will not merge any metadata. TxProposal.prototype.merge = function(incoming) { - var hasChanged = this.mergeBuilder(incoming); + preconditions.checkArgument(_.isFunction(incoming._sync)); + incoming._sync(); + + // Note that all inputs must have the same number of signatures, so checking + // one (0) is OK. + var before = this._inputSigners[0].length; + this.builder.merge(incoming.builder); this._sync(); - return hasChanged; + + var after = this._inputSigners[0].length; + return after !== before; }; //This should be on bitcore / Transaction diff --git a/js/models/TxProposals.js b/js/models/TxProposals.js index d800ddacf..a1b890f7d 100644 --- a/js/models/TxProposals.js +++ b/js/models/TxProposals.js @@ -98,7 +98,6 @@ TxProposals.prototype.toObj = function() { TxProposals.prototype.merge = function(inObj, builderOpts) { var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts); - incomingTx._sync(); var myTxps = this.txps; var ntxid = incomingTx.getId(); @@ -178,16 +177,6 @@ TxProposals.prototype.getTxProposal = function(ntxid, copayers) { }; -TxProposals.prototype.reject = function(ntxid, copayerId) { - var txp = this.get(ntxid); - txp.setRejected(copayerId); -}; - -TxProposals.prototype.seen = function(ntxid, copayerId) { - var txp = this.get(ntxid); - txp.setSeen(copayerId); -}; - //returns the unspent txid-vout used in PENDING Txs TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { var ret = {}; @@ -205,4 +194,22 @@ TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { return ret; }; +/** + * purge + * + * @param deleteAll + * @return {undefined} + */ +TxProposals.prototype.purge = function(deleteAll, maxRejectCount) { + var m = _.size(this.txps); + + if (deleteAll) { + this.deleteAll(); + } else { + this.deletePending(maxRejectCount); + } + var n = _.size(this.txps); + return m - n; +}; + module.exports = TxProposals; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index fdc60b08d..66ceb9c80 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -1,12 +1,10 @@ 'use strict'; -var EventEmitter = require('events').EventEmitter; var _ = require('lodash'); var preconditions = require('preconditions').singleton(); var inherits = require('inherits'); var events = require('events'); var async = require('async'); -var cryptoUtil = require('../util/crypto'); var bitcore = require('bitcore'); var BIP21 = bitcore.BIP21; @@ -19,8 +17,10 @@ var Base58Check = bitcore.Base58.base58Check; var Address = bitcore.Address; var PayPro = bitcore.PayPro; var Transaction = bitcore.Transaction; -var log = require('../log'); +var log = require('../log'); +var cryptoUtil = require('../util/crypto'); +var httpUtil = require('../util/HTTP'); var HDParams = require('./HDParams'); var PublicKeyRing = require('./PublicKeyRing'); var TxProposal = require('./TxProposal'); @@ -30,12 +30,12 @@ var Async = require('./Async'); var Insight = module.exports.Insight = require('./Insight'); var copayConfig = require('../../config'); -var TX_MAX_SIZE_KB = 50; var TX_MAX_INS = 70; + + /** * @desc - * Wallet manages a private key for Copay, network, storage of the wallet for - * persistance, and blockchain information. + * Wallet manages a private key for Copay, network and blockchain information. * * @TODO: Split this leviathan. * @@ -70,6 +70,7 @@ function Wallet(opts) { opts.network = opts.network || Wallet._newAsync(opts.networkOpts[networkName]); opts.blockchain = opts.blockchain || Wallet._newInsight(opts.blockchainOpts[networkName]);; + this.httpUtil = opts.httpUtil || httpUtil; //required params ['network', 'blockchain', @@ -98,7 +99,7 @@ function Wallet(opts) { this.syncedTimestamp = opts.syncedTimestamp || 0; this.lastMessageFrom = {}; - this.paymentRequests = opts.paymentRequests || {}; + this.paymentRequestsCache = {}; var networkName = Wallet.obtainNetworkName(opts); @@ -117,6 +118,11 @@ function Wallet(opts) { inherits(Wallet, events.EventEmitter); + +Wallet.TX_BROADCASTED = 'txBroadcasted'; +Wallet.TX_PROPOSAL_SENT = 'txProposalSent'; +Wallet.TX_SIGNED = 'txSigned'; + Wallet.prototype.emitAndKeepAlive = function(args) { var args = Array.prototype.slice.call(arguments); log.debug('Wallet Emitting:', args); @@ -125,13 +131,13 @@ Wallet.prototype.emitAndKeepAlive = function(args) { }; /** - * @TODO: Document this. Its usage is kind of weird + * @desc Fixed & Forced TransactionBuilder options, for genereration transactions. * * @static - * @property lockTime - * @property signhash - * @property fee - * @property feeSat + * @property lockTime null + * @property signhash SIGHASH + * @property fee null (automatic) + * @property feeSat null */ Wallet.builderOpts = { lockTime: null, @@ -258,13 +264,10 @@ Wallet.prototype._newAddresses = function(dontUpdateUx) { * Processes the data using {@link HDParams#fromList} and merges it with the * {@link Wallet#publicKeyRing}. * - * Triggers a {@link Wallet#store} if the internal state has changed. - * * @param {string} senderId - the sender id * @param {Object} data - the data recived, {@see HDParams#fromList} */ Wallet.prototype._onIndexes = function(senderId, data) { - log.debug('Wallet:' + this.id + ' RECV INDEXES:', data); var inIndexes = HDParams.fromList(data.indexes); var hasChanged = this.publicKeyRing.mergeIndexes(inIndexes); if (hasChanged) { @@ -349,17 +352,14 @@ Wallet.prototype._processProposalEvents = function(senderId, m) { type: 'new', cId: senderId } - } else if (m.newCopayer.length) { + } else if (m.newCopayer && m.newCopayer.length) { ev = { type: 'signed', cId: m.newCopayer[0] }; + } else { + log.error('unknown tx proposal event:', m) } - } else { - ev = { - type: 'corrupt', - cId: senderId - }; } if (ev) this.emitAndKeepAlive('txProposalEvent', ev); @@ -413,31 +413,36 @@ Wallet.prototype._getKeyMap = function(txp) { /** * @callback transactionCallback - * @param {false|Transaction} returnValue + * @param {error} error + * @param {number} transaction ID (if sent) */ /** * @desc * Asyncchronously check with the blockchain if a given transaction was sent. * - * @param {string} ntxid - the transaction + * @param {string} ntxid - the transaction proposal * @param {transactionCallback} cb */ -Wallet.prototype._checkSentTx = function(ntxid, cb) { +Wallet.prototype._checkIfTxIsSent = function(ntxid, cb) { var txp = this.txProposals.get(ntxid); var tx = txp.builder.build(); - var txHex = tx.serialize().toString('hex'); //Use calcHash NOT getHash which could be cached. var txid = bitcore.util.formatHashFull(tx.calcHash()); this.blockchain.getTransaction(txid, function(err, tx) { - if (err) return cb(false); - return cb(txid); + return cb(err, !err ? txid : null); }); }; -Wallet.prototype._processTxProposalSeen = function(ntxid) { +/** + * + * @desc Set Incomming Transaction Proposal seen status + * and send `seen` messages to peers if aplicable. + * @param ntxid + */ +Wallet.prototype._setTxProposalSeen = function(ntxid) { var txp = this.txProposals.get(ntxid); if (!txp.getSeen(this.getMyCopayerId())) { txp.setSeen(this.getMyCopayerId()); @@ -446,22 +451,41 @@ Wallet.prototype._processTxProposalSeen = function(ntxid) { }; -Wallet.prototype._processTxProposalSent = function(ntxid, cb) { + +/** + * @desc updates Tx Proposal Sent status by checking the blockchain + * + * @param ntxid + * @param {transactionCallback} cb + */ +Wallet.prototype._updateTxProposalSent = function(ntxid, cb) { var self = this; var txp = this.txProposals.get(ntxid); - this._checkSentTx(ntxid, function(txid) { + this._checkIfTxIsSent(ntxid, function(err, txid) { + if (err) return cb(err); + if (txid) { if (!txp.getSent()) { txp.setSent(txid); } + self.emitAndKeepAlive('txProposalsUpdated'); } - self.emitAndKeepAlive('txProposalsUpdated'); - if (cb) return cb(null, txid); + if (cb) + return cb(null, txid, txid ? Wallet.TX_BROADCASTED : null); }); }; +/** + * _processTxProposalPayPro + * + * @desc Process and incoming PayPro TX Proposal. Fetchs the payment request + * from the merchant. + * + * @param mergeInfo Proposals merge information, as returned by TxProposals.merge + * @return {fetchPaymentRequestCallback} + */ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { var self = this; var txp = mergeInfo.txp; @@ -472,13 +496,30 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { return cb(); log.info('Received a Payment Protocol TX Proposal'); - self.fetchPaymentTx(txp.paymentProtocolURL, function(err, merchantData) { + self.fetchPaymentRequest({ + url: txp.paymentProtocolURL + }, function(err, merchantData) { if (err) return cb(err); - txp.merchant = merchantData; - return cb(); + + // This will verify current TXP data vs. merchantData (e.g., out addresses) + try { + txp.addMerchantData(merchantData); + } catch (e) { + log.error(e); + err = 'BADPAYPRO: ' + e.toString(); + } + return cb(err); }); }; +/** + * _processIncomingTxProposal + * + * @desc Process an incoming transaction proposal. Runs safety and sanity checks on it. + * + * @param mergeInfo Proposals merge information, as returned by TxProposals.merge + * @return {errCallback} + */ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { if (!mergeInfo) return cb(); var self = this; @@ -486,13 +527,12 @@ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { self._processTxProposalPayPro(mergeInfo, function(err) { if (err) return cb(err); - self._processTxProposalSeen(mergeInfo.ntxid); + self._setTxProposalSeen(mergeInfo.ntxid); var tx = mergeInfo.txp.builder.build(); if (tx.isComplete()) - self._processTxProposalSent(mergeInfo.ntxid); - else if (mergeInfo.hasChanged) { - self.sendTxProposal(mergeInfo.ntxid); + self._updateTxProposalSent(mergeInfo.ntxid); + else { self.emitAndKeepAlive('txProposalsUpdated'); } return cb(); @@ -510,29 +550,36 @@ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { */ Wallet.prototype._onTxProposal = function(senderId, data) { var self = this; - log.debug('Wallet:' + this.id + ' RECV TXPROPOSAL: ', data); var m; try { - m = this.txProposals.merge(data.txProposal, Wallet.builderOpts); - var keyMap = this._getKeyMap(m.txp); + m = self.txProposals.merge(data.txProposal, Wallet.builderOpts); + var keyMap = self._getKeyMap(m.txp); m.newCopayer = m.txp.setCopayers(senderId, keyMap); } catch (e) { log.error('Corrupt TX proposal received from:', senderId, e.toString()); if (m && m.ntxid) - this.txProposals.deleteOne(m.ntxid); + self.txProposals.deleteOne(m.ntxid); m = null; } - self._processIncomingTxProposal(m, function(err) { - if (err) { - log.error('Corrupt TX proposal received from:', senderId, err.toString()); - if (m && m.ntxid) - self.txProposals.deleteOne(m.ntxid); - m = null; - } - self._processProposalEvents(senderId, m); - }); + if (m) { + + self._processIncomingTxProposal(m, function(err) { + + if (err) { + log.error('Corrupt TX proposal received from:', senderId, err.toString()); + if (m && m.ntxid) + self.txProposals.deleteOne(m.ntxid); + m = null; + } else { + if (m && m.hasChanged) + self.sendTxProposal(m.ntxid); + } + + self._processProposalEvents(senderId, m); + }); + } }; /** @@ -549,20 +596,23 @@ Wallet.prototype._onReject = function(senderId, data) { preconditions.checkState(data.ntxid); log.debug('Wallet:' + this.id + ' RECV REJECT:', data); - var txp = this.txProposals.get(data.ntxid); + try { + var txp = this.txProposals.get(data.ntxid); + } catch (e) { + log.info(e); + }; - if (!txp) - throw new Error('Received Reject for an unknown TX from:' + senderId); + if (txp) { + if (txp.signedBy[senderId]) + throw new Error('Received Reject for an already signed TX from:' + senderId); - if (txp.signedBy[senderId]) - throw new Error('Received Reject for an already signed TX from:' + senderId); - - txp.setRejected(senderId); - this.emitAndKeepAlive('txProposalEvent', { - type: 'rejected', - cId: senderId, - txId: data.ntxid - }); + txp.setRejected(senderId); + this.emitAndKeepAlive('txProposalEvent', { + type: 'rejected', + cId: senderId, + txId: data.ntxid + }); + } }; /** @@ -577,24 +627,23 @@ Wallet.prototype._onReject = function(senderId, data) { */ Wallet.prototype._onSeen = function(senderId, data) { preconditions.checkState(data.ntxid); - log.debug('Wallet:' + this.id + ' RECV SEEN:', data); - - var txp = this.txProposals.get(data.ntxid); - txp.setSeen(senderId); - this.emitAndKeepAlive('txProposalEvent', { - type: 'seen', - cId: senderId, - txId: data.ntxid - }); - + try { + var txp = this.txProposals.get(data.ntxid); + } catch (e) {}; + if (txp) { + txp.setSeen(senderId); + this.emitAndKeepAlive('txProposalEvent', { + type: 'seen', + cId: senderId, + txId: data.ntxid + }); + } }; /** * @desc * Handle a ADDRESSBOOK message received * - * {@see Wallet#verifyAddressbookEntry} - * * @param {string} senderId * @param {Object} data * @param {Object} data.addressBook @@ -602,19 +651,23 @@ Wallet.prototype._onSeen = function(senderId, data) { * @emits txProposalEvent */ Wallet.prototype._onAddressBook = function(senderId, data) { - preconditions.checkState(data.addressBook); - log.debug('Wallet:' + this.id + ' RECV ADDRESSBOOK:', data); - var rcv = data.addressBook; - var hasChange; - for (var key in rcv) { - if (!this.addressBook[key]) { - var isVerified = this.verifyAddressbookEntry(rcv[key], senderId, key); - if (isVerified) { - this.addressBook[key] = rcv[key]; - hasChange = true; - } + if (!data.addressBook || !_.isObject(data.addressBook)) + return; + + var self = this, + hasChange; + _.each(data.addressBook, function(value, key) { + if (!self.addressBook[key] && Address.validate(key)) { + + self.addressBook[key] = _.pick(value, ['createdTs', 'label']); + + // Force author to senderId. + self.addressBook[key].copayerId = senderId; + + hasChange = true; } - } + }); + if (hasChange) { this.emitAndKeepAlive('addressBookUpdated'); } @@ -645,8 +698,6 @@ Wallet.prototype.updateSyncedTimestamp = function(ts) { * Triggers a call to {@link Wallet#sendWalletReady} */ Wallet.prototype._onNoMessages = function() { - if (!this.isShared()) return; - log.debug('Wallet:' + this.id + ' No messages at the server. Requesting peer sync from: ' + (this.syncedTimestamp + 1)); this.sendWalletReady(null, parseInt((this.syncedTimestamp + 1) / 1000000)); }; @@ -666,8 +717,8 @@ Wallet.prototype._onData = function(senderId, data, ts) { preconditions.checkArgument(data.type); preconditions.checkArgument(ts); preconditions.checkArgument(_.isNumber(ts)); - log.debug('Wallet:' + this.id + ' RECV', senderId, data); + log.debug('Wallet:' + this.getName() + ' RECV:', data.type, data, senderId); this.updateSyncedTimestamp(ts); @@ -902,8 +953,13 @@ Wallet.prototype._setBlockchainListeners = function() { */ Wallet.prototype.netStart = function() { var self = this; - var net = this.network; + if (!this.isShared()) { + self.emitAndKeepAlive('ready'); + return; + } + + var net = this.network; net.removeAllListeners(); net.on('connect', self._onConnect.bind(self)); net.on('data', self._onData.bind(self)); @@ -938,7 +994,7 @@ Wallet.prototype.netStart = function() { }; - log.debug('Wallet:' + self.id + ' Starting networking: ' + startOpts.copayerId); + log.debug('Wallet:' + self.id + ' Starting network.'); net.start(startOpts, function() { self._setBlockchainListeners(); self.emitAndKeepAlive('ready', net.getPeer()); @@ -1032,6 +1088,23 @@ Wallet.prototype.toObj = function() { return walletObj; }; +/** + * @desc: returns the sizes, by component, of a wallet, in bytes. + * + * @return {object} sizes by component name and 'total' for the total wallet size. + */ +Wallet.prototype.sizes = function() { + var obj = this.toObj(); + var sizes = {}, + total = 0; + _.each(obj, function(val, key) { + var s = JSON.stringify(val).length; + sizes[key] = s; + total += s; + }); + sizes.total = total; + return sizes; +}; Wallet.fromUntrustedObj = function(obj, readOpts) { obj = _.clone(obj); @@ -1139,16 +1212,21 @@ Wallet.fromObj = function(o, readOpts) { /** - * @desc Send a message to other peers - * @param {string[]} recipients - the pubkey of the recipients of the message - * @param {Object} obj - the data to be sent to them + * @desc sends a message to peers + * @param {string[]} recipients - the pubkey of the recipients of the message. Null for sending to all peers. + * @param {Object} obj - the data to be sent to them. + * @param {String} obj.type - Type of the message to be send */ -Wallet.prototype.send = function(recipients, obj) { +Wallet.prototype._sendToPeers = function(recipients, obj) { + if (!this.isShared()) return; + log.info('Wallet:' + this.getName() + ' ### Sending ' + obj.type); + log.debug('Sending obj', obj); + this.network.send(recipients, obj); }; /** - * @desc Send the set of TxProposals to some peers + * @desc Send the set of TxProposals to peers * @param {string[]} recipients - the pubkeys of the recipients */ Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) { @@ -1166,8 +1244,7 @@ Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) { */ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { preconditions.checkArgument(ntxid); - log.debug('Wallet:' + this.id + ' ### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals); - this.send(recipients, { + this._sendToPeers(recipients, { type: 'txProposal', txProposal: this.txProposals.get(ntxid).toObjTrim(), walletId: this.id, @@ -1180,8 +1257,7 @@ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { */ Wallet.prototype.sendSeen = function(ntxid) { preconditions.checkArgument(ntxid); - log.debug('Wallet:' + this.id + ' ### SENDING seen: ' + ntxid + ' TO: All'); - this.send(null, { + this._sendToPeers(null, { type: 'seen', ntxid: ntxid, walletId: this.id, @@ -1194,8 +1270,8 @@ Wallet.prototype.sendSeen = function(ntxid) { */ Wallet.prototype.sendReject = function(ntxid) { preconditions.checkArgument(ntxid); - log.debug('Wallet:' + this.id + ' ### SENDING reject: ' + ntxid + ' TO: All'); - this.send(null, { + + this._sendToPeers(null, { type: 'reject', ntxid: ntxid, walletId: this.id, @@ -1207,9 +1283,7 @@ Wallet.prototype.sendReject = function(ntxid) { * @param {string[]} [recipients] - the pubkeys of the recipients */ Wallet.prototype.sendWalletReady = function(recipients, sinceTs) { - log.debug('Wallet:' + this.id + ' ### SENDING WalletReady TO:', recipients || 'All'); - - this.send(recipients, { + this._sendToPeers(recipients, { type: 'walletReady', walletId: this.id, sinceTs: sinceTs, @@ -1222,9 +1296,7 @@ Wallet.prototype.sendWalletReady = function(recipients, sinceTs) { * @param {string[]} [recipients] - the pubkeys of the recipients */ Wallet.prototype.sendWalletId = function(recipients) { - log.debug('Wallet:' + this.id + ' ### SENDING walletId TO:', recipients || 'All', this.id); - - this.send(recipients, { + this._sendToPeers(recipients, { type: 'walletId', walletId: this.id, opts: this._optsToObj(), @@ -1237,12 +1309,11 @@ Wallet.prototype.sendWalletId = function(recipients) { * @param {string[]} [recipients] - the pubkeys of the recipients */ Wallet.prototype.sendPublicKeyRing = function(recipients) { - log.debug('Wallet:' + this.id + ' ### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj()); - var publicKeyRing = this.publicKeyRing.toObj(); + var publicKeyRingObj = this.publicKeyRing.toObj(); - this.send(recipients, { + this._sendToPeers(recipients, { type: 'publicKeyRing', - publicKeyRing: publicKeyRing, + publicKeyRing: publicKeyRingObj, walletId: this.id, }); }; @@ -1253,9 +1324,7 @@ Wallet.prototype.sendPublicKeyRing = function(recipients) { */ Wallet.prototype.sendIndexes = function(recipients) { var indexes = HDParams.serialize(this.publicKeyRing.indexes); - log.debug('Wallet:' + this.id + ' ### INDEXES TO:', recipients || 'All', indexes); - - this.send(recipients, { + this._sendToPeers(recipients, { type: 'indexes', indexes: indexes, walletId: this.id, @@ -1263,15 +1332,30 @@ Wallet.prototype.sendIndexes = function(recipients) { }; /** + * sendAddressBook * @desc Send our addressBook to other recipients + * * @param {string[]} recipients - the pubkeys of the recipients + * @param onlyKey + * @return {undefined} */ -Wallet.prototype.sendAddressBook = function(recipients) { - if (!Object.keys(this.addressBook).length) return; - log.debug('Wallet:' + this.id + ' ### SENDING addressBook TO:', recipients || 'All', this.addressBook); - this.send(recipients, { +Wallet.prototype.sendAddressBook = function(recipients, onlyKey) { + var toSend = [], + myId = this.getMyCopayerId(); + + if (onlyKey && this.addressBook[onlyKey]) { + toSend = {}; + toSend[onlyKey] = this.addressBook[onlyKey]; + } else { + toSend = _.filter(this.addressBook, function(entry) { + return entry.copayerId === myId; + }); + } + if (_.isEmpty(toSend)) return; + + this._sendToPeers(recipients, { type: 'addressbook', - addressBook: this.addressBook, + addressBook: toSend, walletId: this.id, }); }; @@ -1319,18 +1403,20 @@ Wallet.prototype.generateAddress = function(isChange, cb) { */ Wallet.prototype.getTxProposals = function() { var ret = []; - var copayers = this.getRegisteredCopayerIds(); - for (var ntxid in this.txProposals.txps) { - var txp = this.txProposals.getTxProposal(ntxid, copayers); - txp.signedByUs = txp.signedBy[this.getMyCopayerId()] ? true : false; - txp.rejectedByUs = txp.rejectedBy[this.getMyCopayerId()] ? true : false; - txp.finallyRejected = this.totalCopayers - txp.rejectCount < this.requiredCopayers; + var self = this; + var copayers = self.getRegisteredCopayerIds(); + var myId = self.getMyCopayerId(); + + _.each(self.txProposals.txps, function(txp, ntxid){ + txp.signedByUs = txp.signedBy[myId] ? true : false; + txp.rejectedByUs = txp.rejectedBy[self.getMyCopayerId()] ? true : false; + txp.finallyRejected = self.totalCopayers - txp.rejectCount < self.requiredCopayers; txp.isPending = !txp.finallyRejected && !txp.sentTxid; if (!txp.readonly || txp.finallyRejected || txp.sentTs) { ret.push(txp); } - } + }); return ret; }; @@ -1397,17 +1483,11 @@ Wallet.prototype.getPendingTxProposals = function() { * @return {number} the number of deleted proposals */ Wallet.prototype.purgeTxProposals = function(deleteAll) { - var m = this.txProposals.length(); - - if (deleteAll) { - this.txProposals.deleteAll(); - } else { - this.txProposals.deletePending(this.maxRejectCount()); + var deleted = this.txProposals.purge(deleteAll, this.maxRejectCount()); + if (deleted) { + this.emitAndKeepAlive('txProposalsUpdated'); } - this.emitAndKeepAlive('txProposalsUpdated'); - - var n = this.txProposals.length(); - return m - n; + return deleted; }; /** @@ -1416,200 +1496,281 @@ Wallet.prototype.purgeTxProposals = function(deleteAll) { * @emits txProposalsUpdated */ Wallet.prototype.reject = function(ntxid) { - var txp = this.txProposals.reject(ntxid, this.getMyCopayerId()); + var txp = this.txProposals.get(ntxid); + txp.setRejected(this.getMyCopayerId()); this.sendReject(ntxid); this.emitAndKeepAlive('txProposalsUpdated'); }; /** - * @desc Sign a proposal + * @callback signCallback + * @param {Error} error if any + * @param {number} Transaction ID or Transaction Proposal ID + * @param {status} Wallet.TX_* Status: + * + * TX_BROADCASTED + * TX_SIGNED + * TX_PROPOSAL_SENT + */ + + +/** + * @desc Signs a transaction proposal * @param {string} ntxid the id of the transaction proposal to sign * @emits txProposalsUpdated * @throws {Error} Could not sign proposal * @throws {Error} Bad payment request * @return {boolean} true if signing actually incremented the number of signatures + * @emits txProposalsUpdated */ Wallet.prototype.sign = function(ntxid) { preconditions.checkState(!_.isUndefined(this.getMyCopayerId())); - var myId = this.getMyCopayerId(); var txp = this.txProposals.get(ntxid); - - // If this is a payment protocol request, - // ensure it hasn't been tampered with. - if (!this.verifyPaymentRequest(ntxid)) { - throw new Error('Bad payment request'); - } - - var before = txp.countSignatures(); - var keys = this.privateKey.getForPaths(txp.inputChainPaths); - txp.builder.sign(keys); - if (txp.countSignatures() <= before) { + var signaturesAdded = txp.sign(keys, this.getMyCopayerId()); + if (!signaturesAdded) return false; - } - txp.signedBy[myId] = Date.now(); - this.sendTxProposal(ntxid); this.emitAndKeepAlive('txProposalsUpdated'); - return true; }; -/** - * @callback broadcastCallback - * @param {string} txid - the transaction id on the blockchain - */ -/** - * @desc Broadcasts a transaction to the blockchain - * @param {string} ntxid - the transaction proposal id - * @param {broadcastCallback} cb - */ -Wallet.prototype.sendTx = function(ntxid, cb) { - var txp = this.txProposals.get(ntxid); +/** + * + * @desc signs and send or broadcast a transaction. + * In m-n wallets, + * if m==1 it will broadcast it to the Bitcoin Network + * if n>1 it will send the proposal to the peers + * + * @param ntxid Transaction Proposal Id + * @param {signCallback} cb + * @throws {Error} Could not sign proposal + */ +Wallet.prototype.signAndSend = function(ntxid, cb) { + if (this.sign(ntxid)) { + var txp = this.txProposals.get(ntxid); + if (txp.isFullySigned()) { + return this.broadcastTx(ntxid, cb); + } else { + this.sendTxProposal(ntxid); + return cb(null, ntxid, Wallet.TX_SIGNED); + } + } else { + return cb(new Error('Could not sign the proposal')); + } +}; + + +/** + * @desc Broadcast a tx proposal. In case of failure, check if the resulting + * transactions is already on the blockchain. + * + * @param ntxid + * @param cb + * @return {undefined} + */ +Wallet.prototype._doBroadcastTx = function(ntxid, cb) { + var self = this; + var txp = this.txProposals.get(ntxid); var tx = txp.builder.build(); + if (!tx.isComplete()) throw new Error('Tx is not complete. Can not broadcast'); - log.debug('Wallet:' + this.id + ' Broadcasting Transaction'); - - if (txp.merchant) { - return this.sendPaymentTx(ntxid, cb); - } - - var scriptSig = tx.ins[0].getScript(); - var size = scriptSig.serialize().length; var txHex = tx.serialize().toString('hex'); - log.debug('Wallet:' + this.id + ' Raw transaction: ', txHex); - var self = this; + log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); + log.debug('\tRaw transaction: ', txHex); + this.blockchain.broadcast(txHex, function(err, txid) { - if (err) - log.error('Error sending TX:', err); + if (err || !txid) { - if (txid) { - log.debug('Wallet:' + self.getName() + ' Broadcasted TX. BITCOIND txid:', txid); - self.txProposals.get(ntxid).setSent(txid); - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('txProposalsUpdated'); - return cb(txid); - } else { - log.info('Wallet:' + self.getName() + '. Sent failed. Checking if the TX was sent already'); - self._processTxProposalSent(ntxid, function(err, txid) { - return cb(txid); + log.info('Wallet:' + self.getName() + '. Sent failed:' + + err + '. Checking if the TX was sent already'); + + self._checkIfTxIsSent(ntxid, function(err, txid) { + return cb(err, txid); }); + } else { + log.info('Wallet:' + self.getName() + ' broadcasted a TX. BITCOIND txid:', txid); + return cb(null, txid); } }); }; /** - * @desc Create a Payment Protocol transaction - * @param {Object|string} options - if it's a string, parse it as the uri - * @param {string} options.uri the url for the transaction - * @param {Function} cb + * @desc Broadcasts a transaction to the blockchain, updates tx transactions + * sent status. If the tx proposal is a payment protocol request,it will also + * send the payment message to the server,and process the response. + * + * @param {string} ntxid - the transaction proposal id + * @param {string} txid - the transaction id on the blockchain + * @param {signCallback} cb */ -Wallet.prototype.createPaymentTx = function(options, cb) { +Wallet.prototype.broadcastTx = function(ntxid, cb) { var self = this; - if (_.isString(options)) { - options = { - uri: options - }; - } - options.uri = options.uri || options.url; + self._doBroadcastTx(ntxid, function(err, txid) { + if (err) return cb(err); + preconditions.checkState(txid); - if (options.uri.indexOf('bitcoin:') === 0) { - options.uri = new bitcore.BIP21(options.uri).data.merchant; - if (!options.uri) { - return cb(new Error('No URI.')); + var txp = self.txProposals.get(ntxid); + txp.setSent(txid); + + + // PAYPRO: Payment message is optional, only if payment_url is set + // This is async. and will notify and update txp async. + if (txp.merchant && txp.merchant.pr.pd.payment_url) { + var data = self.createPayProPayment(txp); + self.sendPayProPayment(txp, data, function(err, data) { + if (err) return cb(err); + self.onPayProPaymentAck(ntxid, data); + }); } - } - var req = this.paymentRequests[options.uri]; - if (req) { - delete this.paymentRequests[options.uri]; - this.receivePaymentRequest(options, req.pr, cb); - return; - } + self.sendTxProposal(ntxid); + self.emitAndKeepAlive('txProposalsUpdated'); + return cb(null, txid, Wallet.TX_BROADCASTED); + }); +}; - return Wallet.request({ - method: 'GET', - url: options.uri, - headers: { - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE - }, - responseType: 'arraybuffer' +/** + * @callback {fetchPaymentRequestCallback} + * @param {string=} err - an error, if any + * @param {Object} merchantData - object representing the payment request. Add described on BIP70 merchant_data + */ + +/** + * @desc Creates a Payment Protocol transaction + * @param {Object|string} options - if it's a string, parse it as the url + * @param {string} options.url the url for the transaction + * @return {fetchPaymentRequestCallback} cb + */ +Wallet.prototype.fetchPaymentRequest = function(options, cb) { + preconditions.checkArgument(_.isObject(options)); + preconditions.checkArgument(options.url); + preconditions.checkArgument(options.url.indexOf('http') == 0, 'Bad PayPro URL given:' + options.url); + var self = this; + + if (self.paymentRequestsCache[options.url]) + return cb(null, self.paymentRequestsCache[options.url]); + + this.httpUtil.request({ + method: 'GET', + url: options.url, + headers: { + 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + }, + responseType: 'arraybuffer' + }) + .success(function(rawData) { + log.info('PayPro Request done successfully. Parsing response') + + var merchantData, err; + try { + merchantData = self.parsePaymentRequest(options, rawData); + } catch (e) { + err = e + }; + + log.debug('PayPro request data', merchantData); + + self.paymentRequestsCache[options.url] = merchantData; + return cb(err, merchantData); }) - .success(function(data, status, headers, config) { - data = PayPro.PaymentRequest.decode(data); - var pr = new PayPro(); - pr = pr.makePaymentRequest(data); - return self.receivePaymentRequest(options, pr, cb); - }) - .error(function(data, status, headers, config) { - log.debug('Server did not return PaymentRequest.'); - log.debug('XHR status: ' + status); - if (options.fetch) { - return cb(new Error('Status: ' + status)); - } else { - // Should never happen: - return cb(null, null, null); - } + .error(function(data, status) { + log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); + return cb(new Error('Status: ' + status)); }); }; + /** - * @desc Creates a Payment TxProposal from a uri - * @param {Object} options - * @param {string=} options.uri - * @param {string=} options.url - * @param {Function} cb + * _addOutputsToMerchantData + * + * @desc parses merchant_data internal output representation and stores + * the result in merchant_data.outs = [{address: xx, amountSatStr: xx}], + * to be compatible with TransactionBuilder. + *` + * @param merchantData BIP70 merchant_data (from the payment request) + * @throws {Error} PayPro: Unsupported inputs + * @return {undefined} */ -Wallet.prototype.fetchPaymentTx = function(options, cb) { - var self = this; +Wallet.prototype._addOutputsToMerchantData = function(merchantData) { - options = options || {}; - if (_.isString(options)) { - options = { - uri: options + var total = bignum(0); + var outs = {}; + + _.each(merchantData.pr.pd.outputs, function(output) { + var amount = output.amount; + + // big endian + var v = new Buffer(8); + v[0] = (amount.high >> 24) & 0xff; + v[1] = (amount.high >> 16) & 0xff; + v[2] = (amount.high >> 8) & 0xff; + v[3] = (amount.high >> 0) & 0xff; + v[4] = (amount.low >> 24) & 0xff; + v[5] = (amount.low >> 16) & 0xff; + v[6] = (amount.low >> 8) & 0xff; + v[7] = (amount.low >> 0) & 0xff; + + var script = { + offset: output.script.offset, + limit: output.script.limit, + buffer: new Buffer(output.script.buffer, 'hex') }; - } - options.uri = options.uri || options.url; - options.fetch = true; + var s = script.buffer.slice(script.offset, script.limit); + var network = merchantData.pr.pd.network === 'main' ? 'livenet' : 'testnet'; + var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), network); - var req = this.paymentRequests[options.uri]; - if (req) { - return cb(null, req.merchantData); - } + var a = addr[0].toString(); + outs[a] = bignum.fromBuffer(v, { + endian: 'big', + size: 1 + }).add(outs[a] || bignum(0)); - return this.createPaymentTx(options, function(err, merchantData, pr) { - if (err) return cb(err); - self.paymentRequests[options.uri] = { - merchantData: merchantData, - pr: pr - }; - return cb(null, merchantData); + total = total.add(bignum.fromBuffer(v, { + endian: 'big', + size: 1 + })); }); + + // for now we only support PayPro with 1 output. + if (_.size(outs) !== 1) + throw new Error('PayPro: Unsupported outputs'); + + var out = _.pairs(outs)[0]; + + merchantData.outs = [{ + address: out[0], + amountSatStr: out[1].toString(10), + }]; + merchantData.total = total.toString(10); + + // If user is granted the privilege of choosing + // their own amount, add it to the tx. + if (merchantData.total === 0 && options.amount) { + merchant.outs[0].amountSatStr = merchantData.total = outions.amount; + } }; /** - * @desc Analyzes a payment request and generates a transaction proposal for it. + * @desc Analyzes a payment request and generate merchantData * @param {Object} options - * @param {PayProRequest} pr - * @param {string} pr.payment_details_version - * @param {string} pr.pki_type - * @param {Object} pr.data - * @param {string} pr.serialized_payment_details - * @param {string} pr.signature - * @param {string} options.memo - * @param {string} options.comment - * @param {Function} cb + * @param {string} options.url url where the pay request was acquired + * @param {string} options.amount Only used if pay requesst allow user to set the amount + * @param {PayProRequest} rawData */ -Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { +Wallet.prototype.parsePaymentRequest = function(options, rawData) { var self = this; + var data = PayPro.PaymentRequest.decode(rawData); + var paypro = new PayPro(); + var pr = paypro.makePaymentRequest(data); var ver = pr.get('payment_details_version'); var pki_type = pr.get('pki_type'); var pki_data = pr.get('pki_data'); @@ -1619,18 +1780,11 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { var certs = PayPro.X509Certificates.decode(pki_data); certs = certs.certificate; - // Fix for older versions of bitcore - if (!PayPro.RootCerts) { - PayPro.RootCerts = { - getTrusted: function() {} - }; - } - // Verify Signature var trust = pr.verify(true); if (!trust.verified) { - return cb(new Error('Server sent a bad signature.')); + throw new Error('Server sent a bad signature.'); } details = PayPro.PaymentDetails.decode(details); @@ -1676,526 +1830,138 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { untrusted: !trust.caTrusted, selfSigned: trust.selfSigned }, - request_url: options.uri, + expires: expires, + request_url: options.url, total: bignum('0', 10).toString(10), - // Expose so other copayers can verify signature - // and identity, not to mention data. - raw: pr.serialize().toString('hex') }; - - return this.getUnspent(function(err, safeUnspent, unspent) { - if (options.fetch) { - if (!unspent || !unspent.length) { - return cb(new Error('No unspent outputs available.')); - } - - try { - self.createPaymentTxSync(options, merchantData, safeUnspent); - } catch (e) { - var msg = e.message || ''; - if (msg.indexOf('not enough unspent tx outputs to fulfill')) { - var sat = /(\d+)/.exec(msg)[1]; - e = new Error('No unspent outputs available.'); - e.amount = sat; - return cb(e); - } - } - return cb(null, merchantData, pr); - } - - var ntxid = self.createPaymentTxSync(options, merchantData, safeUnspent); - if (ntxid) { - self.sendIndexes(); - self.sendTxProposal(ntxid); - self.emit('txProposalsUpdated'); - } else { - return cb(new Error('Error creating the transaction')); - } - - log.debug('You are currently on this BTC network:', network); - log.debug('The server sent you a message:', memo); - - return cb(null, ntxid, merchantData); - }); + this._addOutputsToMerchantData(merchantData, options.amount); + return merchantData; }; /** - * @desc Send a payment transaction to a server, complying with BIP70 + * _getPayProRefundOutputs + * Create refund outputs for a PayPro Payment Message + * Uses current transaction's change address. * - * @TODO: Get this out of here. - * - * @param {string} ntxid - the transaction proposal id - * @param {Object} options - * @param {string} options.refund_to - * @param {string} options.memo - * @param {string} options.comment - * @param {Function} cb + * @param txp + * @return {undefined} */ -Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { - var self = this; +Wallet.prototype._getPayProRefundOutputs = function(txp) { + var pkr = this.publicKeyRing; + var index = pkr.getHDParams(this.publicKey); + var amount = +txp.merchant.total.toString(10); - if (!cb) { - cb = options; - options = {}; - } + var output = new PayPro.Output(); + var script = pkr.getScriptPubKeyHex(index.changeIndex, true, this.pubkey); + output.set('script', new Buffer(script, 'hex')); + output.set('amount', amount); + return [output]; +}; - var txp = this.txProposals.get(ntxid); - if (!txp) return; + +/** + * + * @desc Creates a Payment Protocol Payment message for the given TX Proposal + * @param txp Transaction Proposal + * @param txHex + * @return {undefined} + */ +Wallet.prototype.createPayProPayment = function(txp) { var tx = txp.builder.build(); - if (!tx.isComplete()) return; - log.debug('Sending Transaction'); + var txBuf = tx.serialize(); - var refund_outputs = []; - - options.refund_to = options.refund_to || this.publicKeyRing.getPubKeys(0, false, this.getMyCopayerId())[0]; - - if (options.refund_to) { - var total = txp.merchant.pr.pd.outputs.reduce(function(total, _, i) { - // XXX reverse endianness to work around bignum bug: - var txv = tx.outs[i].v; - var v = new Buffer(8); - for (var j = 0; j < 8; j++) v[j] = txv[7 - j]; - return total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }, bignum('0', 10)); - - var rpo = new PayPro(); - rpo = rpo.makeOutput(); - - // XXX Bad - the amount *has* to be a Number in protobufjs - // Possibly does not matter - server can ignore the amount anyway. - rpo.set('amount', +total.toString(10)); - - rpo.set('script', - Buffer.concat([ - new Buffer([ - 118, // OP_DUP - 169, // OP_HASH160 - 76, // OP_PUSHDATA1 - 20, // number of bytes - ]), - // needs to be ripesha'd - bitcore.util.sha256ripe160(options.refund_to), - new Buffer([ - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ]) - ]) - ); - - refund_outputs.push(rpo.message); - } + var refund_outputs = this._getPayProRefundOutputs(txp); // We send this to the serve after receiving a PaymentRequest var pay = new PayPro(); pay = pay.makePayment(); + var merchant_data = txp.merchant.pr.pd.merchant_data; if (merchant_data) { merchant_data = new Buffer(merchant_data, 'hex'); pay.set('merchant_data', merchant_data); } - pay.set('transactions', [tx.serialize()]); + + pay.set('transactions', [txBuf]); pay.set('refund_to', refund_outputs); - options.memo = options.memo || options.comment || 'Hi server, I would like to give you some money.'; - - pay.set('memo', options.memo); + // Unused for now + // options.memo = ''; + // pay.set('memo', options.memo); pay = pay.serialize(); - - log.debug('Sending Payment Message:', pay.toString('hex')); - var buf = new ArrayBuffer(pay.length); var view = new Uint8Array(buf); for (var i = 0; i < pay.length; i++) { view[i] = pay[i]; } - var postInfo = { - method: 'POST', - url: txp.merchant.pr.pd.payment_url, - headers: { - // BIP-71 - 'Accept': PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE - // XHR does not allow these: - // 'Content-Length': (pay.byteLength || pay.length) + '', - // 'Content-Transfer-Encoding': 'binary' - }, - // Technically how this should be done via XHR (used to - // be the ArrayBuffer, now you send the View instead). - data: view, - responseType: 'arraybuffer' - }; - - return Wallet.request(postInfo) - .success(function(data, status, headers, config) { - data = PayPro.PaymentACK.decode(data); - var ack = new PayPro(); - ack = ack.makePaymentACK(data); - return self.receivePaymentRequestACK(ntxid, tx, txp, ack, cb); - }) - .error(function(data, status, headers, config) { - log.debug('Sending to server was not met with a returned tx.'); - log.debug('XHR status: ' + status); - self._processTxProposalSent(ntxid, function(err, txid) { - return cb(txid, txp.merchant); - }); - }); + return view; }; -/** - * @desc Handles a PaymentRequestACK from the server - */ -Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { - var self = this; - var payment = ack.get('payment'); +/** + * onPayProPaymentAck + * + * @desc parse and process a Payment Protocol Payment Ack. Updates + * given TX Proposal with merchant's memo and send it to copayers + * + * @param ntxid ID of the Transaction Proposal + * @param rawData of the Payment Ack + * @emits paymentACK - (merchants's memo) + */ +Wallet.prototype.onPayProPaymentAck = function(ntxid, rawData) { + var data = PayPro.PaymentACK.decode(rawData); + var paypro = new PayPro(); + var ack = paypro.makePaymentACK(data); var memo = ack.get('memo'); + log.debug('Payment Acknowledged!: %s', memo); - log.debug('Our payment was acknowledged!'); - log.debug('Message from Merchant: %s', memo); - - payment = PayPro.Payment.decode(payment); - var pay = new PayPro(); - payment = pay.makePayment(payment); - - txp.merchant.ack = { - memo: memo - }; - - if (payment.message.transactions && payment.message.transactions.length) { - tx = payment.message.transactions[0]; - if (!tx) { - log.debug('Sending to server was not met with a returned tx.'); - return this._processTxProposalSeen(ntxid, function(err, txid) { - log.debug('[Wallet.js.1613:txid:%s]', txid); - return cb(txid, txp.merchant); - }); - } - if (tx.buffer) { - tx.buffer = new Buffer(new Uint8Array(tx.buffer)); - tx.buffer = tx.buffer.slice(tx.offset, tx.limit); - var ptx = new bitcore.Transaction(); - ptx.parse(tx.buffer); - tx = ptx; - } - } else { - log.debug('WARNING: This server does not comply by standards.'); - log.debug('It is not returning a copy of the transaction.'); - } - - var txid = tx.calcHash().toString('hex'); - var txHex = tx.serialize().toString('hex'); - log.debug('Raw transaction: ', txHex); - - // XXX This fixes the invalid signature error: - // we might as well broadcast it ourselves anyway. - this.blockchain.broadcast(txHex, function(err, txid) { - log.debug('BITCOIND txid:', txid); - if (txid) { - self.txProposals.get(ntxid).setSent(txid); - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('txProposalsUpdated'); - return cb(txid, txp.merchant); - } else { - log.debug('PayPro Sent failed. Checking if the TX was sent already'); - self._processTxProposalSent(ntxid, function(err, txid) { - return cb(txid, txp.merchant); - }); - } - }); + var txp = this.txProposals.get(ntxid); + txp.paymentAckMemo = memo; + this.sendTxProposal(ntxid); + this.emitAndKeepAlive('paymentACK', memo); }; + /** - * @desc Create a Payment Transaction Sync (see BIP70) - * @TODO: Document better + * @desc Send a payment transaction to a merchant, complying with BIP70 + * on Acknoledge, updates the TX Proposal with server's memo and send it + * to peers + * + * @param {string} ntxid - the transaction proposal ID for with the + * */ -Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) { +Wallet.prototype.sendPayProPayment = function(txp, data, cb) { var self = this; - var priv = this.privateKey; - var pkr = this.publicKeyRing; - preconditions.checkState(pkr.isComplete()); - if (options.memo) { - preconditions.checkArgument(options.memo.length <= 100); - } - - var opts = { - remainderOut: { - address: this._doGenerateAddress(true).toString() - } + log.debug('Sending Payment Message to merchant server'); + var postInfo = { + method: 'POST', + url: txp.merchant.pr.pd.payment_url, + headers: { + // BIP-71 + 'Accept': PayPro.PAYMENT_ACK_CONTENT_TYPE, + 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE + // XHR does not allow these: + // 'Content-Length': (pay.byteLength || pay.length) + '', + // 'Content-Transfer-Encoding': 'binary' + }, + // Technically how this should be done via XHR (used to + // be the ArrayBuffer, now you send the View instead). + data: data, + responseType: 'arraybuffer' }; - if (_.isUndefined(opts.spendUnconfirmed)) { - opts.spendUnconfirmed = this.spendUnconfirmed; - } - - for (var k in Wallet.builderOpts) { - opts[k] = Wallet.builderOpts[k]; - } - - merchantData.total = bignum(merchantData.total, 10); - - var outs = []; - merchantData.pr.pd.outputs.forEach(function(output) { - var amount = output.amount; - - // big endian - var v = new Buffer(8); - v[0] = (amount.high >> 24) & 0xff; - v[1] = (amount.high >> 16) & 0xff; - v[2] = (amount.high >> 8) & 0xff; - v[3] = (amount.high >> 0) & 0xff; - v[4] = (amount.low >> 24) & 0xff; - v[5] = (amount.low >> 16) & 0xff; - v[6] = (amount.low >> 8) & 0xff; - v[7] = (amount.low >> 0) & 0xff; - - var script = { - offset: output.script.offset, - limit: output.script.limit, - buffer: new Buffer(output.script.buffer, 'hex') - }; - var s = script.buffer.slice(script.offset, script.limit); - var network = merchantData.pr.pd.network === 'main' ? 'livenet' : 'testnet'; - var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), network); - - outs.push({ - address: addr[0].toString(), - amountSatStr: bignum.fromBuffer(v, { - endian: 'big', - size: 1 - }).toString(10) + this.httpUtil.request(postInfo) + .success(function(rawData) { + return cb(null, rawData); + }) + .error(function(data, status) { + log.error('Sending payment notification: XHR status: ' + status); + return cb(new Error(status)); }); - - merchantData.total = merchantData.total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }); - - merchantData.total = merchantData.total.toString(10); - - var b = new Builder(opts) - .setUnspent(unspent) - .setOutputs(outs); - - merchantData.pr.pd.outputs.forEach(function(output, i) { - var script = { - offset: output.script.offset, - limit: output.script.limit, - buffer: new Buffer(output.script.buffer, 'hex') - }; - var s = script.buffer.slice(script.offset, script.limit); - b.tx.outs[i].s = s; - }); - - var selectedUtxos = b.getSelectedUnspent(); - if (selectedUtxos.length > TX_MAX_INS) - throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.length + ' inputs. Aborting'); - - - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - - if (options.fetch) return; - - log.debug('Created transaction: %s', b.tx.getStandardizedObject()); - - var myId = this.getMyCopayerId(); - var now = Date.now(); - - var tx = b.build(); - if (!tx.countInputSignatures(0)) - throw new Error('Could not sign generated tx'); - - var txSize = tx.getSize(); - if (txSize / 1024 > TX_MAX_SIZE_KB) - throw new Error('BIG: Resulting TX is too big ' + txSize + ' bytes. Aborting'); - - - - var me = {}; - me[myId] = now; - var meSeen = {}; - if (priv) meSeen[myId] = now; - - var ntxid = this.txProposals.add(new TxProposal({ - inputChainPaths: inputChainPaths, - signedBy: me, - seenBy: meSeen, - creator: myId, - createdTs: now, - builder: b, - comment: options.memo, - merchant: merchantData, - paymentProtocolURL: options.uri, - })); - - return ntxid; -}; - -/** - * @desc Verifies a PaymentRequest sent by another peer - * This essentially ensures that a copayer hasn't tampered with a - * PaymentRequest message from a payment server. It verifies the signature - * based on the cert, and checks to ensure the desired outputs are the same as - * the ones on the tx proposal. - * @TODO: Document better - */ -Wallet.prototype.verifyPaymentRequest = function(ntxid) { - if (!ntxid) return false; - - var txp = _.isObject(ntxid) ? ntxid : this.txProposals.get(ntxid); - - // If we're not a payment protocol proposal, ignore. - if (!txp.merchant) return true; - - // The copayer didn't send us the raw payment request, unverifiable. - if (!txp.merchant.raw) return false; - - // var tx = txp.builder.tx; - var tx = txp.builder.build(); - - var data = new Buffer(txp.merchant.raw, 'hex'); - data = PayPro.PaymentRequest.decode(data); - var pr = new PayPro(); - pr = pr.makePaymentRequest(data); - - // Verify the signature so we know this is the real request. - var trust = pr.verify(true); - if (!trust.verified) { - // Signature does not match cert. It may have - // been modified by an untrustworthy person. - // We should not sign this transaction proposal! - return false; - } - - var details = pr.get('serialized_payment_details'); - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - - var outputs = pd.get('outputs'); - - if (tx.outs.length < outputs.length) { - // Outputs do not and cannot match. - return false; - } - - // Figure out whether the user is supposed - // to decide the value of the outputs. - var undecided = false; - var total = bignum('0', 10); - for (var i = 0; i < outputs.length; i++) { - var output = outputs[i]; - var amount = output.get('amount'); - // big endian - var v = new Buffer(8); - v[0] = (amount.high >> 24) & 0xff; - v[1] = (amount.high >> 16) & 0xff; - v[2] = (amount.high >> 8) & 0xff; - v[3] = (amount.high >> 0) & 0xff; - v[4] = (amount.low >> 24) & 0xff; - v[5] = (amount.low >> 16) & 0xff; - v[6] = (amount.low >> 8) & 0xff; - v[7] = (amount.low >> 0) & 0xff; - total = total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - } - if (+total.toString(10) === 0) { - undecided = true; - } - - for (var i = 0; i < outputs.length; i++) { - var output = outputs[i]; - - var amount = output.get('amount'); - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array(output.get('script').buffer)) - }; - - // Expected value - // little endian (keep this LE to compare with tx output value) - var ev = new Buffer(8); - ev[0] = (amount.low >> 0) & 0xff; - ev[1] = (amount.low >> 8) & 0xff; - ev[2] = (amount.low >> 16) & 0xff; - ev[3] = (amount.low >> 24) & 0xff; - ev[4] = (amount.high >> 0) & 0xff; - ev[5] = (amount.high >> 8) & 0xff; - ev[6] = (amount.high >> 16) & 0xff; - ev[7] = (amount.high >> 24) & 0xff; - - // Expected script - var es = script.buffer.slice(script.offset, script.limit); - - // Actual value - var av = tx.outs[i].v; - - // Actual script - var as = tx.outs[i].s; - - // XXX allow changing of script as long as address is same - // var as = es; - - // XXX allow changing of script as long as address is same - // var network = pd.get('network') === 'main' ? 'livenet' : 'testnet'; - // var es = bitcore.Address.fromScriptPubKey(new bitcore.Script(es), network)[0]; - // var as = bitcore.Address.fromScriptPubKey(new bitcore.Script(tx.outs[i].s), network)[0]; - - if (undecided) { - av = ev = new Buffer([0]); - } - - // Make sure the tx's output script and values match the payment request's. - if (av.toString('hex') !== ev.toString('hex') || as.toString('hex') !== es.toString('hex')) { - // Verifiable outputs do not match outputs of merchant - // data. We should not sign this transaction proposal! - return false; - } - - // Checking the merchant data itself isn't technically - // necessary as long as we check the transaction, but - // we can do it for good measure. - var ro = txp.merchant.pr.pd.outputs[i]; - - // Actual value - // little endian (keep this LE to compare with the ev above) - var av = new Buffer(8); - av[0] = (ro.amount.low >> 0) & 0xff; - av[1] = (ro.amount.low >> 8) & 0xff; - av[2] = (ro.amount.low >> 16) & 0xff; - av[3] = (ro.amount.low >> 24) & 0xff; - av[4] = (ro.amount.high >> 0) & 0xff; - av[5] = (ro.amount.high >> 8) & 0xff; - av[6] = (ro.amount.high >> 16) & 0xff; - av[7] = (ro.amount.high >> 24) & 0xff; - - // Actual script - var as = new Buffer(ro.script.buffer, 'hex') - .slice(ro.script.offset, ro.script.limit); - - if (av.toString('hex') !== ev.toString('hex') || as.toString('hex') !== es.toString('hex')) { - return false; - } - } - - return true; }; /** @@ -2243,7 +2009,7 @@ Wallet.prototype.subscribeToAddresses = function() { var addrInfo = this.publicKeyRing.getAddressesInfo(); this.blockchain.subscribe(_.pluck(addrInfo, 'addressStr')); - log.debug('Subscribed to ' + addrInfo.length + ' addresses'); + log.debug('Subscribed to ' + addrInfo.length + ' addresses'); }; /** @@ -2263,7 +2029,7 @@ Wallet.prototype.addressIsOwn = function(addrStr) { /** * Estimate a tx fee in satoshis given its input count - * only for spending all wallet funds + * (only used when spending all wallet funds) */ Wallet.estimatedFee = function(unspentCount) { preconditions.checkArgument(_.isNumber(unspentCount)); @@ -2337,15 +2103,14 @@ Wallet.prototype.maxRejectCount = function() { /** * @callback getUnspentCallback - * @TODO: Document this better + * @desc Get a list of unspent transaction outputs * @param {string} error * @param {Object[]} safeUnspendList * @param {Object[]} unspentList - */ -/** - * @desc Get a list of unspent transaction outputs * @param {getUnspentCallback} cb */ + +// TODO: Can we add cache to getUnspent? Wallet.prototype.getUnspent = function(cb) { var self = this; this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { @@ -2368,6 +2133,7 @@ Wallet.prototype.getUnspent = function(cb) { }); }; +// TODO. not used. Wallet.prototype.removeTxWithSpentInputs = function(cb) { var self = this; @@ -2420,137 +2186,189 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { }; /** - * @desc Create a transaction proposal - * @TODO: Document more + * spend + * + * @desc Spends coins from the wallet + * Create a Transaction Proposal and send it + * to copayers (broadcast it in a 1-x wallet) + * @param {object} opts + * @param {string} opts.toAddress address to send coins + * @param {number} opts.amountSat amount in satoshis + * @param {string} opts.comment optional transaction proposal private comment (for copayers) + * @param {string} opts.url optional (payment protocol URL). If this is given, toAddress will be ignored, and amount could be ignored or not, depending on the payment protocol request. + * @param {signCallback} cb */ -Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) { +Wallet.prototype.spend = function(opts, cb) { + preconditions.checkArgument(_.isObject(opts)); + log.debug('create Options', opts); + var self = this; + var toAddress = opts.toAddress; + var amountSat = opts.amountSat; + var comment = opts.comment; + var url = opts.url; - if (_.isFunction(opts)) { - cb = opts; - opts = {}; + // PayPro? Fetch payment data and recurse + if (url && !opts.merchantData) { + return self.fetchPaymentRequest({ + url: url, + memo: comment, + amount: amountSat, + }, function(err, merchantData) { + if (err) return cb(err); + opts.merchantData = merchantData; + opts.toAddress = merchantData.outs[0].address; + opts.amountSat = parseInt(merchantData.outs[0].amountSatStr); + return self.spend(opts, cb); + }); } - opts = opts || {}; - if (_.isUndefined(opts.spendUnconfirmed)) { - opts.spendUnconfirmed = this.spendUnconfirmed; - } + preconditions.checkArgument(amountSat, 'no amount'); + preconditions.checkArgument(toAddress, 'no address'); this.getUnspent(function(err, safeUnspent) { - if (err) return cb(new Error('Could not get list of UTXOs')); + if (err) { + log.info(err); + return cb(new Error('Spend: Could not get list of UTXOs')); + } - var ntxid; + var ntxid, txp; try { - ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts); - log.debug('TX Created: ntxid', ntxid); + txp = self._createTxProposal(toAddress, + amountSat, comment, safeUnspent, opts.builderOpts); } catch (e) { + log.error(e); return cb(e); } + if (opts.merchantData) { + txp.addMerchantData(opts.merchantData); + } + + var ntxid = self.txProposals.add(txp); if (!ntxid) { return cb(new Error('Error creating the transaction')); } + log.debug('TXP Added: ', ntxid); + + console.log('[Wallet.js.2233]'); //TODO self.sendIndexes(); - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('txProposalsUpdated'); - return cb(null, ntxid); + // Needs only one signature? Broadcast it! + if (!self.requiresMultipleSignatures()) { + self.broadcastTx(ntxid, cb); + } else { + self.sendTxProposal(ntxid); + self.emitAndKeepAlive('txProposalsUpdated'); + return cb(null, ntxid, Wallet.TX_PROPOSAL_SENT); + } }); }; -// TODO (eordano): Move this to bitcore -var sanitize = function(address) { - if (/^bitcoin:/g.test(address)) { +/** + * _getAddress + * Returns an Address object from an address string or a BIP21 URL.* + * @param address + * @return { bitcore.Address } + */ + +Wallet._getAddress = function(address) { + if (/ ^ bitcoin: /g.test(address)) { return new BIP21(address).address; } return new Address(address); }; -/** - * @desc Create a transaction proposal - * @TODO: Document more - */ -Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos, opts) { - var pkr = this.publicKeyRing; - var priv = this.privateKey; - opts = opts || {}; - toAddress = sanitize(toAddress); - preconditions.checkArgument(toAddress.network().name === this.getNetworkName(), 'networkname mismatch'); - preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); - preconditions.checkState(priv, 'no private key'); - if (comment) preconditions.checkArgument(comment.length <= 100); +Wallet.prototype._getBuilder = function(opts) { + opts = opts || {}; if (!opts.remainderOut) { opts.remainderOut = { address: this._doGenerateAddress(true).toString() }; } + if (_.isUndefined(opts.spendUnconfirmed)) { + opts.spendUnconfirmed = this.spendUnconfirmed; + } for (var k in Wallet.builderOpts) { opts[k] = Wallet.builderOpts[k]; } - var b = new Builder(opts) - .setUnspent(utxos) - .setOutputs([{ - address: toAddress.data, - amountSatStr: amountSatStr, - }]); + return new Builder(opts); +}; - log.debug('Creating TX: Builder ready'); + +/* + * _createTxProposal + * Creates a transaction proposal and run many sanity checks + * + * @param toAddress + * @param amountSat + * @param comment (optional) + * @param utxos + * @param builderOpts bitcore.TransactionBuilder options(like spendUnconfirmed) + * @return {TxProposal} The newly created transaction proposal.* + * Throws errors on unexpected inputs. + */ + +Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utxos, builderOpts) { + preconditions.checkArgument(toAddress); + preconditions.checkArgument(amountSat); + preconditions.checkArgument(_.isArray(utxos)); + preconditions.checkArgument(!comment || comment.length <= 100, 'Comment too long'); + + var pkr = this.publicKeyRing; + var priv = this.privateKey; + var addr = Wallet._getAddress(toAddress); + + preconditions.checkState(addr && addr.data && addr.isValid(), 'Bad address:' + addr.toString()); + + preconditions.checkArgument(addr.network().name === this.getNetworkName(), 'networkname mismatch'); + preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); + preconditions.checkState(priv, 'no private key'); + + var b = this._getBuilder(builderOpts); + + b.setUnspent(utxos) + .setOutputs([{ + address: addr.data, + amountSatStr: amountSat, + }]); var selectedUtxos = b.getSelectedUnspent(); if (selectedUtxos.length > TX_MAX_INS) - throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.length + ' inputs. Aborting'); + throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.length + + ' inputs. Aborting'); var inputChainPaths = selectedUtxos.map(function(utxo) { return pkr.pathForAddress(utxo.address); }); - - b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - var myId = this.getMyCopayerId(); - var now = Date.now(); + b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); var tx = b.build(); - if (!tx.countInputSignatures(0)) - throw new Error('Could not sign generated tx'); - - var txSize = tx.getSize(); - - if (txSize / 1024 > TX_MAX_SIZE_KB) - throw new Error('BIG: Resulting TX is too big ' + txSize + ' bytes. Aborting'); - - - var me = {}; - me[myId] = now; - - var meSeen = {}; - if (priv) meSeen[myId] = now; - - var ntxid = this.txProposals.add(new TxProposal({ + var myId = this.getMyCopayerId(); + var keys = priv.getForPaths(inputChainPaths); + return new TxProposal({ inputChainPaths: inputChainPaths, - signedBy: me, - seenBy: meSeen, - creator: myId, - createdTs: now, + comment: comment, builder: b, - comment: comment - })); - return ntxid; + creator: myId, + signWith: keys, + }); + + return txp; }; + /** * @desc Updates all the indexes for the current publicKeyRing. This scans * the blockchain looking for transactions on derived addresses. * - * Triggers a wallet {@link Wallet#store} call * @param {Function} callback - called when all indexes have been updated. Receives an error, if any, as first argument */ Wallet.prototype.updateIndexes = function(callback) { @@ -2696,7 +2514,7 @@ Wallet.prototype.getNetwork = function() { */ Wallet.prototype._checkAddressBook = function(key) { if (this.addressBook[key] && this.addressBook[key].copayerId != -1) { - throw new Error('This address already exists in your Address Book: ' + address); + throw new Error('This address already exists in your Address Book'); } }; @@ -2710,44 +2528,17 @@ Wallet.prototype.setAddressBook = function(key, label) { this._checkAddressBook(key); var copayerId = this.getMyCopayerId(); var ts = Date.now(); - var payload = { - address: key, - label: label, - copayerId: copayerId, - createdTs: ts - }; var newEntry = { hidden: false, createdTs: ts, copayerId: copayerId, label: label, - signature: this.signJson(payload) }; this.addressBook[key] = newEntry; - this.sendAddressBook(); + this.sendAddressBook(null, key); this.emitAndKeepAlive('addressBookUpdated'); }; -/** - * @desc Verifies that an addressbook entry is correctly signed by a copayer - * - * @param {Object} rcvEntry - the entry in the address book - * @param {string} senderId - the pubkey of a copayer - * @param {string} key - the base58 encoded address - * @return {boolean} true if the signature matches - */ -Wallet.prototype.verifyAddressbookEntry = function(rcvEntry, senderId, key) { - if (!key) throw new Error('Keys are required'); - var signature = rcvEntry.signature; - var payload = { - address: key, - label: rcvEntry.label, - copayerId: rcvEntry.copayerId, - createdTs: rcvEntry.createdTs - }; - return this.verifySignedJson(senderId, payload, signature); -}; - /** * @desc Hides or unhides an address book entry * @param {string} key - the address in the addressbook @@ -2782,122 +2573,6 @@ Wallet.prototype.isReady = function() { return this.publicKeyRing.isComplete(); }; -/** - * @desc Sign a JSON - * - * @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order - * @param {Object} payload - the payload to verify - * @return {string} base64 encoded string - */ -Wallet.prototype.signJson = function(payload) { - var key = new bitcore.Key(); - key.private = new Buffer(this.getMyCopayerIdPriv(), 'hex'); - key.regenerateSync(); - var sign = bitcore.Message.sign(JSON.stringify(payload), key); - return sign.toString('hex'); -} - -/** - * @desc Verify that a JSON object is correctly signed - * - * @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order - * - * @param {string} senderId - a sender's public key, hex encoded - * @param {Object} payload - the object to verify - * @param {string} signature - a sender's public key, hex encoded - * @return {boolean} - */ -Wallet.prototype.verifySignedJson = function(senderId, payload, signature) { - var pubkey = new Buffer(senderId, 'hex'); - var sign = new Buffer(signature, 'hex'); - var v = bitcore.Message.verifyWithPubKey(pubkey, JSON.stringify(payload), sign); - return v; -} - -/** - * @desc Create a HTTP request - * @TODO: This shouldn't be a wallet responsibility - */ -Wallet.request = function(options, callback) { - if (_.isString(options)) { - options = { - uri: options - }; - } - - options.method = options.method || 'GET'; - options.headers = options.headers || {}; - - var ret = { - success: function(cb) { - this._success = cb; - return this; - }, - error: function(cb) { - this._error = cb; - return this; - }, - _success: function() {; - }, - _error: function(_, err) { - throw err; - } - }; - - var method = (options.method || 'GET').toUpperCase(); - var uri = options.uri || options.url; - var req = options; - - req.headers = req.headers || {}; - req.body = req.body || req.data || {}; - - var xhr = new XMLHttpRequest(); - xhr.open(method, uri, true); - - Object.keys(req.headers).forEach(function(key) { - var val = req.headers[key]; - if (key === 'Content-Length') return; - if (key === 'Content-Transfer-Encoding') return; - xhr.setRequestHeader(key, val); - }); - - if (req.responseType) { - xhr.responseType = req.responseType; - } - - xhr.onload = function(event) { - var response = xhr.response; - var buf = new Uint8Array(response); - var headers = {}; - (xhr.getAllResponseHeaders() || '').replace( - /(?:\r?\n|^)([^:\r\n]+): *([^\r\n]+)/g, - function($0, $1, $2) { - headers[$1.toLowerCase()] = $2; - } - ); - return ret._success(buf, xhr.status, headers, options); - }; - - xhr.onerror = function(event) { - var status; - if (xhr.status === 0 || !xhr.statusText) { - status = 'HTTP Request Error: This endpoint likely does not support cross-origin requests.'; - } else { - status = xhr.statusText; - } - return ret._error(null, status, null, options); - }; - - if (req.body) { - xhr.send(req.body); - } else { - xhr.send(null); - } - - return ret; -}; - - /** * @desc Return a list of past transactions * @@ -2918,6 +2593,9 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { var proposals = self.getTxProposals(); var satToUnit = 1 / self.settings.unitToSatoshi; + var indexedProposals = _.indexBy(proposals, 'sentTxid'); + + function extractInsOuts(tx) { // Inputs var inputs = _.map(tx.vin, function(item) { @@ -3007,17 +2685,16 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { tx.amount = tx.amountSat * satToUnit; tx.minedTs = !_.isNaN(tx.time) ? tx.time * 1000 : undefined; - var proposal = _.findWhere(proposals, { - sentTxid: tx.txid - }); - + var proposal = indexedProposals[tx.txid]; if (proposal) { + // TODO refactor tx.comment = proposal.comment; tx.sentTs = proposal.sentTs; tx.merchant = proposal.merchant; tx.peerActions = proposal.peerActions; tx.finallyRejected = proposal.finallyRejected; tx.merchant = proposal.merchant; + tx.paymentAckMemo = proposal.paymentAckMemo; tx.peerActions = proposal.peerActions; tx.finallyRejected = proposal.finallyRejected; diff --git a/js/plugins/InsightStorage.js b/js/plugins/InsightStorage.js index c65ccbfe2..25a125be8 100644 --- a/js/plugins/InsightStorage.js +++ b/js/plugins/InsightStorage.js @@ -156,6 +156,8 @@ InsightStorage.prototype.setItem = function(name, value, callback) { var passphrase = this.getPassphrase(); var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); var registerUrl = this.storeUrl + '/save'; + + log.debug('setItem ' + name + ' size:'+ (value.length/1024).toFixed(1) + 'kb' ); this.request.post({ url: registerUrl, headers: { diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 5eb2084d0..34f9937be 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -46,7 +46,7 @@ angular.module('copayApp.services') }; root.onError = function(scope) { - if (scope) { + if (scope) { scope.loading = false; } } @@ -64,12 +64,6 @@ angular.module('copayApp.services') }; - root.updateTxsAndBalance = function(w) { - root.updateTxs(w); - root.updateBalance(w, function() { - $rootScope.$digest(); - }); - }; root.installWalletHandlers = function($scope, w) { @@ -129,35 +123,44 @@ angular.module('copayApp.services') } }); w.on('newAddresses', function() { - root.updateTxsAndBalance(w); + root.updateBalance(w); }); w.on('txProposalsUpdated', function() { - root.updateTxsAndBalance(w); + if (root.isFocusedWallet(wid)) { + root.updateTxs(); + } + }); + + w.on('paymentACK', function(memo) { + notification.success('Payment Acknowledged', memo); }); w.on('txProposalEvent', function(e) { - root.updateTxsAndBalance(w); + if (root.isFocusedWallet(wid)) { + root.updateTxs(); + } + // TODO: add wallet name notification var user = w.publicKeyRing.nicknameForCopayer(e.cId); var name = w.getName(); switch (e.type) { case 'new': - notification.info('['+ name +'] New Transaction', + notification.info('[' + name + '] New Transaction', $filter('translate')('You received a transaction proposal from') + ' ' + user); break; case 'signed': - notification.info('['+ name +'] Transaction Signed', + notification.info('[' + name + '] Transaction Signed', $filter('translate')('A transaction was signed by') + ' ' + user); break; case 'rejected': - notification.info('['+ name +'] Transaction Rejected', + notification.info('[' + name + '] Transaction Rejected', $filter('translate')('A transaction was rejected by') + ' ' + user); break; case 'corrupt': - notification.error('['+ name +'] Transaction Error', - $filter('translate')('Received corrupt transaction from') + ' ' + user); + notification.error('[' + name + '] Transaction Error', + $filter('translate')('Received corrupt transaction from') + ' ' + user); break; } $rootScope.$digest(); @@ -181,20 +184,12 @@ angular.module('copayApp.services') notification.enableHtml5Mode(); // for chrome: if support, enable it uriHandler.register(); $rootScope.unitName = config.unitName; - $rootScope.txAlertCount = 0; + $rootScope.pendingTxCount = 0; $rootScope.initialConnection = true; $rootScope.reconnecting = false; $rootScope.isCollapsed = true; $rootScope.iden = iden; - - // TODO - // $rootScope.$watch('txAlertCount', function(txAlertCount) { - // if (txAlertCount && txAlertCount > 0) { - // - // notification.info('New Transaction', ($rootScope.txAlertCount == 1) ? 'You have a pending transaction proposal' : $filter('translate')('You have') + ' ' + $rootScope.txAlertCount + ' ' + $filter('translate')('pending transaction proposals'), txAlertCount); - // } - // }); }; @@ -275,7 +270,7 @@ angular.module('copayApp.services') r.lockedBalanceBTC = (balanceSat - safeBalanceSat) / COIN; - if (r.safeUnspentCount){ + if (r.safeUnspentCount) { var estimatedFee = copay.Wallet.estimatedFee(r.safeUnspentCount); r.topAmount = (((r.availableBalance * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi); } @@ -319,8 +314,6 @@ angular.module('copayApp.services') w.balanceInfo = {}; var scope = root.isFocusedWallet(w.id) && !refreshAll ? $rootScope : w.balanceInfo; - root.updateAddressList(); - var wid = w.getId(); if (_balanceCache[wid]) { @@ -345,7 +338,7 @@ angular.module('copayApp.services') }); }; - root.computeAlternativeAmount = function(w, tx, cb) { + root.setAlternativeAmount = function(w, tx, cb) { rateService.whenAvailable(function() { _.each(tx.outs, function(out) { var valueSat = out.value * w.settings.unitToSatoshi; @@ -356,12 +349,13 @@ angular.module('copayApp.services') }); }; - root.updateTxs = function(w) { - w = w || $rootScope.wallet; - if (!w) return root.onErrorDigest(); + root.updateTxs = function() { + var w = $rootScope.wallet; + if (!w) return; + var res = w.getPendingTxProposals(); _.each(res.txs, function(tx) { - root.computeAlternativeAmount(w, tx); + root.setAlternativeAmount(w, tx); if (tx.merchant) { var url = tx.merchant.request_url; var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1]; @@ -369,14 +363,11 @@ angular.module('copayApp.services') } }); $rootScope.txps = res.txs; - if ($rootScope.pendingTxCount < res.pendingForUs) { - $rootScope.txAlertCount = res.pendingForUs; - } $rootScope.pendingTxCount = res.pendingForUs; }; root.deleteWallet = function($scope, w, cb) { - if (!w) return root.onErrorDigest(); + if (!w) return root.onErrorDigest(); var name = w.getName(); $rootScope.iden.deleteWallet(w.id, function() { notification.info(name + ' deleted', $filter('translate')('This wallet was deleted')); diff --git a/js/util/HTTP.js b/js/util/HTTP.js new file mode 100644 index 000000000..00daa6d93 --- /dev/null +++ b/js/util/HTTP.js @@ -0,0 +1,75 @@ +module.exports = { + request: function(options, callback) { + preconditions.checkArgument(_.isObject(options)); + + options.method = options.method || 'GET'; + options.headers = options.headers || {}; + var ret = { + success: function(cb) { + this._success = cb; + return this; + }, + error: function(cb) { + this._error = cb; + return this; + }, + _success: function() {; + }, + _error: function(_, err) { + throw err; + } + }; + + var method = (options.method || 'GET').toUpperCase(); + var url = options.url; + var req = options; + + req.headers = req.headers || {}; + req.body = req.body || req.data || {}; + + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + + Object.keys(req.headers).forEach(function(key) { + var val = req.headers[key]; + if (key === 'Content-Length') return; + if (key === 'Content-Transfer-Encoding') return; + xhr.setRequestHeader(key, val); + }); + + if (req.responseType) { + xhr.responseType = req.responseType; + } + + xhr.onload = function(event) { + var response = xhr.response; + var buf = new Uint8Array(response); + var headers = {}; + (xhr.getAllResponseHeaders() || '').replace( + /(?:\r?\n|^)([^:\r\n]+): *([^\r\n]+)/g, + function($0, $1, $2) { + headers[$1.toLowerCase()] = $2; + } + ); + return ret._success(buf, xhr.status, headers, options); + }; + + xhr.onerror = function(event) { + var status; + if (xhr.status === 0 || !xhr.statusText) { + status = 'HTTP Request Error: This endpoint likely does not support cross-origin requests.'; + } else { + status = xhr.statusText; + } + return ret._error(null, status, null, options); + }; + + if (req.body) { + xhr.send(req.body); + } else { + xhr.send(null); + } + + return ret; + }, +}; diff --git a/karma.conf.js b/karma.conf.js index da06fc59c..84647dae2 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -56,7 +56,6 @@ module.exports = function(config) { 'js/init.js', 'test/mocks/FakeBlockchainSocket.js', - 'test/mocks/FakePayProServer.js', 'test/mocha.conf.js', diff --git a/test/PayPro.js b/test/PayPro.js deleted file mode 100644 index c8a19bb7d..000000000 --- a/test/PayPro.js +++ /dev/null @@ -1,923 +0,0 @@ -'use strict'; - -var Wallet = copay.Wallet; -var PrivateKey = copay.PrivateKey; -var Network = requireMock('FakeNetwork'); -var Blockchain = requireMock('FakeBlockchain'); -var TransactionBuilder = bitcore.TransactionBuilder; -var Transaction = bitcore.Transaction; -var Address = bitcore.Address; -var PayPro = bitcore.PayPro; -var bignum = bitcore.Bignum; -var startServer = copay.FakePayProServer; // TODO should be require('./mocks/FakePayProServer'); - -var server; - -var walletConfig = { - requiredCopayers: 1, - totalCopayers: 1, - spendUnconfirmed: true, - reconnectDelay: 100, - networkName: 'testnet' -}; - -var getNewEpk = function() { - return new PrivateKey({ - networkName: walletConfig.networkName, - }) - .deriveBIP45Branch() - .extendedPublicKeyString(); -}; - -describe('PayPro (in Wallet) model', function() { - - if (!is_browser) { - var createW = function(N, conf) { - var c = JSON.parse(JSON.stringify(conf || walletConfig)); - if (!N) N = c.totalCopayers; - - var mainPrivateKey = new copay.PrivateKey({ - networkName: walletConfig.networkName - }); - var mainCopayerEPK = mainPrivateKey.deriveBIP45Branch().extendedPublicKeyString(); - c.privateKey = mainPrivateKey; - - c.publicKeyRing = new copay.PublicKeyRing({ - networkName: c.networkName, - requiredCopayers: Math.min(N, c.requiredCopayers), - totalCopayers: N, - }); - c.publicKeyRing.addCopayer(mainCopayerEPK); - - c.txProposals = new copay.TxProposals({ - networkName: c.networkName, - }); - - var network = new Network(walletConfig.network); - var blockchain = new Blockchain(walletConfig.blockchain); - c.network = network; - c.blockchain = blockchain; - - c.addressBook = { - '2NFR2kzH9NUdp8vsXTB4wWQtTtzhpKxsyoJ': { - label: 'John', - copayerId: '026a55261b7c898fff760ebe14fd22a71892295f3b49e0ca66727bc0a0d7f94d03', - createdTs: 1403102115, - hidden: false - }, - '2MtP8WyiwG7ZdVWM96CVsk2M1N8zyfiVQsY': { - label: 'Jennifer', - copayerId: '032991f836543a492bd6d0bb112552bfc7c5f3b7d5388fcbcbf2fbb893b44770d7', - createdTs: 1403103115, - hidden: false - } - }; - - c.networkName = walletConfig.networkName; - c.version = '0.0.1'; - - c.network = sinon.stub(); - c.network.setHexNonce = sinon.stub(); - c.network.setHexNonces = sinon.stub(); - c.network.getHexNonce = sinon.stub(); - c.network.getHexNonces = sinon.stub(); - c.network.send = sinon.stub(); - - return new Wallet(c); - } - - var unspentTest = [{ - "address": "dummy", - "scriptPubKey": "dummy", - "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", - "vout": 1, - "amount": 10, - "confirmations": 7 - }]; - - var createW2 = function(privateKeys, N, conf) { - if (!N) N = 3; - var w = createW(N, conf); - should.exist(w); - - var pkr = w.publicKeyRing; - - for (var i = 0; i < N - 1; i++) { - if (privateKeys) { - var k = privateKeys[i]; - pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : getNewEpk()); - } else { - pkr.addCopayer(getNewEpk()); - } - } - - return w; - }; - - var cachedW2 = null; - var cachedW2obj = null; - var cachedCreateW2 = function() { - if (!cachedW2) { - cachedW2 = createW2(); - cachedW2obj = cachedW2.toObj(); - cachedW2obj.opts.reconnectDelay = 100; - } - - Wallet._newAsync = sinon.stub().returns(new Network(walletConfig.network)); - Wallet._newInsight = sinon.stub().returns(new Blockchain(walletConfig.blockchain)); - - var w = Wallet.fromObj(cachedW2obj, { - blockchainOpts: {}, - networkOpts: {}, - }); - return w; - }; - - var createWallet = function() { - var w = cachedCreateW2(); - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - w.getUnspent = function(cb) { - return setTimeout(function() { - return cb(null, unspentTest, unspentTest); - }, 1); - }; - return w; - }; - - it('#start the example server', function(done) { - startServer(function(err, s) { - if (err) return done(err); - server = s; - server.uri = 'https://localhost:8080/-'; - done(); - }); - }); - - var pr; - var ppw; - - ppw = createWallet(); - - it('#retrieve a payment request message via http', function(done) { - var w = ppw; - should.exist(w); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': 'application/octet-stream', - 'Content-Length': '0' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: {} - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.GET['/-/request'](req, function(err, res, body) { - var data = PayPro.PaymentRequest.decode(body); - pr = new PayPro(); - pr = pr.makePaymentRequest(data); - done(); - }); - }); - - it('#send a payment message via http', function(done) { - var w = ppw; - should.exist(w); - - var ver = pr.get('payment_details_version'); - var pki_type = pr.get('pki_type'); - var pki_data = pr.get('pki_data'); - var details = pr.get('serialized_payment_details'); - var sig = pr.get('signature'); - - var certs = PayPro.X509Certificates.decode(pki_data); - certs = certs.certificate; - - var verified = pr.verify(); - - if (!verified) { - return done(new Error('Server sent a bad signature.')); - } - - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - - var network = pd.get('network'); - var outputs = pd.get('outputs'); - var time = pd.get('time'); - var expires = pd.get('expires'); - var memo = pd.get('memo'); - var payment_url = pd.get('payment_url'); - var merchant_data = pd.get('merchant_data'); - - var priv = w.privateKey; - var pkr = w.publicKeyRing; - - var opts = { - remainderOut: { - address: w._doGenerateAddress(true).toString() - } - }; - - var outs = []; - outputs.forEach(function(output) { - var amount = output.get('amount'); - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - - // big endian - var v = new Buffer(8); - v[0] = (amount.high >> 24) & 0xff; - v[1] = (amount.high >> 16) & 0xff; - v[2] = (amount.high >> 8) & 0xff; - v[3] = (amount.high >> 0) & 0xff; - v[4] = (amount.low >> 24) & 0xff; - v[5] = (amount.low >> 16) & 0xff; - v[6] = (amount.low >> 8) & 0xff; - v[7] = (amount.low >> 0) & 0xff; - - var s = script.buffer.slice(script.offset, script.limit); - var net = network === 'main' ? 'livenet' : 'testnet'; - var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), net); - - outs.push({ - address: addr[0].toString(), - amountSatStr: bitcore.Bignum.fromBuffer(v, { - // XXX for some reason, endian is ALWAYS 'big' - // in node (in the browser it behaves correctly) - endian: 'big', - size: 1 - }).toString(10) - }); - }); - - var b = new bitcore.TransactionBuilder(opts) - .setUnspent(unspentTest) - .setOutputs(outs); - - outputs.forEach(function(output, i) { - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - var s = script.buffer.slice(script.offset, script.limit); - b.tx.outs[i].s = s; - }); - - var selectedUtxos = b.getSelectedUnspent(); - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - if (priv) { - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - } - - var tx = b.build(); - - var refund_outputs = []; - - var refund_to = w.publicKeyRing.getPubKeys(0, false, w.getMyCopayerId())[0]; - - var total = outputs.reduce(function(total, _, i) { - // XXX reverse endianness to work around bignum bug: - var txv = tx.outs[i].v; - var v = new Buffer(8); - for (var j = 0; j < 8; j++) v[j] = txv[7 - j]; - return total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }, bitcore.Bignum('0', 10)); - - var rpo = new PayPro(); - rpo = rpo.makeOutput(); - - rpo.set('amount', +total.toString(10)); - - rpo.set('script', - Buffer.concat([ - new Buffer([ - 118, // OP_DUP - 169, // OP_HASH160 - 76, // OP_PUSHDATA1 - 20, // number of bytes - ]), - // needs to be ripesha'd - bitcore.util.sha256ripe160(refund_to), - new Buffer([ - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ]) - ]) - ); - - refund_outputs.push(rpo.message); - - var pay = new PayPro(); - pay = pay.makePayment(); - pay.set('merchant_data', new Buffer([0, 1])); - pay.set('transactions', [tx.serialize()]); - pay.set('refund_to', refund_outputs); - pay.set('memo', 'Hi server, I would like to give you some money.'); - - pay = pay.serialize(); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE, - 'Content-Length': pay.length + '' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: pay, - data: pay - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.POST['/-/pay'](req, function(err, res, body) { - if (err) return done(err); - - var data = PayPro.PaymentACK.decode(body); - var ack = new PayPro(); - ack = ack.makePaymentACK(data); - - var payment = ack.get('payment'); - var memo = ack.get('memo'); - - payment = PayPro.Payment.decode(payment); - var pay = new PayPro(); - payment = pay.makePayment(payment); - - var tx = payment.message.transactions[0]; - - if (!tx) { - return done(new Error('No tx in payment ACK.')); - } - - if (tx.buffer) { - tx.buffer = new Buffer(new Uint8Array(tx.buffer)); - tx.buffer = tx.buffer.slice(tx.offset, tx.limit); - var ptx = new bitcore.Transaction(); - ptx.parse(tx.buffer); - tx = ptx; - } - - var ackTotal = outputs.reduce(function(total, _, i) { - // XXX reverse endianness to work around bignum bug: - var txv = tx.outs[i].v; - var v = new Buffer(8); - for (var j = 0; j < 8; j++) v[j] = txv[7 - j]; - return total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }, bitcore.Bignum('0', 10)); - - ackTotal.toString(10).should.equal(total.toString(10)); - - done(); - }); - }); - - it('#retrieve a payment request message via http', function(done) { - var w = ppw; - should.exist(w); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': 'application/octet-stream', - 'Content-Length': '0' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: {} - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.GET['/-/request'](req, function(err, res, body) { - var data = PayPro.PaymentRequest.decode(body); - pr = new PayPro(); - pr = pr.makePaymentRequest(data); - done(); - }); - }); - - it('#send a payment message via http', function(done) { - var w = ppw; - should.exist(w); - - var ver = pr.get('payment_details_version'); - var pki_type = pr.get('pki_type'); - var pki_data = pr.get('pki_data'); - var details = pr.get('serialized_payment_details'); - var sig = pr.get('signature'); - - var certs = PayPro.X509Certificates.decode(pki_data); - certs = certs.certificate; - - var verified = pr.verify(); - - if (!verified) { - return done(new Error('Server sent a bad signature.')); - } - - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - - var network = pd.get('network'); - var outputs = pd.get('outputs'); - var time = pd.get('time'); - var expires = pd.get('expires'); - var memo = pd.get('memo'); - var payment_url = pd.get('payment_url'); - var merchant_data = pd.get('merchant_data'); - - var priv = w.privateKey; - var pkr = w.publicKeyRing; - - var opts = { - remainderOut: { - address: w._doGenerateAddress(true).toString() - } - }; - - var outs = []; - outputs.forEach(function(output) { - var amount = output.get('amount'); - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - - // big endian - var v = new Buffer(8); - v[0] = (amount.high >> 24) & 0xff; - v[1] = (amount.high >> 16) & 0xff; - v[2] = (amount.high >> 8) & 0xff; - v[3] = (amount.high >> 0) & 0xff; - v[4] = (amount.low >> 24) & 0xff; - v[5] = (amount.low >> 16) & 0xff; - v[6] = (amount.low >> 8) & 0xff; - v[7] = (amount.low >> 0) & 0xff; - - var s = script.buffer.slice(script.offset, script.limit); - var net = network === 'main' ? 'livenet' : 'testnet'; - var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), net); - - outs.push({ - address: addr[0].toString(), - amountSatStr: bitcore.Bignum.fromBuffer(v, { - // XXX for some reason, endian is ALWAYS 'big' - // in node (in the browser it behaves correctly) - endian: 'big', - size: 1 - }).toString(10) - }); - }); - - var b = new bitcore.TransactionBuilder(opts) - .setUnspent(unspentTest) - .setOutputs(outs); - - outputs.forEach(function(output, i) { - var script = { - offset: output.get('script').offset, - limit: output.get('script').limit, - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)) - }; - var s = script.buffer.slice(script.offset, script.limit); - b.tx.outs[i].s = s; - }); - - var selectedUtxos = b.getSelectedUnspent(); - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - if (priv) { - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - } - - var tx = b.build(); - - var refund_outputs = []; - - var refund_to = w.publicKeyRing.getPubKeys(0, false, w.getMyCopayerId())[0]; - - var total = outputs.reduce(function(total, _, i) { - // XXX reverse endianness to work around bignum bug: - var txv = tx.outs[i].v; - var v = new Buffer(8); - for (var j = 0; j < 8; j++) v[j] = txv[7 - j]; - return total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }, bitcore.Bignum('0', 10)); - - var rpo = new PayPro(); - rpo = rpo.makeOutput(); - - rpo.set('amount', +total.toString(10)); - - rpo.set('script', - Buffer.concat([ - new Buffer([ - 118, // OP_DUP - 169, // OP_HASH160 - 76, // OP_PUSHDATA1 - 20, // number of bytes - ]), - // needs to be ripesha'd - bitcore.util.sha256ripe160(refund_to), - new Buffer([ - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ]) - ]) - ); - - refund_outputs.push(rpo.message); - - var pay = new PayPro(); - pay = pay.makePayment(); - pay.set('merchant_data', new Buffer([0, 1])); - pay.set('transactions', [tx.serialize()]); - pay.set('refund_to', refund_outputs); - pay.set('memo', 'Hi server, I would like to give you some money.'); - - pay = pay.serialize(); - - var req = { - headers: { - 'Host': 'localhost:8080', - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE + ', ' + PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE, - 'Content-Length': pay.length + '' - }, - socket: { - remoteAddress: 'localhost', - remotePort: 8080 - }, - body: pay, - data: pay - }; - - Object.keys(req.headers).forEach(function(key) { - req.headers[key.toLowerCase()] = req.headers[key]; - }); - - server.POST['/-/pay'](req, function(err, res, body) { - if (err) return done(err); - - var data = PayPro.PaymentACK.decode(body); - var ack = new PayPro(); - ack = ack.makePaymentACK(data); - - var payment = ack.get('payment'); - var memo = ack.get('memo'); - - payment = PayPro.Payment.decode(payment); - var pay = new PayPro(); - payment = pay.makePayment(payment); - - var tx = payment.message.transactions[0]; - - if (!tx) { - return done(new Error('No tx in payment ACK.')); - } - - if (tx.buffer) { - tx.buffer = new Buffer(new Uint8Array(tx.buffer)); - tx.buffer = tx.buffer.slice(tx.offset, tx.limit); - var ptx = new bitcore.Transaction(); - ptx.parse(tx.buffer); - tx = ptx; - } - - var ackTotal = outputs.reduce(function(total, _, i) { - // XXX reverse endianness to work around bignum bug: - var txv = tx.outs[i].v; - var v = new Buffer(8); - for (var j = 0; j < 8; j++) v[j] = txv[7 - j]; - return total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }, bitcore.Bignum('0', 10)); - - ackTotal.toString(10).should.equal(total.toString(10)); - - should.exist(ack); - memo.should.equal('Thank you for your payment!'); - - done(); - }); - }); - - ppw = createWallet(); - - it('#retrieve a payment request message via model', function(done) { - var w = ppw; - should.exist(w); - // Caches Payment Request but does not add TX proposal - w.fetchPaymentTx({ - uri: 'https://localhost:8080/-/request' - }, function(err, merchantData) { - if (err) return done(err); - merchantData.pr.pd.payment_url.should.equal('https://localhost:8080/-/pay'); - return done(); - }); - }); - - it('#add tx proposal based on payment message via model', function(done) { - var w = ppw; - should.exist(w); - var options = { - uri: 'https://localhost:8080/-/request' - }; - var req = w.paymentRequests[options.uri]; - should.exist(req); - delete w.paymentRequests[options.uri]; - w.receivePaymentRequest(options, req.pr, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - w._ntxid = ntxid; - merchantData.pr.pd.payment_url.should.equal('https://localhost:8080/-/pay'); - return done(); - }); - }); - - it('#add tx proposal based on payment message via model ', function(done) { - - var w = ppw; - should.exist(w); - - w.sendPaymentTx(w._ntxid, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - should.exist(merchantData.ack); - merchantData.ack.memo.should.equal('Thank you for your payment!'); - return done(); - }); - }); - - it('#send a payment request using payment api', function(done) { - var w = createWallet(); - should.exist(w); - var uri = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var memo = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: uri, - memo: memo - }, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - if (w.isShared()) { - return done(); - } else { - w.sendPaymentTx(ntxid, { - memo: memo - }, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - return done(); - }); - } - }); - }); - - it('#send a payment request with merchant prefix', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'Merchant: ' + server.uri + '/request\nMemo: foo'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - var uri; - - // Replicates code in controllers/send.js: - if (address.indexOf('bitcoin:') === 0) { - uri = new bitcore.BIP21(address).data; - } else if (address.indexOf('Merchant: ') === 0) { - uri = address.split(/\s+/)[1]; - } - - w.createPaymentTx({ - uri: uri, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - if (w.isShared()) { - should.exist(ntxid); - should.exist(merchantData); - return done(); - } else { - should.exist(merchantData); - w.sendTx(ntxid, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - return done(); - }); - } - }); - }); - - it('#send a payment request with bitcoin uri', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: address, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - if (w.isShared()) { - should.exist(ntxid); - should.exist(merchantData); - return done(); - } else { - w.sendTx(ntxid, function(txid, merchantData) { - should.exist(txid); - should.exist(merchantData); - return done(); - }); - } - }); - }); - - it('#try to sign a tampered payment request (raw)', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: address, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - - // Tamper with payment request in its raw form: - var data = new Buffer(merchantData.raw, 'hex'); - data = PayPro.PaymentRequest.decode(data); - var pr = new PayPro(); - pr = pr.makePaymentRequest(data); - var details = pr.get('serialized_payment_details'); - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - var outputs = pd.get('outputs'); - outputs[outputs.length - 1].set('amount', 1000000000); - pd.set('outputs', outputs); - pr.set('serialized_payment_details', pd.serialize()); - merchantData.raw = pr.serialize().toString('hex'); - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(false); - - return done(); - }); - }); - - it('#try to sign a tampered payment request (abstract)', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: address, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - - // Tamper with payment request in its abstract form: - var outputs = merchantData.pr.pd.outputs; - var output = outputs[outputs.length - 1]; - var amount = output.amount; - amount.low = 2; - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(false); - - return done(); - }); - }); - - it('#try to sign a tampered txp tx (abstract)', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: address, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - - // Tamper with payment request in its abstract form: - var txp = w.txProposals.get(ntxid); - var tx = txp.builder.tx || txp.builder.build(); - tx.outs[0].v = new Buffer([2, 0, 0, 0, 0, 0, 0, 0]); - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(false); - - return done(); - }); - }); - - it('#sign an untampered payment request', function(done) { - var w = createWallet(); - should.exist(w); - var address = 'bitcoin:2NBzZdFBoQymDgfzH2Pmnthser1E71MmU47?amount=0.00003&r=' + server.uri + '/request'; - var commentText = 'Hello, server. I\'d like to make a payment.'; - w.createPaymentTx({ - uri: address, - memo: commentText - }, function(err, ntxid, merchantData) { - should.equal(err, null); - should.exist(ntxid); - should.exist(merchantData); - - var myId = w.getMyCopayerId(); - var txp = w.txProposals.get(ntxid); - should.exist(txp); - should.exist(txp.signedBy[myId]); - should.not.exist(txp.rejectedBy[myId]); - - w.verifyPaymentRequest(ntxid).should.equal(true); - - return done(); - }); - }); - - it('#close payment server', function(done) { - server.close(function() { - return done(); - }); - }); - } -}); diff --git a/test/TxProposal.js b/test/TxProposal.js index c74f0ffcd..c1b69a5ac 100644 --- a/test/TxProposal.js +++ b/test/TxProposal.js @@ -10,17 +10,23 @@ var util = bitcore.util; var networks = bitcore.networks; var FakeBuilder = requireMock('FakeBuilder'); var TxProposal = copay.TxProposal; - -var dummyProposal = new TxProposal({ - creator: 1, - createdTs: 1, - builder: new FakeBuilder(), - inputChainPaths: ['m/1'], -}); +var Buffer = bitcore.Buffer; var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; describe('TxProposal', function() { + + function dummyProposal() { + return new TxProposal({ + creator: 'creator', + createdTs: 1, + builder: new FakeBuilder(), + inputChainPaths: ['m/1'], + }) + }; + + + describe('new', function() { it('should fail to create an instance with wrong arguments', function() { @@ -208,8 +214,8 @@ describe('TxProposal', function() { }); it('#_verifyScriptSig, two signatures', function() { // Data taken from bitcore's TransactionBuilder test - var txp = dummyProposal; - var tx = dummyProposal.builder.build(); + var txp = dummyProposal(); + var tx = dummyProposal().builder.build(); var ret = TxProposal._verifySignatures(pubkeys, validScriptSig, tx.hashForSignature()); ret.should.deep.equal(['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3']); }); @@ -223,13 +229,13 @@ describe('TxProposal', function() { Buffer.isBuffer(info.script.getBuffer()).should.equal(true); }); it('#_updateSignedBy', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp._inputSigners.should.deep.equal([ ['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3'] ]); }); describe('#_check', function() { - var txp = dummyProposal; + var txp = dummyProposal(); var backup = txp.builder.tx.ins; it('OK', function() { @@ -272,8 +278,96 @@ describe('TxProposal', function() { txp.builder.tx.ins[0].s = backup; }); }); + + describe('#_checkPayPro', function() { + var txp, md; + beforeEach(function() { + txp = dummyProposal(); + txp.paymentProtocolURL = '123'; + md = { + request_url: '123', + pr: { + pd: { + expires: 123, + memo: 'memo', + + }, + }, + total: '1230', + outs: [{ + address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', + amountSatStr: "123" + }], + expires: 92345678900, + }; + }); + + it('OK no merchant data', function() { + txp._checkPayPro(); + }); + it('OK merchant data', function() { + txp.addMerchantData(md); + }); + it('NOK URL', function() { + txp.paymentProtocolURL = '1234'; + (function() { + txp.addMerchantData(md); + }).should.throw('Mismatch'); + }); + it('NOK OUTS', function() { + md.outs = []; + (function() { + txp.addMerchantData(md); + }).should.throw('outputs'); + }); + it('NOK OUTS (case 2)', function() { + md.outs = [{}, {}]; + (function() { + txp.addMerchantData(md); + }).should.throw('outputs'); + }); + it('NOK OUTS (case 3)', function() { + md.outs = [{}, {}]; + (function() { + txp.addMerchantData(md); + }).should.throw('outputs'); + }); + it('NOK Amount', function() { + md.total = undefined; + (function() { + txp.addMerchantData(md); + }).should.throw('amount'); + }); + it('NOK Outs case 4', function() { + md.outs[0].address = 'aaa'; + (function() { + txp.addMerchantData(md); + }).should.throw('address'); + }); + it('NOK Outs case 5', function() { + md.outs[0].amountSatStr = '432'; + (function() { + txp.addMerchantData(md); + }).should.throw('amount'); + }); + + it('NOK Expired', function() { + md.expires = 1; + (function() { + txp.addMerchantData(md); + }).should.throw('expired'); + }); + + it('OK Expired but sent', function() { + md.expires = 2; + txp.sentTs = 1; + txp.addMerchantData(md); + }); + + }); + describe('#merge', function() { - var txp = dummyProposal; + var txp = dummyProposal(); var backup = txp.builder.tx.ins; it('with self', function() { var hasChanged = txp.merge(txp); @@ -283,7 +377,7 @@ describe('TxProposal', function() { it('with less signatures', function() { var backup = txp.builder.vanilla.scriptSig[0]; txp.builder.merge = function() { - // 2 signatures. + // Only one signatures. this.vanilla.scriptSig = ['0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex'); }; @@ -307,7 +401,7 @@ describe('TxProposal', function() { }); describe('#setCopayers', function() { it("should fails if Tx has no creator", function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.signedBy = { 'hugo': 1 }; @@ -319,7 +413,7 @@ describe('TxProposal', function() { }).should.throw('no creator'); }); it("should fails if Tx is not signed by creator", function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.creator = 'creator'; txp.signedBy = { 'hugo': 1 @@ -336,7 +430,7 @@ describe('TxProposal', function() { it("should fails if Tx has unmapped signatures", function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.creator = 'creator'; txp.signedBy = { creator: 1 @@ -353,7 +447,7 @@ describe('TxProposal', function() { // This was disabled. Unnecessary to check this. it.skip("should be signed by sender", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk1', 'pk0'] @@ -372,7 +466,7 @@ describe('TxProposal', function() { it("should set signedBy (trivial case)", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk1', 'pk0'] @@ -390,7 +484,7 @@ describe('TxProposal', function() { txp.signedBy['creator'].should.gte(ts); }); it("should assign creator", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk0'] @@ -409,7 +503,7 @@ describe('TxProposal', function() { txp.seenBy['creator'].should.equal(txp.createdTs); }) it("New tx should have only 1 signature", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp.signedBy = {}; delete txp['creator']; @@ -431,7 +525,7 @@ describe('TxProposal', function() { }) it("if signed, should not change ts", function() { - var txp = dummyProposal; + var txp = dummyProposal(); var ts = Date.now(); txp._inputSigners = [ ['pk0', 'pk1'] @@ -456,25 +550,25 @@ describe('TxProposal', function() { describe('micelaneous functions', function() { it('should report rejectCount', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectCount().should.equal(0); txp.setRejected(['juan']) txp.rejectCount().should.equal(1); }); it('should report isPending 1', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectedBy = []; txp.sentTxid = 1; txp.isPending(3).should.equal(false); }); it('should report isPending 2', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectedBy = []; txp.sentTxid = null; txp.isPending(3).should.equal(true); }); it('should report isPending 3', function() { - var txp = dummyProposal; + var txp = dummyProposal(); txp.rejectedBy = [1, 2, 3, 4]; txp.sentTxid = null; txp.isPending(3).should.equal(false); diff --git a/test/Wallet.js b/test/Wallet.js index 63665173c..71dd56044 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -1,5 +1,4 @@ 'use strict'; - var Wallet = copay.Wallet; var PrivateKey = copay.PrivateKey; var Network = requireMock('FakeNetwork'); @@ -8,6 +7,8 @@ var Builder = requireMock('FakeBuilder'); var TransactionBuilder = bitcore.TransactionBuilder; var Transaction = bitcore.Transaction; var Address = bitcore.Address; +var PayPro = bitcore.PayPro; +var Buffer = bitcore.Buffer; function assertObjectEqual(a, b) { @@ -58,6 +59,15 @@ var addCopayers = function(w) { describe('Wallet model', function() { + var sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + it('should fail to create an instance', function() { (function() { new Wallet(walletConfig) @@ -210,6 +220,8 @@ describe('Wallet model', function() { blockchainOpts: {}, networkOpts: {}, }); + if (w.httpUtil.request.restore) + w.httpUtil.request.restore(); return w; }; @@ -222,7 +234,7 @@ describe('Wallet model', function() { unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); var f = function() { - var ntxid = w.createTxSync( + var ntxid = w._createTxProposal( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', '123456789', null, @@ -233,18 +245,18 @@ describe('Wallet model', function() { }); + it('#create, check builder opts', function() { var w = cachedCreateW2(); unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var ntxid = w.createTxSync( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, unspentTest ); - var t = w.txProposals; - var opts = JSON.parse(t.txps[ntxid].builder.vanilla.opts); + var opts = JSON.parse(txp.builder.vanilla.opts); opts.signhash.should.equal(1); (opts.lockTime === null).should.be.true; should.not.exist(opts.fee); @@ -257,21 +269,20 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var ntxid = w.createTxSync( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, unspentTest ); - var t = w.txProposals; - var txp = t.txps[ntxid]; Object.keys(txp._inputSigners).length.should.equal(1); var tx = txp.builder.build(); should.exist(tx); chai.expect(txp.comment).to.be.null; tx.isComplete().should.equal(false); Object.keys(txp.seenBy).length.should.equal(1); + Object.keys(txp.signedBy).length.should.equal(1); }); it('#create with comment', function() { @@ -282,15 +293,13 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var ntxid = w.createTxSync( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', comment, unspentTest ); - var t = w.txProposals; - var txp = t.txps[ntxid]; var tx = txp.builder.build(); should.exist(tx); txp.comment.should.equal(comment); @@ -304,16 +313,14 @@ describe('Wallet model', function() { unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); - var badCreate = function() { - w.createTxSync( + (function() { + w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', comment, unspentTest ); - } - - chai.expect(badCreate).to.throw(Error); + }).should.throw('Comment'); }); it('#addressIsOwn', function() { @@ -336,21 +343,19 @@ describe('Wallet model', function() { for (var index = 0; index < 3; index++) { unspentTest[0].address = w.publicKeyRing.getAddress(index, isChange, w.publicKey).toString(); unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(index, isChange, w.publicKey); - w.createTxSync( + var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', null, unspentTest ); - var t = w.txProposals; - var k = Object.keys(t.txps)[0]; - var tx = t.txps[k].builder.build(); + var tx = txp.builder.build(); should.exist(tx); tx.isComplete().should.equal(false); tx.countInputMissingSignatures(0).should.equal(2); - (t.txps[k].signedBy[w.privateKey.getId()] - ts > 0).should.equal(true); - (t.txps[k].seenBy[w.privateKey.getId()] - ts > 0).should.equal(true); + (txp.signedBy[w.privateKey.getId()] - ts > 0).should.equal(true); + (txp.seenBy[w.privateKey.getId()] - ts > 0).should.equal(true); } } }); @@ -762,7 +767,10 @@ describe('Wallet model', function() { 'confirmations': 10, 'confirmationsFromCache': false }]; + sinon.stub(w, 'sendIndexes'); var addr = w.generateAddress().toString(); + w.sendIndexes.restore(); + utxo[0].address = addr; utxo[0].scriptPubKey = (new bitcore.Address(addr)).getScriptPubKey().serialize().toString('hex'); return utxo; @@ -770,77 +778,173 @@ describe('Wallet model', function() { var toAddress = 'mjfAe7YrzFujFf8ub5aUrCaN5GfSABdqjh'; var amountSatStr = '10000'; - it('should create transaction', function(done) { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - ntxid.length.should.equal(64); - done(); - }); - }); - - it('should create & sign transaction from received funds', function(done) { - var k2 = new PrivateKey({ - networkName: walletConfig.networkName - }); - - var w = createW2([k2]); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - w.on('txProposalsUpdated', function() { - w.getTxProposals()[0].signedByUs.should.equal(true); - w.getTxProposals()[0].rejectedByUs.should.equal(false); + describe('#spend', function() { + it('should create transaction', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + ntxid.length.should.equal(64); done(); }); - w.privateKey = k2; - w.sign(ntxid).should.equal.true; }); - }); - it('should fail to reject a signed transaction', function() { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - (function() { + + it('should create & sign transaction from received funds', function(done) { + var k2 = new PrivateKey({ + networkName: walletConfig.networkName + }); + + var w = createW2([k2]); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + w.on('txProposalsUpdated', function() { + w.getTxProposals()[0].signedByUs.should.equal(true); + w.getTxProposals()[0].rejectedByUs.should.equal(false); + done(); + }); + w.privateKey = k2; + w.sign(ntxid).should.equal.true; + }); + }); + it('should fail to reject a signed transaction', function() { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + (function() { + w.reject(ntxid); + }).should.throw('reject a signed'); + }); + }); + + it('should create & reject transaction', function(done) { + var w = cachedCreateW2(); + var oldK = w.privateKey; + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + var s = sinon.stub(w, 'getMyCopayerId').returns('213'); + Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(0); w.reject(ntxid); - }).should.throw('reject a signed'); - }); - }); - - it('should create & reject transaction', function(done) { - var w = cachedCreateW2(); - var oldK = w.privateKey; - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - var s = sinon.stub(w, 'getMyCopayerId').returns('213'); - Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(0); - w.reject(ntxid); - Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(1); - w.txProposals.get(ntxid).rejectedBy['213'].should.gt(1); - s.restore(); - done(); - }); - }); - it('should create & sign & send a transaction', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - w.sendTx(ntxid, function(txid) { - txid.length.should.equal(64); + Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(1); + w.txProposals.get(ntxid).rejectedBy['213'].should.gt(1); + s.restore(); done(); }); }); + it('should send a TX proposal to peers if incomplete', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + + sinon.spy(w, 'sendIndexes'); + sinon.spy(w, 'sendTxProposal'); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, id, status) { + should.not.exist(err); + should.exist(id); + status.should.equal(Wallet.TX_PROPOSAL_SENT); + w.sendTxProposal.calledOnce.should.equal(true); + w.sendIndexes.calledOnce.should.equal(true); + done(); + }); + }); + it('should broadcast a TX if complete', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.spy(w, 'sendIndexes'); + sinon.spy(w, 'sendTxProposal'); + sinon.spy(w, 'broadcastTx'); + sinon.stub(w, 'requiresMultipleSignatures').returns(false); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, id, status) { + should.not.exist(err); + should.exist(id); + status.should.equal(Wallet.TX_BROADCASTED); + w.sendTxProposal.calledOnce.should.equal(true); + w.sendIndexes.calledOnce.should.equal(true); + w.broadcastTx.calledOnce.should.equal(true); + done(); + }); + }); + + it('should return error if failing to send', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.stub(w, 'requiresMultipleSignatures').returns(false); + sinon.spy(w, 'sendIndexes'); + sinon.spy(w, 'sendTxProposal'); + sinon.stub(w, '_doBroadcastTx').yields('error'); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, id, status) { + err.should.equal('error'); + w.sendTxProposal.calledOnce.should.equal(false); + w.sendIndexes.calledOnce.should.equal(true); + done(); + }); + }); + it('should send TxProposal', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + w.sendTxProposal.bind(w).should.throw('Illegal Argument.'); + (function() { + w.sendTxProposal(ntxid); + }).should.not.throw(); + done(); + }); + }); + + it('should send all TxProposal', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + w.sendAllTxProposals.bind(w).should.not.throw(); + (function() { + w.sendAllTxProposals(); + }).should.not.throw(); + done(); + }); + }); + + }); - it('should fail to send incomplete transaction', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - var txp = w.txProposals.get(ntxid); + describe('#broadcastTx', function() { + it('should fail to send incomplete transaction', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); + var ntxid = w.txProposals.add(txp); + // Assign fake builder txp.builder = new Builder(); sinon.stub(txp.builder, 'build').returns({ @@ -849,59 +953,176 @@ describe('Wallet model', function() { } }); (function() { - w.sendTx(ntxid); + w.broadcastTx(ntxid); }).should.throw('Tx is not complete. Can not broadcast'); done(); }); - }); - it('should check if transaction already sent when failing to send', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - sinon.stub(w.blockchain, 'broadcast').yields({ - statusCode: 303 + + it('should broadcast a TX', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + var txp = w._createTxProposal(PP.outs[0].address, PP.outs[0].amountSatStr, 'hola', utxo); + var ntxid = w.txProposals.add(txp); + sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); + + w.broadcastTx(ntxid, function(err, txid, status) { + should.not.exist(err); + txid.should.equal(1234); + status.should.equal(Wallet.TX_BROADCASTED); + done(); }); - var spyCheckSentTx = sinon.spy(w, '_checkSentTx'); - w.sendTx(ntxid, function() {}); - chai.expect(spyCheckSentTx.calledOnce).to.be.true; - done(); }); - }); - it('should send TxProposal', function(done) { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - w.sendTxProposal.bind(w).should.throw('Illegal Argument.'); - (function() { - w.sendTxProposal(ntxid); - }).should.not.throw(); - done(); + + + it('should send Payment Messages on a PayPro payment', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + var txp = w._createTxProposal(PP.outs[0].address, PP.outs[0].amountSatStr, 'hola', utxo); + txp.paymentProtocolURL = PP.merchant_data.request_url; + txp.addMerchantData(PP.merchant_data); + var ntxid = w.txProposals.add(txp); + var success = sinon.stub().yields('paymentACK123').returns({ + error: sinon.stub(), + }); + + sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); + sinon.stub(w.httpUtil, 'request').returns({ + success: success, + }); + sinon.stub(w, 'onPayProPaymentAck'); + + + w.broadcastTx(ntxid, function(err, txid, status) { + should.not.exist(err); + txid.should.equal(1234); + status.should.equal(Wallet.TX_BROADCASTED); + w.httpUtil.request.calledOnce.should.equal(true); + w.httpUtil.request.getCall(0).args[0].url.should.equal('url123'); + success.calledOnce.should.equal(true); + w.onPayProPaymentAck.calledOnce.should.equal(true); + w.onPayProPaymentAck.getCall(0).args[1].should.equal('paymentACK123'); + done(); + }); }); }); - it('should send all TxProposal', function(done) { - var w = cachedCreateW2(); - var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - w.sendAllTxProposals.bind(w).should.not.throw(); - (function() { - w.sendAllTxProposals(); - }).should.not.throw(); - done(); + + describe('#fetchPaymentRequest', function() { + it('should fetch a payment request', function(done) { + var w = cachedCreateW2(); + sinon.stub(w, 'parsePaymentRequest').returns({ + hola: 1 + }); + var opts = { + a: 1, + url: 'http://xxx', + }; + + var rawData = 'wqer'; + var e = sinon.stub(); + e.error = sinon.stub(); + + var s = sinon.stub(); + s.success = sinon.stub().yields(rawData).returns(e); + + sinon.stub(w.httpUtil, 'request').returns(s); + + w.fetchPaymentRequest(opts, function(err, merchantData) { + should.not.exist(err); + should.exist(merchantData); + w.parsePaymentRequest.firstCall.args.should.deep.equal([opts, rawData]); + done(); + }); + }); + + it('should return error on fetch error', function(done) { + var w = cachedCreateW2(); + var opts = { + a: 1, + url: 'http://xxx', + }; + + var rawData = 'wqer'; + var e = sinon.stub(); + e.error = sinon.stub().yields(null, 'status'); + + var s = sinon.stub(); + s.success = sinon.stub().returns(e); + sinon.stub(w.httpUtil, 'request').returns(s); + w.fetchPaymentRequest(opts, function(err, merchantData) { + err.toString().should.contain('status'); + done(); + }); + }); + + }); + + // TODO parsePaymentRequest should have more tests, + // PP.getRequest should be parametrizable + describe('#parsePaymentRequest', function() { + it('should parse a Payment Request', function() { + var now = Date.now() / 1000; + var w = cachedCreateW2(); + var opts = { + url: 'http://xxx', + }; + var data = PP.getRequest(); + var md = w.parsePaymentRequest(opts, data); + md.outs.should.deep.equal(PP.outs); + md.request_url.should.equal(opts.url); + md.pr.untrusted.should.equal(true); + md.expires.should.be.above(now); }); }); - describe('#createTx', function() { + describe('#spend', function() { it('should fail if insight server is down', function(done) { var w = cachedCreateW2(); var utxo = createUTXO(w); - w.blockchain.fixUnspent(utxo); sinon.stub(w, 'getUnspent').yields('error', null); - w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { - chai.expect(err.message).to.equal('Could not get list of UTXOs'); + w.spend({ + toAddress: toAddress, + amountSat: amountSatStr, + }, function(err, ntxid) { + err.message.should.contain('UTXOs'); + done(); + }); + }); + + + it('should fail with broken PayPro', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.stub(w, 'fetchPaymentRequest').yields('error'); + w.spend({ + url: 'test', + }, function(err, ntxid) { + should.exist(err); + done(); + }); + }); + + + it('should create a TX with PayPro', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + sinon.stub(w, 'fetchPaymentRequest').yields(null, { + outs: [{ + address: 'n2Wz7KjyzBJVaNMBN88Lj1YUHMDZSAGeMV', + amountSatStr: '123400', + }], + request_url: 'url', + pr: { + signature: '123', + }, + total: '123400', + }); + w.spend({ + url: 'test', + }, function(err, ntxid) { + should.not.exist(err); done(); }); }); @@ -1010,12 +1231,14 @@ describe('Wallet model', function() { - describe('#send', function() { + describe('#_sendToPeers', function() { it('should call this.network.send', function() { var w = cachedCreateW2(); var save = w.network.send; w.network.send = sinon.spy(); - w.send(); + w._sendToPeers(null, { + type: 'hola' + }); w.network.send.calledOnce.should.equal(true); w.network.send = save; }); @@ -1211,7 +1434,7 @@ describe('Wallet model', function() { createdTs: 1404769393509, hidden: false, label: "adsf", - signature: "3046022100d4cdefef66ab8cea26031d5df03a38fc9ec9b09b0fb31d3a26b6e204918e9e78022100ecdbbd889ec99ea1bfd471253487af07a7fa7c0ac6012ca56e10e66f335e4586" + dummy: 'foo', } }, walletId: "11d23e638ed84c06", @@ -1223,58 +1446,9 @@ describe('Wallet model', function() { Object.keys(w.addressBook).length.should.equal(2); w._onAddressBook(senderId, data, true); Object.keys(w.addressBook).length.should.equal(3); + should.exist(w.addressBook['3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z'].createdTs); + should.not.exist(w.addressBook['3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z'].dummy); }); - - it('should return signed object', function() { - var w = createW(); - var payload = { - address: 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx', - label: 'Faucet', - copayerId: '026a55261b7c898fff760ebe14fd22a71892295f3b49e0ca66727bc0a0d7f94d03', - createdTs: 1403102115 - }; - should.exist(w.signJson(payload)); - }); - - it('should verify signed object', function() { - var w = createW(); - - var payload = { - address: "3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z", - label: "adsf", - copayerId: "03baa45498fee1045fa8f91a2913f638dc3979b455498924d3cf1a11303c679cdb", - createdTs: 1404769393509 - } - - var signature = "3046022100d4cdefef66ab8cea26031d5df03a38fc9ec9b09b0fb31d3a26b6e204918e9e78022100ecdbbd889ec99ea1bfd471253487af07a7fa7c0ac6012ca56e10e66f335e4586"; - - var pubKey = "03baa45498fee1045fa8f91a2913f638dc3979b455498924d3cf1a11303c679cdb"; - - w.verifySignedJson(pubKey, payload, signature).should.equal(true); - payload.label = 'Another'; - w.verifySignedJson(pubKey, payload, signature).should.equal(false); - }); - - it('should verify signed addressbook entry', function() { - var w = createW(); - var key = "3Ae1ieAYNXznm7NkowoFTu5MkzgrTfDz8Z"; - var pubKey = "03baa45498fee1045fa8f91a2913f638dc3979b455498924d3cf1a11303c679cdb"; - w.addressBook[key] = { - copayerId: pubKey, - createdTs: 1404769393509, - hidden: false, - label: "adsf", - signature: "3046022100d4cdefef66ab8cea26031d5df03a38fc9ec9b09b0fb31d3a26b6e204918e9e78022100ecdbbd889ec99ea1bfd471253487af07a7fa7c0ac6012ca56e10e66f335e4586" - }; - - w.verifyAddressbookEntry(w.addressBook[key], pubKey, key).should.equal(true); - w.addressBook[key].label = 'Another'; - w.verifyAddressbookEntry(w.addressBook[key], pubKey, key).should.equal(false); - (function() { - w.verifyAddressbookEntry(); - }).should.throw(); - }); - }); it('#getNetworkName', function() { @@ -1486,43 +1660,74 @@ describe('Wallet model', function() { }); }); - - describe('_onTxProposal', function() { - var w; + var w, data, txp; + + beforeEach(function() { + w = cachedCreateW(); + data = { + txProposal: { + dummy: 1, + }, + }; + sinon.stub(w.txProposals, 'deleteOne'); + }); + + + it('should handle corrupt tx', function() { + w.txProposals.merge = sinon.stub().throws(new Error('test error')); + + sinon.stub(w, 'on'); + w._onTxProposal('senderID', data); + w.on.called.should.equal(false); + }); + + it('should call _processIncomingTxProposal', function(done) { + var args = { + xxx: 'yyy', + new: true, + txp: { + setCopayers: sinon.stub(), + }, + }; + sinon.stub(w.txProposals, 'merge').returns(args); + sinon.stub(w, '_processIncomingTxProposal').yields(null); + sinon.stub(w, '_getKeyMap').returns(null); + + w.on('txProposalEvent', function(e) { + e.type.should.equal('new'); + w._processIncomingTxProposal.getCall(0).args[0].should.deep.equal(args); + done(); + }); + w._onTxProposal('senderID', data); + }); + + it('should handle corrupt tx, case2', function() { + sinon.stub(w.txProposals, 'merge').returns({ + ntxid: '1' + }); + sinon.stub(w, 'on'); + sinon.stub(w, '_getKeyMap').throws(new Error('test error')); + w._onTxProposal('senderID', data); + w.on.called.should.equal(false); + }); + }); + + + describe.skip('_onTxProposal', function() { + var w, data, txp; + beforeEach(function() { w = cachedCreateW(); w._getKeyMap = sinon.stub(); w.sendSeen = sinon.spy(); w.sendTxProposal = sinon.spy(); - }); - - it('should handle corrupt tx', function(done) { - var data = { + data = { txProposal: { dummy: 1, }, }; - w.txProposals.merge = sinon.stub().throws(new Error('test error')); - - var spy = sinon.spy(); - w.on('txProposalEvent', spy); - w.on('txProposalEvent', function(e) { - e.type.should.equal('corrupt'); - done(); - }); - - w._onTxProposal('senderID', data); - spy.called.should.be.true; - }); - - it('should handle new', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { + txp = { getSeen: sinon.stub().returns(false), setSeen: sinon.spy(), setCopayers: sinon.spy(), @@ -1542,21 +1747,24 @@ describe('Wallet model', function() { hasChanged: true, }); + }); + it('should handle new 1', function(done) { + var spy1 = sinon.spy(); var spy2 = sinon.spy(); w.on('txProposalEvent', spy1); w.on('txProposalsUpdated', spy2); w.on('txProposalEvent', function(e) { e.type.should.equal('new'); + spy1.called.should.be.true; + spy2.called.should.be.true; + txp.setSeen.calledOnce.should.be.true; + w.sendSeen.calledOnce.should.equal(true); + w.sendTxProposal.calledOnce.should.equal(true); done(); }); w._onTxProposal('senderID', data); - spy1.called.should.be.true; - spy2.called.should.be.true; - txp.setSeen.calledOnce.should.be.true; - w.sendSeen.calledOnce.should.be.true; - w.sendTxProposal.calledOnce.should.be.true; }); it('should handle signed', function(done) { @@ -1590,83 +1798,36 @@ describe('Wallet model', function() { w.on('txProposalsUpdated', spy2); w.on('txProposalEvent', function(e) { e.type.should.equal('signed'); + spy1.called.should.be.true; + spy2.called.should.be.true; + txp.setSeen.calledOnce.should.be.false; + w.sendSeen.calledOnce.should.be.false; + w.sendTxProposal.calledOnce.should.be.true; + done(); }); w._onTxProposal('senderID', data); - spy1.called.should.be.true; - spy2.called.should.be.true; - txp.setSeen.calledOnce.should.be.false; - w.sendSeen.calledOnce.should.be.false; - w.sendTxProposal.calledOnce.should.be.true; }); - it('should mark as broadcast when complete', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { - getSeen: sinon.stub().returns(true), - setCopayers: sinon.stub().returns(['new copayer']), - getSent: sinon.stub().returns(false), - setSent: sinon.spy(), - builder: { - build: sinon.stub().returns({ - isComplete: sinon.stub().returns(true), - }), - }, - }; - - w.txProposals.get = sinon.stub().returns(txp); - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: false, - hasChanged: true, - }); - w._checkSentTx = sinon.stub().yields('123'); - - w._onTxProposal('senderID', data); - txp.setSent.calledOnce.should.be.true; - txp.setSent.calledWith('123').should.be.true; - w.sendTxProposal.called.should.be.false; - done(); - }); it('should only mark as broadcast if found in the blockchain', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { - getSeen: sinon.stub().returns(true), - setCopayers: sinon.stub().returns(['new copayer']), - getSent: sinon.stub().returns(false), - setSent: sinon.spy(), - builder: { - build: sinon.stub().returns({ - isComplete: sinon.stub().returns(true), - }), - }, - }; - w.txProposals.get = sinon.stub().returns(txp); w.txProposals.merge = sinon.stub().returns({ ntxid: 1, txp: txp, new: false, - hasChanged: true, + hasChanged: false, }); w._checkSentTx = sinon.stub().yields(false); + w.on('txProposalEvent', function(e) { + txp.setSent.called.should.equal(false); + txp.setSent.calledWith(1).should.equal(false); + w.sendTxProposal.called.should.equal(false); + done(); + }); w._onTxProposal('senderID', data); - txp.setSent.called.should.be.false; - txp.setSent.calledWith(1).should.be.false; - w.sendTxProposal.called.should.be.false; - done(); }); it('should not overwrite sent info', function(done) { @@ -1734,13 +1895,13 @@ describe('Wallet model', function() { describe('_onReject', function() { - it('should fails if unknown tx', function() { + it('should do nothing on unknown tx', function() { var w = cachedCreateW(); - (function() { - w._onReject(1, { - ntxid: 1 - }, 1); - }).should.throw('Unknown TXP'); + var spy1 = sinon.spy(w, 'emitAndKeepAlive'); + w._onReject(1, { + ntxid: 1 + }, 1); + spy1.called.should.equal(false); }); it('should fail to reject a signed tx', function() { var w = cachedCreateW(); @@ -1788,13 +1949,13 @@ describe('Wallet model', function() { describe('_onSeen', function() { - it('should fails if unknown tx', function() { + it('should do nothing on unknown tx', function() { var w = cachedCreateW(); - (function() { - w._onReject(1, { - ntxid: 1 - }, 1); - }).should.throw('Unknown TXP'); + var spy1 = sinon.spy(w, 'emitAndKeepAlive'); + w._onReject(1, { + ntxid: 1 + }, 1); + spy1.called.should.equal(false); }); it('should set seen a tx', function() { var w = cachedCreateW(); @@ -2049,7 +2210,7 @@ describe('Wallet model', function() { }]; w.blockchain.getTransactions = sinon.stub().yields(null, { - items: txs.slice(2,3), + items: txs.slice(2, 3), totalItems: txs.length, }); w.getAddressesInfo = sinon.stub().returns([{ @@ -2293,6 +2454,17 @@ describe('Wallet model', function() { }); }); + // TODO + describe.skip('#onPayProPaymentAck', function() { + it('should emit', function() { + var w = cachedCreateW2(); + sinon.stub(w, 'emitAndKeepAlive'); + w.onPayProPaymentAck('id', 'data'); + + w.calledOnce.should.equal(true); + w.getCall(0).args.should.deep.equal(['paymentACK', 'data']); + }); + }); describe.skip('#read', function() { var network, blockchain; @@ -2394,3 +2566,129 @@ describe('Wallet model', function() { var o = '{"opts":{"id":"dbfe10c3fae71cea", "spendUnconfirmed":1,"requiredCopayers":3,"totalCopayers":5,"version":"0.0.5","networkName":"testnet"},"networkNonce":"0000000000000001","networkNonces":[],"publicKeyRing":{"walletId":"dbfe10c3fae71cea","networkName":"testnet","requiredCopayers":3,"totalCopayers":5,"indexes":[{"copayerIndex":2,"changeIndex":0,"receiveIndex":0}],"copayersExtPubKeys":["tpubD6NzVbkrYhZ4YGK8ZhZ8WVeBXNAAoTYjjpw9twCPiNGrGQYFktP3iVQkKmZNiFnUcAFMJRxJVJF6Nq9MDv2kiRceExJaHFbxUCGUiRhmy97","tpubD6NzVbkrYhZ4YKGDJkzWdQsQV3AcFemaQKiwNhV4RL8FHnBFvinidGdQtP8RKj3h34E65RkdtxjrggZYqsEwJ8RhhN2zz9VrjLnrnwbXYNc","tpubD6NzVbkrYhZ4YkDiewjb32Pp3Sz9WK2jpp37KnL7RCrHAyPpnLfgdfRnTdpn6DTWmPS7niywfgWiT42aJb1J6CjWVNmkgsMCxuw7j9DaGKB","tpubD6NzVbkrYhZ4XEtUAz4UUTWbprewbLTaMhR8NUvSJUEAh4Sidxr6rRPFdqqVRR73btKf13wUjds2i8vVCNo8sbKrAnyoTr3o5Y6QSbboQjk","tpubD6NzVbkrYhZ4Yj9AAt6xUVuGPVd8jXCrEE6V2wp7U3PFh8jYYvVad31b4VUXEYXzSnkco4fktu8r4icBsB2t3pCR3WnhVLedY2hxGcPFLKD"],"nicknameFor":{}},"txProposals":{"txps":[],"walletId":"dbfe10c3fae71cea","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPeoHLg3tY75z4xLeEe8MqAXLNcRA6J6UTRvHV8VZTXznt9eoTmSk1fwSrwZtMhY3XkNsceJ14h6sCXHSWinRqMSSbY8tfhHi","networkName":"testnet"},"addressBook":{},"settings":{"unitName":"BTC","unitToSatoshi":100000000,"unitDecimals":8,"alternativeName":"Argentine Peso","alternativeIsoCode":"ARS"}}'; }); + + + + +var x509 = { + priv: 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeFRKdUsyYUdMbjFkWEpLRGg0TXdQTFVrbDNISTVwR25HNWFjNGwvMGlobXE4Y3dDCitGVlBnWk1TNTlheWtpc0IrekM3dnR2a0prL2J2K0JTT1g3b3hkSXN1TDNkS1FGcHVYWFZmcmRiOTV3WW40TSsKL25qRWhYTWxoVk1IL09DaUFnOUpLaFRLV0w2R1JXWkFBaEE3bEJSaGdTTkRUaVRDNTFDYmlLN3hBNnBONCt0UQpIeG9tSlBYclpSa2JCMmtsT2ZXd2J2OTNZM0oxS0ZEK2kwUE1RSEx3N3JoRXVteEM5MytISFVWWVZIN0gxVFBaCkgxYmRVSkowMmdRZXlsSnNzWUNKeWRaUHpOVC96dXRzL0tKV2RSdjVseHdHOXU5dE1OTWdoSmJtQWFNa01HaSsKbzdQTkV5UDNxSEZyWXBZaHM1cHFMSE1STkI3OFFNOUllTmpMRndJREFRQUJBb0lCQVFERVJyalBiQUdjbmwxaAorZGIrOTczNGZ0aElBUkpWSko1dTRFK1JKcThSRWhGTEVLUFlKNW0yUC94dVZBMXpYV2xnYXhaRUZ6d1VRaUpZCjdsOEpLVjlwSHhReVlaQ1M4dndYZzhpWGtzdndQaWRvQmN1YW4vd0RWQ1FCZXk2VkxjVXpSYUd1Ui9sTHNYK1YKN2Z0QjBvUnFsSXFrYmNQZE1NdnFUeG93UnVoUG11Q3JWVGpPNHBiTnFuU09OUExPaUovRkFYYjJwZnpGZnBCUgpHeCtFTW16d2UrSEZuSkJHRGhIWjk5bm4vVEJmYUp6TlZDcURZLzNid3o1WDdIUU5ZN1QrSnlUVUZzZVE5NHhzCnpya2lidGRmVGNUanB1K1VoWm80c1p6Q3IrZkhHWm9FOUdEUHF0ZDRnQ3ByazRFS0pzbXFCRVN4QlhTRGhZZ04KOXBVRDM4c1pBb0dCQU9yZkRqdDZaL0ZDamFuVThXek5GaWYrOVQxQTJ4b013RDVWU2xNdVJyWW1HbGZyMEM5TQpmMUVvZ2l2dVRrYnA3cmtnZFRhWVRTYndmTnFaQkt4Y3R5YzdCaGRwWnhERVdKa2Z5cThxVngvem1Cek1JK1ZzCjJLYi9hcHZXcmJlb3NET0NyeUg1YzhKc1VUOXhUWDNYYnhFanlPSlFCU1lHRE1qUHlKNkU5czZMQW9HQkFOYnYKd2d0S2Nra0tLbDJhNXZzaGR2RENnNnFLL1FnT20vNktUSlVKRVNqaHoydFIrZlBWUjcwVEg5UmhoVFJscERXQgpCd3oyU2NCc1RRNDIvTGsxRnkyMFQvck12S3VmSEw1VE1BNGZ6NWRxMUxIbmN6ejZVazVnWEtBT09rUjlVdVhpClR0eTNoREcyQkM4Nk1LTVJ4SjUxRWJxam94d0VSMTAwU2FuTVBmTWxBb0dBSUhLY1pyOHNhUHBHMC9XbFBPREEKZE5vV1MxWVFidkxnQkR5SVBpR2doejJRV2lFcjY3em53ZkNVdXpqNiszVUtFKzFXQkNyYVRjemZrdHVjOTZyLwphcDRPNDJFZWFnU1dNT0ZoZ1AyYWQ4R1JmRGovcEl4N0NlY3pkVUFkVThnc1A1R0lYR3M0QU40eUEwL0Y0dUxHCloxbklRT3ZKS2syZnFvWjZNdHd2dEswQ2dZRUFnSjdGTGVDRTkzUmYyZGdDZFRHWGJZZlpKc3M1bEFLNkV0NUwKNmJ1ZFN5dWw1Z0VPWkgyekNsQlJjZFJSMUFNbSt1V1ZoSW8xcERLckFlQ2g1MnIvemRmakxLQXNIejkrQWQ3aQpHUEdzVmw0Vm5jaDFTMzQ0bHJKUGUzQklLZ2djL1hncDNTYnNzcHJMY2orT0wyZElrOUpXbzZ1Y3hmMUJmMkwwCjJlbGhBUWtDZ1lCWHN5elZWL1pKcVhOcFdDZzU1TDNVRm9UTHlLU3FsVktNM1dpRzVCS240QWF6VkNITCtHUVUKeHd4U2dSOWZRNEludStyUHJOM0lteWswbEtQR0Y5U3pDUlJUaUpGUjcyc05xbE82bDBWOENXUkFQVFBKY2dxVgoxVThOSEs4YjNaaUlvR0orbXNOenBkeHJqNjJIM0E2K1krQXNOWTRTbVVUWEg5eWpnK251a2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=', + pub: 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4VEp1SzJhR0xuMWRYSktEaDRNdwpQTFVrbDNISTVwR25HNWFjNGwvMGlobXE4Y3dDK0ZWUGdaTVM1OWF5a2lzQit6Qzd2dHZrSmsvYnYrQlNPWDdvCnhkSXN1TDNkS1FGcHVYWFZmcmRiOTV3WW40TSsvbmpFaFhNbGhWTUgvT0NpQWc5SktoVEtXTDZHUldaQUFoQTcKbEJSaGdTTkRUaVRDNTFDYmlLN3hBNnBONCt0UUh4b21KUFhyWlJrYkIya2xPZld3YnY5M1kzSjFLRkQraTBQTQpRSEx3N3JoRXVteEM5MytISFVWWVZIN0gxVFBaSDFiZFVKSjAyZ1FleWxKc3NZQ0p5ZFpQek5UL3p1dHMvS0pXCmRSdjVseHdHOXU5dE1OTWdoSmJtQWFNa01HaStvN1BORXlQM3FIRnJZcFloczVwcUxITVJOQjc4UU05SWVOakwKRndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==', + der: 'MIIDBjCCAe4CCQDI2qWdA3/VpDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE0MDcxNjAxMzM1MVoXDTE1MDcxNjAxMzM1MVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMUybitmhi59XVySg4eDMDy1JJdxyOaRpxuWnOJf9IoZqvHMAvhVT4GTEufWspIrAfswu77b5CZP27/gUjl+6MXSLLi93SkBabl11X63W/ecGJ+DPv54xIVzJYVTB/zgogIPSSoUyli+hkVmQAIQO5QUYYEjQ04kwudQm4iu8QOqTePrUB8aJiT162UZGwdpJTn1sG7/d2NydShQ/otDzEBy8O64RLpsQvd/hx1FWFR+x9Uz2R9W3VCSdNoEHspSbLGAicnWT8zU/87rbPyiVnUb+ZccBvbvbTDTIISW5gGjJDBovqOzzRMj96hxa2KWIbOaaixzETQe/EDPSHjYyxcCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAL6AMMfC3TlRcmsIgHxjVD4XYtISlldnrn2X9zvFbJKCpNy8XQQosQxrhyfzPHQKjlS2L/KCGMnjx9QkYD2Hlp1MJ1uVv9888th/gcZOv3Or3hQyi5K1Sh5xCG+69lUOqUEGu9B4irsqoFomQVbQolSy+t4apdJi7kuEDwFDk4gZiVEfsuX+naN5a6pCnWnhX1Vf4fKwfkLobKKXm2zQVsjxlwBAqOEmJGDLoRMXH56qJnEZ/dqsczaJOHQSi9mFEHL0r5rsEDTT5AVxdnBfNnyGaCH7/zANEko+FGBj1JdJaJgFTXdbxDoyoPTPD+LJqSK5XYToo46y/T0u9CLveNA==', + pem: 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU0Q0NRREkycVdkQTMvVnBEQU5CZ2txaGtpRzl3MEJBUVVGQURCRk1Rc3dDUVlEVlFRR0V3SkIKVlRFVE1CRUdBMVVFQ0F3S1UyOXRaUzFUZEdGMFpURWhNQjhHQTFVRUNnd1lTVzUwWlhKdVpYUWdWMmxrWjJsMApjeUJRZEhrZ1RIUmtNQjRYRFRFME1EY3hOakF4TXpNMU1Wb1hEVEUxTURjeE5qQXhNek0xTVZvd1JURUxNQWtHCkExVUVCaE1DUVZVeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjAKSUZkcFpHZHBkSE1nVUhSNUlFeDBaRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQgpBTVV5Yml0bWhpNTlYVnlTZzRlRE1EeTFKSmR4eU9hUnB4dVduT0pmOUlvWnF2SE1BdmhWVDRHVEV1ZldzcElyCkFmc3d1NzdiNUNaUDI3L2dVamwrNk1YU0xMaTkzU2tCYWJsMTFYNjNXL2VjR0orRFB2NTR4SVZ6SllWVEIvemcKb2dJUFNTb1V5bGkraGtWbVFBSVFPNVFVWVlFalEwNGt3dWRRbTRpdThRT3FUZVByVUI4YUppVDE2MlVaR3dkcApKVG4xc0c3L2QyTnlkU2hRL290RHpFQnk4TzY0Ukxwc1F2ZC9oeDFGV0ZSK3g5VXoyUjlXM1ZDU2ROb0VIc3BTCmJMR0FpY25XVDh6VS84N3JiUHlpVm5VYitaY2NCdmJ2YlREVElJU1c1Z0dqSkRCb3ZxT3p6Uk1qOTZoeGEyS1cKSWJPYWFpeHpFVFFlL0VEUFNIall5eGNDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFMNkFNTWZDMwpUbFJjbXNJZ0h4alZENFhZdElTbGxkbnJuMlg5enZGYkpLQ3BOeThYUVFvc1F4cmh5ZnpQSFFLamxTMkwvS0NHCk1uang5UWtZRDJIbHAxTUoxdVZ2OTg4OHRoL2djWk92M09yM2hReWk1SzFTaDV4Q0crNjlsVU9xVUVHdTlCNGkKcnNxb0ZvbVFWYlFvbFN5K3Q0YXBkSmk3a3VFRHdGRGs0Z1ppVkVmc3VYK25hTjVhNnBDblduaFgxVmY0Zkt3ZgprTG9iS0tYbTJ6UVZzanhsd0JBcU9FbUpHRExvUk1YSDU2cUpuRVovZHFzY3phSk9IUVNpOW1GRUhMMHI1cnNFCkRUVDVBVnhkbkJmTm55R2FDSDcvekFORWtvK0ZHQmoxSmRKYUpnRlRYZGJ4RG95b1BUUEQrTEpxU0s1WFlUb28KNDZ5L1QwdTlDTHZlTkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' +}; + +x509.priv = new Buffer(x509.priv, 'base64'); +x509.pub = new Buffer(x509.pub, 'base64'); +x509.der = new Buffer(x509.der, 'base64'); +x509.pem = new Buffer(x509.pem, 'base64'); + +var PP = {}; + +PP.outs = [{ + address: 'mkYn9qmYwMZfovTb6cd7yCGeNozqUyyhK7', + amountSatStr: '3000' +}]; + +PP.merchant_data = { + request_url: 'url123', + outs: PP.outs, + total: PP.outs[0].amountSatStr, + pr: { + pd: { + payment_url: 'url123' + } + } +}; + + + +PP.getRequest = function() { + + var uid = 0; + + var outputs = []; + + [2000, 1000].forEach(function(value) { + var po = new PayPro(); + po = po.makeOutput(); + // number of satoshis to be paid + po.set('amount', value); + + // TODO use bitcore / script!! + // a TxOut script where the payment should be sent. similar to OP_CHECKSIG + var addr = new bitcore.Address(PP.outs[0].address); + po.set('script', addr.getScriptPubKey().getBuffer()); + outputs.push(po.message); + }); + + /** + * Payment Details + */ + + var mdata = new Buffer([0]); + uid++; + if (uid > 0xffff) { + throw new Error('UIDs bigger than 0xffff not supported.'); + } else if (uid > 0xff) { + mdata = new Buffer([(uid >> 8) & 0xff, (uid >> 0) & 0xff]) + } else { + mdata = new Buffer([0, uid]) + } + var now = Date.now() / 1000 | 0; + var pd = new PayPro(); + pd = pd.makePaymentDetails(); + pd.set('network', 'test'); + pd.set('outputs', outputs); + pd.set('time', now); + pd.set('expires', now + 60 * 60 * 24); + pd.set('memo', 'Hello, this is the server, we would like some money.'); + pd.set('payment_url', 'https://pay_url'); + pd.set('merchant_data', mdata); + + /* + * PaymentRequest + */ + + var cr = new PayPro(); + cr = cr.makeX509Certificates(); + cr.set('certificate', [x509.der]); + + // We send the PaymentRequest to the customer + var pr = new PayPro(); + pr = pr.makePaymentRequest(); + pr.set('payment_details_version', 1); + pr.set('pki_type', 'x509+sha256'); + pr.set('pki_data', cr.serialize()); + pr.set('serialized_payment_details', pd.serialize()); + pr.sign(x509.priv); + + return pr.serialize(); +}; +PP.processPayment = function(payment) { + body = PayPro.Payment.decode(payment); + var pay = new PayPro(); + pay = pay.makePayment(body); + var merchant_data = pay.get('merchant_data'); + var transactions = pay.get('transactions'); + var refund_to = pay.get('refund_to'); + var memo = pay.get('memo'); + + // We send this to the customer after receiving a Payment + // Then we propogate the transaction through bitcoin network + var ack = new PayPro(); + ack = ack.makePaymentACK(); + ack.set('payment', pay.message); + ack.set('memo', 'Thank you for your payment!'); + + ack = ack.serialize(); + + transactions = transactions.map(function(tx) { + tx.buffer = new Buffer(new Uint8Array(tx.buffer)); + tx.buffer = tx.buffer.slice(tx.offset, tx.limit); + var ptx = new bitcore.Transaction(); + ptx.parse(tx.buffer); + return ptx; + }); + + return ack; +}; diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js index 9b3aea95b..8d81531ff 100644 --- a/test/mocks/FakeBuilder.js +++ b/test/mocks/FakeBuilder.js @@ -2,10 +2,22 @@ var bitcore = bitcore || require('bitcore'); var Script = bitcore.Script; -var VALID_SCRIPTSIG_BUF = new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae','hex'); +var VALID_SCRIPTSIG_BUF = new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae', 'hex'); function Tx() { - this.ins = [{s: VALID_SCRIPTSIG_BUF }]; + this.ins = [{ + s: VALID_SCRIPTSIG_BUF + }]; +}; + + +Tx.prototype.serialize = function() { + return new Buffer('1234','hex'); +}; + + +Tx.prototype.getSize = function() { + return 1; }; Tx.prototype.getHashType = function() { @@ -23,26 +35,32 @@ function FakeBuilder() { this.test = 1; this.tx = new Tx(); this.signhash = 1; - this.inputMap = [{ address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', + this.inputMap = [{ + address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', scriptPubKey: new Script(new Buffer('a914dc0623476aefb049066b09b0147a022e6eb8429187', 'hex')), scriptType: 4, - i: 0 }]; + i: 0 + }]; - this.vanilla = { - scriptSig: [VALID_SCRIPTSIG_BUF], - } + this.vanilla = { + scriptSig: [VALID_SCRIPTSIG_BUF], + outs: JSON.stringify([{ + address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', + amountSatStr: '123', + }]), + + } } -FakeBuilder.prototype.merge = function() { -}; +FakeBuilder.prototype.merge = function() {}; -FakeBuilder.prototype.build = function() { +FakeBuilder.prototype.build = function() { return this.tx; }; -FakeBuilder.prototype.toObj = function() { +FakeBuilder.prototype.toObj = function() { return this; }; FakeBuilder.VALID_SCRIPTSIG_BUF = VALID_SCRIPTSIG_BUF; diff --git a/test/mocks/FakePayProServer.js b/test/mocks/FakePayProServer.js deleted file mode 100644 index 3b0972872..000000000 --- a/test/mocks/FakePayProServer.js +++ /dev/null @@ -1,329 +0,0 @@ -'use strict'; - -var is_browser = typeof process == 'undefined' - || typeof process.versions === 'undefined'; -var bitcore = bitcore || require('bitcore'); -var Buffer = bitcore.Buffer; -var PayPro = bitcore.PayPro; -if (is_browser) { - var copay = require('copay'); //browser -} else { - var copay = require('../../copay'); //node -} -var Wallet = copay.Wallet; - -var x509 = { - priv: '' - + 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeFRKdUsyYUdM' - + 'bjFkWEpLRGg0TXdQTFVrbDNISTVwR25HNWFjNGwvMGlobXE4Y3dDCitGVlBnWk1TNTlheWtpc0Ir' - + 'ekM3dnR2a0prL2J2K0JTT1g3b3hkSXN1TDNkS1FGcHVYWFZmcmRiOTV3WW40TSsKL25qRWhYTWxo' - + 'Vk1IL09DaUFnOUpLaFRLV0w2R1JXWkFBaEE3bEJSaGdTTkRUaVRDNTFDYmlLN3hBNnBONCt0UQpI' - + 'eG9tSlBYclpSa2JCMmtsT2ZXd2J2OTNZM0oxS0ZEK2kwUE1RSEx3N3JoRXVteEM5MytISFVWWVZI' - + 'N0gxVFBaCkgxYmRVSkowMmdRZXlsSnNzWUNKeWRaUHpOVC96dXRzL0tKV2RSdjVseHdHOXU5dE1O' - + 'TWdoSmJtQWFNa01HaSsKbzdQTkV5UDNxSEZyWXBZaHM1cHFMSE1STkI3OFFNOUllTmpMRndJREFR' - + 'QUJBb0lCQVFERVJyalBiQUdjbmwxaAorZGIrOTczNGZ0aElBUkpWSko1dTRFK1JKcThSRWhGTEVL' - + 'UFlKNW0yUC94dVZBMXpYV2xnYXhaRUZ6d1VRaUpZCjdsOEpLVjlwSHhReVlaQ1M4dndYZzhpWGtz' - + 'dndQaWRvQmN1YW4vd0RWQ1FCZXk2VkxjVXpSYUd1Ui9sTHNYK1YKN2Z0QjBvUnFsSXFrYmNQZE1N' - + 'dnFUeG93UnVoUG11Q3JWVGpPNHBiTnFuU09OUExPaUovRkFYYjJwZnpGZnBCUgpHeCtFTW16d2Ur' - + 'SEZuSkJHRGhIWjk5bm4vVEJmYUp6TlZDcURZLzNid3o1WDdIUU5ZN1QrSnlUVUZzZVE5NHhzCnpy' - + 'a2lidGRmVGNUanB1K1VoWm80c1p6Q3IrZkhHWm9FOUdEUHF0ZDRnQ3ByazRFS0pzbXFCRVN4QlhT' - + 'RGhZZ04KOXBVRDM4c1pBb0dCQU9yZkRqdDZaL0ZDamFuVThXek5GaWYrOVQxQTJ4b013RDVWU2xN' - + 'dVJyWW1HbGZyMEM5TQpmMUVvZ2l2dVRrYnA3cmtnZFRhWVRTYndmTnFaQkt4Y3R5YzdCaGRwWnhE' - + 'RVdKa2Z5cThxVngvem1Cek1JK1ZzCjJLYi9hcHZXcmJlb3NET0NyeUg1YzhKc1VUOXhUWDNYYnhF' - + 'anlPSlFCU1lHRE1qUHlKNkU5czZMQW9HQkFOYnYKd2d0S2Nra0tLbDJhNXZzaGR2RENnNnFLL1Fn' - + 'T20vNktUSlVKRVNqaHoydFIrZlBWUjcwVEg5UmhoVFJscERXQgpCd3oyU2NCc1RRNDIvTGsxRnky' - + 'MFQvck12S3VmSEw1VE1BNGZ6NWRxMUxIbmN6ejZVazVnWEtBT09rUjlVdVhpClR0eTNoREcyQkM4' - + 'Nk1LTVJ4SjUxRWJxam94d0VSMTAwU2FuTVBmTWxBb0dBSUhLY1pyOHNhUHBHMC9XbFBPREEKZE5v' - + 'V1MxWVFidkxnQkR5SVBpR2doejJRV2lFcjY3em53ZkNVdXpqNiszVUtFKzFXQkNyYVRjemZrdHVj' - + 'OTZyLwphcDRPNDJFZWFnU1dNT0ZoZ1AyYWQ4R1JmRGovcEl4N0NlY3pkVUFkVThnc1A1R0lYR3M0' - + 'QU40eUEwL0Y0dUxHCloxbklRT3ZKS2syZnFvWjZNdHd2dEswQ2dZRUFnSjdGTGVDRTkzUmYyZGdD' - + 'ZFRHWGJZZlpKc3M1bEFLNkV0NUwKNmJ1ZFN5dWw1Z0VPWkgyekNsQlJjZFJSMUFNbSt1V1ZoSW8x' - + 'cERLckFlQ2g1MnIvemRmakxLQXNIejkrQWQ3aQpHUEdzVmw0Vm5jaDFTMzQ0bHJKUGUzQklLZ2dj' - + 'L1hncDNTYnNzcHJMY2orT0wyZElrOUpXbzZ1Y3hmMUJmMkwwCjJlbGhBUWtDZ1lCWHN5elZWL1pK' - + 'cVhOcFdDZzU1TDNVRm9UTHlLU3FsVktNM1dpRzVCS240QWF6VkNITCtHUVUKeHd4U2dSOWZRNElu' - + 'dStyUHJOM0lteWswbEtQR0Y5U3pDUlJUaUpGUjcyc05xbE82bDBWOENXUkFQVFBKY2dxVgoxVThO' - + 'SEs4YjNaaUlvR0orbXNOenBkeHJqNjJIM0E2K1krQXNOWTRTbVVUWEg5eWpnK251a2c9PQotLS0t' - + 'LUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=', - pub: '' - + 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FR' - + 'OEFNSUlCQ2dLQ0FRRUF4VEp1SzJhR0xuMWRYSktEaDRNdwpQTFVrbDNISTVwR25HNWFjNGwvMGlo' - + 'bXE4Y3dDK0ZWUGdaTVM1OWF5a2lzQit6Qzd2dHZrSmsvYnYrQlNPWDdvCnhkSXN1TDNkS1FGcHVY' - + 'WFZmcmRiOTV3WW40TSsvbmpFaFhNbGhWTUgvT0NpQWc5SktoVEtXTDZHUldaQUFoQTcKbEJSaGdT' - + 'TkRUaVRDNTFDYmlLN3hBNnBONCt0UUh4b21KUFhyWlJrYkIya2xPZld3YnY5M1kzSjFLRkQraTBQ' - + 'TQpRSEx3N3JoRXVteEM5MytISFVWWVZIN0gxVFBaSDFiZFVKSjAyZ1FleWxKc3NZQ0p5ZFpQek5U' - + 'L3p1dHMvS0pXCmRSdjVseHdHOXU5dE1OTWdoSmJtQWFNa01HaStvN1BORXlQM3FIRnJZcFloczVw' - + 'cUxITVJOQjc4UU05SWVOakwKRndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==', - der: '' - + 'MIIDBjCCAe4CCQDI2qWdA3/VpDANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTETMBEGA1UE' - + 'CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE0MDcx' - + 'NjAxMzM1MVoXDTE1MDcxNjAxMzM1MVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3Rh' - + 'dGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD' - + 'ggEPADCCAQoCggEBAMUybitmhi59XVySg4eDMDy1JJdxyOaRpxuWnOJf9IoZqvHMAvhVT4GTEufW' - + 'spIrAfswu77b5CZP27/gUjl+6MXSLLi93SkBabl11X63W/ecGJ+DPv54xIVzJYVTB/zgogIPSSoU' - + 'yli+hkVmQAIQO5QUYYEjQ04kwudQm4iu8QOqTePrUB8aJiT162UZGwdpJTn1sG7/d2NydShQ/otD' - + 'zEBy8O64RLpsQvd/hx1FWFR+x9Uz2R9W3VCSdNoEHspSbLGAicnWT8zU/87rbPyiVnUb+ZccBvbv' - + 'bTDTIISW5gGjJDBovqOzzRMj96hxa2KWIbOaaixzETQe/EDPSHjYyxcCAwEAATANBgkqhkiG9w0B' - + 'AQUFAAOCAQEAL6AMMfC3TlRcmsIgHxjVD4XYtISlldnrn2X9zvFbJKCpNy8XQQosQxrhyfzPHQKj' - + 'lS2L/KCGMnjx9QkYD2Hlp1MJ1uVv9888th/gcZOv3Or3hQyi5K1Sh5xCG+69lUOqUEGu9B4irsqo' - + 'FomQVbQolSy+t4apdJi7kuEDwFDk4gZiVEfsuX+naN5a6pCnWnhX1Vf4fKwfkLobKKXm2zQVsjxl' - + 'wBAqOEmJGDLoRMXH56qJnEZ/dqsczaJOHQSi9mFEHL0r5rsEDTT5AVxdnBfNnyGaCH7/zANEko+F' - + 'GBj1JdJaJgFTXdbxDoyoPTPD+LJqSK5XYToo46y/T0u9CLveNA==', - pem: '' - + 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU0Q0NRREkycVdkQTMvVnBEQU5C' - + 'Z2txaGtpRzl3MEJBUVVGQURCRk1Rc3dDUVlEVlFRR0V3SkIKVlRFVE1CRUdBMVVFQ0F3S1UyOXRa' - + 'UzFUZEdGMFpURWhNQjhHQTFVRUNnd1lTVzUwWlhKdVpYUWdWMmxrWjJsMApjeUJRZEhrZ1RIUmtN' - + 'QjRYRFRFME1EY3hOakF4TXpNMU1Wb1hEVEUxTURjeE5qQXhNek0xTVZvd1JURUxNQWtHCkExVUVC' - + 'aE1DUVZVeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJt' - + 'VjAKSUZkcFpHZHBkSE1nVUhSNUlFeDBaRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFE' - + 'Q0NBUW9DZ2dFQgpBTVV5Yml0bWhpNTlYVnlTZzRlRE1EeTFKSmR4eU9hUnB4dVduT0pmOUlvWnF2' - + 'SE1BdmhWVDRHVEV1ZldzcElyCkFmc3d1NzdiNUNaUDI3L2dVamwrNk1YU0xMaTkzU2tCYWJsMTFY' - + 'NjNXL2VjR0orRFB2NTR4SVZ6SllWVEIvemcKb2dJUFNTb1V5bGkraGtWbVFBSVFPNVFVWVlFalEw' - + 'NGt3dWRRbTRpdThRT3FUZVByVUI4YUppVDE2MlVaR3dkcApKVG4xc0c3L2QyTnlkU2hRL290RHpF' - + 'Qnk4TzY0Ukxwc1F2ZC9oeDFGV0ZSK3g5VXoyUjlXM1ZDU2ROb0VIc3BTCmJMR0FpY25XVDh6VS84' - + 'N3JiUHlpVm5VYitaY2NCdmJ2YlREVElJU1c1Z0dqSkRCb3ZxT3p6Uk1qOTZoeGEyS1cKSWJPYWFp' - + 'eHpFVFFlL0VEUFNIall5eGNDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFMNkFNTWZD' - + 'MwpUbFJjbXNJZ0h4alZENFhZdElTbGxkbnJuMlg5enZGYkpLQ3BOeThYUVFvc1F4cmh5ZnpQSFFL' - + 'amxTMkwvS0NHCk1uang5UWtZRDJIbHAxTUoxdVZ2OTg4OHRoL2djWk92M09yM2hReWk1SzFTaDV4' - + 'Q0crNjlsVU9xVUVHdTlCNGkKcnNxb0ZvbVFWYlFvbFN5K3Q0YXBkSmk3a3VFRHdGRGs0Z1ppVkVm' - + 'c3VYK25hTjVhNnBDblduaFgxVmY0Zkt3ZgprTG9iS0tYbTJ6UVZzanhsd0JBcU9FbUpHRExvUk1Y' - + 'SDU2cUpuRVovZHFzY3phSk9IUVNpOW1GRUhMMHI1cnNFCkRUVDVBVnhkbkJmTm55R2FDSDcvekFO' - + 'RWtvK0ZHQmoxSmRKYUpnRlRYZGJ4RG95b1BUUEQrTEpxU0s1WFlUb28KNDZ5L1QwdTlDTHZlTkE9' - + 'PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==' -}; - -x509.priv = new Buffer(x509.priv, 'base64'); -x509.pub = new Buffer(x509.pub, 'base64'); -x509.der = new Buffer(x509.der, 'base64'); -x509.pem = new Buffer(x509.pem, 'base64'); - -function startServer(cb) { - if (Wallet.request._server) { - setTimeout(function() { - return cb(null, Wallet.request._server); - }, 1); - return; - } - - var old = Wallet.request; - - var server = { - GET: { - - /** - * Receive "I want to pay" - */ - - '/-/request': function(req, cb) { - var res = { - statusCode: 200, - headers: {}, - body: {} - }; - - var uid = 0; - - var outputs = []; - - [2000, 1000].forEach(function(value) { - var po = new PayPro(); - po = po.makeOutput(); - // number of satoshis to be paid - po.set('amount', value); - // a TxOut script where the payment should be sent. similar to OP_CHECKSIG - po.set('script', new Buffer([ - 118, // OP_DUP - 169, // OP_HASH160 - 76, // OP_PUSHDATA1 - 20, // number of bytes - 55, - 48, - 254, - 188, - 186, - 4, - 186, - 208, - 205, - 71, - 108, - 251, - 130, - 15, - 156, - 55, - 215, - 70, - 111, - 217, - 136, // OP_EQUALVERIFY - 172 // OP_CHECKSIG - ])); - outputs.push(po.message); - }); - - /** - * Payment Details - */ - - var mdata = new Buffer([0]); - uid++; - if (uid > 0xffff) { - throw new Error('UIDs bigger than 0xffff not supported.'); - } else if (uid > 0xff) { - mdata = new Buffer([(uid >> 8) & 0xff, (uid >> 0) & 0xff]) - } else { - mdata = new Buffer([0, uid]) - } - var now = Date.now() / 1000 | 0; - var pd = new PayPro(); - pd = pd.makePaymentDetails(); - pd.set('network', 'test'); - pd.set('outputs', outputs); - pd.set('time', now); - pd.set('expires', now + 60 * 60 * 24); - pd.set('memo', 'Hello, this is the server, we would like some money.'); - var port = +req.headers.host.split(':')[1] || server.port; - pd.set('payment_url', 'https://localhost:' + port + '/-/pay'); - pd.set('merchant_data', mdata); - - /* - * PaymentRequest - */ - - var cr = new PayPro(); - cr = cr.makeX509Certificates(); - cr.set('certificate', [x509.der]); - - // We send the PaymentRequest to the customer - var pr = new PayPro(); - pr = pr.makePaymentRequest(); - pr.set('payment_details_version', 1); - pr.set('pki_type', 'x509+sha256'); - pr.set('pki_data', cr.serialize()); - pr.set('serialized_payment_details', pd.serialize()); - pr.sign(x509.priv); - - pr = pr.serialize(); - - // BIP-71 - set the content-type - res.headers['Content-Type'] = PayPro.PAYMENT_REQUEST_CONTENT_TYPE; - res.headers['Content-Length'] = pr.length + ''; - res.headers['Content-Transfer-Encoding'] = 'binary'; - - res.body = pr; - - return cb(null, res, res.body); - }, - }, - - /** - * Receive Payment - */ - - POST: { - '/-/pay': function(req, cb) { - var body = req.body; - - var res = { - statusCode: 200, - headers: {}, - body: {} - }; - - body = PayPro.Payment.decode(body); - - var pay = new PayPro(); - pay = pay.makePayment(body); - var merchant_data = pay.get('merchant_data'); - var transactions = pay.get('transactions'); - var refund_to = pay.get('refund_to'); - var memo = pay.get('memo'); - - // We send this to the customer after receiving a Payment - // Then we propogate the transaction through bitcoin network - var ack = new PayPro(); - ack = ack.makePaymentACK(); - ack.set('payment', pay.message); - ack.set('memo', 'Thank you for your payment!'); - - ack = ack.serialize(); - - // BIP-71 - set the content-type - res.headers['Content-Type'] = PayPro.PAYMENT_ACK_CONTENT_TYPE; - res.headers['Content-Length'] = ack.length + ''; - res.headers['Content-Transfer-Encoding'] = 'binary'; - - transactions = transactions.map(function(tx) { - tx.buffer = new Buffer(new Uint8Array(tx.buffer)); - tx.buffer = tx.buffer.slice(tx.offset, tx.limit); - var ptx = new bitcore.Transaction(); - ptx.parse(tx.buffer); - return ptx; - }); - - res.body = ack; - - return cb(null, res, res.body); - } - }, - listen: function(port, cb) { - if (cb) return cb(); - }, - close: function(cb) { - Wallet.request = old; - return cb(); - } - }; - - Wallet.request = function(options) { - var ret = { - success: function(cb) { - this._success = cb; - return this; - }, - error: function(cb) { - this._error = cb; - return this; - }, - _success: function() { - ; - }, - _error: function(_, err) { - throw err; - } - }; - var method = (options.method || 'GET').toUpperCase(); - var uri = options.uri || options.url; - var path = uri.replace(/^https?:\/\/[^\/]+/, ''); - var req = options; - req.headers = req.headers || {}; - req.body = req.data || req.body || {}; - req.socket = { - remoteAddress: 'localhost' - }; - req.headers['Host'] = 'localhost:8080'; - Object.keys(req.headers).forEach(function(key) { - req.headers[key] = req.headers[key] + ''; - req.headers[key.toLowerCase()] = req.headers[key] + ''; - }); - setTimeout(function() { - server[method][path](req, function(err, res, body) { - if (err) return ret._error(null, err, null, options); - Object.keys(res.headers).forEach(function(key) { - res.headers[key] = res.headers[key] + ''; - res.headers[key.toLowerCase()] = res.headers[key] + ''; - }); - return ret._success(body, res.statusCode, res.headers, options); - }); - }, 1); - return ret; - }; - - Wallet.request._server = server; - - setTimeout(function() { - return cb(null, server); - }, 1); -} - -module.exports = startServer; diff --git a/test/unit/controllers/controllersSpec.js b/test/unit/controllers/controllersSpec.js index 3dc682211..1343b5ef1 100644 --- a/test/unit/controllers/controllersSpec.js +++ b/test/unit/controllers/controllersSpec.js @@ -12,12 +12,16 @@ saveAs = function(blob, filename) { }; }; -var startServer = require('../../mocks/FakePayProServer'); - describe("Unit: Controllers", function() { config.plugins.LocalStorage = true; config.plugins.GoogleDrive = null; + var anAddr = 'mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'; + var anAmount = 1000; + var aComment = 'hola'; + + + var invalidForm = { $invalid: true }; @@ -69,24 +73,30 @@ describe("Unit: Controllers", function() { w.getTransactionHistory = sinon.stub().yields(null); w.getNetworkName = sinon.stub().returns('testnet'); - w.createTx = sinon.stub().yields(null); - w.sendTx = sinon.stub().yields(null); + w.spend = sinon.stub().yields(null); + w.sendTxProposal = sinon.stub(); + w.broadcastTx = sinon.stub().yields(null); w.requiresMultipleSignatures = sinon.stub().returns(true); w.getTxProposals = sinon.stub().returns([1, 2, 3]); w.getPendingTxProposals = sinon.stub().returns({ - txs : [{ isPending : true }], + txs: [{ + isPending: true + }], pendingForUs: 1 }); w.getId = sinon.stub().returns(1234); - w.on = sinon.stub().yields({'e': 'errmsg', 'loading': false}); + w.on = sinon.stub().yields({ + 'e': 'errmsg', + 'loading': false + }); w.getBalance = sinon.stub().returns(10000); w.publicKeyRing = sinon.stub().yields(null); w.publicKeyRing.nicknameForCopayer = sinon.stub().returns('nickcopayer'); w.updateFocusedTimestamp = sinon.stub().returns(1415804323); - w.getAddressesInfo = sinon.stub().returns([ - { addressStr: "2MxvwvfshZxw4SkkaJZ8NDKLyepa9HLMKtu", - isChange: false } - ]); + w.getAddressesInfo = sinon.stub().returns([{ + addressStr: "2MxvwvfshZxw4SkkaJZ8NDKLyepa9HLMKtu", + isChange: false + }]); var iden = {}; iden.deleteWallet = sinon.stub().yields(null); @@ -240,31 +250,35 @@ describe("Unit: Controllers", function() { }); it('should create a transaction proposal with given values', function() { - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - sendForm.amount.$setViewValue(1000); + sendForm.address.$setViewValue(anAddr); + sendForm.amount.$setViewValue(anAmount); + sendForm.comment.$setViewValue(aComment); scope.loadTxs = sinon.spy(); var w = scope.wallet; scope.submitForm(sendForm); - sinon.assert.callCount(w.createTx, 1); - sinon.assert.callCount(w.sendTx, 0); + sinon.assert.callCount(w.spend, 1); + sinon.assert.callCount(w.broadcastTx, 0); sinon.assert.callCount(scope.loadTxs, 1); - w.createTx.getCall(0).args[0].should.equal('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - w.createTx.getCall(0).args[1].should.equal(1000 * scope.wallet.settings.unitToSatoshi); - (typeof w.createTx.getCall(0).args[2]).should.equal('undefined'); + var spendArgs = w.spend.getCall(0).args[0]; + spendArgs.toAddress.should.equal(anAddr); + spendArgs.amountSat.should.equal(anAmount * scope.wallet.settings.unitToSatoshi); + spendArgs.comment.should.equal(aComment); }); it('should handle big values in 100 BTC', function() { var old = scope.wallet.settings.unitToSatoshi; scope.wallet.settings.unitToSatoshi = 100000000;; - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); + sendForm.address.$setViewValue(anAddr); sendForm.amount.$setViewValue(100); + sendForm.address.$setViewValue(anAddr); + scope.loadTxs = sinon.spy(); scope.submitForm(sendForm); var w = scope.wallet; - w.createTx.getCall(0).args[1].should.equal(100 * scope.wallet.settings.unitToSatoshi); + w.spend.getCall(0).args[0].amountSat.should.equal(100 * scope.wallet.settings.unitToSatoshi); scope.wallet.settings.unitToSatoshi = old; }); @@ -276,11 +290,11 @@ describe("Unit: Controllers", function() { var old = $rootScope.wallet.settings.unitToSatoshi; $rootScope.wallet.settings.unitToSatoshi = 100000000;; - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); + sendForm.address.$setViewValue(anAddr); sendForm.amount.$setViewValue(5000); scope.submitForm(sendForm); - w.createTx.getCall(0).args[1].should.equal(5000 * $rootScope.wallet.settings.unitToSatoshi); + w.spend.getCall(0).args[0].amountSat.should.equal(5000 * $rootScope.wallet.settings.unitToSatoshi); $rootScope.wallet.settings.unitToSatoshi = old; })); @@ -300,39 +314,6 @@ describe("Unit: Controllers", function() { done(); }); }); - - it('should create and send a transaction proposal', function() { - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - sendForm.amount.$setViewValue(1000); - scope.loadTxs = sinon.spy(); - - var w = scope.wallet; - w.requiresMultipleSignatures = sinon.stub().returns(false); - w.totalCopayers = w.requiredCopayers = 1; - - - scope.submitForm(sendForm); - sinon.assert.callCount(w.createTx, 1); - sinon.assert.callCount(w.sendTx, 1); - sinon.assert.callCount(scope.loadTxs, 1); - }); - - it('should not send txp when there is an error at creation', function() { - sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); - sendForm.amount.$setViewValue(1000); - scope.wallet.totalCopayers = scope.wallet.requiredCopayers = 1; - scope.loadTxs = sinon.spy(); - var w = scope.wallet; - w.createTx.yields('error'); - w.isShared = sinon.stub().returns(false); - - - scope.submitForm(sendForm); - - sinon.assert.callCount(w.createTx, 1); - sinon.assert.callCount(w.sendTx, 0); - sinon.assert.callCount(scope.loadTxs, 1); - }); }); describe("Unit: Version Controller", function() { diff --git a/util/build.js b/util/build.js index 908729562..4b4b1f74d 100644 --- a/util/build.js +++ b/util/build.js @@ -128,12 +128,6 @@ var createBundle = function(opts) { b.require('./test/mocks/FakeNetwork', { expose: './mocks/FakeNetwork' }); - b.require('./test/mocks/FakePayProServer', { - expose: './mocks/FakePayProServer' - }); - b.require('./test/mocks/FakePayProServer', { - expose: '../../mocks/FakePayProServer' - }); b.require('./test/mocks/FakeBuilder', { expose: './mocks/FakeBuilder' }); diff --git a/views/history.html b/views/history.html index 9336d8639..12f548780 100644 --- a/views/history.html +++ b/views/history.html @@ -71,9 +71,11 @@

- {{btx.merchant.pr.pd.memo}} + {{btx.merchant.pr.pd.memo}}

+ {{btx.paymentAckMemo}} [{{btx.merchant.domain}}] +