Initial release of JSON payment protocol module
This commit is contained in:
commit
72ccb98518
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"esversion": 6,
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"devel": false,
|
||||
"eqeqeq": true,
|
||||
"eqnull": false,
|
||||
"freeze": true,
|
||||
"funcscope": false,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": "nofunc",
|
||||
"maxdepth": 4,
|
||||
"maxerr": 99999, // so that jshint doesn't give up on a file with tons of errors
|
||||
"maxlen": 120,
|
||||
"maxparams": 4,
|
||||
"maxstatements": 35,
|
||||
"mocha": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"node": true,
|
||||
"noempty": true,
|
||||
"nonew": true,
|
||||
"quotmark": "single",
|
||||
"regexp": true,
|
||||
"smarttabs": false,
|
||||
"strict": true,
|
||||
"trailing": true,
|
||||
"undef": true,
|
||||
"unused": true
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
examples
|
|
@ -0,0 +1,246 @@
|
|||
'use strict';
|
||||
const async = require('async');
|
||||
const request = require('request');
|
||||
const promptly = require('promptly');
|
||||
|
||||
const JsonPaymentProtocol = require('../index'); //or require('json-payment-protocol')
|
||||
const paymentProtocol = new JsonPaymentProtocol({strictSSL: false});
|
||||
|
||||
let config = {
|
||||
network: 'test',
|
||||
currency: 'BTC',
|
||||
rpcServer: {
|
||||
username: 'fakeUser',
|
||||
password: 'fakePassword',
|
||||
ipAddress: '127.0.0.1',
|
||||
port: '18332'
|
||||
}
|
||||
};
|
||||
|
||||
if (config.rpcServer.username === 'fakeUser') {
|
||||
return console.log('You should update the config in this file to match the actual configuration of your bitcoind' +
|
||||
' RPC interface');
|
||||
}
|
||||
|
||||
/**
|
||||
* While this client does show effective use of json payment protocol, it may not follow best practices generate
|
||||
* payments via bitcoinRPC. We do not recommend copying this code verbatim in any product designed for actual users.
|
||||
*/
|
||||
|
||||
let paymentUrl;
|
||||
let requiredFee = 0;
|
||||
let outputObject = {};
|
||||
|
||||
async.waterfall([
|
||||
function askForPaymentUrl(cb) {
|
||||
promptly.prompt('What is the payment protocol uri?', {required: true}, cb);
|
||||
},
|
||||
function retrievePaymentRequest(uri, cb) {
|
||||
paymentProtocol.getRawPaymentRequest(uri, (err, rawResponse) => {
|
||||
if (err) {
|
||||
console.log('Error retrieving payment request', err);
|
||||
return cb(err);
|
||||
}
|
||||
return cb(null, rawResponse);
|
||||
});
|
||||
},
|
||||
function parsePaymentRequest(rawResponse, cb) {
|
||||
paymentProtocol.parsePaymentRequest(rawResponse.rawBody, rawResponse.headers, (err, paymentRequest) => {
|
||||
if (err) {
|
||||
console.log('Error parsing payment request', err);
|
||||
return cb(err);
|
||||
}
|
||||
return cb(null, paymentRequest);
|
||||
});
|
||||
},
|
||||
function checkAndDisplayPaymentRequestToUser(paymentRequest, cb) {
|
||||
requiredFee = (paymentRequest.requiredFeePerByte * 1024) / 1e8;
|
||||
|
||||
//Make sure request is for the currency we support
|
||||
if (paymentRequest.currency.toLowerCase() !== config.currency.toLowerCase()) {
|
||||
console.log('Server requested a payment in', paymentRequest.currency, 'but we are configured to accept',
|
||||
config.currency);
|
||||
return cb(new Error('Payment request currency did not match our own'));
|
||||
}
|
||||
|
||||
if (paymentRequest.network.toLowerCase() !== config.network.toLowerCase()) {
|
||||
console.log('Server requested a payment on the', paymentRequest.network, 'network but we are configured for the',
|
||||
config.network, 'network');
|
||||
return cb(new Error('Payment request network did not match our own'));
|
||||
}
|
||||
|
||||
console.log('Server is requesting payments for:');
|
||||
console.log('---');
|
||||
|
||||
paymentRequest.outputs.forEach(function (output) {
|
||||
let cryptoAmount = round(output.amount / 1e8, 8);
|
||||
console.log(cryptoAmount + ' to ' + output.address);
|
||||
outputObject[output.address] = cryptoAmount;
|
||||
});
|
||||
|
||||
console.log('---');
|
||||
|
||||
paymentUrl = paymentRequest.paymentUrl;
|
||||
|
||||
cb();
|
||||
},
|
||||
function createRawTransaction(cb) {
|
||||
let createCommand = {
|
||||
jsonrpc: '1.0',
|
||||
method: 'createrawtransaction',
|
||||
params: [
|
||||
[],
|
||||
outputObject
|
||||
]
|
||||
};
|
||||
|
||||
execRpcCommand(createCommand, (err, rawTransaction) => {
|
||||
if (err) {
|
||||
console.log('Error creating raw transaction', err);
|
||||
return cb(err);
|
||||
}
|
||||
else if (!rawTransaction) {
|
||||
console.log('No raw tx generated');
|
||||
return cb(new Error('No tx generated'));
|
||||
}
|
||||
else {
|
||||
return cb(null, rawTransaction);
|
||||
}
|
||||
});
|
||||
},
|
||||
function fundRawTransaction(rawTransaction, cb) {
|
||||
let fundCommand = {
|
||||
jsonrpc: '1.0',
|
||||
method: 'fundrawtransaction',
|
||||
params: [
|
||||
rawTransaction,
|
||||
{
|
||||
feeRate: requiredFee
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
execRpcCommand(fundCommand, (err, fundedRawTransaction) => {
|
||||
if (err) {
|
||||
console.log('Error funding transaction', err);
|
||||
return cb(err);
|
||||
}
|
||||
if (!fundedRawTransaction) {
|
||||
console.log('No funded tx generated');
|
||||
return cb(new Error('No funded tx generated'));
|
||||
}
|
||||
else {
|
||||
cb(null, fundedRawTransaction.hex);
|
||||
}
|
||||
});
|
||||
},
|
||||
function signRawTransaction(fundedRawTransaction, cb) {
|
||||
let command = {
|
||||
jsonrpc: '1.0',
|
||||
method: 'signrawtransaction',
|
||||
params: [fundedRawTransaction]
|
||||
};
|
||||
|
||||
execRpcCommand(command, function (err, signedTransaction) {
|
||||
if (err) {
|
||||
console.log('Error signing transaction:', err);
|
||||
return cb(err);
|
||||
}
|
||||
if (!signedTransaction) {
|
||||
console.log('Bitcoind did not return a signed transaction');
|
||||
return cb(new Error('Missing signed tx'));
|
||||
}
|
||||
cb(null, signedTransaction.hex);
|
||||
});
|
||||
},
|
||||
function displayTransactionToUserForApproval(signedRawTransaction, cb) {
|
||||
let command = {
|
||||
jsonrpc: '1.0',
|
||||
method: 'decoderawtransaction',
|
||||
params: [signedRawTransaction]
|
||||
};
|
||||
|
||||
execRpcCommand(command, function (err, decodedTransaction) {
|
||||
if (err) {
|
||||
console.log('Error signing transaction:', err);
|
||||
return cb(err);
|
||||
}
|
||||
if (!decodedTransaction) {
|
||||
console.log('Bitcoind did not return a decoded transaction');
|
||||
return cb(new Error('Missing decoded tx'));
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(decodedTransaction, null, 2));
|
||||
|
||||
promptly.confirm('Send payment shown above? (y/n)', function (err, accept) {
|
||||
if (!accept) {
|
||||
console.log('Payment cancelled');
|
||||
return cb(new Error('Payment Cancelled'));
|
||||
}
|
||||
return cb(null, signedRawTransaction);
|
||||
});
|
||||
});
|
||||
},
|
||||
function sendTransactionToServer(signedRawTransaction, cb) {
|
||||
paymentProtocol.sendPayment(config.currency, signedRawTransaction, paymentUrl, function (err, response) {
|
||||
if (err) {
|
||||
console.log('Error sending payment to server', err);
|
||||
return cb(err);
|
||||
}
|
||||
else {
|
||||
console.log('Payment accepted by server');
|
||||
return cb(null, signedRawTransaction);
|
||||
}
|
||||
});
|
||||
},
|
||||
//Note we only broadcast AFTER a SUCCESS response from the server
|
||||
function broadcastPayment(signedRawTransaction, cb) {
|
||||
let command = {
|
||||
jsonrpc: '1.0',
|
||||
method: 'sendrawtransaction',
|
||||
params: [signedRawTransaction]
|
||||
};
|
||||
|
||||
execRpcCommand(command, function (err, signedTransaction) {
|
||||
if (err) {
|
||||
console.log('Error broadcasting transaction:', err);
|
||||
return cb(err);
|
||||
}
|
||||
if (!signedTransaction) {
|
||||
console.log('Bitcoind failed to broadcast transaction');
|
||||
return cb(new Error('Failed to broadcast tx'));
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
function execRpcCommand(command, callback) {
|
||||
request
|
||||
.post({
|
||||
url: 'http://' + config.rpcServer.ipAddress + ':' + config.rpcServer.port,
|
||||
body: command,
|
||||
json: true,
|
||||
auth: {
|
||||
user: config.rpcServer.username,
|
||||
pass: config.rpcServer.password,
|
||||
sendImmediately: false
|
||||
}
|
||||
}, function (err, response, body) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (body.error) {
|
||||
return callback(body.error);
|
||||
}
|
||||
if (body.result) {
|
||||
return callback(null, body.result);
|
||||
}
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
||||
function round(value, places) {
|
||||
let tmp = Math.pow(10, places);
|
||||
return Math.ceil(value * tmp) / tmp;
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
'use strict';
|
||||
//Native
|
||||
const crypto = require('crypto');
|
||||
const query = require('querystring');
|
||||
const url = require('url');
|
||||
const util = require('util');
|
||||
|
||||
//Modules
|
||||
const _ = require('lodash');
|
||||
const request = require('request');
|
||||
|
||||
function PaymentProtocol(options) {
|
||||
this.options = _.merge({
|
||||
strictSSL: true
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to the given url and returns the raw JSON string retrieved as well as the headers
|
||||
* @param paymentUrl {string} the payment protocol specific url
|
||||
* @param callback {function} (err, body, headers)
|
||||
*/
|
||||
PaymentProtocol.prototype.getRawPaymentRequest = function getRawPaymentRequest(paymentUrl, callback) {
|
||||
let paymentUrlObject = url.parse(paymentUrl);
|
||||
|
||||
//Detect 'bitcoin:' urls and extract payment-protocol section
|
||||
if (paymentUrlObject.protocol !== 'http' && paymentUrlObject.protocol !== 'https') {
|
||||
let uriQuery = query.decode(paymentUrlObject.query);
|
||||
if (!uriQuery.r) {
|
||||
return callback(new Error('Invalid payment protocol url'));
|
||||
}
|
||||
else {
|
||||
paymentUrl = uriQuery.r;
|
||||
}
|
||||
}
|
||||
|
||||
let requestOptions = _.merge(this.options, {
|
||||
url: paymentUrl,
|
||||
headers: {
|
||||
'Accept': 'application/payment-request'
|
||||
}
|
||||
});
|
||||
|
||||
request.get(requestOptions, (err, response) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
return callback(new Error(response.body.toString()));
|
||||
}
|
||||
|
||||
return callback(null, {rawBody: response.body, headers: response.headers});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to the given url and returns the raw JSON string retrieved as well as the headers
|
||||
* @param url {string} the payment protocol specific url (https)
|
||||
*/
|
||||
PaymentProtocol.prototype.getRawPaymentRequestAsync = util.promisify(PaymentProtocol.prototype.getRawPaymentRequest);
|
||||
|
||||
/**
|
||||
* Given a raw payment protocol body, parses it and validates it against the digest header
|
||||
* @param rawBody {string} Raw JSON string retrieved from the payment protocol server
|
||||
* @param headers {object} Headers sent by the payment protocol server
|
||||
* @param callback {function} (err, paymentRequest)
|
||||
*/
|
||||
PaymentProtocol.prototype.parsePaymentRequest = function parsePaymentRequest(rawBody, headers, callback) {
|
||||
let paymentRequest;
|
||||
|
||||
if (!rawBody) {
|
||||
return callback(new Error('Parameter rawBody is required'));
|
||||
}
|
||||
if (!headers) {
|
||||
return callback(new Error('Parameter headers is required'));
|
||||
}
|
||||
|
||||
try {
|
||||
paymentRequest = JSON.parse(rawBody);
|
||||
}
|
||||
catch (e) {
|
||||
return callback(new Error(`Unable to parse request - ${e}`));
|
||||
}
|
||||
|
||||
if (!headers.digest) {
|
||||
return callback(new Error('Digest missing from response headers'));
|
||||
}
|
||||
|
||||
let digest = headers.digest.split('=')[1];
|
||||
let hash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex');
|
||||
|
||||
if (digest !== hash) {
|
||||
return callback(new Error(`Response body hash does not match digest header. Actual: ${hash} Expected: ${digest}`));
|
||||
}
|
||||
|
||||
return callback(null, paymentRequest);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a raw payment protocol body, parses it and validates it against the digest header
|
||||
* @param rawBody {string} Raw JSON string retrieved from the payment protocol server
|
||||
* @param headers {object} Headers sent by the payment protocol server
|
||||
*/
|
||||
PaymentProtocol.prototype.parsePaymentRequestAsync = util.promisify(PaymentProtocol.prototype.parsePaymentRequest);
|
||||
|
||||
/**
|
||||
* Sends a given payment to the server for validation
|
||||
* @param currency {string} Three letter currency code of proposed transaction (ie BTC, BCH)
|
||||
* @param signedRawTransaction {string} Hexadecimal format raw signed transaction
|
||||
* @param url {string} the payment protocol specific url (https)
|
||||
* @param callback {function} (err, response)
|
||||
*/
|
||||
PaymentProtocol.prototype.sendPayment = function sendPayment(currency, signedRawTransaction, url, callback) {
|
||||
let paymentResponse;
|
||||
|
||||
//Basic sanity checks
|
||||
if (typeof signedRawTransaction !== 'string') {
|
||||
return callback(new Error('signedRawTransaction must be a string'));
|
||||
}
|
||||
if (!/^[0-9a-f]+$/i.test(signedRawTransaction)) {
|
||||
return callback(new Error('signedRawTransaction must be in hexadecimal format'));
|
||||
}
|
||||
|
||||
let requestOptions = _.merge(this.options, {
|
||||
url: url,
|
||||
headers: {
|
||||
'Content-Type': 'application/payment'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
currency: currency,
|
||||
transactions: [signedRawTransaction]
|
||||
})
|
||||
});
|
||||
|
||||
request.post(requestOptions, (err, response) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
return callback(new Error(response.body.toString()));
|
||||
}
|
||||
|
||||
try {
|
||||
paymentResponse = JSON.parse(response.body);
|
||||
}
|
||||
catch (e) {
|
||||
return callback(new Error('Unable to parse response from server'));
|
||||
}
|
||||
|
||||
callback(null, paymentResponse);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a given payment to the server for validation
|
||||
* @param currency {string} Three letter currency code of proposed transaction (ie BTC, BCH)
|
||||
* @param signedRawTransaction {string} Hexadecimal format raw signed transaction
|
||||
* @param url {string} the payment protocol specific url (https)
|
||||
*/
|
||||
PaymentProtocol.prototype.sendPaymentAsync = util.promisify(PaymentProtocol.prototype.sendPayment);
|
||||
|
||||
module.exports = PaymentProtocol;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "json-payment-protocol",
|
||||
"version": "0.1.0",
|
||||
"description": "Simple interface for retrieving JSON payment requests and submitting payments",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"request": "^2.83.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"async": "^2.6.0",
|
||||
"promptly": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "BitPay",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitpay/jsonPaymentProtocol.git"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
### JSON Payment Protocol Interface
|
||||
|
||||
This is the first version of the JSON payment protocol interface. If you have questions about the specification itself, [view the documentation](specification.md).
|
||||
|
||||
### Getting Started
|
||||
|
||||
`npm install json-payment-protocol`
|
||||
|
||||
### Usage
|
||||
|
||||
We support both callbacks and promises. For promises just add Async to the end of the function name. Be careful to follow the notes about when to broadcast your payment. **Broadcasting a payment before getting a success notification back from the server in most cases will lead to a failed payment for the sender.** The sender will bear the cost of paying transaction fees yet again to get their money back.
|
||||
|
||||
#### Callbacks
|
||||
```
|
||||
const JsonPaymentProtocol = require('json-payment-protocol');
|
||||
const paymentProtocol = new JsonPaymentProtocol();
|
||||
|
||||
let requestUrl = 'bitcoin:?r=https://test.bitpay.com/i/Jr629pwsXKdTCneLyZja4t';
|
||||
|
||||
paymentProtocol.getRawPaymentRequest(requestUrl, function (err, response) {
|
||||
if (err) {
|
||||
return console.log('Error retrieving payment request', err);
|
||||
}
|
||||
paymentProtocol.parsePaymentRequest(response.body, response.headers, function (err, paymentRequest) {
|
||||
if (err) {
|
||||
return console.log('Error parsing payment request', err);
|
||||
}
|
||||
|
||||
console.log('Payment request retrieved');
|
||||
console.log(paymentRequest);
|
||||
|
||||
//TODO: Create the rawTransaction and sign it in your wallet instead of this, do NOT broadcast yet
|
||||
let currency = 'BTC'
|
||||
let signedRawTransaction = '02000000010c2b0d60448d5cdfebe222014407bdb408b8427f837447484911efddea700323000000006a47304402201d3ed3117f1968c3b0a078f15f8462408c745ff555b173eff3dfe0a25e063c0c02200551572ec33d45ece8e64275970bd1b1694621f0ed8fac2f7e18095f170fe3fe012102d4edb773e3bd94e1251790f5cc543cbfa76c2b0abad14898674b1c4e27176ef2ffffffff02c44e0100000000001976a914dd826377dcf2075e5065713453cfad675ba9434f88aca070002a010000001976a914e7d0344ba970301e93cd7b505c7ae1b5bcf5639288ac00000000';
|
||||
|
||||
paymentProtocol.sendPayment(currency, signedRawTransaction, paymentRequest.paymentUrl, function(err, response) {
|
||||
if (err) {
|
||||
//DO NOT BROADCAST PAYMENT
|
||||
return console.log('Error sending payment to server');
|
||||
}
|
||||
console.log('Payment sent successfully');
|
||||
//TODO: Broadcast payment to network here
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Promises
|
||||
```
|
||||
const JsonPaymentProtocol = require('json-payment-protocol');
|
||||
const paymentProtocol = new JsonPaymentProtocol();
|
||||
|
||||
let requestUrl = 'bitcoin:?r=https://test.bitpay.com/i/Jr629pwsXKdTCneLyZja4t';
|
||||
paymentProtocol
|
||||
.getRawPaymentRequestAsync(requestUrl)
|
||||
.then((response) => {
|
||||
return paymentProtocol.parsePaymentRequestAsync(response.rawBody, response.headers);
|
||||
})
|
||||
.then((paymentRequest) => {
|
||||
console.log('Payment request retrieved');
|
||||
console.log(paymentRequest);
|
||||
|
||||
//TODO: Create the rawTransaction and sign it in your wallet instead of this, do NOT broadcast yet
|
||||
let currency = 'BTC'
|
||||
let signedRawTransaction = '02000000010c2b0d60448d5cdfebe222014407bdb408b8427f837447484911efddea700323000000006a47304402201d3ed3117f1968c3b0a078f15f8462408c745ff555b173eff3dfe0a25e063c0c02200551572ec33d45ece8e64275970bd1b1694621f0ed8fac2f7e18095f170fe3fe012102d4edb773e3bd94e1251790f5cc543cbfa76c2b0abad14898674b1c4e27176ef2ffffffff02c44e0100000000001976a914dd826377dcf2075e5065713453cfad675ba9434f88aca070002a010000001976a914e7d0344ba970301e93cd7b505c7ae1b5bcf5639288ac00000000';
|
||||
|
||||
return paymentProtocol.sendPaymentAsync(currency, signedRawTransaction, paymentRequest.paymentUrl);
|
||||
})
|
||||
.then((response) => {
|
||||
console.log('Payment sent successfully');
|
||||
//TODO: Broadcast payment to network here
|
||||
})
|
||||
.catch((err) => {
|
||||
//DO NOT BROADCAST PAYMENT
|
||||
return console.log('Error processing payment request', err);
|
||||
});
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
Options passed to `new JsonPaymentProtocol()` are passed to request, so if you need to use a proxy or set any other request.js flags you can do so by including them when instantiating your instance. For example:
|
||||
|
||||
```
|
||||
new JsonPaymentProtocol({
|
||||
proxy: 'socks://mySocksProxy.local'
|
||||
})
|
||||
```
|
||||
|
||||
### URI Formats
|
||||
You can provide either the `bitcoin:?r=https://bitpay.com/i/invoice` format or `https://bitpay.com/i/invoice` directly.
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
# JSON Payment Protocol Specification
|
||||
|
||||
Revision 0.5
|
||||
|
||||
## Application Logic
|
||||
|
||||
1. (Web) User selects preferred currency on invoice if multiple options are available
|
||||
2. (Client) Wallet obtains payment protocol uri
|
||||
3. (Client) Fetches payment information from server
|
||||
4. (Server) Verifies invoice exists and is still accepting payments, responds with payment request
|
||||
5. (Client) Validates payment request hash
|
||||
6. (Client) Generates a payment to match conditions on payment request
|
||||
7. (Client) Submits proposed signed transaction to server
|
||||
8. (Server) Validates invoice exists and is still accepting payments
|
||||
9. (Server) Validates payment matches address, amount, and currency of invoice and has a reasonable transaction fee.
|
||||
10. (Server) Broadcasts payment to network and notifies client payment was accepted.
|
||||
11. (Client) If payment is accepted by server, wallet broadcasts payment
|
||||
|
||||
If at any time the payment is rejected by the server **your client should not broadcast the payment**.
|
||||
Broadcasting a payment before getting a success notification back from the server will in most cases lead to a failed payment for the sender. The sender will bear the cost of paying transaction fees yet again to get their money back.
|
||||
|
||||
## Payment Request
|
||||
|
||||
### Request
|
||||
A GET request should be made to the payment protocol url.
|
||||
|
||||
### Response
|
||||
The response will be a JSON format payload quite similar to the BIP70 format.
|
||||
|
||||
#### Headers
|
||||
On a successful request, the response will contain one header of note.
|
||||
|
||||
* `digest` - A SHA256 hash of the JSON response string, should be verified by the client before proceeding
|
||||
|
||||
#### Body
|
||||
* `network` - Which network is this request for (main / test / regtest)
|
||||
* `currency` - Three digit currency code representing which coin the request is based on
|
||||
* `requiredFeePerByte` - The minimum fee per byte required on this transaction, if lower than that we will reject it
|
||||
* `outputs` - What output(s) your transaction must include in order to be accepted
|
||||
* `time` - ISO Date format of when the invoice was generated
|
||||
* `expires` - ISO Date format of when the invoice will expire
|
||||
* `memo` - A plain text description of the payment request, can be displayed to the user / kept for records
|
||||
* `paymentUrl` - The url where the payment should be sent
|
||||
* `paymentId` - The invoice ID, can be kept for records
|
||||
|
||||
#### Response Body Example
|
||||
```
|
||||
{
|
||||
"network": "test",
|
||||
"currency": "BTC",
|
||||
"requiredFeePerByte": 200,
|
||||
"outputs": [
|
||||
{
|
||||
"amount": 39300,
|
||||
"address": "mthVG9kuRTJQtXieJVDSrrvWyM7QDZ3rcV"
|
||||
}
|
||||
],
|
||||
"time": "2018-01-12T22:04:54.364Z",
|
||||
"expires": "2018-01-12T22:19:54.364Z",
|
||||
"memo": "Payment request for BitPay invoice TmyrxFvAi4DjFNy3c7EjVm for merchant Robs Fake Business",
|
||||
"paymentUrl": "https://test.bitpay.com/i/TmyrxFvAi4DjFNy3c7EjVm",
|
||||
"paymentId": "TmyrxFvAi4DjFNy3c7EjVm"
|
||||
}
|
||||
```
|
||||
|
||||
## Payment Payload
|
||||
|
||||
### Request
|
||||
A POST request should be made to the payment protocol url with a `Content-Type` header set to `application/payment`. A JSON format body should be included with the following fields:
|
||||
|
||||
```
|
||||
{
|
||||
"currency": "<currency 3 letter code>",
|
||||
"transactions": [
|
||||
"<transaction in hexedecimal string format>"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Request Body
|
||||
```
|
||||
{
|
||||
"currency": "BTC",
|
||||
"transactions": [
|
||||
"02000000011f0f762184cbc8e94b307fab6f805168724f123a23cd48aac4a9bac8768cfd67000000004847304402205079b96def679f04de9698dd8b9f58dff3e4a13c075f5939c6edfbb8698c8cc802203eac5a3d6410a9f94a86828a4e207f8083fe0bf1c77a74a0cb7add49100d427001ffffffff0284990000000000001976a9149097a519e42061e4977b07b69735ed842b755c0088ac08cd042a010000001976a914cf4b90bca14deab1315c125b8b74b7d31eea97b288ac00000000"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
The response will be a JSON format payload containing the original payment body and a memo field which should be displayed to the user.
|
||||
|
||||
#### Response Example
|
||||
```
|
||||
{
|
||||
"payment": {
|
||||
"transactions": [
|
||||
"020000000121053733b28b90707a3c63a48171f71abfdc7288bf9d78170e73cfedbbbdfcea00000000484730440220545d53b54873a5afbaf01a77943828f25c6a28d9c5ca4d0968130b5788fc6f9302203e45125723844e4752202792b764b6538342ad169d3828dad18eb231ea01f05101ffffffff02b09a0000000000001976a9149659267896dda4e5aef150e4ca83f0d76022c7b288ac84dd042a010000001976a914fa1a5ed99ce09fd901e9ca7d6f8fcc56d3d5eccf88ac00000000"
|
||||
]
|
||||
},
|
||||
"memo": "Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed."
|
||||
}
|
||||
```
|
||||
|
||||
### Curl Example
|
||||
```
|
||||
curl -v -H 'Content-Type: application/payment' -d '{"currency": "BTC", "transactions":["02000000012319227d3995427b05429df7ea30b87cb62f986ba3003311a2cf2177fb5b0ae8000000004847304402205bd75d6b654a70dcc8f548b630c39aec1d2c1de6900b5376ef607efc705f65b002202dd1036f091d4d6047e2f5bcd230ec8bcd5ad2f0785908d78f08a52b8850559f01ffffffff02b09a0000000000001976a9140b2a833c4183c51b86f5dcbb2eeeaca2dfb44bae88acdccb042a010000001976a914f0fd63e5880cbed2fa856e1f4174fc875eeccc5a88ac00000000"]}' https://test.bitpay.com/i/7QBCJ2TpazTKKnczzJQJMc
|
||||
* Trying 127.0.0.1...
|
||||
* TCP_NODELAY set
|
||||
* Connected to test.bitpay.com (127.0.0.1) port 8088 (#0)
|
||||
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
> POST /i/7QBCJ2TpazTKKnczzJQJMc HTTP/1.1
|
||||
> Host: test.bitpay.com
|
||||
> User-Agent: curl/7.54.0
|
||||
> Accept: */*
|
||||
> Content-Type: application/payment-ack
|
||||
> Content-Length: 403
|
||||
>
|
||||
* upload completely sent off: 403 out of 403 bytes
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Length: 520
|
||||
< Date: Fri, 12 Jan 2018 22:44:13 GMT
|
||||
< Connection: keep-alive
|
||||
<
|
||||
* Connection #0 to host test.bitpay.com left intact
|
||||
{"payment":{"transactions":["02000000012319227d3995427b05429df7ea30b87cb62f986ba3003311a2cf2177fb5b0ae8000000004847304402205bd75d6b654a70dcc8f548b630c39aec1d2c1de6900b5376ef607efc705f65b002202dd1036f091d4d6047e2f5bcd230ec8bcd5ad2f0785908d78f08a52b8850559f01ffffffff02b09a0000000000001976a9140b2a833c4183c51b86f5dcbb2eeeaca2dfb44bae88acdccb042a010000001976a914f0fd63e5880cbed2fa856e1f4174fc875eeccc5a88ac00000000"]},"memo":"Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed."}%
|
||||
```
|
||||
|
||||
|
||||
## Errors
|
||||
|
||||
All errors are communicated in plaintext with an appropriate status code.
|
||||
|
||||
### Example Error
|
||||
|
||||
```
|
||||
curl -v https://test.bitpay.com/i/48gZau8ao76bqAoEwAKSwx -H 'Accept: application/payment-request'
|
||||
* Trying 104.17.68.20...
|
||||
* TCP_NODELAY set
|
||||
* Connected to test.bitpay.com (104.17.68.20) port 443 (#0)
|
||||
* TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
> GET /i/48gZau8ao76bqAoEwAKSwx HTTP/1.1
|
||||
> Host: test.bitpay.com
|
||||
> User-Agent: curl/7.54.0
|
||||
> Accept: application/payment-request
|
||||
>
|
||||
< HTTP/1.1 400 Bad Request
|
||||
< Date: Fri, 26 Jan 2018 01:54:03 GMT
|
||||
< Content-Type: text/html; charset=utf-8
|
||||
< Content-Length: 44
|
||||
< Connection: keep-alive
|
||||
< Strict-Transport-Security: max-age=31536000
|
||||
< X-Download-Options: noopen
|
||||
< X-Content-Type-Options: nosniff
|
||||
< Access-Control-Allow-Origin: *
|
||||
< Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||
< Access-Control-Allow-Headers: Host, Connection, Content-Length, Accept, Origin, User-Agent, Content-Type, Accept-Encoding, Accept-Language
|
||||
<
|
||||
* Connection #0 to host test.bitpay.com left intact
|
||||
This invoice is no longer accepting payments
|
||||
```
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Http Status Code | Response | Cause |
|
||||
|---|---|---|
|
||||
| 404 | This invoice was not found or has been archived | Invalid invoiceId, or invoice has been archived (current TTL is 3 days) |
|
||||
| 400 | Unsupported Content-Type for payment | Your Content-Type header was not valid |
|
||||
| 400 | Invoice no longer accepting payments | Invoice is either paid or has expired |
|
||||
| 400 | We were unable to parse your payment. Please try again or contact your wallet provider | Request body could not be parsed / empty body |
|
||||
| 400 | Request must include exactly one (1) transaction | Included no transaction in body / Included multiple transactions in body |
|
||||
| 400 | Your transaction was an in an invalid format, it must be a hexadecimal string | Make sure you're sending the raw hex string format of your signed transaction
|
||||
| 400 | We were unable to parse the transaction you sent. Please try again or contact your wallet provider | Transaction was hex, but it contained invalid transaction data or was in the wrong format |
|
||||
| 400 | The transaction you sent does not have any output to the bitcoin address on the invoice | The transaction you sent does not pay to the address listed on the invoice |
|
||||
| 400 | The amount on the transaction (X BTC) does not match the amount requested (Y BTC). This payment will not be accepted. | Payout amount to address does not match amount that was requested |
|
||||
| 400 | Transaction fee (X sat/kb) is below the current minimum threshold (Y sat/kb) | Your fee must be at least the amount sent in the payment request as `requiredFeePerByte`|
|
||||
| 400 | This invoice is priced in BTC, not BCH. Please try with a BTC wallet instead | Your transaction currency did not match the one on the invoice |
|
||||
| 422 | One or more input transactions for your transaction were not found on the blockchain. Make sure you're not trying to use unconfirmed change | Spending outputs which have not yet been broadcast to the network |
|
||||
| 422 | One or more input transactions for your transactions are not yet confirmed in at least one block. Make sure you're not trying to use unconfirmed change | Spending outputs which have not yet confirmed in at least one block on the network |
|
||||
| 500 | Error broadcasting payment to network | Our Bitcoin node returned an error when attempting to broadcast your transaction to the network. This could mean our node is experiencing an outage or your transaction is a double spend. |
|
||||
|
||||
Another issue you may see is that you are being redirected to `bitpay.com/invoice?id=xxx` instead of being sent a payment-request. In that case you are not setting your `Accept` header to a valid value and we assume you are a browser or other unknown requester.
|
||||
|
||||
## MIME Types
|
||||
|
||||
|Mime|Description|
|
||||
|---|---|
|
||||
|application/payment-request| Associated with the server's payment request, this specified on the client `Accept` header when retrieving the payment request|
|
||||
|application/payment| Used by the client when sending their proposed payment transaction payload|
|
||||
|application/payment-ack| Used by the server to state acceptance of the client's proposed payment transaction|
|
Loading…
Reference in New Issue