One of the key difficulties in building a service on top of BCH is receiving payments and identifying what that particular payment was actually for.
The problem with the traditional method of displaying a standard BCH address is that there's no additional information attached to that address. When a user sends a transaction to that address, how are we meant to know what the user has actually purchased? We don't have a Reference Number or an Invoice ID - just an Address.
The conventional solution to this is to generate a new address each time an Invoice is requested and, in our backend, "attach" the order information to that new address. Although this can work (if done correctly), it does add overhead and makes it difficult to implement services where you would like the funds to land directly at a particular address (without acting as a temporary custodian of the funds and forwarding to the target address).
This is where protocols like BIP70 become extremely useful. One of the key advantages of protocols like BIP70 is that we end up with the ability to attach arbitrary data to a payment (or technically, to the 'payment request'). This means we can attach that Reference Number or Invoice ID - and a lot of other data too if we wanted.
However, BIP70 has problems too in that it is difficult to implement. Adding to this, there's JSONPaymentProtocol which uses the same bitcoincash:?r=pathToSomeInvoiceID
URL scheme, so some wallets may request a BIP70 invoice, while others may request a JSONPaymentProtocol invoice. This means that, if we don't support both, some wallets will fail as the Payment Request URL does not contain any information stating which protocol the service supports and which the wallet should use. Depending on the wallet, it will do one or the other.
Supporting both protocols is one of my goals with CashPayServer. I'm also trying simplify some of the other more difficult aspects of accepting payments. For example, giving the developer event hooks (broadcasted, confirmed, etc) and making it convenient to give the user quick feedback once the transaction has been broadcast to the network by providing a default UI that can be integrated on-site.
In this respect, my intention with CashPayServer is to allow developers to provide a pleasant User Experience with very little code - but also keep it extensible (and secure) enough to permit more advanced use-cases.
Integration
I'd like to demonstrate what a simple CashPayServer integration might look like, from a coding perspective. The following was built in under 300 lines of code, in total (frontend, backend - and comments inclusive). It is not intended to look pretty, but to demonstrate how an actual CashPayServer integration might be done (from a coding perspective).
Live Demo: https://store.example.developers.cash/
I'm not going to walk-through all of the frontend code. This isn't as relevant as most would integrate with an existing service (or a new service being built) and this is just intended as a simple example for reference. For any that are interested, it's been built with Vue's Qusar framework (and is only about 180LoC in total).
To give a brief, we have the following endpoints:
Backend (Server-Side) (Using Node/ExpressJS)
/request-invoice [POST]
This handles the creation of the invoice. The CashPay JS library supports doing generating invoices from the browser also, but to follow best-practices, I'm going to generate this server-side as, for most implementations, this is how it should be done./webhook [POST]
This handles thebroadcasted
andconfirmed
webhooks. These will be triggered when an Invoice has been paid (broadcasted
) and when that transaction has been included in a block (confirmed
).
The full backend code can be viewed here (88 lines of code)
https://pastebin.com/kw5CiuDy
Frontend (Browser)
index.html
Built as a Single-Page-App using Vue/Quasar (from CDN).
Most code in here is templating/styling.
view-source:https://store.example.developers.cash/ (180 lines of code)/catalog.json [GET]
This stores the list of items for sale. It is static, but leveraged by both the frontend and backend.
https://store.example.developers.cash/catalog.json
Frontend (Browser)
The first thing we do is include the CashPay JS library from a CDN. This could also be included with NPM as a module - but to keep this simple, let's just use this for now.
<script src="https://cdn.jsdelivr.net/npm/@developers.cash/cash-pay-server-js/dist/cashpay.min.js"></script>
A few lines above this, we're also including animate.css. The reason for this is that the default container will use animate.css classes to make things look a bit more visually appealing. However, this is entirely optional.
Now let's jump to the piece of the frontend code that triggers once the user is ready to make a payment.
async onPay() {
// ... UI Code here
// Request invoice from server endpoint
let invoice = await CashPay.Invoice.fromServerEndpoint('./request-invoice', {
cart: this.cart,
customer: this.customer
})
// Hook into the broadcasted event
invoice.on('broadcasted', e => {
this.$q.notify(`Payment has been received!`)
// Wait five seconds and then clear cart and show the catalog page
setTimeout(() => {
this.reset()
}, 5000)
})
// And then load it into our container on page
await invoice.create(document.getElementById('invoice-container'))
},
What is happening here is that an invoice is being read back from our server-side /request-invoice
endpoint. We are then setting an event listener for the broadcasted
event so that we can make additional changes to the front-end state (empty the cart, change page, etc). We then call create(...)
and pass it the container we want the QR code, etc, to appear in. Passing a container is optional - if this is left blank, we can still hook into the Events (created
, requested
, broadcasting
, broadcasted
and failed
) and render everything manually - or use reactive properties if we are using something like a SPA framework. But we want this example to be simple and convenient, so we're going with the container.
Server-side
We need to do a little bit of setting up when we first initialize the server-side code.
const CashPay = require('@developers.cash/cash-pay-server-js')
const Catalog = require('./public/catalog.json')
// ...
// We need to tell our Webhook to trust any requests signed with pay.infra.cash keys
let webhook = new CashPay.Webhook()
await webhook.addTrusted('https://v1.pay.infra.cash')
// Let's store a list of Orders as InvoiceID:Status pairs. Usually these would be stored in a database.
let orders = {}
We include CashPayJS and our catalog of items (stored in a JSON). We then setup CashPay's Webhook and add the https://pay.infra.cash
Server as "Trusted". This is necessary so that we can reject any spoofed Webhook requests (i.e. someone manually sending a POST request to our endpoint with fake data).
We also create an orders
InvoiceIDs:Status keypair so that we can make sure that the invoices received at our Webhook endpoint were actually created by us. In a typical implementation, these InvoiceID's would probably be stored against an orders
table in the Database backend with a status (e.g. PENDING
, BROADCASTING
, BROADCASTED
, CONFIRMED
) and other details (items
, total
, etc). However, we're trying to keep this example simple. In the database case, the same general principles would apply, but as the order
and the totals would already exist, you would probably just store the relevant orderId in the privateData and then pull the other details from your database.
We then define a route for the /request-invoice
endpoint that we leverage in our frontend code.
app.post('/request-invoice', async (req, res) => {
// Don't trust the browser to send correct prices and item details. Use the item id and read from the catalog directly.
let items = req.body.cart.map(cartItem =>
catalog.find((catalogItem) => cartItem.id === catalogItem.id)
)
// Calculate the total amount due
let total = items.reduce((total, item) => total += item.price, 0)
// Set the parameters of the invoice
let invoice = new CashPay.Invoice()
invoice.addAddress('bitcoincash:qplf0j8krjrsv95v0t3zj9dc4rcutw5khyy8dc80fu', `${total}USD`)
.setWebhook(['broadcasting', 'broadcasted', 'confirmed'], 'https://store.example.developers.cash/webhook')
.setPrivateData({
customer: req.body.customer, // Customer details
items: items
})
// Actually create the invoice
await invoice.create()
// Create an entry for this InvoiceID in our list of orders
orders[invoice.id] = 'pending'
// Return the invoice (this method will omit sensitive data (unless 'true' is passed as a param)
return res.send(invoice.payload())
})
One cool feature here is that we can pass Fiat amounts which will be automatically converted to BCH equivalents by CashPayServer (based on Coinbase exchange rates). You could also add multiple addresses/amounts which is useful for services that wish to take commission (e.g. add 10% onto the amount due and pay it to a different address).
If we run this as is, we'd now have a QR Code rendered on our page. However, if we tried to pay, it would actually fail as there is no route to the Webhook.
Let's setup the Webhook endpoint:
app.post('/webhook', async (req, res) => {
try {
// An exception will be thrown if signature does not match
await webhook.verifySignature(req.body, req.headers)
// The type of event (e.g. broadcasting, broadcasted or confirmed)
let eventType = req.body.event
let invoice = req.body.invoice
// Make sure this invoice was created by us
if (typeof orders[invoice.id] === 'undefined') {
throw new Error(`${eventType}: Invoice ${invoice.id} does not exist in our orders.`)
}
// Mark the invoice as broadcasting/broadcasted/confirmed
orders[invoice.id] = eventType
// Get the data associated with the invoice
let data = JSON.parse(invoice.options.privateData)
// Output event type and list of items to console
console.log(`${data.customer.email} ${eventType}:`)
console.log(` ${data.customer.comments}`)
data.items.forEach(item => { console.log(` ${item.id}: ${item.title}`) })
// You must send a 200 response, otherwise CashPay will throw an error
res.status(200).send('Success')
} catch(err) {
console.log(err)
res.status(500).send(err.message)
}
})
This isn't doing much. It's simply verifying that the Webhook payload actually came from the Trusted Server we added earlier (https://pay.infra.cash
), marking the invoice as broadcasting
, broadcasted
or confirmed
and then outputting the items that the user purchased to the console. A better example might do something like send an email to both the user and the merchant when payment is broadcasted
and/or confirmed
, but I do not have an SMTP handy.
But, that's pretty much what would be required to integrate, in a nutshell. There are some other edge-cases that still need to be fleshed out (what if your server goes down?), but that's something I'll look at covering in future as there are ways around that for now.
Summary
I'm probably still a month away from having a version of this that's safe for Public Use. Most of the schema I'm happy with, but would gladly take some developer feedback, particularly from those who could make use of a service such as this (e.g. what features would you need, what is currently not supported, improvements to the JS interface, etc).
Feel free to contact me if any suggestions:
Telegram: https://t.me/jimtendo
I will add quickly (in case someone has not seen my other posts) that this platform will be self-hostable (once stable, I'll create a Docker Container for easy deployment). There is no need to trust my https://pay.infra.cash instance - this is simply being offered as a convenience for those who wish to get up and running quickly.
Additionally, I will have some more formal documentation on the API's supported, capabilities for customization, etc, within a few weeks.
EDIT: Updated API in examples (2020-06-03)
Tested it out and it worked smoothly!