<NodeJS>

Cash Pay Server

Self-hosted BIP70 and JSON Payment Request service.

Server-Browser-Webhook Pattern

The Browser-Webhook approach is easy - but it is not very secure. As the browser is responsible for creating the Invoice, this means that a malicious user could specify a particular InvoiceID using the setData(...) method and then modify the amounts or addresses the invoice sends to. We could always check the Webhook to make sure the address(es) and amount(s) are correct - but this requires a substantial amount of extra code in our Webhook Endpoints. An easier approach might be to create the Invoice server-side and then pass it back to the browser to prevent a malicious user from modifying the invoice.

The flow would then become the following:

  1. Create the Invoice Server Side and return it to the browser.
  2. Load the Server-side Invoice into the browser using Invoice.fromExisting(...)
  3. Setup the broadcasted and confirmed Webhooks to update the DB backend.

The advantage of this approach is security - as the Invoice is created server-side, it means you do not have to check that the outputs (i.e. address and amount) match in your broadcasted/confirmed Webhooks.

Create Invoice Server-Side

First we want to create a Server-Side endpoint to:

  1. Create the Invoice
  2. Return the invoice back to the browser
// ...
const CashPay = require('@developers.cash/cash-pay-server-js')
// ...

// Usually you would persist invoices and their status to a DB
let invoices = {}

app.post('/request-invoice', async (req, res) => {
  // This is just an example, but usually you would populate the
  // invoice with data from your database.
  let invoice = new CashPay.Invoice({ endpoint: 'https://pay.dev.infra.cash' })
    .addAddress('bitcoincash:qqcgzn44uqykje0gx7nvqhyq3jecgk5jfgrkegdpm7', '1USD')
    .setData('REFERENCE_NUMBER')
    .setWebhook(['broadcasted', 'confirmed'], 'https://ENDPOINT:8080/webhook')

  // Create the invoice (REST call to server)
  await invoice.create()

  // TODO
  // Now that you have an invoiceID, you will likely want to persist
  // this somwhere in your database. For example purposes, we're
  // not persisting this information and just storing it as a key:value
  // object where key is the invoiceID and value is the state of the
  // invoice.
  invoices[invoice.getId()] = 'UNPAID'

  // Return the raw invoice
  res.send(invoice.getRawInvoice())
});

Note that if the typeof window === 'undefined' then a Websocket listener for Invoice events will not be initialized by default as it is expected that a Websocket listener is only desired in Browser.

Load that Invoice in the Browser

Next step is to retrieve that invoice from the server and load it into the browser.

<!-- This example uses a CDN - but if you're using a Webpack Build System, you could also just use the NPM package -->
<script src="https://cdn.jsdelivr.net/npm/@developers.cash/cash-pay-server-js/dist/cashpay.min.js"></script>

<!-- This is where the invoice will appear on page -->
<div id="invoice-container" style="width:350px"></div>

<!-- This code creates the invoice -->
<script>
// Wrap in async function so we can use 'await'
async function setupInvoice() {
  // Get the container
  let container = document.getElementById('invoice-container')
  
  // Get the invoice (TODO Update with your endpoint)
  let invoice = await CashPay.Invoice.fromServerEndpoint('http://ENDPOINT:8080/request-invoice')
  
  // This is where you would define the behaviour that should take
  // place once the payment has been made
  invoice.on('broadcasted', event => {
    console.log('broadcasted')
  });
  
  invoice.on('requested', event => {
    console.log('requested')
  });
  
  invoice.on('failed', event => {
    console.log(event);
  });
  
  // Render the invoice in the container
  invoice.create(container) 
}

setupInvoice();
</script>

We use a convenience function here from the cash-pay-server-js library, but this could just as well be done using a custom AJAX request and then calling the method CashPay.Invoice.fromExisting(...).

Setup Webhooks

We would now want to add code to update our backend. In a typical implementation, this would set the status of the Invoice in the Backend Database (persistent storage). However, for simplicity, in this example, we are just using an InvoiceID:Status Key:Value pair that we dubbed invoices in Step #1.

// This should only be initialized once
let webhook = new CashPay.Webhook();
await webhook.addTrusted('https://pay.infra.cash');

router.post('/broadcasted', async (req, res) => {
  // Get the signature headers that were passed
  let headers = {
    'x-identity': req.get('x-identity'),
    'digest': req.get('digest'),
    'x-signature': req.get('x-signature'),
    'x-signature-type': req.get('x-signature-type')
  }

  // Make sure the request is signed
  if (webhook.verifyPayload(req.body, headers)) {
    return res.status(403).send('Signature verification failed')
  }

  // Convert the invoice into a native CashPay.Invoice
  let invoice = CashPay.Invoice.fromExisting(req.body.invoice)

  // Make sure this is an invoice that we're expecting
  if (typeof invoices[invoice.getId()] === 'undefined') {
    return res.status(400).send('Not expecting this invoice')
  }
  
  if (req.body.type === 'broadcasted') {
	 invoices[invoice.getId()] = 'IN MEMPOOL'
  } else if (req.body.type === 'confirmed') {
     invoices[invoice.getId()] = 'CONFIRMED'
  }

  console.log(`Invoice ${invoice.getId()} marked as ${req.body.tye}`)
});

On the note of the Signature Verification, this is important. If this was not done, this means any malicious user could ping our endpoint with a valid payload and mark a payment as broadcasted/confirmed, despite never actually paying it. By implementing the webhook.verifyPayload function we can ensure that the Webhook payload has come from the server that we defined with the addTrusted(...) method by checking the cryptographic signature.