commit
253092a1b2
23 changed files with 2352 additions and 101 deletions
|
|
@ -24,7 +24,7 @@ module.exports = function(grunt) {
|
|||
tasks: ['markdown']
|
||||
},
|
||||
scripts: {
|
||||
files: ['*.js', '**/*.js', '*.html', '!**/node_modules/**', '!lib/**js', '!browser/vendor-bundle.js', '!js/copayBundle.js'],
|
||||
files: ['*.js', '*/*/*.js', '**/*.js', '*.html', '!**/node_modules/**', '!lib/**js', '!browser/vendor-bundle.js', '!js/copayBundle.js'],
|
||||
tasks: ['shell'],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
22
app.js
22
app.js
|
|
@ -12,6 +12,28 @@ app.start = function(port, callback) {
|
|||
app.set('port', port);
|
||||
app.use(express.static(__dirname));
|
||||
|
||||
if (process.env.USE_HTTPS) {
|
||||
var path = require('path');
|
||||
|
||||
var bc = path.dirname(require.resolve('bitcore/package.json'));
|
||||
var pserver = require(bc + '/examples/PayPro/server.js');
|
||||
|
||||
pserver.removeListener('request', pserver.app);
|
||||
|
||||
pserver.on('request', function(req, res) {
|
||||
if (req.url.indexOf('/-/') === 0) {
|
||||
return pserver.app(req, res);
|
||||
}
|
||||
return app(req, res);
|
||||
});
|
||||
|
||||
pserver.listen(port, function() {
|
||||
callback('https://localhost:' + port);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
app.listen(port, function() {
|
||||
callback('http://localhost:' + port);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"sjcl": "1.0.0",
|
||||
"file-saver": "*",
|
||||
"qrcode-decoder-js": "*",
|
||||
"bitcore": "0.1.34",
|
||||
"bitcore": "git://github.com/bitpay/bitcore.git#master",
|
||||
"angular-moment": "~0.7.1",
|
||||
"socket.io-client": ">=1.0.0",
|
||||
"mousetrap": "1.4.6",
|
||||
|
|
|
|||
5
copay.js
5
copay.js
|
|
@ -14,5 +14,10 @@ var Insight = module.exports.Insight = require('./js/models/blockchain/Insight')
|
|||
var StorageLocalEncrypted = module.exports.StorageLocalEncrypted = require('./js/models/storage/LocalEncrypted');
|
||||
|
||||
module.exports.WalletFactory = require('./js/models/core/WalletFactory');
|
||||
module.exports.Wallet = require('./js/models/core/Wallet');
|
||||
module.exports.version = require('./version');
|
||||
module.exports.API = require('./API');
|
||||
|
||||
|
||||
// test hack :s, will fix
|
||||
module.exports.FakePayProServer = require('./test/mocks/FakePayProServer');
|
||||
|
|
|
|||
|
|
@ -61,16 +61,44 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
|
||||
var w = $rootScope.wallet;
|
||||
|
||||
w.createTx(address, amount, commentText, function(ntxid) {
|
||||
function done(ntxid, merchantData) {
|
||||
// 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.isShared()) {
|
||||
$scope.loading = false;
|
||||
var message = 'The transaction proposal has been created';
|
||||
if (merchantData) {
|
||||
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!', message);
|
||||
$scope.loadTxs();
|
||||
} else {
|
||||
w.sendTx(ntxid, function(txid) {
|
||||
w.sendTx(ntxid, function(txid, merchantData) {
|
||||
if (txid) {
|
||||
notification.success('Transaction broadcast', 'Transaction id: ' + 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 += ' Message from server: ' + merchantData.ack.memo;
|
||||
message += ' For merchant: ' + merchantData.pr.pd.payment_url;
|
||||
}
|
||||
notification.success('Transaction broadcast', message);
|
||||
} else {
|
||||
notification.error('Error', 'There was an error sending the transaction.');
|
||||
}
|
||||
|
|
@ -79,7 +107,29 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
});
|
||||
}
|
||||
$rootScope.pendingPayment = null;
|
||||
});
|
||||
}
|
||||
|
||||
var uri;
|
||||
if (address.indexOf('bitcoin:') === 0) {
|
||||
uri = copay.HDPath.parseBitcoinURI(address);
|
||||
} else if (address.indexOf('Merchant: ') === 0) {
|
||||
uri = {
|
||||
merchant: address.split(/\s+/)[1]
|
||||
};
|
||||
} else if (/^https?:\/\//.test(address)) {
|
||||
uri = {
|
||||
merchant: address
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -118,7 +168,6 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
qrcode.imagedata = context.getImageData(0, 0, qrcode.width, qrcode.height);
|
||||
|
||||
try {
|
||||
//alert(JSON.stringify(qrcode.process(context)));
|
||||
qrcode.decode();
|
||||
} catch (e) {
|
||||
// error decoding QR
|
||||
|
|
@ -299,11 +348,21 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
$scope.loading = true;
|
||||
$rootScope.txAlertCount = 0;
|
||||
var w = $rootScope.wallet;
|
||||
w.sendTx(ntxid, function(txid) {
|
||||
w.sendTx(ntxid, function(txid, merchantData) {
|
||||
if (!txid) {
|
||||
notification.error('Error', 'There was an error sending the transaction');
|
||||
} else {
|
||||
notification.success('Transaction broadcast', 'Transaction id: '+txid);
|
||||
if (!merchantData) {
|
||||
notification.success('Transaction broadcast', 'Transaction id: ' + txid);
|
||||
} 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('Transaction sent', message);
|
||||
}
|
||||
}
|
||||
|
||||
if (cb) return cb();
|
||||
|
|
@ -340,4 +399,101 @@ angular.module('copayApp.controllers').controller('SendController',
|
|||
$scope.loadTxs();
|
||||
};
|
||||
|
||||
|
||||
$scope.onChanged = function() {
|
||||
var scope = $scope;
|
||||
var value = scope.address;
|
||||
var uri;
|
||||
|
||||
if (/^https?:\/\//.test(value)) {
|
||||
uri = {
|
||||
merchant: value
|
||||
};
|
||||
} else {
|
||||
uri = copay.HDPath.parseBitcoinURI(value);
|
||||
}
|
||||
if (!uri || !uri.merchant) {
|
||||
return;
|
||||
}
|
||||
notification.info('Fetching Payment',
|
||||
'Retrieving Payment Request from ' + uri.merchant);
|
||||
|
||||
// Payment Protocol URI (BIP-72)
|
||||
scope.wallet.fetchPaymentTx(uri.merchant, function(err, merchantData) {
|
||||
var balance = $rootScope.availableBalance;
|
||||
var available = +(balance * config.unitToSatoshi).toFixed(0);
|
||||
|
||||
if (merchantData && available < +merchantData.total) {
|
||||
err = new Error('No unspent outputs available.');
|
||||
err.amount = merchantData.total;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
scope.sendForm.address.$isValid = false;
|
||||
|
||||
if (err.amount) {
|
||||
scope.sendForm.amount.$setViewValue(+err.amount / config.unitToSatoshi);
|
||||
scope.sendForm.amount.$render();
|
||||
scope.sendForm.amount.$isValid = false;
|
||||
scope.notEnoughAmount = true;
|
||||
$rootScope.merchantError = true;
|
||||
var lastAddr = scope.sendForm.address.$viewValue;
|
||||
var unregister = scope.$watch('address', function() {
|
||||
if (scope.sendForm.address.$viewValue !== lastAddr) {
|
||||
delete $rootScope.merchantError;
|
||||
scope.sendForm.amount.$setViewValue('');
|
||||
scope.sendForm.amount.$render();
|
||||
unregister();
|
||||
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
|
||||
$rootScope.$apply();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
notification.error('Error', err.message || 'Bad payment server.');
|
||||
|
||||
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
|
||||
$rootScope.$apply();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
merchantData.unitTotal = (+merchantData.total / config.unitToSatoshi) + '';
|
||||
merchantData.expiration = new Date(
|
||||
merchantData.pr.pd.expires * 1000).toISOString();
|
||||
|
||||
$rootScope.merchant = merchantData;
|
||||
|
||||
scope.sendForm.address.$isValid = true;
|
||||
|
||||
scope.sendForm.amount.$setViewValue(merchantData.unitTotal);
|
||||
scope.sendForm.amount.$render();
|
||||
scope.sendForm.amount.$isValid = true;
|
||||
|
||||
// If the address changes to a non-payment-protocol one,
|
||||
// delete the `merchant` property from the scope.
|
||||
var unregister = scope.$watch('address', function() {
|
||||
var val = scope.sendForm.address.$viewValue || '';
|
||||
var uri = copay.HDPath.parseBitcoinURI(val);
|
||||
if (!uri || !uri.merchant) {
|
||||
delete $rootScope.merchant;
|
||||
scope.sendForm.amount.$setViewValue('');
|
||||
scope.sendForm.amount.$render();
|
||||
unregister();
|
||||
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
|
||||
$rootScope.$apply();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
|
||||
$rootScope.$apply();
|
||||
}
|
||||
|
||||
notification.info('Payment Request',
|
||||
'Server is requesting ' + merchantData.unitTotal + ' ' + config.unitName + '.' + ' Message: ' + merchantData.pr.pd.memo);
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.directives')
|
||||
.directive('validAddress', [
|
||||
.directive('validAddress', function() {
|
||||
var bitcore = require('bitcore');
|
||||
var Address = bitcore.Address;
|
||||
var bignum = bitcore.Bignum;
|
||||
|
||||
function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
var validator = function(value) {
|
||||
var uri;
|
||||
|
||||
var bitcore = require('bitcore');
|
||||
var Address = bitcore.Address;
|
||||
if (/^https?:\/\//.test(value)) {
|
||||
uri = {
|
||||
merchant: value
|
||||
};
|
||||
} else {
|
||||
uri = copay.HDPath.parseBitcoinURI(value);
|
||||
}
|
||||
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
var validator = function(value) {
|
||||
// Regular Address
|
||||
if (!uri || !uri.merchant) {
|
||||
var a = new Address(value);
|
||||
ctrl.$setValidity('validAddress', a.isValid() && a.network().name === config.networkName);
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
ctrl.$parsers.unshift(validator);
|
||||
ctrl.$formatters.unshift(validator);
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
ctrl.$setValidity('validAddress', true);
|
||||
return uri.merchant;
|
||||
};
|
||||
|
||||
ctrl.$parsers.unshift(validator);
|
||||
ctrl.$formatters.unshift(validator);
|
||||
}
|
||||
};
|
||||
})
|
||||
.directive('enoughAmount', ['$rootScope',
|
||||
function($rootScope) {
|
||||
var bitcore = require('bitcore');
|
||||
|
|
@ -203,32 +217,34 @@ angular.module('copayApp.directives')
|
|||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
element.bind('click', function() {
|
||||
window.open('bitcoin:'+attrs.address, '_blank');
|
||||
window.open('bitcoin:' + attrs.address, '_blank');
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
// From https://gist.github.com/asafge/7430497
|
||||
.directive('ngReallyClick', [function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
element.bind('click', function() {
|
||||
var message = attrs.ngReallyMessage;
|
||||
if (message && confirm(message)) {
|
||||
scope.$apply(attrs.ngReallyClick);
|
||||
}
|
||||
});
|
||||
}
|
||||
// From https://gist.github.com/asafge/7430497
|
||||
.directive('ngReallyClick', [
|
||||
|
||||
function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
element.bind('click', function() {
|
||||
var message = attrs.ngReallyMessage;
|
||||
if (message && confirm(message)) {
|
||||
scope.$apply(attrs.ngReallyClick);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
])
|
||||
.directive('match', function () {
|
||||
}
|
||||
])
|
||||
.directive('match', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
match: '='
|
||||
match: '='
|
||||
},
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch(function() {
|
||||
|
|
@ -249,16 +265,18 @@ angular.module('copayApp.directives')
|
|||
|
||||
return {
|
||||
restric: 'A',
|
||||
scope: { clipCopy: '=clipCopy' },
|
||||
scope: {
|
||||
clipCopy: '=clipCopy'
|
||||
},
|
||||
link: function(scope, elm) {
|
||||
var client = new ZeroClipboard(elm);
|
||||
|
||||
client.on( 'ready', function(event) {
|
||||
client.on( 'copy', function(event) {
|
||||
client.on('ready', function(event) {
|
||||
client.on('copy', function(event) {
|
||||
event.clipboardData.setData('text/plain', scope.clipCopy);
|
||||
});
|
||||
|
||||
client.on( 'aftercopy', function(event) {
|
||||
client.on('aftercopy', function(event) {
|
||||
elm.removeClass('btn-copy').addClass('btn-copied').html('Copied!');
|
||||
setTimeout(function() {
|
||||
elm.addClass('btn-copy').removeClass('btn-copied').html('');
|
||||
|
|
@ -266,8 +284,8 @@ angular.module('copayApp.directives')
|
|||
});
|
||||
});
|
||||
|
||||
client.on( 'error', function(event) {
|
||||
console.log( 'ZeroClipboard error of type "' + event.name + '": ' + event.message );
|
||||
client.on('error', function(event) {
|
||||
console.log('ZeroClipboard error of type "' + event.name + '": ' + event.message);
|
||||
ZeroClipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,18 +55,23 @@ HDPath.parseBitcoinURI = function(uri) {
|
|||
var data = decodeURIComponent(uri);
|
||||
var splitDots = data.split(':');
|
||||
ret.protocol = splitDots[0];
|
||||
data = splitDots[1];
|
||||
data = splitDots.slice(1).join(':');
|
||||
var splitQuestion = data.split('?');
|
||||
ret.address = splitQuestion[0];
|
||||
|
||||
if (splitQuestion.length > 1) {
|
||||
var data = {};
|
||||
var search = splitQuestion[1];
|
||||
data = JSON.parse('{"' + search.replace(/&/g, '","').replace(/=/g, '":"') + '"}',
|
||||
function(key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
});
|
||||
ret.amount = parseFloat(data.amount);
|
||||
ret.message = data.message;
|
||||
var parts = search.split('&');
|
||||
var part;
|
||||
var i = 0;
|
||||
for (; i < parts.length; i++) {
|
||||
part = parts[i].split('=');
|
||||
data[part[0]] = decodeURIComponent(part[1]);
|
||||
}
|
||||
ret.amount = parseFloat(data.amount);
|
||||
ret.message = data.message;
|
||||
ret.merchant = data.r;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ function TxProposal(opts) {
|
|||
this.sentTxid = opts.sentTxid || null;
|
||||
this.comment = opts.comment || null;
|
||||
this.readonly = opts.readonly || null;
|
||||
this.merchant = opts.merchant || null;
|
||||
this._sync();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ var Key = bitcore.Key;
|
|||
var buffertools = bitcore.buffertools;
|
||||
var preconditions = require('preconditions').instance();
|
||||
|
||||
|
||||
function TxProposals(opts) {
|
||||
opts = opts || {};
|
||||
this.walletId = opts.walletId;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ var http = require('http');
|
|||
var EventEmitter = imports.EventEmitter || require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var preconditions = require('preconditions').singleton();
|
||||
var parseBitcoinURI = require('./HDPath').parseBitcoinURI;
|
||||
|
||||
var bitcore = require('bitcore');
|
||||
var bignum = bitcore.Bignum;
|
||||
|
|
@ -14,6 +15,8 @@ var Builder = bitcore.TransactionBuilder;
|
|||
var SecureRandom = bitcore.SecureRandom;
|
||||
var Base58Check = bitcore.Base58.base58Check;
|
||||
var Address = bitcore.Address;
|
||||
var PayPro = bitcore.PayPro;
|
||||
var Transaction = bitcore.Transaction;
|
||||
|
||||
var HDParams = require('./HDParams');
|
||||
var PublicKeyRing = require('./PublicKeyRing');
|
||||
|
|
@ -53,6 +56,8 @@ function Wallet(opts) {
|
|||
this.addressBook = opts.addressBook || {};
|
||||
this.publicKey = this.privateKey.publicHex;
|
||||
|
||||
this.paymentRequests = opts.paymentRequests || {};
|
||||
|
||||
//network nonces are 8 byte buffers, representing a big endian number
|
||||
//one nonce for oneself, and then one nonce for each copayer
|
||||
this.network.setHexNonce(opts.networkNonce);
|
||||
|
|
@ -108,7 +113,7 @@ Wallet.prototype.unlock = function() {
|
|||
Wallet.prototype.checkAndLock = function() {
|
||||
if (this.getLock()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.setLock();
|
||||
return false;
|
||||
|
|
@ -709,6 +714,14 @@ Wallet.prototype.sign = function(ntxid, cb) {
|
|||
// if (cb) cb(false);
|
||||
// }
|
||||
//
|
||||
|
||||
// If this is a payment protocol request,
|
||||
// ensure it hasn't been tampered with.
|
||||
if (!self.verifyPaymentRequest(ntxid)) {
|
||||
if (cb) cb(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var keys = self.privateKey.getForPaths(txp.inputChainPaths);
|
||||
|
||||
var b = txp.builder;
|
||||
|
|
@ -730,6 +743,11 @@ Wallet.prototype.sign = function(ntxid, cb) {
|
|||
|
||||
Wallet.prototype.sendTx = function(ntxid, cb) {
|
||||
var txp = this.txProposals.get(ntxid);
|
||||
|
||||
if (txp.merchant) {
|
||||
return this.sendPaymentTx(ntxid, cb);
|
||||
}
|
||||
|
||||
var tx = txp.builder.build();
|
||||
if (!tx.isComplete())
|
||||
throw new Error('Tx is not complete. Can not broadcast');
|
||||
|
|
@ -760,6 +778,629 @@ Wallet.prototype.sendTx = function(ntxid, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Wallet.prototype.createPaymentTx = function(options, cb) {
|
||||
var self = this;
|
||||
|
||||
if (typeof options === 'string') {
|
||||
options = { uri: options };
|
||||
}
|
||||
options.uri = options.uri || options.url;
|
||||
|
||||
if (options.uri.indexOf('bitcoin:') === 0) {
|
||||
options.uri = parseBitcoinURI(options.uri).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({
|
||||
method: 'GET',
|
||||
url: options.uri,
|
||||
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);
|
||||
})
|
||||
.error(function(data, status, headers, config) {
|
||||
return cb(new Error('Status: ' + JSON.stringify(status)));
|
||||
});
|
||||
};
|
||||
|
||||
Wallet.prototype.fetchPaymentTx = function(options, cb) {
|
||||
var self = this;
|
||||
|
||||
options = options || {};
|
||||
if (typeof options === 'string') {
|
||||
options = { uri: options };
|
||||
}
|
||||
options.uri = options.uri || options.url;
|
||||
options.fetch = true;
|
||||
|
||||
var req = this.paymentRequests[options.uri];
|
||||
if (req) {
|
||||
return cb(null, req.merchantData);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
|
||||
var self = this;
|
||||
|
||||
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;
|
||||
|
||||
// Fix for older versions of bitcore
|
||||
if (!PayPro.RootCerts) {
|
||||
PayPro.RootCerts = {
|
||||
getTrusted: function() {}
|
||||
};
|
||||
}
|
||||
|
||||
var trusted = certs.map(function(cert) {
|
||||
var der = cert.toString('hex');
|
||||
var pem = PayPro.prototype._DERtoPEM(der, 'CERTIFICATE');
|
||||
return PayPro.RootCerts.getTrusted(pem);
|
||||
}).filter(Boolean);
|
||||
|
||||
// Verify Signature
|
||||
var verified = pr.verify();
|
||||
|
||||
if (!verified) {
|
||||
return cb(new Error('Server sent a bad signature.'));
|
||||
}
|
||||
|
||||
var ca = trusted[0];
|
||||
|
||||
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 merchantData = {
|
||||
pr: {
|
||||
payment_details_version: ver,
|
||||
pki_type: pki_type,
|
||||
pki_data: certs,
|
||||
pd: {
|
||||
network: network,
|
||||
outputs: outputs.map(function(output) {
|
||||
return {
|
||||
amount: output.get('amount'),
|
||||
script: {
|
||||
offset: output.get('script').offset,
|
||||
limit: output.get('script').limit,
|
||||
// NOTE: For some reason output.script.buffer
|
||||
// is only an ArrayBuffer
|
||||
buffer: new Buffer(new Uint8Array(
|
||||
output.get('script').buffer)).toString('hex')
|
||||
}
|
||||
};
|
||||
}),
|
||||
time: time,
|
||||
expires: expires,
|
||||
memo: memo || 'This server would like some BTC from you.',
|
||||
payment_url: payment_url,
|
||||
merchant_data: merchant_data.toString('hex')
|
||||
},
|
||||
signature: sig.toString('hex'),
|
||||
ca: ca,
|
||||
untrusted: !ca
|
||||
},
|
||||
request_url: options.uri,
|
||||
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, unspent);
|
||||
} 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, unspent);
|
||||
if (ntxid) {
|
||||
self.sendIndexes();
|
||||
self.sendTxProposal(ntxid);
|
||||
self.store();
|
||||
self.emit('txProposalsUpdated');
|
||||
}
|
||||
|
||||
self.log('You are currently on this BTC network:');
|
||||
self.log(network);
|
||||
self.log('The server sent you a message:');
|
||||
self.log(memo);
|
||||
|
||||
return cb(ntxid, merchantData);
|
||||
});
|
||||
};
|
||||
|
||||
Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) {
|
||||
var self = this;
|
||||
|
||||
if (!cb) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var txp = this.txProposals.get(ntxid);
|
||||
if (!txp) return;
|
||||
|
||||
var tx = txp.builder.build();
|
||||
if (!tx.isComplete()) return;
|
||||
this.log('Sending Transaction');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
merchant_data = new Buffer(merchant_data, 'hex');
|
||||
pay.set('merchant_data', merchant_data);
|
||||
pay.set('transactions', [tx.serialize()]);
|
||||
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);
|
||||
|
||||
pay = pay.serialize();
|
||||
|
||||
this.log('Sending Payment Message:');
|
||||
this.log(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];
|
||||
}
|
||||
|
||||
return Wallet.request({
|
||||
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'
|
||||
})
|
||||
.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) {
|
||||
return cb(new Error('Status: ' + JSON.stringify(status)));
|
||||
});
|
||||
};
|
||||
|
||||
Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) {
|
||||
var self = this;
|
||||
|
||||
var payment = ack.get('payment');
|
||||
var memo = ack.get('memo');
|
||||
|
||||
this.log('Our payment was acknowledged!');
|
||||
this.log('Message from Merchant: %s', memo);
|
||||
|
||||
payment = PayPro.Payment.decode(payment);
|
||||
var pay = new PayPro();
|
||||
payment = pay.makePayment(payment);
|
||||
|
||||
txp.merchant.ack = {
|
||||
memo: memo
|
||||
};
|
||||
|
||||
var tx = payment.message.transactions[0];
|
||||
|
||||
if (!tx) {
|
||||
this.log('Sending to server was not met with a returned tx.');
|
||||
return this._checkSentTx(ntxid, function(txid) {
|
||||
self.log('[Wallet.js.1048:txid:%s]', txid);
|
||||
if (txid) self.store();
|
||||
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;
|
||||
}
|
||||
|
||||
var txid = tx.getHash().toString('hex');
|
||||
var txHex = tx.serialize().toString('hex');
|
||||
this.log('Raw transaction: ', txHex);
|
||||
this.log('BITCOIND txid:', txid);
|
||||
this.txProposals.get(ntxid).setSent(txid);
|
||||
this.sendTxProposal(ntxid);
|
||||
this.store();
|
||||
|
||||
return cb(txid, txp.merchant);
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
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();
|
||||
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;
|
||||
|
||||
this.log('');
|
||||
this.log('Created transaction:');
|
||||
this.log(b.tx.getStandardizedObject());
|
||||
this.log('');
|
||||
|
||||
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 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
|
||||
}));
|
||||
return ntxid;
|
||||
};
|
||||
|
||||
// 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.
|
||||
Wallet.prototype.verifyPaymentRequest = function(ntxid) {
|
||||
if (!ntxid) return false;
|
||||
|
||||
var txp = typeof ntxid !== 'object'
|
||||
? this.txProposals.get(ntxid)
|
||||
: 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.
|
||||
if (!pr.verify()) {
|
||||
// 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;
|
||||
};
|
||||
|
||||
Wallet.prototype.addSeenToTxProposals = function() {
|
||||
var ret = false;
|
||||
var myId = this.getMyCopayerId();
|
||||
|
||||
for (var k in this.txProposals.txps) {
|
||||
var txp = this.txProposals.txps[k];
|
||||
if (!txp.seenBy[myId]) {
|
||||
|
||||
txp.seenBy[myId] = Date.now();
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
// TODO: remove this method and use getAddressesInfo everywhere
|
||||
Wallet.prototype.getAddresses = function(opts) {
|
||||
|
|
@ -853,6 +1494,20 @@ Wallet.prototype.getUnspent = function(cb) {
|
|||
|
||||
Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) {
|
||||
var self = this;
|
||||
|
||||
if (typeof amountSatStr === 'function') {
|
||||
var cb = amountSatStr;
|
||||
var merchant = toAddress;
|
||||
return this.createPaymentTx({ uri: merchant }, cb);
|
||||
}
|
||||
|
||||
if (typeof comment === 'function') {
|
||||
var cb = comment;
|
||||
var merchant = toAddress;
|
||||
var comment = amountSatStr;
|
||||
return this.createPaymentTx({ uri: merchant, memo: comment }, cb);
|
||||
}
|
||||
|
||||
if (typeof opts === 'function') {
|
||||
cb = opts;
|
||||
opts = {};
|
||||
|
|
@ -1105,4 +1760,85 @@ Wallet.prototype.verifySignedJson = function(senderId, payload, signature) {
|
|||
return v;
|
||||
}
|
||||
|
||||
// NOTE: Angular $http module does not send ArrayBuffers correctly, so we're
|
||||
// not going to use it. We'll have to write our own. Otherwise, we could
|
||||
// hex-encoded our messages and decode them on the other side, but that
|
||||
// deviates from BIP-70.
|
||||
|
||||
// if (typeof angular !== 'undefined') {
|
||||
// var $http = angular.bootstrap().get('$http');
|
||||
// }
|
||||
|
||||
Wallet.request = function(options, callback) {
|
||||
if (typeof options === 'string') {
|
||||
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) {
|
||||
return ret._error(null, new Error(event.message), null, options);
|
||||
};
|
||||
|
||||
if (req.body) {
|
||||
xhr.send(req.body);
|
||||
} else {
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
module.exports = require('soop')(Wallet);
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ module.exports = function(config) {
|
|||
//App-specific Code
|
||||
'js/app.js',
|
||||
'js/routes.js',
|
||||
'js/services/*.js',
|
||||
'js/directives.js',
|
||||
'js/filters.js',
|
||||
'js/services/*.js',
|
||||
'js/controllers/*.js',
|
||||
'js/init.js',
|
||||
|
||||
|
|
@ -54,6 +54,7 @@ module.exports = function(config) {
|
|||
'test/lib/chai-should.js',
|
||||
'test/lib/chai-expect.js',
|
||||
'test/mocks/FakeWallet.js',
|
||||
'test/mocks/FakePayProServer.js',
|
||||
|
||||
'test/mocha.conf.js',
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@
|
|||
},
|
||||
"version": "0.4.2",
|
||||
"dependencies": {
|
||||
"preconditions": "^1.0.7",
|
||||
"sinon": "1.9.1",
|
||||
"mocha": "^1.18.2",
|
||||
"mocha-lcov-reporter": "0.0.1",
|
||||
"mocha": "^1.18.2"
|
||||
"optimist": "^0.6.1",
|
||||
"preconditions": "^1.0.7",
|
||||
"sinon": "1.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"shell": "node shell/scripts/launch.js",
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
"github-releases": "0.2.0",
|
||||
"grunt-markdown": "0.5.0",
|
||||
"browser-pack": "2.0.1",
|
||||
"bitcore": "0.1.34",
|
||||
"bitcore": "git://github.com/bitpay/bitcore.git#master",
|
||||
"node-cryptojs-aes": "0.4.0",
|
||||
"blanket": "1.1.6",
|
||||
"express": "4.0.0",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@
|
|||
<script src="../node_modules/chai/chai.js"></script>
|
||||
<script src="../lib/crypto-js/rollups/aes.js"></script>
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="../lib/bitcore/browser/bundle.js"></script>
|
||||
<script src="../lib/bitcore/browser/bundle.js"></script>
|
||||
<script src="../js/copayBundle.js"></script>
|
||||
<script src="test.blockchain.Insight.js"></script>
|
||||
<script src="test.Message.js"></script>
|
||||
<script src="test.network.WebRTC.js"></script>
|
||||
<script src="test.PayPro.js"></script>
|
||||
<script src="test.PrivateKey.js"></script>
|
||||
<script src="test.PublicKeyRing.js"></script>
|
||||
<script src="test.storage.LocalEncrypted.js"></script>
|
||||
|
|
|
|||
346
test/mocks/FakePayProServer.js
Normal file
346
test/mocks/FakePayProServer.js
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
'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;
|
||||
|
||||
console.log('Received payment "request" from %s.', req.socket.remoteAddress);
|
||||
|
||||
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;
|
||||
|
||||
console.log('Received Payment Message Body:');
|
||||
console.log(body.toString('hex'));
|
||||
|
||||
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');
|
||||
|
||||
console.log('Received Payment from %s.', req.socket.remoteAddress);
|
||||
console.log('Customer Message: %s', memo);
|
||||
console.log('Payment Message:');
|
||||
console.log(pay);
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
transactions.forEach(function(tx) {
|
||||
var id = tx.getHash().toString('hex');
|
||||
console.log('');
|
||||
console.log('Sending transaction with txid: %s', id);
|
||||
console.log(tx.getStandardizedObject());
|
||||
});
|
||||
|
||||
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;
|
||||
|
|
@ -1,3 +1,14 @@
|
|||
|
||||
var is_browser = typeof process == 'undefined'
|
||||
|| typeof process.versions === 'undefined';
|
||||
if (is_browser) {
|
||||
var copay = require('copay'); //browser
|
||||
} else {
|
||||
var copay = require('../copay'); //node
|
||||
}
|
||||
var Wallet = copay.Wallet;
|
||||
|
||||
|
||||
var FakeWallet = function() {
|
||||
this.id = 'testID';
|
||||
this.balance = 10000;
|
||||
|
|
@ -53,7 +64,20 @@ FakeWallet.prototype.isShared = function() {
|
|||
|
||||
FakeWallet.prototype.isReady = function() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
FakeWallet.prototype.fetchPaymentTx = function(opts, cb) {
|
||||
cb(null, {
|
||||
pr: {
|
||||
pd: {
|
||||
expires: 12
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
FakeWallet.prototype.createPaymentTx = Wallet.prototype.createPaymentTx;
|
||||
|
||||
|
||||
FakeWallet.prototype.getBalance = function(cb) {
|
||||
|
|
|
|||
896
test/test.PayPro.js
Normal file
896
test/test.PayPro.js
Normal file
|
|
@ -0,0 +1,896 @@
|
|||
'use strict';
|
||||
|
||||
var chai = chai || require('chai');
|
||||
var should = chai.should();
|
||||
var sinon = require('sinon');
|
||||
var is_browser = typeof process == 'undefined'
|
||||
|| typeof process.versions === 'undefined';
|
||||
if (is_browser) {
|
||||
var copay = require('copay'); //browser
|
||||
} else {
|
||||
var copay = require('../copay'); //node
|
||||
}
|
||||
var copayConfig = require('../config');
|
||||
var Wallet = copay.Wallet;
|
||||
var PrivateKey = copay.PrivateKey;
|
||||
var Storage = require('./mocks/FakeStorage');
|
||||
var Network = require('./mocks/FakeNetwork');
|
||||
var Blockchain = require('./mocks/FakeBlockchain');
|
||||
var bitcore = bitcore || require('bitcore');
|
||||
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 config = {
|
||||
requiredCopayers: 1,
|
||||
totalCopayers: 1,
|
||||
spendUnconfirmed: true,
|
||||
reconnectDelay: 100,
|
||||
networkName: 'testnet',
|
||||
};
|
||||
|
||||
var getNewEpk = function() {
|
||||
return new PrivateKey({
|
||||
networkName: config.networkName,
|
||||
})
|
||||
.deriveBIP45Branch()
|
||||
.extendedPublicKeyString();
|
||||
};
|
||||
|
||||
describe('PayPro (in Wallet) model', function() {
|
||||
var createW = function(N, conf) {
|
||||
var c = JSON.parse(JSON.stringify(conf || config));
|
||||
if (!N) N = c.totalCopayers;
|
||||
|
||||
var mainPrivateKey = new copay.PrivateKey({
|
||||
networkName: config.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 storage = new Storage(config.storage);
|
||||
var network = new Network(config.network);
|
||||
var blockchain = new Blockchain(config.blockchain);
|
||||
c.storage = storage;
|
||||
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 = config.networkName;
|
||||
c.verbose = config.verbose;
|
||||
c.version = '0.0.1';
|
||||
|
||||
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;
|
||||
}
|
||||
var w = Wallet.fromObj(cachedW2obj, cachedW2.storage, cachedW2.network, cachedW2.blockchain);
|
||||
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(ntxid, merchantData) {
|
||||
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(ntxid, merchantData) {
|
||||
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 = copay.HDPath.parseBitcoinURI(address);
|
||||
} else if (address.indexOf('Merchant: ') === 0) {
|
||||
uri = address.split(/\s+/)[1];
|
||||
}
|
||||
|
||||
w.createTx(uri, commentText, function(ntxid, merchantData) {
|
||||
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.createTx(address, commentText, function(ntxid, merchantData) {
|
||||
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.createTx(address, commentText, function(ntxid, merchantData) {
|
||||
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.createTx(address, commentText, function(ntxid, merchantData) {
|
||||
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.createTx(address, commentText, function(ntxid, merchantData) {
|
||||
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.createTx(address, commentText, function(ntxid, merchantData) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -13,9 +13,11 @@ var TransactionBuilder = bitcore.TransactionBuilder;
|
|||
var util = bitcore.util;
|
||||
var networks = bitcore.networks;
|
||||
var sinon = require('sinon');
|
||||
try {
|
||||
var is_browser = typeof process == 'undefined'
|
||||
|| typeof process.versions === 'undefined';
|
||||
if (is_browser) {
|
||||
var copay = require('copay'); //browser
|
||||
} catch (e) {
|
||||
} else {
|
||||
var copay = require('../copay'); //node
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ var TransactionBuilder = bitcore.TransactionBuilder;
|
|||
var util = bitcore.util;
|
||||
var networks = bitcore.networks;
|
||||
var sinon = require('sinon');
|
||||
var copay = require('../copay');
|
||||
try {
|
||||
var copay = require('copay'); //browser
|
||||
} catch (e) {
|
||||
var copay = require('../copay'); //node
|
||||
}
|
||||
|
||||
var FakeBuilder = require('./mocks/FakeBuilder');
|
||||
var TxProposal = copay.TxProposal;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
var chai = chai || require('chai');
|
||||
var should = chai.should();
|
||||
var sinon = require('sinon');
|
||||
try {
|
||||
var is_browser = (typeof process == 'undefined' || typeof process.versions === 'undefined');
|
||||
if (is_browser) {
|
||||
var copay = require('copay'); //browser
|
||||
} catch (e) {
|
||||
} else {
|
||||
var copay = require('../copay'); //node
|
||||
}
|
||||
var copayConfig = require('../config');
|
||||
var Wallet = require('../js/models/core/Wallet');
|
||||
var Wallet = copay.Wallet;
|
||||
var PrivateKey = copay.PrivateKey;
|
||||
var Storage = require('./mocks/FakeStorage');
|
||||
var Network = require('./mocks/FakeNetwork');
|
||||
|
|
|
|||
|
|
@ -19,7 +19,13 @@ CryptoJS.AES.decrypt = function(a) {
|
|||
'use strict';
|
||||
var chai = chai || require('chai');
|
||||
var should = chai.should();
|
||||
var copay = copay || require('../copay');
|
||||
var is_browser = typeof process == 'undefined'
|
||||
|| typeof process.versions === 'undefined';
|
||||
if (is_browser) {
|
||||
var copay = require('copay'); //browser
|
||||
} else {
|
||||
var copay = require('../copay'); //node
|
||||
}
|
||||
var LocalEncrypted = copay.StorageLocalEncrypted;
|
||||
|
||||
var fakeWallet = 'fake-wallet-id';
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ saveAs = function(o) {
|
|||
saveAsLastCall = o;
|
||||
};
|
||||
|
||||
var startServer = require('../../mocks/FakePayProServer');
|
||||
|
||||
describe("Unit: Controllers", function() {
|
||||
var invalidForm = {
|
||||
|
|
@ -18,6 +19,8 @@ describe("Unit: Controllers", function() {
|
|||
|
||||
var scope;
|
||||
|
||||
var server;
|
||||
|
||||
beforeEach(module('copayApp.services'));
|
||||
beforeEach(module('copayApp.controllers'));
|
||||
|
||||
|
|
@ -150,6 +153,7 @@ describe("Unit: Controllers", function() {
|
|||
scope.$digest();
|
||||
form = scope.form;
|
||||
sendForm = scope.form2;
|
||||
scope.sendForm = sendForm;
|
||||
}));
|
||||
|
||||
it('should have a SendController controller', function() {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ var puts = function(error, stdout, stderr) {
|
|||
//sys.puts(stderr);
|
||||
};
|
||||
|
||||
var pack = function (params) {
|
||||
var pack = function(params) {
|
||||
var file = require.resolve('soop');
|
||||
var dir = file.substr(0, file.length - String('soop.js').length);
|
||||
var preludePath = dir + 'example/custom_prelude.js';
|
||||
|
|
@ -48,9 +48,6 @@ var createBundle = function(opts) {
|
|||
b.require('./copay', {
|
||||
expose: 'copay'
|
||||
});
|
||||
b.require('./copay', {
|
||||
expose: '../copay'
|
||||
});
|
||||
b.require('./version');
|
||||
// b.external('bitcore');
|
||||
b.require('./js/models/core/WalletFactory', {
|
||||
|
|
@ -60,26 +57,35 @@ var createBundle = function(opts) {
|
|||
b.require('./js/models/core/Wallet', {
|
||||
expose: '../js/models/core/Wallet'
|
||||
});
|
||||
b.require('./js/models/core/Wallet', {
|
||||
expose: '../../js/models/core/Wallet'
|
||||
});
|
||||
b.require('./test/mocks/FakeStorage', {
|
||||
expose: './mocks/FakeStorage'
|
||||
});
|
||||
b.require('./test/mocks/FakeBlockchain', {
|
||||
expose: './mocks/FakeBlockchain'
|
||||
});
|
||||
b.require('./test/mocks/FakeLocalStorage', {
|
||||
expose: './mocks/FakeLocalStorage'
|
||||
});
|
||||
b.require('./js/models/core/Wallet', {
|
||||
expose: '../js/models/core/Wallet'
|
||||
b.require('./js/models/core/Message', {
|
||||
expose: '../js/models/core/Message'
|
||||
});
|
||||
b.require('./test/mocks/FakeBlockchain', {
|
||||
expose: './mocks/FakeBlockchain'
|
||||
});
|
||||
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'
|
||||
});
|
||||
b.require('./js/models/network/WebRTC', {
|
||||
expose: '../js/models/network/WebRTC'
|
||||
expose: '../js/models/network/WebRTC'
|
||||
});
|
||||
b.require('./js/models/blockchain/Insight', {
|
||||
expose: '../js/models/blockchain/Insight'
|
||||
|
|
@ -93,24 +99,21 @@ var createBundle = function(opts) {
|
|||
b.require('./js/models/core/Passphrase', {
|
||||
expose: '../js/models/core/Passphrase'
|
||||
});
|
||||
b.require('./js/models/core/Message', {
|
||||
expose: '../js/models/core/Message'
|
||||
b.require('./js/models/core/HDPath', {
|
||||
expose: '../js/models/core/HDPath'
|
||||
});
|
||||
b.require('./config', {
|
||||
expose: '../config'
|
||||
});
|
||||
b.require('./js/models/core/HDPath', {
|
||||
expose: '../js/models/core/HDPath'
|
||||
});
|
||||
});
|
||||
|
||||
if (opts.debug) {
|
||||
if (opts.dontminify) {
|
||||
//include dev dependencies
|
||||
b.require('sinon');
|
||||
b.require('blanket');
|
||||
b.require('soop');
|
||||
}
|
||||
|
||||
if (!opts.debug) {
|
||||
if (!opts.dontminify) {
|
||||
b.transform({
|
||||
global: true
|
||||
}, 'uglifyify');
|
||||
|
|
@ -125,10 +128,10 @@ if (require.main === module) {
|
|||
};
|
||||
var program = require('commander');
|
||||
program
|
||||
.version('0.0.1')
|
||||
.option('-d, --debug', 'Development. Don\'t minify the codem and include debug packages.')
|
||||
.option('-o, --stdout', 'Specify output as stdout')
|
||||
.parse(process.argv);
|
||||
.version('0.0.1')
|
||||
.option('-d, --dontminify', 'Development. Don\'t minify the code.')
|
||||
.option('-o, --stdout', 'Specify output as stdout')
|
||||
.parse(process.argv);
|
||||
|
||||
createVersion();
|
||||
var copayBundle = createBundle(program);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</label>
|
||||
<div class="small-10 columns">
|
||||
<input type="text" id="address" name="address" ng-disabled="loading"
|
||||
placeholder="Send to" ng-model="address" valid-address required>
|
||||
placeholder="Send to" ng-model="address" ng-change="onChanged()" valid-address required>
|
||||
<small class="icon-input" ng-show="!sendForm.address.$invalid && address"><i class="fi-check"></i></small>
|
||||
<small class="icon-input" ng-show="sendForm.address.$invalid && address"><i class="fi-x"></i></small>
|
||||
</div>
|
||||
|
|
@ -64,18 +64,19 @@
|
|||
<small ng-show="notEnoughAmount" class="has-error">Insufficient funds</small>
|
||||
</label>
|
||||
<div class="small-9 columns">
|
||||
<input type="number" id="amount" ng-disabled="loading"
|
||||
<input type="number" id="amount"
|
||||
ng-disabled="loading || ($root.merchant && +$root.merchant.total > 0) || $root.merchantError"
|
||||
name="amount" placeholder="Amount" ng-model="amount"
|
||||
min="0.0001" max="10000000000" enough-amount required
|
||||
min="0.00005400" max="10000000000" enough-amount required
|
||||
autocomplete="off"
|
||||
>
|
||||
<small class="icon-input" ng-show="!sendForm.amount.$invalid && amount"><i class="fi-check"></i></small>
|
||||
<small class="icon-input" ng-show="sendForm.amount.$invalid && !sendForm.amount.$pristine && !notEnoughAmount"><i class="fi-x"></i></small>
|
||||
<a class="small input-note" title="Send all funds"
|
||||
ng-show="$root.availableBalance > 0"
|
||||
ng-show="$root.availableBalance > 0 && (!$root.merchant || +$root.merchant.total === 0)"
|
||||
ng-click="topAmount(sendForm)">
|
||||
Use all funds ({{getAvailableAmount()}} {{$root.unitName}})
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
<div class="small-3 columns">
|
||||
<span class="postfix">{{$root.unitName}}</span>
|
||||
|
|
@ -124,9 +125,28 @@
|
|||
</small>
|
||||
</p>
|
||||
<div ng-show="wallet.isShared()">
|
||||
<h6>Note</h6>
|
||||
<p ng-class="{'hidden': !commentText}">{{commentText}}</p>
|
||||
</div>
|
||||
<h6>Note</h6>
|
||||
<p ng-class="{'hidden': !commentText}">{{commentText}}</p>
|
||||
</div>
|
||||
<div ng-show="!!$root.merchant">
|
||||
<h6>Merchant Data:</h6>
|
||||
<p class="text-gray">
|
||||
Note: This is a payment protocol transaction.
|
||||
</p>
|
||||
<h6>Server Says:</h6>
|
||||
<p class="text-gray">
|
||||
{{$root.merchant.pr.pd.memo}}
|
||||
</p>
|
||||
<h6>Certificate:</h6>
|
||||
<p class="text-gray">
|
||||
<span ng-show="!!$root.merchant.pr.ca">{{$root.merchant.pr.ca}}</span>
|
||||
<span ng-show="!$root.merchant.pr.ca" style="color:red;weight:bold;">Untrusted</span>
|
||||
</p>
|
||||
<h6>Payment Expiration:</h6>
|
||||
<p class="text-gray">
|
||||
{{$root.merchant.expiration}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -144,8 +164,8 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
ng-repeat="(addr, info) in $root.wallet.addressBook"
|
||||
<tr
|
||||
ng-repeat="(addr, info) in $root.wallet.addressBook"
|
||||
ng-class="{'addressbook-disabled': info.hidden}">
|
||||
<td><a ng-click="copyAddress(addr)" title="Copy address">{{info.label}}</a></td>
|
||||
<td class="ellipsis">{{addr}} <span class="btn-copy" clip-copy="addr"></span></td>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue