mirror of https://github.com/BTCPrivate/copay.git
refactor PayPro at Wallet. Fetch working
This commit is contained in:
parent
ae94139236
commit
efb5b28c50
12
README.md
12
README.md
|
@ -196,4 +196,16 @@ Copay uses the Javascript library Bitcore for Bitcoin related functions. Bitcore
|
|||
var cmd = `node util/build_bitcore.js`
|
||||
cd <BITCORE_HOME>
|
||||
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.
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
|
|
@ -100,6 +100,38 @@ 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._afterSend = function(err, ntxid, merchantData) {
|
||||
$scope.loading = false;
|
||||
|
||||
if (err)
|
||||
return $scope._showError(err);
|
||||
|
||||
|
||||
if (w.requiresMultipleSignatures()) {
|
||||
notification.success('Success', 'The transaction proposal created');
|
||||
$scope.loadTxs();
|
||||
} else {
|
||||
$scope.send(ntxid);
|
||||
}
|
||||
$rootScope.pendingPayment = null;
|
||||
};
|
||||
|
||||
$scope.submitForm = function(form) {
|
||||
if (form.$invalid) {
|
||||
$scope.error = 'Unable to send transaction proposal';
|
||||
|
@ -112,92 +144,32 @@ 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
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
w.createTx({
|
||||
toAddress: address,
|
||||
amountSat: amount,
|
||||
comment: commentText,
|
||||
url: (payInfo && payInfo.merchant) ? url : null,
|
||||
}, $scope._afterSend);
|
||||
};
|
||||
|
||||
// QR code Scanner
|
||||
|
@ -411,13 +383,7 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
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);
|
||||
notification.success('Success', 'Payment sent. ' + merchantData.ack.memo);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,9 +406,7 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
|
||||
var p = w.txProposals.getTxProposal(ntxid);
|
||||
if (p.builder.isFullySigned()) {
|
||||
$scope.send(ntxid, function() {
|
||||
$scope.loadTxs();
|
||||
});
|
||||
$scope.send(ntxid);
|
||||
} else {
|
||||
$scope.loadTxs();
|
||||
}
|
||||
|
@ -546,7 +510,7 @@ 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);
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ 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 +18,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');
|
||||
|
@ -70,6 +71,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',
|
||||
|
@ -472,9 +474,10 @@ 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(txp.paymentProtocolURL, function(err, merchantData) {
|
||||
if (err) return cb(err);
|
||||
|
||||
// This will verify current TXP data vs. merchantData (e.g., out addresses)
|
||||
try {
|
||||
txp.addMerchantData(merchantData);
|
||||
} catch (e) {
|
||||
|
@ -1510,96 +1513,125 @@ Wallet.prototype.sendTx = function(ntxid, cb) {
|
|||
|
||||
/**
|
||||
* @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 {Object|string} options - if it's a string, parse it as the url
|
||||
* @param {string} options.url the url for the transaction
|
||||
* @param {Function} cb
|
||||
*/
|
||||
Wallet.prototype.createPaymentTx = function(options, 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 (_.isString(options)) {
|
||||
options = {
|
||||
uri: options
|
||||
};
|
||||
}
|
||||
options.uri = options.uri || options.url;
|
||||
|
||||
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 req = this.paymentRequests[options.uri];
|
||||
if (req) {
|
||||
delete this.paymentRequests[options.uri];
|
||||
this.receivePaymentRequest(options, req.pr, cb);
|
||||
return;
|
||||
}
|
||||
|
||||
return Wallet.request({
|
||||
this.httpUtil.request({
|
||||
method: 'GET',
|
||||
url: options.uri,
|
||||
url: options.url,
|
||||
headers: {
|
||||
'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
.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);
|
||||
.success(function(rawData) {
|
||||
log.info('PayPro Request done successfully. Parsing response')
|
||||
|
||||
var data = PayPro.PaymentRequest.decode(rawData);
|
||||
var paypro = new PayPro();
|
||||
var pr = paypro.makePaymentRequest(data);
|
||||
var merchantData, err;
|
||||
try {
|
||||
merchantData = self.parsePaymentRequest(options, pr);
|
||||
} catch (e) { err = e};
|
||||
|
||||
log.debug('PayPro request data', merchantData);
|
||||
return cb(err, merchantData);
|
||||
})
|
||||
.error(function(data, status, headers, config) {
|
||||
log.debug('Server did not return PaymentRequest.');
|
||||
log.debug('XHR status: ' + status);
|
||||
if (options.fetch) {
|
||||
.error(function(data, status) {
|
||||
log.debug('Server did not return PaymentRequest.\nXHR status: ' + status);
|
||||
preconditions.checkState(options.fetch);
|
||||
return cb(new Error('Status: ' + status));
|
||||
} else {
|
||||
// Should never happen:
|
||||
return cb(null, null, null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Creates a Payment TxProposal from a uri
|
||||
* @param {Object} options
|
||||
* @param {string=} options.uri
|
||||
* @param {string=} options.url
|
||||
* @param {Function} cb
|
||||
/*
|
||||
* addOutputsToMerchantData
|
||||
*
|
||||
* NOTE: We use to: set the TX scripts with the payment request scripts:
|
||||
* but this is a hack around transaction builder, so we dont do it anymore.
|
||||
* See Readme.md. For now we only support p2scripthash or p2pubkeyhash
|
||||
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;
|
||||
});
|
||||
*
|
||||
*/
|
||||
Wallet.prototype.fetchPaymentTx = function(options, cb) {
|
||||
var self = this;
|
||||
|
||||
options = options || {};
|
||||
if (_.isString(options)) {
|
||||
options = {
|
||||
uri: options
|
||||
Wallet.prototype._addOutputsToMerchantData = function(merchantData) {
|
||||
|
||||
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
|
||||
|
@ -1609,9 +1641,8 @@ Wallet.prototype.fetchPaymentTx = function(options, cb) {
|
|||
* @param {string} pr.signature
|
||||
* @param {string} options.memo
|
||||
* @param {string} options.comment
|
||||
* @param {Function} cb
|
||||
*/
|
||||
Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
|
||||
Wallet.prototype.parsePaymentRequest = function(options, pr) {
|
||||
var self = this;
|
||||
|
||||
var ver = pr.get('payment_details_version');
|
||||
|
@ -1623,18 +1654,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);
|
||||
|
@ -1681,38 +1705,11 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
|
|||
selfSigned: trust.selfSigned
|
||||
},
|
||||
expires: expires,
|
||||
request_url: options.uri,
|
||||
request_url: options.url,
|
||||
total: bignum('0', 10).toString(10),
|
||||
};
|
||||
|
||||
return this.getUnspent(function(err, safeUnspent, unspent) {
|
||||
|
||||
if (options.fetch) {
|
||||
if (!unspent || !unspent.length) {
|
||||
return cb(new Error('No unspent outputs available.'));
|
||||
}
|
||||
|
||||
var err;
|
||||
try {
|
||||
self.createPaymentTxSync(options, merchantData, safeUnspent);
|
||||
} catch (e) { err = e;}
|
||||
return cb(err, 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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1827,14 +1824,14 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) {
|
|||
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 this.httpUtil.request(postInfo)
|
||||
.success(function(rawData) {
|
||||
var data = PayPro.PaymentACK.decode(rawData);
|
||||
var paypro = new PayPro();
|
||||
ack = paypro.makePaymentACK(data);
|
||||
return self.receivePaymentRequestACK(ntxid, tx, txp, ack, cb);
|
||||
})
|
||||
.error(function(data, status, headers, config) {
|
||||
.error(function(data, status ) {
|
||||
log.debug('Sending to server was not met with a returned tx.');
|
||||
log.debug('XHR status: ' + status);
|
||||
self._processTxProposalSent(ntxid, function(err, txid) {
|
||||
|
@ -1906,148 +1903,6 @@ Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Create a Payment Transaction Sync (see BIP70)
|
||||
* @TODO: Document better
|
||||
*/
|
||||
Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
var a = addr[0].toString();
|
||||
outs[a] = bignum.fromBuffer(v, {
|
||||
endian: 'big',
|
||||
size: 1
|
||||
}).add(outs[a] || bignum(0));
|
||||
|
||||
merchantData.total = merchantData.total.add(bignum.fromBuffer(v, {
|
||||
endian: 'big',
|
||||
size: 1
|
||||
}));
|
||||
});
|
||||
|
||||
// TODO, 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 = merchantData.total.toString(10);
|
||||
|
||||
var b = new Builder(opts)
|
||||
.setUnspent(unspent)
|
||||
.setOutputs(merchantData.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 Mark that a user has seen a given TxProposal
|
||||
* @return {boolean} true if the internal state has changed
|
||||
|
@ -2273,26 +2128,45 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) {
|
|||
* @desc Create a transaction proposal
|
||||
* @TODO: Document more
|
||||
*/
|
||||
Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) {
|
||||
Wallet.prototype.createTx = function(opts, cb) {
|
||||
preconditions.checkArgument(_.isObject(opts));
|
||||
preconditions.checkArgument(opts.amountSat);
|
||||
log.debug(opts);
|
||||
|
||||
var self = this;
|
||||
var toAddress = opts.toAddress;
|
||||
var amountSat = opts.amountSat;
|
||||
preconditions.checkArgument(!opts.comment || opts.comment.length <= 100);
|
||||
var url = opts.url;
|
||||
|
||||
if (_.isFunction(opts)) {
|
||||
cb = opts;
|
||||
opts = {};
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
if (_.isUndefined(opts.spendUnconfirmed)) {
|
||||
opts.spendUnconfirmed = this.spendUnconfirmed;
|
||||
}
|
||||
if (url && !opts.merchantData) {
|
||||
w.fetchPaymentRequest({
|
||||
url: url,
|
||||
memo: comment,
|
||||
amount: amountSat,
|
||||
}, function(err, merchantData) {
|
||||
if (err) return cb(err);
|
||||
opts.merchantData = merchantData;
|
||||
opts.amountSat = merchantData.outs[0].address;
|
||||
opts.toAddress = merchantData.outs[0].amount;
|
||||
self.createTx(opts, cb);
|
||||
});
|
||||
};
|
||||
preconditions.checkArgument(amountSat);
|
||||
preconditions.checkArgument(toAddress);
|
||||
|
||||
this.getUnspent(function(err, safeUnspent) {
|
||||
if (err) return cb(new Error('Could not get list of UTXOs'));
|
||||
|
||||
var ntxid;
|
||||
try {
|
||||
ntxid = self.createTxSync(toAddress, amountSatStr, comment, safeUnspent, opts);
|
||||
log.debug('TX Created: ntxid', ntxid);
|
||||
var txp = self.createTxProposal(toAddress, amountSat, safeUnspent, opts.builderOpts);
|
||||
|
||||
if (opts.merchantData)
|
||||
txp.addMerchantData(opts.merchantData);
|
||||
|
||||
var ntxid = self.addNewTxProposal(txp);
|
||||
log.debug('TXP Added: ', ntxid);
|
||||
} catch (e) {
|
||||
return cb(e);
|
||||
}
|
||||
|
@ -2320,36 +2194,41 @@ var sanitize = function(address) {
|
|||
* @desc Create a transaction proposal
|
||||
* @TODO: Document more
|
||||
*/
|
||||
Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos, opts) {
|
||||
Wallet.prototype.createTxProposal = function(toAddress, amountSat, utxos, builderOpts) {
|
||||
preconditions.checkArgument(toAddress);
|
||||
preconditions.checkArgument(amountSat);
|
||||
preconditions.checkArgument(_.isArray(utxos));
|
||||
builderOpts = builderOpts || {};
|
||||
|
||||
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);
|
||||
|
||||
if (!opts.remainderOut) {
|
||||
opts.remainderOut = {
|
||||
if (!builderOpts.remainderOut) {
|
||||
builderOpts.remainderOut = {
|
||||
address: this._doGenerateAddress(true).toString()
|
||||
};
|
||||
}
|
||||
|
||||
for (var k in Wallet.builderOpts) {
|
||||
opts[k] = Wallet.builderOpts[k];
|
||||
if (_.isUndefined(builderOpts.spendUnconfirmed)) {
|
||||
builderOpts.spendUnconfirmed = this.spendUnconfirmed;
|
||||
}
|
||||
|
||||
var b = new Builder(opts)
|
||||
for (var k in Wallet.builderOpts) {
|
||||
builderOpts[k] = Wallet.builderOpts[k];
|
||||
}
|
||||
|
||||
var b = new Builder(builderOpts)
|
||||
.setUnspent(utxos)
|
||||
.setOutputs([{
|
||||
address: toAddress.data,
|
||||
amountSatStr: amountSatStr,
|
||||
amountSatStr: amountSat,
|
||||
}]);
|
||||
|
||||
log.debug('Creating TX: Builder ready');
|
||||
|
||||
var selectedUtxos = b.getSelectedUnspent();
|
||||
|
||||
if (selectedUtxos.length > TX_MAX_INS)
|
||||
|
@ -2363,9 +2242,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
|
|||
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.sign(keys);
|
||||
|
||||
|
||||
var tx = b.build();
|
||||
|
@ -2373,26 +2250,36 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos
|
|||
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');
|
||||
|
||||
return new TxProposal({
|
||||
inputChainPaths: inputChainPaths,
|
||||
comment: comment,
|
||||
builder: b,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/* addNewTxProposal
|
||||
* adds a transaction proposal to the list. Sets current copayer and creation metadata.
|
||||
*
|
||||
* @param {txp} Transaction Proposal Object
|
||||
* @desc returns normalized transaction ID
|
||||
* @param {ntxid}
|
||||
*/
|
||||
Wallet.prototype.addNewTxProposal = function(txp) {
|
||||
var myId = this.getMyCopayerId();
|
||||
var now = Date.now();
|
||||
var me = {};
|
||||
me[myId] = now;
|
||||
|
||||
var meSeen = {};
|
||||
if (priv) meSeen[myId] = now;
|
||||
// Add metadata to TxP
|
||||
txp.signedBy = txp.seenBy = me;
|
||||
txp.creator = myId;
|
||||
txp.createdTs = now;
|
||||
|
||||
var ntxid = this.txProposals.add(new TxProposal({
|
||||
inputChainPaths: inputChainPaths,
|
||||
signedBy: me,
|
||||
seenBy: meSeen,
|
||||
creator: myId,
|
||||
createdTs: now,
|
||||
builder: b,
|
||||
comment: comment
|
||||
}));
|
||||
var ntxid = this.txProposals.add(txp);
|
||||
return ntxid;
|
||||
};
|
||||
|
||||
|
@ -2664,88 +2551,6 @@ Wallet.prototype.verifySignedJson = function(senderId, payload, signature) {
|
|||
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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue