├── .gitignore ├── .npmignore ├── bip70.md ├── examples ├── bitcoinRpc.js ├── config.example.js └── fetchEccKeysAndVerify.js ├── index.js ├── package-lock.json ├── package.json ├── paymentFlow.png ├── readme.md ├── securityUpdates.md ├── v1 └── specification.md └── v2 ├── flow.png └── specification.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | examples/config.js 4 | *.swp 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples -------------------------------------------------------------------------------- /bip70.md: -------------------------------------------------------------------------------- 1 | # BIP-70 Modifications 2 | 3 | In addition to JSON payment protocol, BitPay Bitcoin and Bitcoin Cash invoices use a mildly modified version of [BIP-70](https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki). We include 4 | one additional field which specifies the fee rate the transaction must have in order to be accepted. This minimum fee is required to ensure a reasonable confirmation time for payments which are sent to BitPay. 5 | To further ensure this we also require payments be made with confirmed inputs. Payments using unconfirmed inputs, such as unconfirmed change, will be rejected. Bitcoin (BTC) invoices are temporarily exempt from these rules to allow wallets time to adjust. 6 | 7 | Since RBF payments can be modified after they are broadcast, they will also be rejected by our payment protocol server. Make sure to disable the RBF flag for any transactions sent to BitPay. 8 | 9 | * `required_fee_rate` - The minimum fee per byte required on your transaction. Bitcoin Cash payments will be rejected if fee rate included for the transaction is not at least this value. _May be fractional value_ ie 0.123 sat/byte 10 | 11 | ## Application Logic 12 | 13 | Since rejecting invalid payments before they are broadcast to the network is a primary goal of payment protocol, we recommend following this 14 | flow when submitting a payment. 15 | 16 | 1. Fetch payment from url (standard BIP 70) 17 | 2. Create unsigned, funded transaction (standard BIP 70) 18 | 3. Sign transaction, keep unsigned transaction (**bitpay specific**) 19 | 4. Send the unsigned transaction and weighed size of the signed transaction to the server (**bitpay specific**) 20 | 5. If server rejects at this point, do not continue. Otherwise, send via standard payment protocol and broadcast to p2p in parallel 21 | 22 | Please note that you should **NOT** broadcast a payment to the P2P network if we respond with an http status code other than `200` in 23 | the verification step. Broadcasting a payment before getting a success notification back from the server will lead to a failed payment 24 | for the sender. The sender will bear the cost of paying transaction fees yet again to get their money back. 25 | 26 | ## Payment Request 27 | 28 | ### Request 29 | A GET request should be made to the payment protocol url. 30 | 31 | ### Response 32 | The response will payload identical to the BIP70 format with one additional field for `required_fee_rate`. 33 | 34 | #### Headers 35 | On a successful request, the response will contain the standard BIP-70 headers. 36 | 37 | 38 | ## Payment Verification 39 | 40 | ### Request 41 | A POST request should be made to the payment protocol url with the header `application/bitcoin-verify-payment` or `application/bitcoincash-verify-payment`. 42 | 43 | ### Request Body 44 | The body should contain the **unsigned** transaction as well as the weighted size in vbytes of the fully signed transaction. Weighted size 45 | really only applies for transactions with segwit inputs, if you have a non-segwit transaction the byte size is the correct value to send. 46 | With bitcoin core this is simply the vsize value of a transaction. If you're not certain about calculating the weighted size, please see 47 | the [bitcoin documentation about it](https://en.bitcoin.it/wiki/Weight_units). The format of this request should be based on this protobuf 48 | proto: 49 | ``` 50 | message PaymentVerification { 51 | required bytes unsigned_transaction = 1; 52 | required uint64 weighted_size = 2 [default = 0]; 53 | } 54 | ``` 55 | 56 | ### Response 57 | A 200 status code will be returned for valid payments, all other status codes will return with an error message stating why the payment was rejected. 58 | 59 | 60 | ## Payment 61 | 62 | ### Request 63 | A POST request should be made to the payment protocol url with the standard BIP-70 payment header (`application/bitcoin-payment` or `application/bitcoincash-payment`) 64 | 65 | ### Request Body 66 | The body should contain the **signed** transaction in BIP-70 format 67 | 68 | ### Response 69 | A 200 status code will be returned for valid payments, all other status codes will return with an error message stating why the payment was rejected. 70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/bitcoinRpc.js: -------------------------------------------------------------------------------- 1 | const promptly = require('promptly'); 2 | const request = require('request'); 3 | 4 | const PaymentProtocol = require('../index'); // or require('json-payment-protocol') 5 | 6 | let config; 7 | 8 | try { 9 | config = require('./config'); 10 | } catch (e) { 11 | return console.log('You need to create a config.js file in examples based on the config.example.js file'); 12 | } 13 | 14 | const client = new PaymentProtocol({ 15 | rejectUnauthorized: false, 16 | headers: { 17 | bp_partner: 'BitPay', // Your wallet name here 18 | bp_partner_version: '0.1.0' // Your current wallet version here 19 | } 20 | }, config.trustedKeys); 21 | 22 | let rpcUrl; 23 | let rpcUser; 24 | let rpcPass; 25 | 26 | async function main() { 27 | const url = await promptly.prompt('What is the payment protocol uri?', { required: true }); 28 | const paymentOptions = await client.getPaymentOptions(url); 29 | const rates = await fetchExchangeRates(); 30 | 31 | // Show user information about the payment 32 | console.log(paymentOptions.responseData.memo); 33 | 34 | let index = 1; 35 | let choices = []; 36 | let unavailable = []; 37 | 38 | // Add methods for ease of calculating rates later 39 | function getBaseRate(code) { 40 | const rate = rates.data.find((el) => { 41 | return el.code === code; 42 | }); 43 | return rate.rate; 44 | } 45 | 46 | function getExchangeRate(fromCode, toCode) { 47 | const fromRate = getBaseRate(fromCode); 48 | const toRate = getBaseRate(toCode); 49 | return round(toRate / fromRate, 8); 50 | } 51 | 52 | let choice; 53 | let selected = paymentOptions.responseData.paymentOptions.filter((option) => { 54 | return option.selected; 55 | })[0]; 56 | 57 | if (selected) { 58 | let { chain, estimatedAmount, decimals, minerFee } = selected; 59 | 60 | // Must display network cost if provided, not doing so would be hiding costs 61 | const networkCost = minerFee / Math.pow(10, decimals); 62 | const usdCost = networkCost * getExchangeRate(chain, 'USD'); 63 | 64 | console.log('Currency is already selected on this invoice:', `${chain} ${round(estimatedAmount * Math.pow(10, -decimals), decimals)} - (Network Cost: $${round(usdCost, 2)})`); 65 | 66 | choice = selected; 67 | } else { 68 | for (let { chain, estimatedAmount, decimals, minerFee } of paymentOptions.responseData.paymentOptions) { 69 | if (!config.rpcServers[chain]) { 70 | return unavailable.push({ chain, estimatedAmount, decimals }); 71 | } 72 | choices.push(index); 73 | 74 | // Must display network cost if provided, not doing so would be hiding costs 75 | const networkCost = minerFee / Math.pow(10, decimals); 76 | const usdCost = networkCost * getExchangeRate(chain, 'USD'); 77 | console.log(`${index++}. ${chain} ${round(estimatedAmount * Math.pow(10, -decimals), decimals)} - (Network Cost: $${round(usdCost, 2)})`); 78 | } 79 | 80 | if (unavailable.length) { 81 | console.log(`There are ${unavailable.length} additional options that this wallet does not support or for which you do not have sufficient balance:`); 82 | for (let { chain, estimatedAmount, decimals } of unavailable) { 83 | console.log(`- ${chain} ${estimatedAmount * Math.pow(10, -decimals)}`); 84 | } 85 | } 86 | 87 | console.log('---'); 88 | 89 | choice = await promptly.choose('What payment method would you like to use?', choices); 90 | choice = paymentOptions.responseData.paymentOptions[choice - 1]; 91 | } 92 | 93 | let rpcConfig = config.rpcServers[choice.chain]; 94 | rpcUrl = `http://${rpcConfig.ipAddress}:${rpcConfig.port}`; 95 | rpcUser = rpcConfig.username; 96 | rpcPass = rpcConfig.password; 97 | 98 | const { responseData: paymentRequest } = await client.selectPaymentOption(paymentOptions.requestUrl, choice.chain, choice.currency); 99 | 100 | /** 101 | * Wallet creates a transaction matching data in the instructions 102 | */ 103 | 104 | // Format outputs as expected for bitcoin rpc client 105 | let outputObject = {}; 106 | paymentRequest.instructions.forEach((instruction) => { 107 | instruction.outputs.forEach(function(output) { 108 | let cryptoAmount = round(output.amount / 1e8, 8); 109 | console.log(cryptoAmount + ' to ' + output.address); 110 | outputObject[output.address] = cryptoAmount; 111 | }); 112 | }); 113 | 114 | let fundedTx; 115 | let signedTx; 116 | let decodedTx; 117 | 118 | try { 119 | let rawTx = await createRawTransaction(outputObject); 120 | fundedTx = await fundRawTransaction(rawTx, paymentRequest.instructions[0].requiredFeeRate / 1000); 121 | signedTx = await signRawTransaction(fundedTx); 122 | decodedTx = await decodeRawTransaction(signedTx); 123 | } catch (e) { 124 | console.log('Error generating payment transaction'); 125 | throw e; 126 | } 127 | 128 | /** 129 | * Send un-signed transaction to server for verification of fee and output amounts 130 | */ 131 | 132 | try { 133 | await client.verifyUnsignedPayment({ 134 | paymentUrl: paymentOptions.requestUrl, 135 | chain: choice.chain, 136 | // For chains which can support multiple currencies via tokens, a currency code is required to identify which token is being used 137 | currency: choice.currency, 138 | unsignedTransactions: [{ 139 | tx: fundedTx, 140 | // `vsize` for bitcoin core w/ segwit support, `size` for other clients 141 | weightedSize: decodedTx.vsize || decodedTx.size 142 | }] 143 | }); 144 | } catch (e) { 145 | console.log('Error verifying payment'); 146 | throw e; 147 | } 148 | 149 | // Display tx to user for confirmation 150 | console.log(JSON.stringify(decodedTx, null, 2)); 151 | 152 | const signPayment = await promptly.confirm('Send this payment? (y/n)'); 153 | if (!signPayment) { 154 | throw new Error('User aborted'); 155 | } 156 | 157 | /** 158 | * Send signed transaction to server for actual payment 159 | */ 160 | 161 | await sendPayment(); 162 | await sendP2pPayment(); 163 | 164 | async function sendPayment() { 165 | try { 166 | await client.sendSignedPayment({ 167 | paymentUrl: paymentOptions.requestUrl, 168 | chain: choice.chain, 169 | currency: choice.currency, 170 | signedTransactions: [{ 171 | tx: signedTx, 172 | // `vsize` for bitcoin core w/ segwit support, `size` for other clients 173 | weightedSize: decodedTx.vsize || decodedTx.size 174 | }] 175 | }); 176 | console.log('Payment successfully sent via payment protocol'); 177 | } catch (e) { 178 | console.log('Error sending payment', e); 179 | let confirm = promptly.confirm('Retry payment?', { default: false }); 180 | if (confirm) { 181 | return await sendPayment(); 182 | } else { 183 | throw e; 184 | } 185 | } 186 | } 187 | 188 | async function sendP2pPayment() { 189 | try { 190 | await broadcastP2P(signedTx); 191 | } catch (e) { 192 | console.log('Error broadcasting transaction to p2p network', e); 193 | let confirm = promptly.confirm('Retry broadcast?', { default: false }); 194 | if (confirm) { 195 | return await sendPayment(); 196 | } else { 197 | throw e; 198 | } 199 | } 200 | } 201 | } 202 | 203 | function fetchExchangeRates() { 204 | return new Promise((resolve, reject) => { 205 | request({ 206 | method: 'GET', 207 | uri: 'https://bitpay.com/rates/', 208 | json: true 209 | }, (err, response, body) => { 210 | if (err) { 211 | return reject(err); 212 | } 213 | return resolve(body); 214 | }); 215 | }); 216 | } 217 | 218 | /** 219 | * Generates a bitcoin Transaction 220 | * @param outputObject {Object} addresses and output amounts 221 | * @return {Promise} Raw Transaction in hex format 222 | */ 223 | async function createRawTransaction(outputObject) { 224 | let createCommand = { 225 | jsonrpc: '1.0', 226 | method: 'createrawtransaction', 227 | params: [ 228 | [], 229 | outputObject, 230 | ] 231 | }; 232 | let rawTransaction; 233 | 234 | try { 235 | rawTransaction = await execRpcCommand(createCommand); 236 | } catch (err) { 237 | console.log('Error creating raw transaction', err); 238 | throw err; 239 | } 240 | 241 | if (!rawTransaction) { 242 | console.log('No raw tx generated'); 243 | throw new Error('No tx generated'); 244 | } 245 | 246 | return rawTransaction; 247 | } 248 | 249 | /** 250 | * Adds inputs and change output to a given raw transaction 251 | * @param {String} rawTransaction - hexadecimal format transaction 252 | * @param {Number} requiredFee - fee in sat per kb 253 | * @return {Promise} - funded raw transaction in hexadecimal format 254 | */ 255 | async function fundRawTransaction(rawTransaction, requiredFee) { 256 | let fundCommand = { 257 | jsonrpc: '1.0', 258 | method: 'fundrawtransaction', 259 | params: [ 260 | rawTransaction, 261 | { feeRate: requiredFee }, 262 | ] 263 | }; 264 | 265 | let fundedRawTransaction; 266 | 267 | try { 268 | fundedRawTransaction = await execRpcCommand(fundCommand); 269 | } catch (err) { 270 | console.log('Error funding transaction', err); 271 | throw err; 272 | } 273 | 274 | if (!fundedRawTransaction) { 275 | console.log('No funded tx generated'); 276 | throw new Error('No funded tx generated'); 277 | } 278 | 279 | return fundedRawTransaction.hex; 280 | } 281 | 282 | /** 283 | * Signs transaction for broadcast 284 | * @param {String} fundedRawTransaction - Hexadecimal format funded transaction 285 | * @return {Promise} - signedTransaction in hexadecimal format 286 | */ 287 | async function signRawTransaction(fundedRawTransaction) { 288 | let command = { 289 | jsonrpc: '1.0', 290 | method: 'signrawtransaction', 291 | params: [fundedRawTransaction] 292 | }; 293 | 294 | let signedTransaction; 295 | 296 | try { 297 | signedTransaction = await execRpcCommand(command); 298 | } catch (err) { 299 | console.log('Error signing transaction', err); 300 | throw err; 301 | } 302 | 303 | if (!signedTransaction) { 304 | console.log('Bitcoind did not return a signed transaction'); 305 | throw new Error('Missing signed tx'); 306 | } 307 | 308 | return signedTransaction.hex; 309 | } 310 | 311 | /** 312 | * Decodes a hexadecimal format transaction 313 | * @param {string} rawTransaction 314 | * @return {Promise<*>} 315 | */ 316 | async function decodeRawTransaction(rawTransaction) { 317 | let command = { 318 | jsonrpc: '1.0', 319 | method: 'decoderawtransaction', 320 | params: [rawTransaction] 321 | }; 322 | 323 | let decodedTransaction; 324 | 325 | try { 326 | decodedTransaction = await execRpcCommand(command); 327 | } catch (err) { 328 | console.log('Error decoding transaction', err); 329 | throw err; 330 | } 331 | 332 | if (!decodedTransaction) { 333 | console.log('Bitcoind did not decode the transaction'); 334 | throw new Error('Missing decoded tx'); 335 | } 336 | 337 | return decodedTransaction; 338 | } 339 | 340 | /** 341 | * Sends a signed transaction to the bitcoin p2p network 342 | * @param signedTransaction 343 | * @return {Promise<*>} 344 | */ 345 | async function broadcastP2P(signedTransaction) { 346 | let command = { 347 | jsonrpc: '1.0', 348 | method: 'sendrawtransaction', 349 | params: [signedTransaction] 350 | }; 351 | 352 | let result; 353 | 354 | try { 355 | result = await execRpcCommand(command); 356 | } catch (err) { 357 | console.log('Error broadcasting transaction'); 358 | throw err; 359 | } 360 | 361 | if (!result) { 362 | console.log('Bitcoind failed to broadcast transaction'); 363 | throw new Error('Failed to broadcast tx'); 364 | } 365 | 366 | return result; 367 | } 368 | 369 | /** 370 | * Executes an RPC command 371 | * @param {Object} command 372 | * @return {Promise} 373 | */ 374 | function execRpcCommand(command) { 375 | return new Promise((resolve, reject) => { 376 | request 377 | .post({ 378 | url: rpcUrl, 379 | body: command, 380 | json: true, 381 | auth: { 382 | user: rpcUser, 383 | pass: rpcPass, 384 | sendImmediately: false 385 | } 386 | }, function(err, response, body) { 387 | if (err) { 388 | return reject(err); 389 | } 390 | if (!body) { 391 | return reject(new Error('No body returned by bitcoin RPC server')); 392 | } 393 | if (body.error) { 394 | return reject(body.error); 395 | } 396 | if (body.result) { 397 | return resolve(body.result); 398 | } 399 | return resolve(); 400 | }); 401 | }); 402 | } 403 | 404 | /** 405 | * Rounds a number to a specific precision of digits 406 | * @param value 407 | * @param places 408 | * @return {number} 409 | */ 410 | function round(value, places) { 411 | let tmp = Math.pow(10, places); 412 | return Math.round(value * tmp) / tmp; 413 | } 414 | 415 | main().catch(e => console.log(e)); 416 | -------------------------------------------------------------------------------- /examples/config.example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | network: 'test', 3 | rpcServer: { 4 | BTC: { 5 | // should set this to match your own bitcoin-core rpc settings 6 | username: 'fakeUser', 7 | password: 'fakePassword', 8 | ipAddress: '127.0.0.1', 9 | port: '18332' 10 | }, 11 | BCH: { 12 | // should set this to match your own bitcoin-abc rpc settings 13 | username: 'fakeUser', 14 | password: 'fakePassword', 15 | ipAddress: '127.0.0.1', 16 | port: '18332' 17 | } 18 | }, 19 | trustedKeys: { 20 | // The idea is that you or the wallet provider will populate this with keys that are trusted, we have provided a few possible approaches 21 | // in the specification.md document within the 'key-storing suggestions' section 22 | 23 | // Each key here is the pubkey hash so that we can do quick look-ups using the x-identity header sent in the payment request 24 | 'mh65MN7drqmwpCRZcEeBEE9ceQCQ95HtZc': { 25 | // This is displayed to the user, somewhat like the organization field on an SSL certificate 26 | owner: 'BitPay (TESTNET ONLY - DO NOT TRUST FOR ACTUAL BITCOIN)', 27 | // Which domains this key is valid for 28 | domains: ['test.bitpay.com'], 29 | // The actual public key which should be used to validate the signatures 30 | publicKey: '03159069584176096f1c89763488b94dbc8d5e1fa7bf91f50b42f4befe4e45295a', 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /examples/fetchEccKeysAndVerify.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | const bs58 = require('bs58'); 4 | const kbpgp = require('kbpgp'); 5 | const request = require('request-promise'); 6 | 7 | let bitpayPgpKeys = {}; 8 | let githubPgpKeys = {}; 9 | let importedPgpKeys = {}; 10 | let signatureCount = 0; 11 | 12 | let eccPayload; 13 | let parsedEccPayload; 14 | let eccKeysHash; 15 | 16 | let keyRequests = []; 17 | 18 | keyRequests.push((() => { 19 | console.log('Fetching keys from github.com/bitpay/pgp-keys...'); 20 | return request({ 21 | method: 'GET', 22 | url: 'https://api.github.com/repos/bitpay/pgp-keys/contents/keys', 23 | headers: { 24 | 'user-agent': 'BitPay Key-Check Utility' 25 | }, 26 | json: true 27 | }).then((pgpKeyFiles) => { 28 | let fileDataPromises = []; 29 | pgpKeyFiles.forEach((file) => { 30 | fileDataPromises.push((() => { 31 | return request({ 32 | method: 'GET', 33 | url: file.download_url, 34 | headers: { 35 | 'user-agent': 'BitPay Key-Check Utility' 36 | } 37 | }).then((body) => { 38 | let hash = crypto.createHash('sha256').update(body).digest('hex'); 39 | githubPgpKeys[hash] = body; 40 | return Promise.resolve(); 41 | }); 42 | })()); 43 | }); 44 | return Promise.all(fileDataPromises); 45 | }); 46 | })()); 47 | 48 | keyRequests.push((() => { 49 | console.log('Fetching keys from bitpay.com/pgp-keys...'); 50 | return request({ 51 | method: 'GET', 52 | url: 'https://bitpay.com/pgp-keys.json', 53 | headers: { 54 | 'user-agent': 'BitPay Key-Check Utility' 55 | }, 56 | json: true 57 | }).then((body) => { 58 | body.pgpKeys.forEach(function(key) { 59 | let hash = crypto.createHash('sha256').update(key.publicKey).digest('hex'); 60 | bitpayPgpKeys[hash] = key.publicKey; 61 | }); 62 | return Promise.resolve(); 63 | }); 64 | })()); 65 | 66 | Promise.all(keyRequests).then(() => { 67 | if (Object.keys(githubPgpKeys).length !== Object.keys(bitpayPgpKeys).length) { 68 | console.log('Warning: Different number of keys returned by key lists'); 69 | } 70 | 71 | let bitpayOnlyKeys = Object.keys(bitpayPgpKeys).filter((keyHash) => { 72 | return !githubPgpKeys[keyHash]; 73 | }); 74 | 75 | let githubOnlyKeys = Object.keys(githubPgpKeys).filter((keyHash) => { 76 | return !bitpayPgpKeys[keyHash]; 77 | }); 78 | 79 | if (bitpayOnlyKeys.length) { 80 | console.log('BitPay returned some keys which are not present in github'); 81 | Object.keys(bitpayOnlyKeys).forEach((keyHash) => { 82 | console.log(`Hash ${keyHash} Key: ${bitpayOnlyKeys[keyHash]}`); 83 | }); 84 | } 85 | 86 | if (githubOnlyKeys.length) { 87 | console.log('GitHub returned some keys which are not present in BitPay'); 88 | Object.keys(githubOnlyKeys).forEach((keyHash) => { 89 | console.log(`Hash ${keyHash} Key: ${githubOnlyKeys[keyHash]}`); 90 | }); 91 | } 92 | 93 | if (!githubOnlyKeys.length && !bitpayOnlyKeys.length) { 94 | console.log(`Both sites returned ${Object.keys(githubPgpKeys).length} keys. Key lists from both are identical.`); 95 | return Promise.resolve(); 96 | } else { 97 | return Promise.reject('Aborting signature checks due to key mismatch'); 98 | } 99 | }).then(() => { 100 | console.log('Importing PGP keys for later use...'); 101 | return Promise.all(Object.values(bitpayPgpKeys).map((pgpKeyString) => { 102 | return new Promise((resolve, reject) => { 103 | kbpgp.KeyManager.import_from_armored_pgp({armored: pgpKeyString}, (err, km) => { 104 | if (err) { 105 | return reject(err); 106 | } 107 | importedPgpKeys[km.pgp.key(km.pgp.primary).get_fingerprint().toString('hex')] = km; 108 | return resolve(); 109 | }); 110 | }); 111 | })); 112 | }).then(() => { 113 | console.log('Fetching current ECC keys from bitpay.com/signingKeys/paymentProtocol.json'); 114 | return request({ 115 | method: 'GET', 116 | url: 'https://bitpay.com/signingKeys/paymentProtocol.json', 117 | headers: { 118 | 'user-agent': 'BitPay Key-Check Utility' 119 | } 120 | }).then((rawEccPayload) => { 121 | if (rawEccPayload.indexOf('rate limit') !== -1) { 122 | return Promise.reject('Rate limited by BitPay'); 123 | } 124 | eccPayload = rawEccPayload; 125 | parsedEccPayload = JSON.parse(rawEccPayload); 126 | if (new Date(parsedEccPayload.expirationDate) < Date.now()) { 127 | return console.log('The currently published ECC keys are expired'); 128 | } 129 | eccKeysHash = crypto.createHash('sha256').update(rawEccPayload).digest('hex'); 130 | return Promise.resolve(); 131 | }); 132 | }).then(() => { 133 | console.log(`Fetching signatures for ECC payload with hash ${eccKeysHash}`); 134 | return request({ 135 | method: 'GET', 136 | url: `https://bitpay.com/signatures/${eccKeysHash}.json`, 137 | headers: { 138 | 'user-agent': 'BitPay Key-Check Utility' 139 | }, 140 | json: true 141 | }).then((signatureData) => { 142 | console.log('Verifying each signature is valid and comes from the set of PGP keys retrieved earlier'); 143 | Promise.all(signatureData.signatures.map((signature) => { 144 | return new Promise((resolve, reject) => { 145 | let pgpKey = importedPgpKeys[signature.identifier]; 146 | if (!pgpKey) { 147 | return reject(`PGP key ${signature.identifier} missing for signature`); 148 | } 149 | let armoredSignature = Buffer.from(signature.signature, 'hex').toString(); 150 | 151 | kbpgp.unbox({armored: armoredSignature, data: Buffer.from(eccPayload), keyfetch: pgpKey}, (err, result) => { 152 | if (err) { 153 | return reject(`Unable to verify signature from ${signature.identifier} ${err}`); 154 | } 155 | signatureCount++; 156 | console.log(`Good signature from ${signature.identifier} (${pgpKey.get_userids()[0].get_username()})`); 157 | return Promise.resolve(); 158 | }); 159 | }); 160 | })); 161 | }); 162 | }).then(() => { 163 | if (signatureCount >= (Object.keys(bitpayPgpKeys).length / 2) ) { 164 | console.log(`----\nThe following ECC key set has been verified against signatures from ${signatureCount} of the ${Object.keys(bitpayPgpKeys).length} published BitPay PGP keys.`); 165 | console.log(eccPayload); 166 | 167 | let keyMap = {}; 168 | 169 | console.log('----\nValid keymap for use in bitcoinRpc example:'); 170 | 171 | parsedEccPayload.publicKeys.forEach((pubkey) => { 172 | // Here we are just generating the pubkey hash (btc address) of each of the public keys received for easy lookup later 173 | // as this is what will be provided by the x-identity header 174 | let a = crypto.createHash('sha256').update(pubkey, 'hex').digest(); 175 | let b = crypto.createHash('rmd160').update(a).digest('hex'); 176 | let c = '00' + b; // This is assuming livenet 177 | let d = crypto.createHash('sha256').update(c, 'hex').digest(); 178 | let e = crypto.createHash('sha256').update(d).digest('hex'); 179 | 180 | let pubKeyHash = bs58.encode(Buffer.from(c + e.substr(0, 8), 'hex')); 181 | 182 | 183 | keyMap[pubKeyHash] = { 184 | owner: parsedEccPayload.owner, 185 | networks: ['main'], 186 | domains: parsedEccPayload.domains, 187 | publicKey: pubkey 188 | } 189 | }); 190 | 191 | console.log(keyMap); 192 | } else { 193 | return Promise.reject(`Insufficient good signatures ${signatureCount} for a proper validity check`); 194 | } 195 | }).catch((err) => { 196 | console.log(`Error encountered ${err}`); 197 | }); 198 | 199 | process.on('unhandledRejection', console.log); 200 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Native 4 | const crypto = require('crypto'); 5 | const https = require('https'); 6 | const query = require('querystring'); 7 | const url = require('url'); 8 | 9 | // Modules 10 | const secp256k1 = require('secp256k1'); 11 | 12 | function PaymentProtocol(requestOptions, trustedKeys) { 13 | this.options = Object.assign({}, { agent: false }, requestOptions); 14 | this.trustedKeys = trustedKeys; 15 | if (!this.trustedKeys || !Object.keys(this.trustedKeys).length) { 16 | throw new Error('Invalid constructor, no trusted keys added to agent'); 17 | } 18 | } 19 | 20 | /** 21 | * Internal method for making requests asynchronously 22 | * @param {Object} options 23 | * @return {Promise} 24 | * @private 25 | */ 26 | PaymentProtocol.prototype._asyncRequest = async function(options) { 27 | let requestOptions = Object.assign({}, this.options, options); 28 | const parsedUrl = url.parse(requestOptions.url); 29 | 30 | // Copy headers directly as they're objects 31 | requestOptions.headers = Object.assign({}, this.options.headers, options.headers); 32 | 33 | requestOptions.hostname = parsedUrl.hostname; 34 | requestOptions.path = parsedUrl.path; 35 | requestOptions.port = parsedUrl.port; 36 | delete requestOptions.url; 37 | 38 | return new Promise((resolve, reject) => { 39 | const request = https.request(requestOptions, (response) => { 40 | const body = []; 41 | response.on('data', chunk => body.push(chunk)); 42 | response.on('end', () => { 43 | if (response.statusCode !== 200) { 44 | console.log('Status', response.statusCode); 45 | return reject(new Error(body.join(''))); 46 | } 47 | resolve({ 48 | rawBody: body.join(''), 49 | headers: response.headers 50 | }); 51 | }); 52 | }); 53 | request.on('error', reject); 54 | if (requestOptions.body) { 55 | request.write(requestOptions.body); 56 | } 57 | request.end(); 58 | }); 59 | }; 60 | 61 | /** 62 | * Makes a request to the given url and returns the raw JSON string retrieved as well as the headers 63 | * @param {string} paymentUrl the payment protocol specific url 64 | * @param {boolean} unsafeBypassValidation bypasses signature verification on the request (DO NOT USE IN PRODUCTION) 65 | */ 66 | PaymentProtocol.prototype.getPaymentOptions = async function(paymentUrl, unsafeBypassValidation = false) { 67 | const paymentUrlObject = url.parse(paymentUrl); 68 | 69 | // Detect 'bitcoin:' urls and extract payment-protocol section 70 | if (paymentUrlObject.protocol !== 'http:' && paymentUrlObject.protocol !== 'https:') { 71 | let uriQuery = query.decode(paymentUrlObject.query); 72 | if (!uriQuery.r) { 73 | throw new Error('Invalid payment protocol url'); 74 | } else { 75 | paymentUrl = uriQuery.r; 76 | } 77 | } 78 | 79 | const { rawBody, headers } = await this._asyncRequest({ 80 | method: 'GET', 81 | url: paymentUrl, 82 | headers: { 83 | Accept: 'application/payment-options', 84 | 'x-paypro-version': 2 85 | } 86 | }); 87 | 88 | return await this.verifyResponse(paymentUrl, rawBody, headers, unsafeBypassValidation); 89 | }; 90 | 91 | /** 92 | * Selects which chain and currency option the user will be using for payment 93 | * @param {string} paymentUrl the payment protocol specific url 94 | * @param chain 95 | * @param currency 96 | * @param unsafeBypassValidation 97 | * @return {Promise<{requestUrl, responseData}|{keyData, requestUrl, responseData}>} 98 | */ 99 | PaymentProtocol.prototype.selectPaymentOption = async function(paymentUrl, chain, currency, unsafeBypassValidation = false) { 100 | const { rawBody, headers } = await this._asyncRequest({ 101 | url: paymentUrl, 102 | method: 'POST', 103 | headers: { 104 | 'Content-Type': 'application/payment-request', 105 | 'x-paypro-version': 2 106 | }, 107 | body: JSON.stringify({ 108 | chain, 109 | currency 110 | }) 111 | }); 112 | 113 | return await this.verifyResponse(paymentUrl, rawBody, headers, unsafeBypassValidation); 114 | }; 115 | 116 | /** 117 | * Sends an unsigned raw transaction to the server for verification of outputs and fee amount 118 | * @param {string} paymentUrl - the payment protocol specific url 119 | * @param {string} chain - The cryptocurrency chain of the payment (BTC, BCH, ETH, etc) 120 | * @param {string} currency - When spending a token on top of a chain, such as GUSD on ETH this would be GUSD, 121 | * if no token is used this should be blank 122 | * @param [{tx: string, weightedSize: number}] unsignedTransactions - Hexadecimal format unsigned transactions 123 | * @param {boolean} unsafeBypassValidation 124 | * @return {Promise<{responseData: any}>} 125 | */ 126 | PaymentProtocol.prototype.verifyUnsignedPayment = async function({ 127 | paymentUrl, 128 | chain, 129 | currency, 130 | unsignedTransactions, 131 | unsafeBypassValidation = false 132 | }) { 133 | const { rawBody, headers } = await this._asyncRequest({ 134 | url: paymentUrl, 135 | method: 'POST', 136 | headers: { 137 | 'Content-Type': 'application/payment-verification', 138 | 'x-paypro-version': 2 139 | }, 140 | body: JSON.stringify({ 141 | chain, 142 | currency, 143 | transactions: unsignedTransactions 144 | }) 145 | }); 146 | 147 | return await this.verifyResponse(paymentUrl, rawBody, headers, unsafeBypassValidation); 148 | }; 149 | 150 | /** 151 | * Sends a signed transaction as the final step for payment 152 | * @param {string} paymentUrl the payment protocol specific url 153 | * @param {string} chain 154 | * @param {string} currency 155 | * @param {[string]} signedTransactions 156 | * @param {number} weightedSize 157 | * @param {boolean} unsafeBypassValidation 158 | * @return {Promise<{keyData: Object, requestUrl: String, responseData: Object}|{requestUrl: String, responseData: Object}>} 159 | */ 160 | PaymentProtocol.prototype.sendSignedPayment = async function({ 161 | paymentUrl, 162 | chain, 163 | currency, 164 | signedTransactions, 165 | unsafeBypassValidation = false 166 | }) { 167 | const { rawBody, headers } = await this._asyncRequest({ 168 | url: paymentUrl, 169 | method: 'POST', 170 | headers: { 171 | 'Content-Type': 'application/payment', 172 | 'x-paypro-version': 2 173 | }, 174 | body: JSON.stringify({ 175 | chain, 176 | currency, 177 | transactions: signedTransactions 178 | }) 179 | }); 180 | 181 | return await this.verifyResponse(paymentUrl, rawBody, headers, unsafeBypassValidation); 182 | }; 183 | 184 | /** 185 | * Verifies the signature on any response from the payment requestor 186 | * @param {String} requestUrl - Url which the request was made to 187 | * @param {String} rawBody - The raw string body of the response 188 | * @param {Object} headers - 189 | * @param {Boolean} unsafeBypassValidation 190 | * @return {Promise<{keyData: Object, requestUrl: String, responseData: Object}|{requestUrl: String, responseData: Object}>} 191 | */ 192 | PaymentProtocol.prototype.verifyResponse = async function(requestUrl, rawBody, headers, unsafeBypassValidation) { 193 | if (!requestUrl) { 194 | throw new Error('Parameter requestUrl is required'); 195 | } 196 | if (!rawBody) { 197 | throw new Error('Parameter rawBody is required'); 198 | } 199 | if (!headers) { 200 | throw new Error('Parameter headers is required'); 201 | } 202 | 203 | let responseData; 204 | try { 205 | responseData = JSON.parse(rawBody); 206 | } catch (e) { 207 | throw new Error('Invalid JSON in response body'); 208 | } 209 | 210 | if (unsafeBypassValidation) { 211 | return { requestUrl, responseData }; 212 | } 213 | 214 | const hash = headers.digest.split('=')[1]; 215 | const signature = headers.signature; 216 | const signatureType = headers['x-signature-type']; 217 | const identity = headers['x-identity']; 218 | let host; 219 | 220 | try { 221 | host = url.parse(requestUrl).hostname; 222 | } catch (e) { 223 | } 224 | 225 | if (!host) { 226 | throw new Error('Invalid requestUrl'); 227 | } 228 | if (!signatureType) { 229 | throw new Error('Response missing x-signature-type header'); 230 | } 231 | if (typeof signatureType !== 'string') { 232 | throw new Error('Invalid x-signature-type header'); 233 | } 234 | if (signatureType !== 'ecc') { 235 | throw new Error(`Unknown signature type ${signatureType}`); 236 | } 237 | if (!signature) { 238 | throw new Error('Response missing signature header'); 239 | } 240 | if (typeof signature !== 'string') { 241 | throw new Error('Invalid signature header'); 242 | } 243 | if (!identity) { 244 | throw new Error('Response missing x-identity header'); 245 | } 246 | if (typeof identity !== 'string') { 247 | throw new Error('Invalid identity header'); 248 | } 249 | 250 | if (!this.trustedKeys[identity]) { 251 | throw new Error(`Response signed by unknown key (${identity}), unable to validate`); 252 | } 253 | 254 | const keyData = this.trustedKeys[identity]; 255 | const actualHash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex'); 256 | 257 | if (hash !== actualHash) { 258 | throw new Error(`Response body hash does not match digest header. Actual: ${actualHash} Expected: ${hash}`); 259 | } 260 | 261 | if (!keyData.domains.includes(host)) { 262 | throw new Error(`The key on the response (${identity}) is not trusted for domain ${host}`); 263 | } 264 | 265 | let valid = secp256k1.verify( 266 | Buffer.from(hash, 'hex'), 267 | Buffer.from(signature, 'hex'), 268 | Buffer.from(keyData.publicKey, 'hex') 269 | ); 270 | 271 | if (!valid) { 272 | throw new Error('Response signature invalid'); 273 | } 274 | 275 | return { requestUrl, responseData, keyData }; 276 | }; 277 | 278 | module.exports = PaymentProtocol; 279 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-payment-protocol", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bindings": { 8 | "version": "1.5.0", 9 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 10 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 11 | "requires": { 12 | "file-uri-to-path": "1.0.0" 13 | } 14 | }, 15 | "bip66": { 16 | "version": "1.1.5", 17 | "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", 18 | "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", 19 | "requires": { 20 | "safe-buffer": "5.2.0" 21 | } 22 | }, 23 | "bn.js": { 24 | "version": "4.11.8", 25 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", 26 | "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" 27 | }, 28 | "brorand": { 29 | "version": "1.1.0", 30 | "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", 31 | "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" 32 | }, 33 | "browserify-aes": { 34 | "version": "1.2.0", 35 | "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", 36 | "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", 37 | "requires": { 38 | "buffer-xor": "1.0.3", 39 | "cipher-base": "1.0.4", 40 | "create-hash": "1.2.0", 41 | "evp_bytestokey": "1.0.3", 42 | "inherits": "2.0.4", 43 | "safe-buffer": "5.2.0" 44 | } 45 | }, 46 | "buffer-xor": { 47 | "version": "1.0.3", 48 | "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", 49 | "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" 50 | }, 51 | "cipher-base": { 52 | "version": "1.0.4", 53 | "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", 54 | "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", 55 | "requires": { 56 | "inherits": "2.0.4", 57 | "safe-buffer": "5.2.0" 58 | } 59 | }, 60 | "create-hash": { 61 | "version": "1.2.0", 62 | "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", 63 | "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", 64 | "requires": { 65 | "cipher-base": "1.0.4", 66 | "inherits": "2.0.4", 67 | "md5.js": "1.3.5", 68 | "ripemd160": "2.0.2", 69 | "sha.js": "2.4.11" 70 | } 71 | }, 72 | "create-hmac": { 73 | "version": "1.1.7", 74 | "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", 75 | "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", 76 | "requires": { 77 | "cipher-base": "1.0.4", 78 | "create-hash": "1.2.0", 79 | "inherits": "2.0.4", 80 | "ripemd160": "2.0.2", 81 | "safe-buffer": "5.2.0", 82 | "sha.js": "2.4.11" 83 | } 84 | }, 85 | "drbg.js": { 86 | "version": "1.0.1", 87 | "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", 88 | "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", 89 | "requires": { 90 | "browserify-aes": "1.2.0", 91 | "create-hash": "1.2.0", 92 | "create-hmac": "1.1.7" 93 | } 94 | }, 95 | "elliptic": { 96 | "version": "6.5.0", 97 | "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", 98 | "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", 99 | "requires": { 100 | "bn.js": "4.11.8", 101 | "brorand": "1.1.0", 102 | "hash.js": "1.1.7", 103 | "hmac-drbg": "1.0.1", 104 | "inherits": "2.0.4", 105 | "minimalistic-assert": "1.0.1", 106 | "minimalistic-crypto-utils": "1.0.1" 107 | } 108 | }, 109 | "evp_bytestokey": { 110 | "version": "1.0.3", 111 | "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", 112 | "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", 113 | "requires": { 114 | "md5.js": "1.3.5", 115 | "safe-buffer": "5.2.0" 116 | } 117 | }, 118 | "file-uri-to-path": { 119 | "version": "1.0.0", 120 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 121 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 122 | }, 123 | "hash-base": { 124 | "version": "3.0.4", 125 | "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", 126 | "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", 127 | "requires": { 128 | "inherits": "2.0.4", 129 | "safe-buffer": "5.2.0" 130 | } 131 | }, 132 | "hash.js": { 133 | "version": "1.1.7", 134 | "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", 135 | "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", 136 | "requires": { 137 | "inherits": "2.0.4", 138 | "minimalistic-assert": "1.0.1" 139 | } 140 | }, 141 | "hmac-drbg": { 142 | "version": "1.0.1", 143 | "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", 144 | "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", 145 | "requires": { 146 | "hash.js": "1.1.7", 147 | "minimalistic-assert": "1.0.1", 148 | "minimalistic-crypto-utils": "1.0.1" 149 | } 150 | }, 151 | "inherits": { 152 | "version": "2.0.4", 153 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 154 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 155 | }, 156 | "md5.js": { 157 | "version": "1.3.5", 158 | "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", 159 | "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", 160 | "requires": { 161 | "hash-base": "3.0.4", 162 | "inherits": "2.0.4", 163 | "safe-buffer": "5.2.0" 164 | } 165 | }, 166 | "minimalistic-assert": { 167 | "version": "1.0.1", 168 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 169 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 170 | }, 171 | "minimalistic-crypto-utils": { 172 | "version": "1.0.1", 173 | "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", 174 | "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" 175 | }, 176 | "nan": { 177 | "version": "2.14.0", 178 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 179 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" 180 | }, 181 | "ripemd160": { 182 | "version": "2.0.2", 183 | "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", 184 | "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", 185 | "requires": { 186 | "hash-base": "3.0.4", 187 | "inherits": "2.0.4" 188 | } 189 | }, 190 | "safe-buffer": { 191 | "version": "5.2.0", 192 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 193 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 194 | }, 195 | "secp256k1": { 196 | "version": "3.7.1", 197 | "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz", 198 | "integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==", 199 | "requires": { 200 | "bindings": "1.5.0", 201 | "bip66": "1.1.5", 202 | "bn.js": "4.11.8", 203 | "create-hash": "1.2.0", 204 | "drbg.js": "1.0.1", 205 | "elliptic": "6.5.0", 206 | "nan": "2.14.0", 207 | "safe-buffer": "5.2.0" 208 | } 209 | }, 210 | "sha.js": { 211 | "version": "2.4.11", 212 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 213 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 214 | "requires": { 215 | "inherits": "2.0.4", 216 | "safe-buffer": "5.2.0" 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-payment-protocol", 3 | "version": "2.0.0", 4 | "description": "Simple interface for retrieving JSON payment requests and submitting payments", 5 | "main": "index.js", 6 | "dependencies": { 7 | "secp256k1": "^3.5.0" 8 | }, 9 | "devDependencies": { 10 | "async": "2.6.0", 11 | "bs58": "4.0.1", 12 | "eslint": "6.2.1", 13 | "kbpgp": "2.0.77", 14 | "promptly": "2.2.0", 15 | "request": "^2.83.0", 16 | "request-promise": "4.2.2" 17 | }, 18 | "engines": { 19 | "node": ">=8.0.0" 20 | }, 21 | "scripts": { 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "author": "BitPay", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/bitpay/jsonPaymentProtocol.git" 28 | }, 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /paymentFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitpay/jsonPaymentProtocol/24014a7d29a0fac8a47523843aaafd5b7e07b901/paymentFlow.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### JSON Payment Protocol Interface v2 2 | 3 | This is the second version of the JSON payment protocol interface. If you have questions about the v2 specification itself, [view the documentation](v2/specification.md). 4 | 5 | [If you have questions about the first version of the specification view the documentation](v1/specification.md). 6 | 7 | 8 | ### Getting Started with v2 9 | 10 | `npm install json-payment-protocol` 11 | 12 | ### v2 Usage 13 | 14 | This library is now using async await structure for all functions. Be careful to follow the notes about when to broadcast your payment. 15 | Broadcasting a payment before getting a success notification back from the server in most cases will lead to a failed payment for the sender. 16 | The sender will bear the cost of paying transaction fees yet again to get their money back. 17 | 18 | #### Example 19 | ```js 20 | const JsonPaymentProtocol = require('json-payment-protocol'); 21 | 22 | // Options such as additional headers, etc which you want to pass to the node https client on every request 23 | const requestOptions = {}; 24 | 25 | const trustedKeys = { 26 | 'mh65MN7drqmwpCRZcEeBEE9ceQCQ95HtZc': { 27 | // This is displayed to the user, somewhat like the organization field on an SSL certificate 28 | owner: 'BitPay (TESTNET ONLY - DO NOT TRUST FOR ACTUAL BITCOIN)', 29 | // Which domains this key is valid for 30 | domains: ['test.bitpay.com'], 31 | // The actual public key which should be used to validate the signatures 32 | publicKey: '03159069584176096f1c89763488b94dbc8d5e1fa7bf91f50b42f4befe4e45295a', 33 | } 34 | }; 35 | 36 | const client = new JsonPaymentProtocol(requestOptions, trustedKeys); 37 | 38 | 39 | let requestUrl = 'bitcoin:?r=https://test.bitpay.com/i/Jr629pwsXKdTCneLyZja4t'; 40 | 41 | const paymentOptions = await client.getPaymentOptions(requestUrl); 42 | 43 | // The paymentOptions response will contain one or more currency / chain options. If you are a multi-currency wallet then you should 44 | // display the compatible payment options to the user. If only one option is supported it is 45 | 46 | const { responseData: paymentRequest } = await client.selectPaymentOption(paymentOptions.requestUrl, userChoice.chain, userChoice.currency); 47 | 48 | // Parse response data instructions and create an appropriate unsigned and signed transaction 49 | // This is pseudocode 50 | let unsignedTransaction = await myWallet.createTransaction(responseData); 51 | let signedTransaction = await myWallet.signTransaction(unsignedTransaction); 52 | 53 | // We send the unsigned transaction(s) first with their size to verify if this payment will be accepted 54 | try { 55 | await client.verifyUnsignedPayment({ 56 | paymentUrl: paymentOptions.requestUrl, 57 | chain: userChoice.chain, 58 | // For chains which can support multiple currencies via tokens, a currency code is required to identify which token is being used 59 | currency: userChoice.currency, 60 | unsignedTransactions: [{ 61 | tx: unsignedTransaction.rawHex, 62 | // `vsize` for bitcoin core w/ segwit support, `size` for other clients 63 | weightedSize: signedTransaction.vsize || signedTransaction.size 64 | }] 65 | }); 66 | } catch (e) { 67 | // If an error occurs here, it is most likely an issue with the transaction (insufficient fee, rbf, unconfirmed inputs, etc) 68 | // It could also be a network error or the invoice may no longer be accepting payments (already paid or expired) 69 | return console.log('Error verifying payment with server', e); 70 | } 71 | 72 | // If the payment is valid we send the signed payment 73 | try { 74 | await client.sendSignedPayment({ 75 | paymentUrl: paymentOptions.requestUrl, 76 | chain: choice.chain, 77 | currency: choice.currency, 78 | signedTransactions: [{ 79 | tx: signedTransaction, 80 | // `vsize` for bitcoin core w/ segwit support, `size` for other clients 81 | weightedSize: signedTransaction.vsize || signedTransaction.size 82 | }] 83 | }); 84 | await broadcastP2P(signedTransaction); 85 | console.log('Payment successfully sent'); 86 | } catch (e) { 87 | console.log('Error sending payment', e); 88 | } 89 | ``` 90 | 91 | ### Options 92 | 93 | 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: 94 | 95 | ```js 96 | new JsonPaymentProtocol({ 97 | proxy: 'socks://mySocksProxy.local', 98 | headers: { 99 | 'user-agent': 'myWallet' 100 | } 101 | }) 102 | ``` 103 | 104 | ### URI Formats 105 | You can provide either the `bitcoin:?r=https://bitpay.com/i/invoice` format or `https://bitpay.com/i/invoice` directly. 106 | 107 | -------------------------------------------------------------------------------- /securityUpdates.md: -------------------------------------------------------------------------------- 1 | # Security Updates 2 | 3 | ### 2019-02-01 4 | Recently a bitcoin community member discussed a potential bug in our modified BIP-70 and JSON payment protocol flows when implemented by a 5 | malicious server. This bug would not affect BitPay itself, but has the potential to affect wallets using our payment process 6 | recommendations. This was due to an implicit trust by the client that the server is not malicious. 7 | 8 | A proposed malicious flow is as follows: 9 | 10 | 1. In person Eve asks Adam if she can buy his bitcoin, hands him cash provides a payment protocol url 11 | 2. Adams wallet interacts with the url, and sends signed transaction to the server for verification 12 | 3. Server rejects signed transaction, but secretly stores it 13 | 4. Wallet notifies Adam that the transaction was rejected 14 | 5. Eve asks for her money back, Adam complies since transaction was rejected 15 | 6. Later Eve has the server broadcast the signed transaction, Eve now has both the cash and the crypto 16 | 17 | To resolve this we're advising a change to the payment protocol flow to protect users. 18 | 19 | #### Existing flow: 20 | 21 | 1. Wallet requests payment data 22 | 2. Wallet creates unsigned transaction 23 | 2. Wallet signs transaction 24 | 3. Wallet sends **SIGNED** transaction to server for verification 25 | 4. Server verifies or rejects payment and notifies wallet 26 | 5. Wallet broadcasts if server accepts, stops if rejected 27 | 28 | #### Updated flow: 29 | 30 | 1. Wallet requests payment data 31 | 2. Wallet creates unsigned transaction 32 | 3. Wallet signs transaction 33 | 4. Wallet sends **UNSIGNED** transaction and weighted size of signed transaction to server for verification 34 | 5. Server verifies or rejects payment and notifies wallet 35 | 6. Wallet sends **SIGNED** transaction to server and broadcasts to p2p at the same time 36 | 37 | This new flow prevents a malicious server from being able to broadcast without the user's knowledge as it now only ever has access to the 38 | unsigned transaction before accepting or rejecting. This is also why we recommend broadcasting to the p2p network at the same time as the 39 | sending the payment via payment protocol to the server. Since you've already gotten approval you should be free to broadcast and ignore 40 | any rejections by the server. 41 | 42 | While JSON payment protocol is vulnerable, trying to use a malicious server is less viable than the modified BIP-70 flow due to the need 43 | for each wallet to whitelist ECC keys. Since BIP-70 only requires a valid x509 certificate, anyone with a domain could run a malicious 44 | server. If your wallet uses the modified BIP-70 flow you should update as soon as possible. 45 | 46 | Our servers will remain backwards compatible with the old flow for the interim, however to protect users we highly recommend wallets update 47 | to use the more secure flow. 48 | 49 | More details of *what* exactly needs to be sent *where* can be found in the updated specification document. 50 | -------------------------------------------------------------------------------- /v1/specification.md: -------------------------------------------------------------------------------- 1 | # JSON Payment Protocol Specification 2 | 3 | Revision 0.7 4 | 5 | ## Application Logic 6 | 7 | 1. (Web) User selects preferred currency on invoice if multiple options are available 8 | 2. (Client) Wallet obtains payment protocol uri 9 | 3. (Client) Fetches payment information from server 10 | 4. (Server) Verifies invoice exists and is still accepting payments, responds with payment request 11 | 5. (Client) Validates payment request hash 12 | 6. (Client) Validates payment request signature 13 | 7. (Client) Generates a payment to match conditions on payment request 14 | 8. (Client) Submits proposed unsigned transaction and size of signed transaction to server 15 | 9. (Server) Validates invoice exists and is still accepting payments 16 | 10. (Server) Validates payment matches address, amount, and currency of invoice and has a reasonable transaction fee. 17 | 11. (Server) Notifies client payment will be accepted 18 | 12. (Client) Sends payment to server and broadcasts to p2p network 19 | 13. (Server) Validates signed payment and broadcasts payment to network. 20 | 21 | In general, the payment should not be broadcast by the client. If at time of verification the payment is rejected by the server **your client must not broadcast the payment**. 22 | 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. 23 | 24 | ![Payment Flow](/paymentFlow.png?raw=true) 25 | 26 | ## Payment Request 27 | 28 | ### Request 29 | A GET request should be made to the payment protocol url. 30 | 31 | #### Headers 32 | * `Accept` should be set to `application/payment-request`. 33 | 34 | ### Response 35 | The response will be a JSON format payload quite similar to the BIP70 format. 36 | 37 | #### Headers 38 | On a successful request, the response will contain the following headers. 39 | 40 | * `digest` - A SHA256 hash of the JSON response string, should be verified by the client before proceeding 41 | * `x-identity` - An identifier to represent which public key should be used to verify the signature. For example for BitPay's ECC keys we will include the public key hash in this header. Implementations should **NOT** include the public key here directly. 42 | * `x-signature-type` The signature format used to sign the payload. For the foreseeable future BitPay will always use `ECC`. However, we wanted to grant some flexibility to the specification. 43 | * `x-signature` - A cryptographic signature of the SHA256 hash of the payload. This is to prove that the payment request was not tampered with before being received by the wallet. 44 | 45 | #### Body 46 | * `network` - Which network is this request for (main / test / regtest) 47 | * `currency` - Three digit currency code representing which coin the request is based on 48 | * `requiredFeeRate` - The minimum fee per byte required on this transaction. Payment will be rejected if fee rate included for the transaction is not at least this value. _May be fractional value_ ie 0.123 sat/byte 49 | * `outputs` - What output(s) your transaction must include in order to be accepted 50 | * `time` - ISO Date format of when the invoice was generated 51 | * `expires` - ISO Date format of when the invoice will expire 52 | * `memo` - A plain text description of the payment request, can be displayed to the user / kept for records 53 | * `paymentUrl` - The url where the payment should be sent 54 | * `paymentId` - The invoice ID, can be kept for records 55 | 56 | #### Response Body Example 57 | ``` 58 | { 59 | "network": "test", 60 | "currency": "BTC", 61 | "requiredFeePerByte": 200, 62 | "outputs": [ 63 | { 64 | "amount": 39300, 65 | "address": "mthVG9kuRTJQtXieJVDSrrvWyM7QDZ3rcV" 66 | } 67 | ], 68 | "time": "2018-01-12T22:04:54.364Z", 69 | "expires": "2018-01-12T22:19:54.364Z", 70 | "memo": "Payment request for BitPay invoice TmyrxFvAi4DjFNy3c7EjVm for merchant Robs Fake Business", 71 | "paymentUrl": "https://test.bitpay.com/i/TmyrxFvAi4DjFNy3c7EjVm", 72 | "paymentId": "TmyrxFvAi4DjFNy3c7EjVm" 73 | } 74 | ``` 75 | 76 | 77 | ## Payment Verification Payload 78 | Our next step is to generate a funded transaction and send the unsigned version as well as the weighted size to the server, to make sure the 79 | payment is valid and will be accepted. 80 | 81 | ### Request 82 | A POST request should be made to the payment protocol url with a `Content-Type` header set to `application/verify-payment`. A JSON format body should be included with the following fields: 83 | 84 | ``` 85 | { 86 | "currency": "", 87 | "unsignedTransaction": "", 88 | "weightedSize": 89 | } 90 | ``` 91 | 92 | #### Example Request Body 93 | ``` 94 | { 95 | "currency": "BTC", 96 | "unsignedTransaction": "0200000001919572700aef4a9b66ac2389ea8e8899b1c2c0b3ffe03c12c2d28e7a2574d3540100000000feffffff02c80f5f91000000001976a9140cd9a12aa54ad7b098988c67692a62196c1dbdc988ac98470200000000001976a9140f8cf402ad6478377750d572089d1e1a3ca099a788ac00000000" 97 | "weightedSize": 225 98 | } 99 | ``` 100 | 101 | ### Curl Example 102 | ``` 103 | curl -v -H 'Content-Type: application/verify-payment' -d '{"currency": "BTC", "unsignedTransaction": "0200000001919572700aef4a9b66ac2389ea8e8899b1c2c0b3ffe03c12c2d28e7a2574d3540100000000feffffff02c80f5f91000000001976a9140cd9a12aa54ad7b098988c67692a62196c1dbdc988ac98470200000000001976a9140f8cf402ad6478377750d572089d1e1a3ca099a788ac00000000", "weightedSize":225}' https://test.bitpay.com/i/YFujEPNdx8WGEUsysjdLfa 104 | * Trying 127.0.0.1... 105 | * TCP_NODELAY set 106 | * Connected to test.bitpay.com (127.0.0.1) port 443 (#0) 107 | > POST /i/YFujEPNdx8WGEUsysjdLfa HTTP/1.1 108 | > Host: test.bitpay.com 109 | > User-Agent: curl/7.58.0 110 | > Accept: */* 111 | > Content-Type: application/verify-payment 112 | > Content-Length: 304 113 | > 114 | * upload completely sent off: 304 out of 304 bytes 115 | < HTTP/1.1 200 OK 116 | < Strict-Transport-Security: max-age=31536000 117 | < Content-Length: 343 118 | < Date: Thu, 31 Jan 2019 21:51:31 GMT 119 | < Connection: keep-alive 120 | < 121 | * Connection #0 to host test.bitpay.com left intact 122 | {"payment":{"currency":"BTC","unsignedTransaction":"0200000001919572700aef4a9b66ac2389ea8e8899b1c2c0b3ffe03c12c2d28e7a2574d3540100000000feffffff02c80f5f91000000001976a9140cd9a12aa54ad7b098988c67692a62196c1dbdc988ac98470200000000001976a9140f8cf402ad6478377750d572089d1e1a3ca099a788ac00000000","weightedSize":225},"memo":"Payment appears valid"}% 123 | ``` 124 | 125 | ## Payment Payload 126 | Now that the server has told us our payment is acceptable, we can send the fully signed transaction. 127 | 128 | ### Request 129 | 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: 130 | 131 | ``` 132 | { 133 | "currency": "", 134 | "transactions": [ 135 | "" 136 | ] 137 | } 138 | ``` 139 | 140 | #### Example Request Body 141 | ``` 142 | { 143 | "currency": "BTC", 144 | "transactions": [ 145 | "02000000011f0f762184cbc8e94b307fab6f805168724f123a23cd48aac4a9bac8768cfd67000000004847304402205079b96def679f04de9698dd8b9f58dff3e4a13c075f5939c6edfbb8698c8cc802203eac5a3d6410a9f94a86828a4e207f8083fe0bf1c77a74a0cb7add49100d427001ffffffff0284990000000000001976a9149097a519e42061e4977b07b69735ed842b755c0088ac08cd042a010000001976a914cf4b90bca14deab1315c125b8b74b7d31eea97b288ac00000000" 146 | ] 147 | } 148 | ``` 149 | 150 | ### Response 151 | The response will be a JSON format payload containing the original payment body and a memo field which should be displayed to the user. 152 | 153 | #### Response Example 154 | ``` 155 | { 156 | "payment": { 157 | "transactions": [ 158 | "020000000121053733b28b90707a3c63a48171f71abfdc7288bf9d78170e73cfedbbbdfcea00000000484730440220545d53b54873a5afbaf01a77943828f25c6a28d9c5ca4d0968130b5788fc6f9302203e45125723844e4752202792b764b6538342ad169d3828dad18eb231ea01f05101ffffffff02b09a0000000000001976a9149659267896dda4e5aef150e4ca83f0d76022c7b288ac84dd042a010000001976a914fa1a5ed99ce09fd901e9ca7d6f8fcc56d3d5eccf88ac00000000" 159 | ] 160 | }, 161 | "memo": "Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed." 162 | } 163 | ``` 164 | 165 | ### Curl Example 166 | ``` 167 | curl -v -H 'Content-Type: application/payment' -d '{"currency": "BTC", "transactions":["02000000012319227d3995427b05429df7ea30b87cb62f986ba3003311a2cf2177fb5b0ae8000000004847304402205bd75d6b654a70dcc8f548b630c39aec1d2c1de6900b5376ef607efc705f65b002202dd1036f091d4d6047e2f5bcd230ec8bcd5ad2f0785908d78f08a52b8850559f01ffffffff02b09a0000000000001976a9140b2a833c4183c51b86f5dcbb2eeeaca2dfb44bae88acdccb042a010000001976a914f0fd63e5880cbed2fa856e1f4174fc875eeccc5a88ac00000000"]}' https://test.bitpay.com/i/7QBCJ2TpazTKKnczzJQJMc 168 | * Trying 127.0.0.1... 169 | * TCP_NODELAY set 170 | * Connected to test.bitpay.com (127.0.0.1) port 443 (#0) 171 | * TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 172 | > POST /i/7QBCJ2TpazTKKnczzJQJMc HTTP/1.1 173 | > Host: test.bitpay.com 174 | > User-Agent: curl/7.54.0 175 | > Accept: */* 176 | > Content-Type: application/payment-ack 177 | > Content-Length: 403 178 | > 179 | * upload completely sent off: 403 out of 403 bytes 180 | < HTTP/1.1 200 OK 181 | < Content-Length: 520 182 | < Date: Fri, 12 Jan 2018 22:44:13 GMT 183 | < Connection: keep-alive 184 | < 185 | * Connection #0 to host test.bitpay.com left intact 186 | {"payment":{"transactions":["02000000012319227d3995427b05429df7ea30b87cb62f986ba3003311a2cf2177fb5b0ae8000000004847304402205bd75d6b654a70dcc8f548b630c39aec1d2c1de6900b5376ef607efc705f65b002202dd1036f091d4d6047e2f5bcd230ec8bcd5ad2f0785908d78f08a52b8850559f01ffffffff02b09a0000000000001976a9140b2a833c4183c51b86f5dcbb2eeeaca2dfb44bae88acdccb042a010000001976a914f0fd63e5880cbed2fa856e1f4174fc875eeccc5a88ac00000000"]},"memo":"Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed."}% 187 | ``` 188 | 189 | ## Signatures 190 | 191 | Many wallet developers have voiced complaints about needing to use x509 PKI in order to verify payments, so here we're attempting to provide an answer. We will ensure payload integrity for all payment requests via an ECDSA signature. 192 | For those unaware, this is the exact same method all bitcoin transactions are authenticated. This should make it much easier for wallets to implement since they already have code to do this. We will distribute public keys which can be used to verify the signatures. 193 | How you choose to store the keys for verifying providers is up to you as a wallet developer, but we do make some recommendations below. 194 | 195 | Since there will potentially be multiple providers using this system each with multiple keys, the payment request will include an `x-identity` header will contain a unique identifier to indicate which public key was used to sign the payload. In the case of ECSDA signatures we 196 | will provide the RIPEMD160+SHA256 hash of the public key in this header (same format as a bitcoin address). We have chosen not to send the public key itself here as that would lead to the possibility of wallet developers naively trusting whatever public key was sent via the header 197 | and verifying against that. By only sending a hash of the public key the wallet developer is required to follow best practices of retrieving the public keys from a trusted source. 198 | 199 | ### Key Distribution 200 | The JSON payment protocol provider will make keys available via the route: 201 | * [paymentRequestDomain]/signingKeys/paymentProtocol.json 202 | 203 | This route will serve a JSON payload which conforms to the format: 204 | 205 | ``` 206 | { 207 | "owner": "Company name that may be displayed to the user", 208 | "expirationDate": "ISO format date when these keys expire", 209 | "validDomains": [ 210 | "myDomain.com", 211 | "payments.myDomain.com" 212 | ], 213 | "publicKeys": [ 214 | "hexadecimalEncodedPublicKeyHere" 215 | ] 216 | } 217 | ``` 218 | 219 | An example of this fully completed: 220 | ``` 221 | { 222 | "owner": "BitPay, Inc.", 223 | "expirationDate": "2018-06-01T00:00:00.000Z", 224 | "validDomains": [ 225 | "test.bitpay.com" 226 | ], 227 | "publicKeys": [ 228 | "03361b5c0d5d2fec5c9313ab79b82266c576254697546a4868d860423557f3a52f" 229 | ] 230 | } 231 | ``` 232 | 233 | 234 | ### Key Signature Distribution 235 | The JSON payment protocol provider will distribute PGP signatures of the distributed keys available via: 236 | * [paymentRequestDomain]/signatures/[sha256HashOfKeyPayload].json 237 | 238 | The SHA256 should be performed on the raw body of the keys sent down by the server. 239 | 240 | This ensures that even if the provider's SSL certificate is compromised that the attacker cannot forge payment requests. 241 | 242 | This route will server a JSON payload which conforms to the format: 243 | ``` 244 | { 245 | "keyHash": "SHA256 hash of key payload", 246 | "signatures": [ 247 | { 248 | "created": "ISO Date when this signature was created", 249 | "identifier": "GPG fingerprint", 250 | "signature": "hexadecimal encoded detached GPG signature of key payload" 251 | } 252 | ] 253 | } 254 | ``` 255 | 256 | An example of this fully completed: 257 | ``` 258 | { 259 | "keyHash": "622c5dc05501b848221a9e0b2e9a84c0869cdb7604d785a0486fe817c9c34fe1", 260 | "signatures": [ 261 | { 262 | "created": "2018-03-07T01:46:39.310Z", 263 | "identifier": "3c936ad8b8fa8de3290bc45cae7eefda5240818d", 264 | "signature": "2d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a436f6d6d656e743a20475047546f6f6c73202d2068747470733a2f2f677067746f6f6c732e6f72670a0a6951497a424141424367416446694545504a4e71324c6a366a654d7043385263726e3776326c4a4167593046416c714d62574d4143676b51726e3776326c4a410a6759306c73672f2b4e7839486577622b31527631347038676e7a4c563763536c6f4f422f5262586b694136426a76336a4166735a55623175484d6e617a3644370a736e6c306e47775233497966554c6d3254647536444e3043314e767549367374442f7362666473572b6c726a72666d6d6b377a2f36726c593169684a6d5061330a6758344f4d63577061352f4e56636a436f432b546b51434b4d77684237424b7030344a363679326f78382f583736574c6d685544366b6b6155565979656637520a79566666784c6d766747627a476d7433445958316a59524a766d4e6b6f5749466f30557254576d7a55457961505538457950386d415075347a413749453834650a546f57634e38714b576564797879396e615147493679376a5149445a72454a30315a56785371326b6352506e464d694432772f6c69704b6b504a4e577556764e0a3834644f4372324859302b584769726c74744c673167363077314a5333455354714938374a786766716f53695257666c4d4f736667712f6e302b7243337057620a535030397245457a307069705376374d5a723944436142435261544142333156567a6f4c48536e67452b4a4f5264615a4a2f7274796f315366634c5a75314d320a4241735178786c7142306449617174534830386b50496e6a5a746776346358766142556443507178774d6f444c664d643152324330705033337453465533534b0a4e6b786b4d326744536a41392b2b5754625267553543556c3642725942516a62415836773673715354775a42796e736e6270366c6d6f6f6c4b314341762b55500a7144444d4c6e6a696a62465861324476326833495650456246786178336a73303367557448496e4141552f4c4b4b4948583730664d644b45566c735845482b4b0a53395238446c4c355854467a38435175693175692f394f484f77523549672b5436532f5a3678504f51594d4d69562f6e6a48553d0a3d62357a450a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d" 265 | }, 266 | { 267 | "created": "2018-03-07T01:46:39.310Z", 268 | "identifier": "3c936ad8b8fa8de3290bc45cae7eefda5240818d", 269 | "signature": "2d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a436f6d6d656e743a20475047546f6f6c73202d2068747470733a2f2f677067746f6f6c732e6f72670a0a6951497a424141424367416446694545504a4e71324c6a366a654d7043385263726e3776326c4a4167593046416c714d62574d4143676b51726e3776326c4a410a6759306c73672f2b4e7839486577622b31527631347038676e7a4c563763536c6f4f422f5262586b694136426a76336a4166735a55623175484d6e617a3644370a736e6c306e47775233497966554c6d3254647536444e3043314e767549367374442f7362666473572b6c726a72666d6d6b377a2f36726c593169684a6d5061330a6758344f4d63577061352f4e56636a436f432b546b51434b4d77684237424b7030344a363679326f78382f583736574c6d685544366b6b6155565979656637520a79566666784c6d766747627a476d7433445958316a59524a766d4e6b6f5749466f30557254576d7a55457961505538457950386d415075347a413749453834650a546f57634e38714b576564797879396e615147493679376a5149445a72454a30315a56785371326b6352506e464d694432772f6c69704b6b504a4e577556764e0a3834644f4372324859302b584769726c74744c673167363077314a5333455354714938374a786766716f53695257666c4d4f736667712f6e302b7243337057620a535030397245457a307069705376374d5a723944436142435261544142333156567a6f4c48536e67452b4a4f5264615a4a2f7274796f315366634c5a75314d320a4241735178786c7142306449617174534830386b50496e6a5a746776346358766142556443507178774d6f444c664d643152324330705033337453465533534b0a4e6b786b4d326744536a41392b2b5754625267553543556c3642725942516a62415836773673715354775a42796e736e6270366c6d6f6f6c4b314341762b55500a7144444d4c6e6a696a62465861324476326833495650456246786178336a73303367557448496e4141552f4c4b4b4948583730664d644b45566c735845482b4b0a53395238446c4c355854467a38435175693175692f394f484f77523549672b5436532f5a3678504f51594d4d69562f6e6a48553d0a3d62357a450a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d" 270 | } 271 | ] 272 | } 273 | ``` 274 | Please note that these example signatures may not match the example key payload, don't use them for testing 275 | 276 | 277 | ### PGP Key Distribution 278 | It is ultimately up to the JSON payment protocol provider as to how they will distribute the PGP public keys used to sign their payment protocol signing keys. We recommend using multiple distribution paths, at least one of which should not be on the same domain as the payment requests. 279 | 280 | ### BitPay Specific Signature Details 281 | 282 | #### Signing 283 | For the foreseeable future all of BitPay's payment requests will be signed via ECDSA using the SECP 256 k1 curve (the same as used in Bitcoin itself). This is to allow very simple compatibility with wallets, as every wallet should already be fully able to verify ECC signatures of bitcoin transactions. 284 | 285 | #### PGP Key Distribution 286 | BitPay will make its PGP keys available via the following resources: 287 | 288 | * https://github.com/bitpay/pgp-keys 289 | * https://bitpay.com/pgp-keys (or in JSON format https://bitpay.com/pgp-keys.json) 290 | 291 | Please take time to verify the keys in both places. 292 | 293 | #### Key-storing suggestions 294 | Some wallets may not be sure about the best way to store the ECC public keys for JSON payment protocol providers. There are likely to be two or three common approaches. Regardless of your approach, we recommend keeping in mind that keys can be compromised and therefore may require being changed 295 | relatively quickly. It also may also be reasonable for advanced users to want to add their own keys for development purposes, but keep in mind that less advanced users may be tricked into doing this if appropriate warnings are not provided. Remember you will always need to store the domains and the 296 | owner for each key so that you can prevent a key from being used on the wrong domain, and so that the user knows who owns the key. 297 | 298 | ##### Approach one 299 | The simplest approach would be to simply verify all the keys we've distributed locally and then directly bake them into your wallet code. This has the advantage of being quite secure, however the direct disadvantage is that if a key is revoked or the keys change for any reason your user is stuck until 300 | you release an update. 301 | 302 | ##### Pros: 303 | * Wallet signature logic is very simple 304 | 305 | ##### Cons: 306 | * Key revocation may result in needing to push emergency updates to your wallet (remember Apple App store can be slow to accept updates) 307 | 308 | ##### Approach two 309 | The second approach would be to generate your own ECC key pair and hard code it's public key into your app. You would then verify our (and others) keys in your development environment and if valid publish them to your own API, signed by your own ECC key. Wallets would then verify against the keys from 310 | your API each time they wanted to verify a payment request. This allows for relatively quick changing of which keys are considered valid by simply updating your own API. 311 | 312 | ##### Pros: 313 | * Usable across multiple providers 314 | * Pretty reasonable key update time 315 | 316 | ##### Cons: 317 | * Still have cases where keys could be out of date for hours (or days for some wallets) 318 | 319 | ##### Approach three 320 | The third approach is to bake our PGP keys into your wallet. On first boot the wallet would retrieve our signing keys and their signatures and verify them. Once verified the app would store the SHA256 hash of the keys. The wallet could then periodically retrieve our key list and only re-validate them 321 | if the SHA256 hash is different than the one previously stored. 322 | ##### Pros: 323 | * Always up to date 324 | ##### Cons: 325 | * Potentially need a specific implementation per-provider 326 | 327 | ## Errors 328 | 329 | All errors are communicated in plaintext with an appropriate status code. 330 | 331 | ### Example Error 332 | 333 | ``` 334 | curl -v https://test.bitpay.com/i/48gZau8ao76bqAoEwAKSwx -H 'Accept: application/payment-request' 335 | * Trying 104.17.68.20... 336 | * TCP_NODELAY set 337 | * Connected to test.bitpay.com (104.17.68.20) port 443 (#0) 338 | * TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 339 | > GET /i/48gZau8ao76bqAoEwAKSwx HTTP/1.1 340 | > Host: test.bitpay.com 341 | > User-Agent: curl/7.54.0 342 | > Accept: application/payment-request 343 | > 344 | < HTTP/1.1 400 Bad Request 345 | < Date: Fri, 26 Jan 2018 01:54:03 GMT 346 | < Content-Type: text/html; charset=utf-8 347 | < Content-Length: 44 348 | < Connection: keep-alive 349 | < Strict-Transport-Security: max-age=31536000 350 | < X-Download-Options: noopen 351 | < X-Content-Type-Options: nosniff 352 | < Access-Control-Allow-Origin: * 353 | < Access-Control-Allow-Methods: GET, POST, OPTIONS 354 | < Access-Control-Allow-Headers: Host, Connection, Content-Length, Accept, Origin, User-Agent, Content-Type, Accept-Encoding, Accept-Language 355 | < 356 | * Connection #0 to host test.bitpay.com left intact 357 | This invoice is no longer accepting payments 358 | ``` 359 | 360 | ### Common Errors 361 | 362 | | Http Status Code | Response | Cause | 363 | |---|---|---| 364 | | 404 | This invoice was not found or has been archived | Invalid invoiceId, or invoice has been archived (current TTL is 3 days) | 365 | | 400 | Unsupported Content-Type for payment | Your Content-Type header was not valid | 366 | | 400 | Invoice no longer accepting payments | Invoice is either paid or has expired | 367 | | 400 | We were unable to parse your payment. Please try again or contact your wallet provider | Request body could not be parsed / empty body | 368 | | 400 | Request must include exactly one (1) transaction | Included no transaction in body / Included multiple transactions in body | 369 | | 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 370 | | 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 | 371 | | 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 | 372 | | 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 | 373 | | 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`| 374 | | 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 | 375 | | 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 | 376 | | 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 | 377 | | 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. | 378 | 379 | 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. 380 | 381 | ## MIME Types 382 | 383 | |Mime|Description| 384 | |---|---| 385 | |application/payment-request| Associated with the server's payment request, this specified on the client `Accept` header when retrieving the payment request| 386 | |application/verify-payment| Used by the client when sending their proposed unsigned payment transaction payload| 387 | |application/payment| Used by the client when sending their proposed payment transaction payload| 388 | |application/payment-ack| Used by the server to state acceptance of the client's proposed payment transaction| 389 | -------------------------------------------------------------------------------- /v2/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitpay/jsonPaymentProtocol/24014a7d29a0fac8a47523843aaafd5b7e07b901/v2/flow.png -------------------------------------------------------------------------------- /v2/specification.md: -------------------------------------------------------------------------------- 1 | # JSON Payment Protocol Specification v2 2 | 3 | Revision 0.1 4 | 5 | ## Application Logic 6 | 7 | 1. (Client) Wallet obtains payment protocol uri 8 | 2. (Client) Selects a currency from the list of payment-options 9 | 3. (Client) Fetches payment information from server 10 | 4. (Server) Verifies invoice exists and is still accepting payments, responds with payment-request 11 | 5. (Client) Validates payment request hash 12 | 6. (Client) Validates payment request signature 13 | 8. (Client) Generates a payment to match instructions on payment-request 14 | 9. (Client) Submits proposed unsigned transaction (and size of signed transaction for BTC/BCH) to server 15 | 10. (Server) Validates invoice exists and is still accepting payments 16 | 11. (Server) Validates payment matches address, amount, and currency of invoice and has a reasonable transaction fee. 17 | 12. (Server) Notifies client payment will be accepted 18 | 13. (Client) Sends payment to server and broadcasts to p2p network 19 | 14. (Server) Validates signed payment and broadcasts payment to network. 20 | 21 | In general, the payment should not be broadcast by the client. If at time of verification the payment is rejected by the server **your client must not broadcast the payment**. 22 | 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. 23 | 24 | ![Payment Flow](flow.png) 25 | 26 | ## Payment Options 27 | 28 | ### Request 29 | A GET request should be made to payment protcol url. 30 | Example: 31 | * /i/someinvoiceid 32 | 33 | #### Headers 34 | * `Accept` = `application/payment-options`. 35 | * `x-paypro-version` = 2 36 | 37 | 38 | ### Response 39 | A list of payment options will be returned. 40 | 41 | ``` 42 | { 43 | "time": "2020-01-24T18:57:44.509Z", 44 | "expires": "2020-01-24T19:12:44.509Z", 45 | "memo": "Payment request for BitPay invoice AonN46AuFPYTC8PKG8cPPV for merchant BitPay Visa® Load (USD-USA)", 46 | "paymentUrl": "https://bitpay.com/i/AonN46AuFPYTC8PKG8cPPV", 47 | "paymentId": "AonN46AuFPYTC8PKG8cPPV", 48 | "paymentOptions": [ 49 | { 50 | "chain": "BTC", 51 | "currency": "BTC", 52 | "network": "main", 53 | "estimatedAmount": 13400, 54 | "requiredFeeRate": 6.544, 55 | "minerFee": 1600, 56 | "decimals": 8, 57 | "selected": false 58 | }, 59 | { 60 | "chain": "BCH", 61 | "currency": "BCH", 62 | "network": "main", 63 | "estimatedAmount": 315200, 64 | "requiredFeeRate": 1, 65 | "minerFee": 0, 66 | "decimals": 8, 67 | "selected": false 68 | }, 69 | { 70 | "chain": "ETH", 71 | "currency": "ETH", 72 | "network": "main", 73 | "estimatedAmount": 6195000000000000, 74 | "requiredFeeRate": 13555555557, 75 | "minerFee": 0, 76 | "decimals": 18, 77 | "selected": false 78 | }, 79 | { 80 | "chain": "ETH", 81 | "currency": "GUSD", 82 | "network": "main", 83 | "estimatedAmount": 100, 84 | "requiredFeeRate": 13555555557, 85 | "minerFee": 0, 86 | "decimals": 2, 87 | "selected": false 88 | }, 89 | { 90 | "chain": "ETH", 91 | "currency": "PAX", 92 | "network": "main", 93 | "estimatedAmount": 1000000000000000000, 94 | "requiredFeeRate": 13555555557, 95 | "minerFee": 0, 96 | "decimals": 18, 97 | "selected": false 98 | }, 99 | { 100 | "chain": "ETH", 101 | "currency": "USDC", 102 | "network": "main", 103 | "estimatedAmount": 1000000, 104 | "requiredFeeRate": 13555555557, 105 | "minerFee": 0, 106 | "decimals": 6, 107 | "selected": false 108 | }, 109 | { 110 | "chain": "XRP", 111 | "currency": "XRP", 112 | "network": "main", 113 | "estimatedAmount": 4494841, 114 | "requiredFeeRate": 12, 115 | "minerFee": 0, 116 | "decimals": 6, 117 | "selected": true 118 | } 119 | ] 120 | } 121 | ``` 122 | #### Body 123 | * `time` - ISO Date format of when the invoice was generated 124 | * `expires` - ISO Date format of when the invoice will expire 125 | * `memo` - A plain text description of the payment request, can be displayed to the user / kept for records 126 | * `paymentUrl` - The url where the payment should be sent 127 | * `paymentId` - The invoice ID, can be kept for records 128 | * `paymentOptions` - An array of payment options. Each option includes the chain and currency 129 | 130 | #### Payment Options 131 | Each payment option includes 132 | * `chain` - The chain that the transaction should be valid on 133 | * `currency` - The currency on a given chain that the trasaction should be denominated in 134 | * `network` - The network that the transaction should be valid on 135 | * `estimatedAmount` - Amount of currency units required to pay this invoice 136 | * `decimals` - Number of decimal places the currency uses 137 | 138 | ## Payment Request 139 | 140 | ### Request 141 | A POST request should be made to the payment protocol url with a JSON dictionary containing `{chain, currency}` fields 142 | 143 | #### Examples: 144 | 145 | * /i/someinvoiceid 146 | 147 | #### Headers 148 | * `Content-Type` = `application/payment-request`. 149 | * `x-paypro-version` = 2 150 | 151 | **Note: Do NOT use the standard `application/json` `Content-Type`** 152 | 153 | **Note: Do NOT include an `Accept` property** 154 | 155 | #### Request Body 156 | * `chain` = a chain that was present in the payment-options response 157 | * `currency` = Optional, the particular currency on the chain you will pay with. Defaults to chain 158 | 159 | ```JSON 160 | { 161 | "chain": "", 162 | "currency": "", 163 | } 164 | ``` 165 | 166 | 167 | ### Response 168 | #### BTC Response 169 | ``` 170 | { 171 | "time": "2019-06-13T18:34:09.010Z", 172 | "expires": "2019-06-13T18:49:09.010Z", 173 | "memo": "Payment request for BitPay invoice PfCwZLxWctSrdgYcnJM8G8 for merchant Micah's Cool Store", 174 | "paymentUrl": "https://mriggan.bp:8088/i/PfCwZLxWctSrdgYcnJM8G8", 175 | "paymentId": "PfCwZLxWctSrdgYcnJM8G8", 176 | "chain": "BTC", 177 | "network": "regtest", 178 | "instructions": [ 179 | { 180 | "type": "transaction", 181 | "requiredFeeRate": 20, 182 | "outputs": [ 183 | { 184 | "amount": 15100, 185 | "address": "my6aWcW2r3WiXpnK2MMWHfGgQ1VFmA2LLv" 186 | } 187 | ] 188 | } 189 | ] 190 | } 191 | ``` 192 | #### BCH Response 193 | ``` 194 | { 195 | "time": "2019-06-13T18:35:19.138Z", 196 | "expires": "2019-06-13T18:50:19.138Z", 197 | "memo": "Payment request for BitPay invoice TiXuyEmcJRCcinoFoY3Cym for merchant Micah's Cool Store", 198 | "paymentUrl": "https://mriggan.bp:8088/i/TiXuyEmcJRCcinoFoY3Cym", 199 | "paymentId": "TiXuyEmcJRCcinoFoY3Cym", 200 | "chain": "BCH", 201 | "network": "regtest", 202 | "instructions": [ 203 | { 204 | "type": "transaction", 205 | "requiredFeeRate": 1, 206 | "outputs": [ 207 | { 208 | "amount": 239200, 209 | "address": "n3YaQSkTXrpbQyAns1kQQRxsECMn9ifx5n" 210 | } 211 | ] 212 | } 213 | ] 214 | } 215 | ``` 216 | 217 | #### ETH Response 218 | ``` 219 | { 220 | "time": "2019-06-13T18:33:00.827Z", 221 | "expires": "2019-06-13T18:48:00.827Z", 222 | "memo": "Payment request for BitPay invoice S6cxqqUNUcMV41dfBS8XHn for merchant Micah's Cool Store", 223 | "paymentUrl": "https://mriggan.bp:8088/i/S6cxqqUNUcMV41dfBS8XHn", 224 | "paymentId": "S6cxqqUNUcMV41dfBS8XHn", 225 | "chain": "ETH", 226 | "network": "regtest", 227 | "instructions": [ 228 | { 229 | "type": "transaction", 230 | "value": 3836000000000000, 231 | "to": "0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A", 232 | "data": "0xb6b4af05000000000000000000000000000000000000000000000000000da0d2595bc000000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000016b55554827c63ba4a65dfaf06093315d08f3240fdd0724409e09ea250226f656964dcb44d17a565f7590c67cdb3241eb969f1c24db402ef5714e822a574afa8b6802a2a4ca000000000000000000000000000000000000000000000000000000000000001c456e391b0b2ef6d8d130fdff97085189eecc10f3f78f2b5cdaed24609ca89fd42aafed00dccb025ba383d4afecbf136b6a2e480620bb2c30f8d5a3e71d7f88090000000000000000000000000000000000000000000000000000000000000000", 233 | "gasPrice": 100000000000 234 | } 235 | ] 236 | } 237 | ``` 238 | 239 | #### ETH - GUSD Response 240 | ``` 241 | { 242 | "time": "2019-06-13T18:31:47.350Z", 243 | "expires": "2019-06-13T18:46:47.350Z", 244 | "memo": "Payment request for BitPay invoice U6V72eVXTBsF5VQbTxVamu for merchant Micah's Cool Store", 245 | "paymentUrl": "https://mriggan.bp:8088/i/U6V72eVXTBsF5VQbTxVamu", 246 | "paymentId": "U6V72eVXTBsF5VQbTxVamu", 247 | "chain": "ETH", 248 | "network": "regtest", 249 | "currency": "GUSD", 250 | "instructions": [ 251 | { 252 | "type": "transaction", 253 | "value": 0, 254 | "to": "0xFEb423814D0208e9e2a3F5B0F0171e97376E20Bc", 255 | "data": "0x095ea7b300000000000000000000000037d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a0000000000000000000000000000000000000000000000000000000000000064", 256 | "gasPrice": 100000000000 257 | }, 258 | { 259 | "type": "transaction", 260 | "value": 0, 261 | "to": "0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A", 262 | "data": "0xb6b4af050000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000016b5554293eea9865f7c52cfb4af7b4aa755d2d339ae61a2c2713fcc467e6f4dcd5149114d627c1ade24c1c22b4e4b87de860524acb1696dfacb31e5fd299e98e3ec9b692d8000000000000000000000000000000000000000000000000000000000000001bf0aa17a43365bc04a43b166f642aaf8e99f59972e52de08ad25e54ac5f57110571378ecd381ff0275ac73f2030fe6f93a330a2d6860d433707ca95ae2f1dd1c8000000000000000000000000feb423814d0208e9e2a3f5b0f0171e97376e20bc", 263 | "gasPrice": 100000000000 264 | } 265 | ] 266 | } 267 | ``` 268 | 269 | #### XRP Response 270 | ``` 271 | { 272 | "time": "2020-01-24T18:57:44.509Z", 273 | "expires": "2020-01-24T19:12:44.509Z", 274 | "memo": "Payment request for BitPay invoice AonN46AuFPYTC8PKG8cPPV for merchant BitPay Visa® Load (USD-USA)", 275 | "paymentUrl": "https://bitpay.com/i/AonN46AuFPYTC8PKG8cPPV", 276 | "paymentId": "AonN46AuFPYTC8PKG8cPPV", 277 | "chain": "XRP", 278 | "network": "main", 279 | "instructions": [ 280 | { 281 | "type": "transaction", 282 | "requiredFeeRate": 12, 283 | "outputs": [ 284 | { 285 | "amount": 4494841, 286 | "invoiceID": "326661FF10DF1F00B10DD96D58B4D026FB32CBF2E8DF5D3E22716092AAD4F082", 287 | "address": "rKpTKoJSFbCoZkwydRv7NWTiBgNrdTXJ24" 288 | } 289 | ] 290 | } 291 | ] 292 | } 293 | ``` 294 | 295 | #### Headers 296 | On a successful request, the response will contain the following headers. 297 | 298 | * `digest` - A SHA256 hash of the JSON response string, should be verified by the client before proceeding 299 | * `x-identity` - An identifier to represent which public key should be used to verify the signature. For example for BitPay's ECC keys we will include the public key hash in this header. Implementations should **NOT** include the public key here directly. 300 | * `x-signature-type` The signature format used to sign the payload. For the foreseeable future BitPay will always use `ECC`. However, we wanted to grant some flexibility to the specification. 301 | * `x-signature` - A cryptographic signature of the SHA256 hash of the payload. This is to prove that the payment request was not tampered with before being received by the wallet. 302 | 303 | #### Body 304 | * `time` - ISO Date format of when the invoice was generated 305 | * `expires` - ISO Date format of when the invoice will expire 306 | * `memo` - A plain text description of the payment request, can be displayed to the user / kept for records 307 | * `paymentUrl` - The url where the payment should be sent 308 | * `paymentId` - The invoice ID, can be kept for records 309 | * `chain` - Three letter code for the chain these instructions are valid for 310 | * `currency` - Optional, Three letter code for the token these instructions are valid for 311 | * `instructions` - An array of instructions that can be used to construct transactions that will fufill this payment 312 | 313 | ## Payment Verification 314 | Our next step is to generate a funded transaction and send the unsigned version as well as the weighted size to the server, to make sure the 315 | payment is valid and will be accepted. 316 | 317 | ### Request 318 | A POST request should be made to the payment protocol url. 319 | 320 | #### Examples: 321 | * /i/someinvoiceid 322 | 323 | #### Headers 324 | * `Content-Type` = `application/payment-verification`. 325 | * `x-paypro-version` = 2 326 | 327 | 328 | #### Request Body 329 | ```JSON 330 | { 331 | "chain": "", 332 | "transactions": "<{tx: string, weightedSize?: number}>", 333 | "currency": "", 334 | } 335 | ``` 336 | 337 | * *weightedSize* is the length of the signed transaction in bytes, or the transaction "weight" in the Bitcoin blockchain. 338 | * *tx* is the hex-encoded unsigned transaction. In Bitcoin-family cryptocurrencies, "unsigned" means that the input script has length 0 339 | 340 | #### Success 341 | 342 | A 200 return code means that the transaction is valid. Additional fields are available for display and analysis as described in the example section. 343 | 344 | #### Troubleshooting 345 | 346 | * 400 347 | Read a detailed problem description from the http error stream 348 | 349 | 350 | #### Example ETH - GUSD Body 351 | ```JSON 352 | { 353 | "chain": "ETH", 354 | "currency": "GUSD", 355 | "transactions": [ 356 | { 357 | "tx": "0xf8aa3c85174876e800830493e094feb423814d0208e9e2a3f5b0f0171e97376e20bc80b844095ea7b300000000000000000000000037d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a00000000000000000000000000000000000000000000000000000000000000641ca01c389df7ea8e3bfb2b2d5f18677cf06924796ad051185032970e7905cd212998a01244f5b7bd0ed69eb79c78f67cb9be3b3f976d7546dd0fa172fa3a31a2a12a3a" 358 | }, 359 | { 360 | "tx": "0xf9018b3d85174876e800830493e09437d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a80b90124b6b4af050000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000016b5589330e6aafc7133617b0a196c9a74c030d8a6b0582313f87f551eece47c62c8f12fb097a8f09c5964f8875471d788b6543bc98bbfe1601de04279d67b05bb172a2870d000000000000000000000000000000000000000000000000000000000000001b2f6df07a001e20d8fddd8ff7941540afe9d414bff0122664d769901ab5f496c15b7ad8c6092c7fba6877ffd1c37167c66494f5e6995e4431bebf1d8608ee8ab5000000000000000000000000feb423814d0208e9e2a3f5b0f0171e97376e20bc1ba06caeb6eec8aa7abbf761e755233f6cefb5d92d05107b00cc3e7799e449088003a046a6f3d863b81faa7a82a8c67b433ef449346745038443bfd6015b617ea3a58e" 361 | } 362 | 363 | ] 364 | } 365 | ``` 366 | 367 | #### Example ETH - GUSD Response 368 | ``` 369 | { 370 | "payment": { 371 | "currency": "GUSD", 372 | "chain": "ETH", 373 | "transactions": [ 374 | { 375 | "tx": "0xf8aa3c85174876e800830493e094feb423814d0208e9e2a3f5b0f0171e97376e20bc80b844095ea7b300000000000000000000000037d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a00000000000000000000000000000000000000000000000000000000000000641ca01c389df7ea8e3bfb2b2d5f18677cf06924796ad051185032970e7905cd212998a01244f5b7bd0ed69eb79c78f67cb9be3b3f976d7546dd0fa172fa3a31a2a12a3a" 376 | }, 377 | { 378 | "tx": "0xf9018b3d85174876e800830493e09437d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a80b90124b6b4af050000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000016b5589330e6aafc7133617b0a196c9a74c030d8a6b0582313f87f551eece47c62c8f12fb097a8f09c5964f8875471d788b6543bc98bbfe1601de04279d67b05bb172a2870d000000000000000000000000000000000000000000000000000000000000001b2f6df07a001e20d8fddd8ff7941540afe9d414bff0122664d769901ab5f496c15b7ad8c6092c7fba6877ffd1c37167c66494f5e6995e4431bebf1d8608ee8ab5000000000000000000000000feb423814d0208e9e2a3f5b0f0171e97376e20bc1ba06caeb6eec8aa7abbf761e755233f6cefb5d92d05107b00cc3e7799e449088003a046a6f3d863b81faa7a82a8c67b433ef449346745038443bfd6015b617ea3a58e" 379 | } 380 | ] 381 | }, 382 | "memo": "Payment appears valid" 383 | } 384 | ``` 385 | 386 | #### Example XRP Body 387 | ``` 388 | { 389 | "chain": "XRP", 390 | "currency": "XRP", 391 | "transactions": [ 392 | { 393 | "tx": "120000228000000024000000095011F6751F266C7E664CB3CDAE77D091ED73E3D365591D8FA769BEA9F694C5C4A5DF614000000000448E1768400000000000000C81148FF291E50F16A206B96D383E1F86CC47E21727DA8314C5B8A782192BFF4D8FF629A88BEEB417A4D9EEB2" 394 | } 395 | ] 396 | } 397 | ``` 398 | 399 | #### Example XRP Response 400 | ``` 401 | { 402 | "payment": { 403 | "currency": "XRP", 404 | "chain": "XRP", 405 | "transactions": [ 406 | { 407 | "tx": "120000228000000024000000095011F6751F266C7E664CB3CDAE77D091ED73E3D365591D8FA769BEA9F694C5C4A5DF614000000000448E1768400000000000000C81148FF291E50F16A206B96D383E1F86CC47E21727DA8314C5B8A782192BFF4D8FF629A88BEEB417A4D9EEB2" 408 | } 409 | ] 410 | }, 411 | "memo": "Payment appears valid" 412 | } 413 | ``` 414 | 415 | 416 | ## Payment 417 | Now that the server has told us our payment is acceptable, we can send the fully signed transaction. 418 | 419 | ### Request 420 | A POST request should be made to the payment protocol url with `{chain, transactions, currency}` 421 | 422 | #### Examples: 423 | * /i/someinvoiceid 424 | 425 | #### Headers 426 | * `Content-Type` = `application/payment 427 | * `x-paypro-version` = 2 428 | 429 | ```JSON 430 | { 431 | "chain": "", 432 | "transactions": "<{tx: string, weightedSize?: number}>", 433 | "currency": "", 434 | } 435 | ``` 436 | 437 | 438 | #### Example ETH - GUSD Request Body 439 | ```JSON 440 | { 441 | "chain": "ETH", 442 | "currency": "GUSD", 443 | "transactions": [ 444 | { 445 | "tx": "0xf8aa3c85174876e800830493e094feb423814d0208e9e2a3f5b0f0171e97376e20bc80b844095ea7b300000000000000000000000037d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a00000000000000000000000000000000000000000000000000000000000000641ca01c389df7ea8e3bfb2b2d5f18677cf06924796ad051185032970e7905cd212998a01244f5b7bd0ed69eb79c78f67cb9be3b3f976d7546dd0fa172fa3a31a2a12a3a" 446 | }, 447 | { 448 | "tx": "0xf9018b3d85174876e800830493e09437d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a80b90124b6b4af050000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000016b55a4b42732524f761ed693a9be3b8410aa0a15207c32490f8a46cb0dff405a292529d435d26be7a653ff1c90c7b67a8472506d2bb71e167096f90a1c6f125a25f156dc28000000000000000000000000000000000000000000000000000000000000001bbba3e08a7eb0f11c89ef605655095854f2043e8530072884d678d49ec513fb385bc35d61b78124add7615bde2c2858179ef2e092c9c9d42164055ea853e72926000000000000000000000000feb423814d0208e9e2a3f5b0f0171e97376e20bc1ba083ac15f2263f0120768fcf9693b8fedb041f7157594fdef379f18435917ba334a04384d86b055a97d55555ef6653c41b03609d6f16a9f82abceae4fdb54bdcb35c" 449 | } 450 | 451 | ] 452 | } 453 | ``` 454 | 455 | 456 | #### Example XRP Request Body 457 | ``` 458 | { 459 | "chain": "XRP", 460 | "currency": "XRP", 461 | "transactions": [ 462 | { 463 | "tx": "120000228000000024000000095011F6751F266C7E664CB3CDAE77D091ED73E3D365591D8FA769BEA9F694C5C4A5DF614000000000448E1768400000000000000C7321030834A2B07BD552337FEA00C32E8DDDDB541BC7353DA7E982B4C191BFE432D3BE74473045022100A2D1E2A0B0E01EAAA36F67F650463391E4390F9F540D4C7553E1698F866A16E002201C5CB68137805ECC73F2D4459E7840842C255065A716475CDDFCA007EA570A1581148FF291E50F16A206B96D383E1F86CC47E21727DA8314C5B8A782192BFF4D8FF629A88BEEB417A4D9EEB2" 464 | } 465 | ] 466 | } 467 | ``` 468 | 469 | ### Response 470 | The response will be a JSON format payload containing the original payment body and a memo field which should be displayed to the user. 471 | 472 | #### ETH - GUSD Response Example 473 | ```JSON 474 | { 475 | "payment": { 476 | "transactions": [ 477 | { 478 | "tx": "0xf8aa3c85174876e800830493e094feb423814d0208e9e2a3f5b0f0171e97376e20bc80b844095ea7b300000000000000000000000037d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a00000000000000000000000000000000000000000000000000000000000000641ca01c389df7ea8e3bfb2b2d5f18677cf06924796ad051185032970e7905cd212998a01244f5b7bd0ed69eb79c78f67cb9be3b3f976d7546dd0fa172fa3a31a2a12a3a" 479 | }, 480 | { 481 | "tx": "0xf9018b3d85174876e800830493e09437d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a80b90124b6b4af050000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000016b55a4b42732524f761ed693a9be3b8410aa0a15207c32490f8a46cb0dff405a292529d435d26be7a653ff1c90c7b67a8472506d2bb71e167096f90a1c6f125a25f156dc28000000000000000000000000000000000000000000000000000000000000001bbba3e08a7eb0f11c89ef605655095854f2043e8530072884d678d49ec513fb385bc35d61b78124add7615bde2c2858179ef2e092c9c9d42164055ea853e72926000000000000000000000000feb423814d0208e9e2a3f5b0f0171e97376e20bc1ba083ac15f2263f0120768fcf9693b8fedb041f7157594fdef379f18435917ba334a04384d86b055a97d55555ef6653c41b03609d6f16a9f82abceae4fdb54bdcb35c" 482 | } 483 | ] 484 | }, 485 | "memo": "Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed." 486 | } 487 | ``` 488 | #### XRP Response Example 489 | ``` 490 | { 491 | "payment": { 492 | "transactions": [ 493 | { 494 | "tx": "120000228000000024000000095011F6751F266C7E664CB3CDAE77D091ED73E3D365591D8FA769BEA9F694C5C4A5DF614000000000448E1768400000000000000C7321030834A2B07BD552337FEA00C32E8DDDDB541BC7353DA7E982B4C191BFE432D3BE74473045022100A2D1E2A0B0E01EAAA36F67F650463391E4390F9F540D4C7553E1698F866A16E002201C5CB68137805ECC73F2D4459E7840842C255065A716475CDDFCA007EA570A1581148FF291E50F16A206B96D383E1F86CC47E21727DA8314C5B8A782192BFF4D8FF629A88BEEB417A4D9EEB2" 495 | } 496 | ] 497 | }, 498 | "memo": "Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed." 499 | } 500 | ``` 501 | 502 | ## Signatures 503 | 504 | Many wallet developers have voiced complaints about needing to use x509 PKI in order to verify payments, so here we're attempting to provide an answer. We will ensure payload integrity for all payment requests via an ECDSA signature. 505 | For those unaware, this is the exact same method all bitcoin transactions are authenticated. This should make it much easier for wallets to implement since they already have code to do this. We will distribute public keys which can be used to verify the signatures. 506 | How you choose to store the keys for verifying providers is up to you as a wallet developer, but we do make some recommendations below. 507 | 508 | Since there will potentially be multiple providers using this system each with multiple keys, the payment request will include an `x-identity` header will contain a unique identifier to indicate which public key was used to sign the payload. In the case of ECSDA signatures we 509 | will provide the RIPEMD160+SHA256 hash of the public key in this header (same format as a bitcoin address). We have chosen not to send the public key itself here as that would lead to the possibility of wallet developers naively trusting whatever public key was sent via the header 510 | and verifying against that. By only sending a hash of the public key the wallet developer is required to follow best practices of retrieving the public keys from a trusted source. 511 | 512 | ### Key Distribution 513 | The JSON payment protocol provider will make keys available via the route: 514 | * [paymentRequestDomain]/signingKeys/paymentProtocol.json 515 | 516 | This route will serve a JSON payload which conforms to the format: 517 | 518 | ```JSON 519 | { 520 | "owner": "Company name that may be displayed to the user", 521 | "expirationDate": "ISO format date when these keys expire", 522 | "validDomains": [ 523 | "myDomain.com", 524 | "payments.myDomain.com" 525 | ], 526 | "publicKeys": [ 527 | "hexadecimalEncodedPublicKeyHere" 528 | ] 529 | } 530 | ``` 531 | 532 | An example of this fully completed: 533 | ```JSON 534 | { 535 | "owner": "BitPay, Inc.", 536 | "expirationDate": "2018-06-01T00:00:00.000Z", 537 | "validDomains": [ 538 | "test.bitpay.com" 539 | ], 540 | "publicKeys": [ 541 | "03361b5c0d5d2fec5c9313ab79b82266c576254697546a4868d860423557f3a52f" 542 | ] 543 | } 544 | ``` 545 | 546 | 547 | ### Key Signature Distribution 548 | The JSON payment protocol provider will distribute PGP signatures of the distributed keys available via: 549 | * [paymentRequestDomain]/signatures/[sha256HashOfKeyPayload].json 550 | 551 | The SHA256 should be performed on the raw body of the keys sent down by the server. 552 | 553 | This ensures that even if the provider's SSL certificate is compromised that the attacker cannot forge payment requests. 554 | 555 | This route will server a JSON payload which conforms to the format: 556 | ```JSON 557 | { 558 | "keyHash": "SHA256 hash of key payload", 559 | "signatures": [ 560 | { 561 | "created": "ISO Date when this signature was created", 562 | "identifier": "GPG fingerprint", 563 | "signature": "hexadecimal encoded detached GPG signature of key payload" 564 | } 565 | ] 566 | } 567 | ``` 568 | 569 | An example of this fully completed: 570 | ```JSON 571 | { 572 | "keyHash": "622c5dc05501b848221a9e0b2e9a84c0869cdb7604d785a0486fe817c9c34fe1", 573 | "signatures": [ 574 | { 575 | "created": "2018-03-07T01:46:39.310Z", 576 | "identifier": "3c936ad8b8fa8de3290bc45cae7eefda5240818d", 577 | "signature": "2d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a436f6d6d656e743a20475047546f6f6c73202d2068747470733a2f2f677067746f6f6c732e6f72670a0a6951497a424141424367416446694545504a4e71324c6a366a654d7043385263726e3776326c4a4167593046416c714d62574d4143676b51726e3776326c4a410a6759306c73672f2b4e7839486577622b31527631347038676e7a4c563763536c6f4f422f5262586b694136426a76336a4166735a55623175484d6e617a3644370a736e6c306e47775233497966554c6d3254647536444e3043314e767549367374442f7362666473572b6c726a72666d6d6b377a2f36726c593169684a6d5061330a6758344f4d63577061352f4e56636a436f432b546b51434b4d77684237424b7030344a363679326f78382f583736574c6d685544366b6b6155565979656637520a79566666784c6d766747627a476d7433445958316a59524a766d4e6b6f5749466f30557254576d7a55457961505538457950386d415075347a413749453834650a546f57634e38714b576564797879396e615147493679376a5149445a72454a30315a56785371326b6352506e464d694432772f6c69704b6b504a4e577556764e0a3834644f4372324859302b584769726c74744c673167363077314a5333455354714938374a786766716f53695257666c4d4f736667712f6e302b7243337057620a535030397245457a307069705376374d5a723944436142435261544142333156567a6f4c48536e67452b4a4f5264615a4a2f7274796f315366634c5a75314d320a4241735178786c7142306449617174534830386b50496e6a5a746776346358766142556443507178774d6f444c664d643152324330705033337453465533534b0a4e6b786b4d326744536a41392b2b5754625267553543556c3642725942516a62415836773673715354775a42796e736e6270366c6d6f6f6c4b314341762b55500a7144444d4c6e6a696a62465861324476326833495650456246786178336a73303367557448496e4141552f4c4b4b4948583730664d644b45566c735845482b4b0a53395238446c4c355854467a38435175693175692f394f484f77523549672b5436532f5a3678504f51594d4d69562f6e6a48553d0a3d62357a450a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d" 578 | }, 579 | { 580 | "created": "2018-03-07T01:46:39.310Z", 581 | "identifier": "3c936ad8b8fa8de3290bc45cae7eefda5240818d", 582 | "signature": "2d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a436f6d6d656e743a20475047546f6f6c73202d2068747470733a2f2f677067746f6f6c732e6f72670a0a6951497a424141424367416446694545504a4e71324c6a366a654d7043385263726e3776326c4a4167593046416c714d62574d4143676b51726e3776326c4a410a6759306c73672f2b4e7839486577622b31527631347038676e7a4c563763536c6f4f422f5262586b694136426a76336a4166735a55623175484d6e617a3644370a736e6c306e47775233497966554c6d3254647536444e3043314e767549367374442f7362666473572b6c726a72666d6d6b377a2f36726c593169684a6d5061330a6758344f4d63577061352f4e56636a436f432b546b51434b4d77684237424b7030344a363679326f78382f583736574c6d685544366b6b6155565979656637520a79566666784c6d766747627a476d7433445958316a59524a766d4e6b6f5749466f30557254576d7a55457961505538457950386d415075347a413749453834650a546f57634e38714b576564797879396e615147493679376a5149445a72454a30315a56785371326b6352506e464d694432772f6c69704b6b504a4e577556764e0a3834644f4372324859302b584769726c74744c673167363077314a5333455354714938374a786766716f53695257666c4d4f736667712f6e302b7243337057620a535030397245457a307069705376374d5a723944436142435261544142333156567a6f4c48536e67452b4a4f5264615a4a2f7274796f315366634c5a75314d320a4241735178786c7142306449617174534830386b50496e6a5a746776346358766142556443507178774d6f444c664d643152324330705033337453465533534b0a4e6b786b4d326744536a41392b2b5754625267553543556c3642725942516a62415836773673715354775a42796e736e6270366c6d6f6f6c4b314341762b55500a7144444d4c6e6a696a62465861324476326833495650456246786178336a73303367557448496e4141552f4c4b4b4948583730664d644b45566c735845482b4b0a53395238446c4c355854467a38435175693175692f394f484f77523549672b5436532f5a3678504f51594d4d69562f6e6a48553d0a3d62357a450a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d" 583 | } 584 | ] 585 | } 586 | ``` 587 | Please note that these example signatures may not match the example key payload, don't use them for testing 588 | 589 | 590 | ### PGP Key Distribution 591 | It is ultimately up to the JSON payment protocol provider as to how they will distribute the PGP public keys used to sign their payment protocol signing keys. We recommend using multiple distribution paths, at least one of which should not be on the same domain as the payment requests. 592 | 593 | ### BitPay Specific Signature Details 594 | 595 | #### Signing 596 | For the foreseeable future all of BitPay's payment requests will be signed via ECDSA using the SECP 256 k1 curve (the same as used in Bitcoin itself). This is to allow very simple compatibility with wallets, as every wallet should already be fully able to verify ECC signatures of bitcoin transactions. 597 | 598 | #### PGP Key Distribution 599 | BitPay will make its PGP keys available via the following resources: 600 | 601 | * https://github.com/bitpay/pgp-keys 602 | * https://bitpay.com/pgp-keys (or in JSON format https://bitpay.com/pgp-keys.json) 603 | 604 | Please take time to verify the keys in both places. 605 | 606 | #### Key-storing suggestions 607 | Some wallets may not be sure about the best way to store the ECC public keys for JSON payment protocol providers. There are likely to be two or three common approaches. Regardless of your approach, we recommend keeping in mind that keys can be compromised and therefore may require being changed 608 | relatively quickly. It also may also be reasonable for advanced users to want to add their own keys for development purposes, but keep in mind that less advanced users may be tricked into doing this if appropriate warnings are not provided. Remember you will always need to store the domains and the 609 | owner for each key so that you can prevent a key from being used on the wrong domain, and so that the user knows who owns the key. 610 | 611 | ##### Approach one 612 | The simplest approach would be to simply verify all the keys we've distributed locally and then directly bake them into your wallet code. This has the advantage of being quite secure, however the direct disadvantage is that if a key is revoked or the keys change for any reason your user is stuck until 613 | you release an update. 614 | 615 | ##### Pros: 616 | * Wallet signature logic is very simple 617 | 618 | ##### Cons: 619 | * Key revocation may result in needing to push emergency updates to your wallet (remember Apple App store can be slow to accept updates) 620 | 621 | ##### Approach two 622 | The second approach would be to generate your own ECC key pair and hard code it's public key into your app. You would then verify our (and others) keys in your development environment and if valid publish them to your own API, signed by your own ECC key. Wallets would then verify against the keys from 623 | your API each time they wanted to verify a payment request. This allows for relatively quick changing of which keys are considered valid by simply updating your own API. 624 | 625 | ##### Pros: 626 | * Usable across multiple providers 627 | * Pretty reasonable key update time 628 | 629 | ##### Cons: 630 | * Still have cases where keys could be out of date for hours (or days for some wallets) 631 | 632 | ##### Approach three 633 | The third approach is to bake our PGP keys into your wallet. On first boot the wallet would retrieve our signing keys and their signatures and verify them. Once verified the app would store the SHA256 hash of the keys. The wallet could then periodically retrieve our key list and only re-validate them 634 | if the SHA256 hash is different than the one previously stored. 635 | ##### Pros: 636 | * Always up to date 637 | ##### Cons: 638 | * Potentially need a specific implementation per-provider 639 | 640 | ## Errors 641 | 642 | All errors are communicated in plaintext with an appropriate status code. 643 | 644 | ### Example Error 645 | 646 | ``` 647 | curl -v https://test.bitpay.com/i/48gZau8ao76bqAoEwAKSwx 648 | * Trying 104.17.68.20... 649 | * TCP_NODELAY set 650 | * Connected to test.bitpay.com (104.17.68.20) port 443 (#0) 651 | * TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 652 | > GET /i/48gZau8ao76bqAoEwAKSwx HTTP/1.1 653 | > Host: test.bitpay.com 654 | > User-Agent: curl/7.54.0 655 | > Accept: application/payment-request 656 | > 657 | < HTTP/1.1 400 Bad Request 658 | < Date: Fri, 26 Jan 2018 01:54:03 GMT 659 | < Content-Type: text/html; charset=utf-8 660 | < Content-Length: 44 661 | < Connection: keep-alive 662 | < Strict-Transport-Security: max-age=31536000 663 | < X-Download-Options: noopen 664 | < X-Content-Type-Options: nosniff 665 | < Access-Control-Allow-Origin: * 666 | < Access-Control-Allow-Methods: GET, POST, OPTIONS 667 | < Access-Control-Allow-Headers: Host, Connection, Content-Length, Accept, Origin, User-Agent, Content-Type, Accept-Encoding, Accept-Language 668 | < 669 | * Connection #0 to host test.bitpay.com left intact 670 | This invoice is no longer accepting payments 671 | ``` 672 | 673 | ### Common Errors 674 | 675 | | Http Status Code | Response | Cause | 676 | |---|---|---| 677 | | 404 | This invoice was not found or has been archived | Invalid invoiceId, or invoice has been archived (current TTL is 3 days) | 678 | | 400 | Unsupported Content-Type for payment | Your Content-Type header was not valid | 679 | | 400 | Invoice no longer accepting payments | Invoice is either paid or has expired | 680 | | 400 | We were unable to parse your payment. Please try again or contact your wallet provider | Request body could not be parsed / empty body | 681 | | 400 | Request must include exactly one (1) transaction | Included no transaction in body / Included multiple transactions in body | 682 | | 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 683 | | 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 | 684 | | 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 | 685 | | 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 | 686 | | 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`| 687 | | 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 | 688 | | 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 | 689 | | 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 | 690 | | 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. | 691 | 692 | 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. 693 | 694 | ## MIME Types 695 | 696 | |Mime|Description| 697 | |---|---| 698 | |application/payment-options| Retrieve the options that the invoice can be paid with, this is specified with the `Accept` header.| 699 | |application/payment-request| Associated with the server's payment request, this specified on the client `Accept` header when retrieving the payment request| 700 | |application/payment-verification| Used by the client when sending their proposed unsigned payment transaction payload| 701 | |application/payment| Used by the client when sending their proposed payment transaction payload| 702 | --------------------------------------------------------------------------------