Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
Merge pull request #43 from matiu/feat/paypro
Browse files Browse the repository at this point in the history
Feat/paypro
  • Loading branch information
cmgustavo committed Mar 27, 2015
2 parents b9763cd + 0914ca5 commit 2842278
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 68 deletions.
129 changes: 86 additions & 43 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ if (process && !process.browser) {
request = require('browser-request');
}

var PayPro = require('bitcore-payment-protocol');


var PayProRequest = require('./payprorequest');
var log = require('./log');
var Credentials = require('./credentials');
var Verifier = require('./verifier');
Expand All @@ -42,6 +46,8 @@ function API(opts) {
var parsedUrl = url.parse(this.baseUrl);
this.basePath = parsedUrl.path;
this.baseHost = parsedUrl.protocol + '//' + parsedUrl.host;
this.payProGetter = null; // Only for testing

if (this.verbose) {
log.setLevel('debug');
} else {
Expand Down Expand Up @@ -559,32 +565,51 @@ API.prototype.getStatus = function(cb) {
});
};

API.prototype._computeProposalSignature = function(args) {
$.shouldBeNumber(args.amount);
var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message, args.payProUrl);
return WalletUtils.signMessage(hash, this.credentials.requestPrivKey);
}

API.prototype.fetchPayPro = function(opts, cb) {
$.checkArgument(opts)
.checkArgument(opts.payProUrl);

PayProRequest.get({
url: opts.payProUrl,
getter: this.payProGetter,
}, function(err, paypro) {
if (err)
return cb(err || 'Could not fetch PayPro request');

return cb(null, paypro);
});
};

/**
* Send a transaction proposal
*
* @param {Object} opts
* @param {String} opts.toAddress
* @param {Number} opts.amount
* @param {String} opts.message
* @param {String} opts.payProUrl Optional: Tx is from a from a payment protocol URL
* @returns {Callback} cb - Return error or the transaction proposal
*/
API.prototype.sendTxProposal = function(opts, cb) {
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(opts);
$.shouldBeNumber(opts.amount);

var self = this;

var args = {
toAddress: opts.toAddress,
amount: opts.amount,
message: API._encryptMessage(opts.message, self.credentials.sharedEncryptingKey),
message: API._encryptMessage(opts.message, this.credentials.sharedEncryptingKey),
payProUrl: opts.payProUrl,
};
var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message);
args.proposalSignature = WalletUtils.signMessage(hash, self.credentials.requestPrivKey);
log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature);
log.debug('Generating & signing tx proposal:', JSON.stringify(args));
args.proposalSignature = this._computeProposalSignature(args);

self._doPostRequest('/v1/txproposals/', args, function(err, txp) {
this._doPostRequest('/v1/txproposals/', args, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
Expand Down Expand Up @@ -667,27 +692,36 @@ API.prototype.getTxProposals = function(opts, cb) {
if (err) return cb(err);

API._processTxps(txps, self.credentials.sharedEncryptingKey);

var fake = _.any(txps, function(txp) {
return (!opts.doNotVerify && !Verifier.checkTxProposal(self.credentials, txp));
});

if (fake)
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));

var result;
if (opts.forAirGapped) {
result = {
txps: JSON.parse(JSON.stringify(txps)),
encryptedPkr: WalletUtils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey),
m: self.credentials.m,
n: self.credentials.n,
};
} else {
result = txps;
}

return cb(null, result);
async.every(txps,
function(txp, acb) {
if (opts.doNotVerify) return acb(true);

Verifier.checkTxProposal(self.credentials, txp, {
payProGetter: self.payProGetter
}, function(err, isLegit) {
if (err)
return cb(new Error('Cannot check transaction now:' + err));

return acb(isLegit);
});
},
function(isLegit) {
if (!isLegit)
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));

var result;
if (opts.forAirGapped) {
result = {
txps: JSON.parse(JSON.stringify(txps)),
encryptedPkr: WalletUtils.encryptMessage(JSON.stringify(self.credentials.publicKeyRing), self.credentials.personalEncryptingKey),
m: self.credentials.m,
n: self.credentials.n,
};
} else {
result = txps;
}
return cb(null, result);
});
});
};

Expand All @@ -707,21 +741,29 @@ API.prototype.signTxProposal = function(txp, cb) {
if (!self.canSign() && !txp.signatures)
return cb(new Error('You do not have the required keys to sign transactions'));

if (!Verifier.checkTxProposal(self.credentials, txp)) {
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
}

var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey);
Verifier.checkTxProposal(self.credentials, txp, {
payProGetter: self.payProGetter,
}, function(err, isLegit) {

var url = '/v1/txproposals/' + txp.id + '/signatures/';
var args = {
signatures: signatures
};
if (err)
return cb(new Error('Cannot check transaction now:' + err));

self._doPostRequest(url, args, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
if (!isLegit)
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));

var signatures = txp.signatures || WalletUtils.signTxp(txp, self.credentials.xPrivKey);

var url = '/v1/txproposals/' + txp.id + '/signatures/';
var args = {
signatures: signatures
};

self._doPostRequest(url, args, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
})
};

/**
Expand Down Expand Up @@ -756,9 +798,10 @@ API.prototype.signTxProposalFromAirGapped = function(txp, encryptedPkr, m, n) {
self.credentials.n = n;
self.credentials.addPublicKeyRing(publicKeyRing);

if (!Verifier.checkTxProposal(self.credentials, txp)) {
// When forAirGapped=true -> checkTxProposal is sync
if (!Verifier.checkTxProposalBody(self.credentials, txp))
throw new Error('Fake transaction proposal');
}

return WalletUtils.signTxp(txp, self.credentials.xPrivKey);
};

Expand Down
148 changes: 148 additions & 0 deletions lib/payprorequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
var $ = require('preconditions').singleton();

var WalletUtils = require('bitcore-wallet-utils');
var Bitcore = WalletUtils.Bitcore;
var PayPro = require('bitcore-payment-protocol');
var PayProRequest = {};

PayProRequest._nodeGet = function(opts, cb) {
opts.agent = false;
var http = opts.http || (opts.proto === 'http' ? require("http") : require("https"));

http.get(opts, function(res) {
if (res.statusCode != 200)
return cb('HTTP Request Error');

var data = []; // List of Buffer objects
res.on("data", function(chunk) {
data.push(chunk); // Append Buffer object
});
res.on("end", function() {
data = Buffer.concat(data); // Make one large Buffer of it
return cb(null, data);
});
});
};

PayProRequest._browserGet = function(opts, cb) {
var method = (opts.method || 'GET').toUpperCase();
var url = opts.url;
var req = opts;

req.headers = req.headers || {};
req.body = req.body || req.data || '';

var xhr = opts.xhr || new XMLHttpRequest();
xhr.open(method, url, true);

Object.keys(req.headers).forEach(function(key) {
var val = req.headers[key];
if (key === 'Content-Length') return;
if (key === 'Content-Transfer-Encoding') return;
xhr.setRequestHeader(key, val);
});
xhr.responseType = 'arraybuffer';

xhr.onload = function(event) {
var response = xhr.response;
return cb(null, new Uint8Array(response));
};

xhr.onerror = function(event) {
var status;
if (xhr.status === 0 || !xhr.statusText) {
status = 'HTTP Request Error';
} else {
status = xhr.statusText;
}
return cb(status);
};

xhr.send(null);
};

PayProRequest.get = function(opts, cb) {
$.checkArgument(opts && opts.url);

var getter = opts.getter;

opts.headers = opts.headers || {
'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE,
'Content-Type': 'application/octet-stream',
};

if (!opts.getter) {
var env = opts.env;
if (!env)
env = (process && !process.browser) ? 'node' : 'browser';

if (env == "node") {
getter = PayProRequest._nodeGet;
} else {
getter = PayProRequest._browserGet;
}
}

var match = opts.url.match(/^((http[s]?|ftp):\/)?\/?([^:\/\s]+)((\/\w+)*\/)([\w\-\.]+[^#?\s]+)(.*)?(#[\w\-]+)?$/);

opts.proto = RegExp.$2;
opts.host = RegExp.$3;
opts.path = RegExp.$4 + RegExp.$6;

getter(opts, function(err, dataBuffer) {
if (err) return cb(err);
var body = PayPro.PaymentRequest.decode(dataBuffer);
var request = (new PayPro()).makePaymentRequest(body);

var signature = request.get('signature');
var serializedDetails = request.get('serialized_payment_details');

// Verify the signature
var verified = request.verify(true);

// Get the payment details
var decodedDetails = PayPro.PaymentDetails.decode(serializedDetails);
var pd = new PayPro();
pd = pd.makePaymentDetails(decodedDetails);

var outputs = pd.get('outputs');
if (outputs.length > 1)
return cb(new Error('Payment Protocol Error: Requests with more that one output are not supported'))

var output = outputs[0];

var amount = output.get('amount');
amount = amount.low + amount.high * 0x100000000;


var network = pd.get('network') == 'test' ? 'testnet' : 'livenet';

// We love payment protocol
var offset = output.get('script').offset;
var limit = output.get('script').limit;

// NOTE: For some reason output.script.buffer
// is only an ArrayBuffer
var buffer = new Buffer(new Uint8Array(output.get('script').buffer));
var scriptBuf = buffer.slice(offset, limit);
var addr = new Bitcore.Address.fromScript(new Bitcore.Script(scriptBuf), network);

return cb(null, {
verified: verified.verified,
verifyData: {
caName: verified.caName,
selfSigned: verified.selfSigned,
},
expires: pd.get('expires'),
memo: pd.get('memo'),
time: pd.get('time'),
toAddress: addr.toString(),
amount: amount,
network: network,
domain: opts.host,
url: opts.url,
});
});
};

module.exports = PayProRequest;
Loading

0 comments on commit 2842278

Please sign in to comment.