Join 54,182 users and earn money for participation

Bitcore - Payment Protocol Library Implementation

9 83 boost
Avatar for jimtendo
Written by   28
1 year ago

I recently attempted to implement the Bitcore Payment Protocol Library and ran into several pieces that took me a few hours of debugging to wrap my head around.

I've decided to share some sample code that I wrote to, hopefully, save others from this headache in case they also decide to implement using this library.

The notable "gotchas" were these:

  • When setting the Payment Details from an output, use the ".message" field: e.g. details.set('outputs', output.message);

  • Bitcore Payment Protocol Lib expects the Certificates in "der" format - not "pem". For the Private Key, ".pem" appears fine.

  • Be careful when you are reading the above Certificates in. You must read these as binary files - not UTF8 files.

  • If you're using a Certificate that is not derived direct from a Root Cert Authority (e.g. you're using LetsEncrypt), then you must include the chain of certs.

Code with GOTCHA lines is below (untested, but is mostly copy+paste of what I used with a few modifications).

This doesn't appear to be working with Bitcoin.com Wallet still. If I work out why, I'll post amended code - but BIP70 implementations seem to be inconsistent across the board.

const express = require('express')
const fs = require('fs');
const router = express.Router()
// GOTCHA - Not mine, but apparently order here matters!
const PaymentProtocol = require('bitcore-payment-protocol');
const bitcore = require('bitcore-lib-cash');

// BIP70 Payment Request Endpoint
router.get('/req', async (req, res) => { 
    // Setup certificates
    // GOTCHA: NOTE THAT THESE ARE IN .der FORMAT - DO NOT READ AS UTF8
    var certificate = fs.readFileSync('./cert.der');
    var chain = fs.readFileSync('./chain.der');
    // GOTCHA: NOTE THAT THIS IS IN .pem and that I'm therefore reading as "utf8"
    var privKey = fs.readFileSync('./privkey.pem', 'utf8');
    var now = Date.now() / 1000 | 0;
    
    // Create an output
    // GOTCHA: ADDRESS MUST BE A BITCOINCASH: ADDRESS!
    var address = bitcore.Address.fromString("bitcoincash:qz2fn6wwwxs2wcdf9cfdhv4ln0qvjadg6csjcjasuf");
    var script = bitcore.Script.buildPublicKeyHashOut(address);
    var rawScript = new Buffer(script.toString(), 'ucs2');
    var output = new PaymentProtocol().makeOutput();
    output.set('amount', 10000);
    output.set('script', rawScript);
    
    // Construct the payment details
    var details = new PaymentProtocol('BCH').makePaymentDetails();
    details.set('network', 'main');
    details.set('outputs', output.message);
    details.set('time', now);
    details.set('expires', now + 60 * 60 * 24);
    details.set('memo', "Message that the user will see");
    details.set('payment_url', `https://your-service.com/ack`);
    details.set('merchant_data', new Buffer("INVOICE_ID_AND_THINGS_OF_THAT_SORT")); // identify the request
    
    // Load the X509 certificate
    var certificates = new PaymentProtocol().makeX509Certificates();
    // GOTCHA: CHAIN THE CERTIFICATES - THIS IS WHY IT'S AN ARRAy
    certificates.set('certificate', [certificate, chain]);

    // Form the request
    var request = new PaymentProtocol().makePaymentRequest();
    request.set('payment_details_version', 1);
    request.set('pki_type', 'x509+sha256');
    request.set('pki_data', certificates.serialize());
    request.set('serialized_payment_details', details.serialize());
    request.sign(privKey);

    // Serialize the request
    var rawBody = request.serialize();
    
    // Set output headers
    res.set({
      'Content-Type': PaymentProtocol.LEGACY_PAYMENT.BCH.REQUEST_CONTENT_TYPE,
      'Content-Length': request.length,
      'Content-Transfer-Encoding': 'binary'
    });
    
    res.send(rawBody);
});

// GOTCHA
// In ExpressJS, due to the Content-Type header, req.body will be empty by default.
// You have to explicitly tell ExpressJS to process raw body as follows:
// app.use(bodyParser.raw({ type:'*/*' }));
// That said, I was lazy - you can probably just put this middleware straight
// into this endpoint and actually select based on Content Type.
router.post('/ack', async (req, res) => { 
    // Decode payment
    var body = PaymentProtocol.Payment.decode(req.body);
    var payment = new PaymentProtocol().makePayment(body);
    var merchantData = payment.get('merchant_data');
    var transactions = payment.get('transactions');
    var refundTo = payment.get('refund_to');
    var memo = payment.get('memo');
    
    // TODO Send payment
    
    // Make a payment acknowledgement
    var ack = new PaymentProtocol().makePaymentACK();
    ack.set('payment', payment.message);
    ack.set('memo', 'Thank you for your payment!');
    var rawBody = ack.serialize();
    
    res.set({
      'Content-Type': PaymentProtocol.LEGACY_PAYMENT.BCH.PAYMENT_ACK_CONTENT_TYPE,
      'Content-Length': rawBody.length,
      'Content-Transfer-Encoding': 'binary'
    });
    
    res.send(rawBody);
});

Useful links:

https://github.com/bitpay/bitcore-payment-protocol

https://github.com/bitpay/bitcore-payment-protocol/blob/master/docs/index.md

https://medium.com/@nusrath501khan/creating-a-bip-70-payment-request-183933c33259

Godspeed.

0
$ 2.15
$ 1.00 from @im_uname
$ 0.50 from @Read.Cash
$ 0.50 from @Cain
+ 2
Avatar for jimtendo
Written by   28
1 year ago
Enjoyed this article?  Earn Bitcoin Cash by sharing it! Explain
...and you will also help the author collect more tips.

Comments

Great to see more technical articles. Personally, I'd add "JavaScript" somewhere in the title - otherwise it's not obvious about which language this is. And I agree with btcfork - a photo of Jeffrey Epstein as a lead image is pretty weird for this article :)

$ 0.00
1 year ago

Well, good to know about it man ! :D This post is pretty interesting articule, hope to see something new from you soon ! God bless you !

$ 0.00
9 months ago

This comment is a question to @Read.Cash:

How does a post like this end up with a Jeffrey Epstein lead image?

WTF...

$ 0.00
User's avatar btcfork
This user is who they claim to be.
We have manually verified this user via some other channel.
1 year ago

🤷‍♂️ No idea. The author has to choose the image himself.

I thought that guy on the lead image looked familiar :)

$ 0.00
1 year ago

How does one, as author, choose a lead image if an article contains no image?

I don't mind tinkering to verify, but is it possible in practice to include an image, set it as lead, then delete it from the post and it will still show up as lead for an article?

I always thought that in case an article has not set a lead image, one will be chosen by the site itself (through whatever magic).

$ 0.00
User's avatar btcfork
This user is who they claim to be.
We have manually verified this user via some other channel.
1 year ago

It's actually our problem. The "lead image" currently is actually "thumbnail image" only. It doesn't show in the article at all. We didn't have time to properly develop it at first, so there's now this confustion. We'll fix it today, so that authors would have a choice of whether to show it in article (default) or use for the thumbnail only.

$ 0.00
1 year ago

Nice article! Jfyi, Bitcore also supports Json Payment Protocol, which is arguably simpler to implement and debug...and plays better with javascript.

$ 0.00
1 year ago

For bitcoincom support, try to use the same address for both req and ack:

details.set('payment_url', `https://your-service.com/ack`);

BitcoinCom expects this address to be the same as the address it initially fetched the request from.

It's also really easy to accidentally get the BitcoinCom wallet to follow the JPP ruleset instead of BIP70.

$ 1.05
1 year ago

You're correct on both accounts there.

I don't understand how to STOP the Bitcoin.com Wallet from following JPP. Looking at the code for Bitcoin.com Wallet, it looks like it will always follow it.

From walletService.js:

            if (signedTxp.payProUrl && signedTxp.coin == 'bch') {
              payproService.broadcastBchTx(signedTxp, handleBroadcastTx);
            } else {
              root.broadcastTx(wallet, signedTxp, handleBroadcastTx);
            }

Painful.

$ 0.00
1 year ago