UNPKG

bitcore-wallet-client

Version:
273 lines 11 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PayPro = void 0; const crypto_wallet_core_1 = require("crypto-wallet-core"); const preconditions_1 = require("preconditions"); const superagent_1 = __importDefault(require("superagent")); const url_1 = __importDefault(require("url")); const JsonPaymentProtocolKeys_1 = __importDefault(require("../util/JsonPaymentProtocolKeys")); const errors_1 = require("./errors"); const $ = (0, preconditions_1.singleton)(); const Bitcore = crypto_wallet_core_1.BitcoreLib; const Bitcore_ = { btc: Bitcore, bch: crypto_wallet_core_1.BitcoreLibCash }; const JSON_PAYMENT_REQUEST_CONTENT_TYPE = 'application/payment-request'; const JSON_PAYMENT_VERIFY_CONTENT_TYPE = 'application/verify-payment'; const JSON_PAYMENT_CONTENT_TYPE = 'application/payment'; const JSON_PAYMENT_ACK_CONTENT_TYPE = 'application/payment-ack'; const MAX_FEE_PER_KB = 500000; class PayPro { constructor() { } static _verify(requestUrl, headers, network, trustedKeys, callback) { let hash = headers.digest.split('=')[1]; let signature = headers.signature; let signatureType = headers['x-signature-type']; let identity = headers['x-identity']; let host; if (network == 'testnet') network = 'test'; if (network == 'livenet') network = 'main'; if (!requestUrl) { return callback(new Error('You must provide the original payment request url')); } if (!trustedKeys) { return callback(new Error('You must provide a set of trusted keys')); } try { host = url_1.default.parse(requestUrl).hostname; } catch (e) { } if (!host) { return callback(new Error('Invalid requestUrl')); } if (!signatureType) { return callback(new Error('Response missing x-signature-type header')); } if (typeof signatureType !== 'string') { return callback(new Error('Invalid x-signature-type header')); } if (signatureType !== 'ecc') { return callback(new Error(`Unknown signature type ${signatureType}`)); } if (!signature) { return callback(new Error('Response missing signature header')); } if (typeof signature !== 'string') { return callback(new Error('Invalid signature header')); } if (!identity) { return callback(new Error('Response missing x-identity header')); } if (typeof identity !== 'string') { return callback(new Error('Invalid identity header')); } if (!trustedKeys[identity]) { return callback(new Error(`Response signed by unknown key (${identity}), unable to validate`)); } let keyData = trustedKeys[identity]; if (keyData.domains.indexOf(host) === -1) { return callback(new Error(`The key on the response (${identity}) is not trusted for domain ${host}`)); } else if (!keyData.networks.includes(network)) { return callback(new Error(`The key on the response is not trusted for transactions on the '${network}' network`)); } try { const hashbuf = Buffer.from(hash, 'hex'); const sigbuf = Buffer.from(signature, 'hex'); const s_r = Buffer.alloc(32); const s_s = Buffer.alloc(32); sigbuf.copy(s_r, 0, 0); sigbuf.copy(s_s, 0, 32); const s_rBN = Bitcore.crypto.BN.fromBuffer(s_r); const s_sBN = Bitcore.crypto.BN.fromBuffer(s_s); const pub = Bitcore.PublicKey.fromString(keyData.publicKey); const sig = new Bitcore.crypto.Signature(); sig.set({ r: s_rBN, s: s_sBN }); const valid = Bitcore.crypto.ECDSA.verify(hashbuf, sig, pub); if (!valid) { throw new Error('Invalid signature'); } return callback(null, keyData.owner); } catch (err) { return callback(new Error('Response signature invalid')); } } static runRequest(opts, cb) { $.checkArgument(opts.network, 'should pass network'); var r = this.r[opts.method.toLowerCase()](opts.url); for (const [k, v] of Object.entries(opts.headers || {})) { if (v) r.set(k, v); } if (opts.args) { if (opts.method.toLowerCase() == 'post' || opts.method.toLowerCase() == 'put') { r.send(opts.args); } else { r.query(opts.args); } } r.end((err, res) => { if (err) return cb(err); var body = res.text; if (!res || res.statusCode != 200) { if (res.statusCode == 400) { return cb(new errors_1.Errors.INVOICE_EXPIRED()); } else if (res.statusCode == 404) { return cb(new errors_1.Errors.INVOICE_NOT_AVAILABLE()); } else if (res.statusCode == 422) { return cb(new errors_1.Errors.UNCONFIRMED_INPUTS_NOT_ACCEPTED()); } let m = res ? res.statusMessage || res.statusCode : ''; return cb(new Error('Could not fetch invoice: ' + m)); } if (opts.noVerify) return cb(null, body); if (!res.headers.digest) { return cb(new Error('Digest missing from response headers')); } let digest = res.headers.digest.toString().split('=')[1]; let hash = Bitcore.crypto.Hash.sha256(Buffer.from(body, 'utf8')).toString('hex'); if (digest !== hash) { return cb(new Error(`Response body hash does not match digest header. Actual: ${hash} Expected: ${digest}`)); } PayPro._verify(opts.url, res.headers, opts.network, opts.trustedKeys, err => { if (err) return cb(err); let ret; try { ret = JSON.parse(body); } catch (e) { return cb(new Error('Could not payment request:' + body)); } ret.verified = 1; return cb(null, ret); }); }); } static get(opts, cb) { $.checkArgument(opts && opts.url); opts.trustedKeys = opts.trustedKeys || JsonPaymentProtocolKeys_1.default; var coin = opts.coin || 'btc'; var bitcore = Bitcore_[coin]; var COIN = coin.toUpperCase(); opts.headers = opts.headers || { Accept: JSON_PAYMENT_REQUEST_CONTENT_TYPE, 'Content-Type': 'application/octet-stream' }; opts.method = 'GET'; opts.network = opts.network || 'livenet'; PayPro.runRequest(opts, function (err, data) { if (err) return cb(err); var ret = {}; ret.verified = true; if (data.network == 'test') ret.network = 'testnet'; if (data.network == 'main') ret.network = 'livenet'; if (!data.network) return cb(new Error('No network at payment request')); if (data.currency != COIN) return cb(new Error('Currency mismatch. Expecting:' + COIN)); ret.coin = coin; if (data.requiredFeeRate > MAX_FEE_PER_KB) return cb(new Error('Fee rate too high:' + data.requiredFeeRate)); ret.requiredFeeRate = data.requiredFeeRate; if (!data.outputs || data.outputs.length != 1) { return cb(new Error('Must have 1 output')); } if (typeof data.outputs[0].amount !== 'number') { return cb(new Error('Bad output amount')); } ret.amount = data.outputs[0].amount; try { ret.toAddress = new bitcore.Address(data.outputs[0].address).toString(true); } catch (e) { return cb(new Error('Bad output address ' + e)); } ret.memo = data.memo; ret.paymentId = data.paymentId; try { ret.expires = new Date(data.expires).toISOString(); } catch (e) { return cb(new Error('Bad expiration')); } return cb(null, ret); }); } static send(opts, cb) { $.checkArgument(opts.rawTxUnsigned) .checkArgument(opts.url) .checkArgument(opts.rawTx); var coin = opts.coin || 'btc'; var COIN = coin.toUpperCase(); opts.network = opts.network || 'livenet'; opts.method = 'POST'; opts.headers = opts.headers || { 'Content-Type': JSON_PAYMENT_VERIFY_CONTENT_TYPE }; let size = opts.rawTx.length / 2; opts.args = JSON.stringify({ currency: COIN, unsignedTransaction: opts.rawTxUnsigned, weightedSize: size }); opts.noVerify = true; PayPro.runRequest(opts, function (err, rawData) { if (err) { console.log('Error at verify-payment:', err.message ? err.message : '', opts); return cb(err); } opts.headers = { 'Content-Type': JSON_PAYMENT_CONTENT_TYPE, Accept: JSON_PAYMENT_ACK_CONTENT_TYPE }; if (opts.bp_partner) { opts.headers['BP_PARTNER'] = opts.bp_partner; if (opts.bp_partner_version) { opts.headers['BP_PARTNER_VERSION'] = opts.bp_partner_version; } } opts.args = JSON.stringify({ currency: COIN, transactions: [opts.rawTx] }); opts.noVerify = true; PayPro.runRequest(opts, function (err, rawData) { if (err) { console.log('Error at payment:', err.message ? err.message : '', opts); return cb(err); } var memo; if (rawData) { try { var data = JSON.parse(rawData.toString()); memo = data.memo; } catch (e) { console.log('Could not decode paymentACK'); } } return cb(null, rawData, memo); }); }); } } exports.PayPro = PayPro; PayPro.r = superagent_1.default; //# sourceMappingURL=paypro.js.map