Payments API Integration in Node.js

In this tutorial, we will understand how to integrate Blockonomics Payments API in Node.js using expressCart.

Resources

Source Code https://github.com/mrvautin/expressCart
Youtube Videohttps://www.youtube.com/watch?v=ucmJmqSJb4c

Tech Stack

  • Node.js
  • HTML/CSS
  • MongoDB

Setting Up

  • Complete the expressCart installation here

Enabling Blockonomics as a Payment Option

To enable Blockonomics, you need to configure the settings. Firstly provide your API Key and other details as mentioned here. Then, to enable it as a payment option, go to config/settings.json and add blockonomics in the paymentGateway.

    "paymentGateway": [
        "stripe",
        "blockonomics"
    ],

Adding New Store in Blockonomics

To start using Blockonomics in your project, you have to create a new store by navigating to the Merchant’s Page. When you create a new store, you specify the HTTP CallBack URL. In this case, the HTTP CallBack URL will be <your-cart-url>/blockonomics/checkout_return.

Understanding the Code

Payment Routes

Each payment gateway has a separate section in lib/payments and thus, for blockonomics, we can find the routes at lib/payments/blockonomics.js. There are three different routes, namely

/checkout_cancel => cancel the current transaction and return to checkout page
/checkout_return => HTTP Callback Endpoint provided to blockonomics
/checkout_action => Endpoint that creates new address, set prices, insert order into database

/checkout_action

Whenever you click on the button Pay with Bitcoin, this route is called and we fetch the current rate and new address using blockonomics APIs that you specified in the config. After getting the details, we create an order in the database with the default status of the order as Pending. The status will be updated only in the HTTP Callback URL route.

router.post('/checkout_action', (req, res, next) => {
  const blockonomicsConfig = getPaymentConfig('blockonomics');
  const config = req.app.config;
  const db = req.app.db;
  const blockonomicsParams = {};
  // get current rate
  axios
  .get(blockonomicsConfig.hostUrl + blockonomicsConfig.priceApi + config.currencyISO)
  .then((response) => {
    blockonomicsParams.expectedBtc = Math.round(req.session.totalCartAmount / response.data.price * Math.pow(10, 8)) / Math.pow(10, 8);
    // get new address
    axios
      .post(blockonomicsConfig.hostUrl + blockonomicsConfig.newAddressApi, {}, { headers: { 'Content-Type': 'application/json', 'User-Agent': 'blockonomics', Accept: 'application/json', Authorization: `Bearer ${blockonomicsConfig.apiKey}` } })
      .then((response) => {
          blockonomicsParams.address = response.data.address;
          blockonomicsParams.timestamp = Math.floor(new Date() / 1000);
          // create order with status Pending and save ref

            const orderDoc = {
                orderPaymentId: blockonomicsParams.address,
                orderPaymentGateway: 'Blockonomics',
                orderExpectedBtc: blockonomicsParams.expectedBtc,
                orderTotal: req.session.totalCartAmount,
                orderShipping: req.session.totalCartShipping,
                orderItemCount: req.session.totalCartItems,
                orderProductCount: req.session.totalCartProducts,
                orderCustomer: getId(req.session.customerId),
                orderEmail: req.session.customerEmail,
                orderCompany: req.session.customerCompany,
                orderFirstname: req.session.customerFirstname,
                orderLastname: req.session.customerLastname,
                orderAddr1: req.session.customerAddress1,
                orderAddr2: req.session.customerAddress2,
                orderCountry: req.session.customerCountry,
                orderState: req.session.customerState,
                orderPostcode: req.session.customerPostcode,
                orderPhoneNumber: req.session.customerPhone,
                orderComment: req.session.orderComment,
                orderStatus: 'Pending',
                orderDate: new Date(),
                orderProducts: req.session.cart,
                orderType: 'Single'
            };
            db.orders.insertOne(orderDoc, (err, newDoc) => {
                if(err){
                    console.info(err.stack);
                }
                // get the new ID
                const newId = newDoc.insertedId;
                // add to lunr index
                indexOrders(req.app)
                .then(() => {
                    // set the order ID in the session, to link to it from blockonomics payment page
                    blockonomicsParams.pendingOrderId = newId;
                    req.session.blockonomicsParams = blockonomicsParams;
                    res.redirect('/blockonomics_payment');
              });
            });
      });
  });
});

/ checkout_return

Whenever Blockonomics sends a status update about any transaction, this route gets called. The four things that we get from the GET request are:

  const status = req.query.status || -1; // status of transaction
  const address = req.query.addr || 'na'; // btc address
  const amount = (req.query.value || 0) / 1e8; // the amount in btc
  const txid = req.query.txid || 'na'; // transaction id

The status of 2 means the transaction is confirmed and you have successfully received the money. So, after the status is 2, make sure that you cross-check the amount the sender sends and the amount you expect the sender to send. You can always choose to expect 90-95% of the original price to be received because of the network fees. Finally, we update the status of this transaction inside our database. Please note that here we are using a bitcoin address to uniquely identify a given order. Thus, using a different bitcoin address by calling our new address API every time you want to create new order is required.

router.get('/checkout_return', async (req, res, next) => {
  const db = req.app.db;
  const config = req.app.config;

  const status = req.query.status || -1;
  const address = req.query.addr || 'na';
  const amount = (req.query.value || 0) / 1e8;
  const txid = req.query.txid || 'na';

  if(Number(status) === 2){
    // we are interested only in final confirmations
    const order = await db.orders.findOne({ orderPaymentId: address });
    if(order){
      if(amount >= order.orderExpectedBtc){
        try{
            await db.orders.updateOne({
                _id: order._id },
                { $set: { orderStatus: 'Paid', orderReceivedBtc: amount, orderBlockonomicsTxid: txid }
            }, { multi: false });
            // if approved, send email etc
                    // set payment results for email
            const paymentResults = {
                message: 'Your payment was successfully completed',
                messageType: 'success',
                paymentEmailAddr: order.orderEmail,
                paymentApproved: true,
                paymentDetails: `<p><strong>Order ID: </strong>${order._id}</p><p><strong>Transaction ID: </strong>${order.orderPaymentId}</p>`
            };

            // send the email with the response
            // TODO: Should fix this to properly handle result
            sendEmail(req.session.paymentEmailAddr, `Your payment with ${config.cartTitle}`, getEmailTemplate(paymentResults));
            res.status(200).json({ err: '' });
        }catch(ex){
            console.info('Error updating status success blockonomics', ex);
            res.status(200).json({ err: 'Error updating status' });
        }
        return;
      }
      console.info('Amount not sufficient blockonomics', address);
      try{
          await db.orders.updateOne({
              _id: order._id },
              { $set: { orderStatus: 'Declined', orderReceivedBtc: amount }
          }, { multi: false });
      }catch(ex){
          console.info('Error updating status insufficient blockonomics', ex);
      }
      res.status(200).json({ err: 'Amount not sufficient' });
      return;
    }
    res.status(200).json({ err: 'Order not found' });
    console.info('Order not found blockonomics', address);
    return;
  }
  res.status(200).json({ err: 'Payment not final' });
  console.info('Payment not final blockonomics', address);
});

Blockonomics Websocket

If you go to public\javascripts\expressCart.js then on line number 549, you can see the blockonomics WebSocket in action. The WebSocket helps us to get the status update of transactions so that we can navigate from the checkout page to the order confirmation page. Please note that order confirmation simply means that the order has been successfully placed and it should not guarantee that the order payment has been successfully received. Such guarantees can only be made after the status of the transaction becomes 2.

We can choose the timer, how much time we want to give to the customer to make the payments, by default it is set to 10 mins.

// checkout-blockonomics page (blockonomics_payment route) handling START ***
    if($('#blockonomics_div').length > 0){
        var orderid = $('#blockonomics_div').data('orderid') || '';
        var address = $('#blockonomics_div').data('address') || '';
        var blSocket = new WebSocket('wss://www.blockonomics.co/payment/' + address);
        blSocket.onopen = function (msg){
        };
        var timeOutMinutes = 10;
        setTimeout(function (){
            $('#blockonomics_waiting').html('<b>Payment expired</b><br><br><b><a href=\'/checkout/payment\'>Click here</a></b> to try again.<br><br>If you already paid, your order will be processed automatically.');
            showNotification('Payment expired', 'danger');
            blSocket.close();
        }, 1000 * 60 * timeOutMinutes);

        var countdownel = $('#blockonomics_timeout');
        var endDatebl = new Date((new Date()).getTime() + 1000 * 60 * timeOutMinutes);
        var blcountdown = setInterval(function (){
            var now = new Date().getTime();
            var distance = endDatebl - now;
            if(distance < 0){
                clearInterval(blcountdown);
                return;
            }
            var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
            var seconds = Math.floor((distance % (1000 * 60)) / 1000);
            countdownel.html(minutes + 'm ' + seconds + 's');
        }, 1000);

        blSocket.onmessage = function (msg){
            var data = JSON.parse(msg.data);
            if((data.status === 0) || (data.status === 1) || (data.status === 2)){
                // redirect to order confirmation page
                var orderMessage = '<br>View <b><a href="/payment/' + orderid + '">Order</a></b>';
                $('#blockonomics_waiting').html('Payment detected (<b>' + data.value / 1e8 + ' BTC</b>).' + orderMessage);
                showNotification('Payment detected', 'success');
                $('#cart-count').html('0');
                blSocket.close();
                $.ajax({ method: 'POST', url: '/product/emptycart' }).done(function (){
                    window.location.replace('/payment/' + orderid);
                });
            }
        };
    }
    // checkout-blockonomics page (blockonomics_payment route) handling ***  END

Customizing ExpressCart

We can very easily customize the checkout page or the entire expressCart to make our own brand out of it. In this example, we are going to customize two things:

First, the message that is shown just above the timer. We can find this line at views\themes\Cloth\checkout-blockonomics.hbs

<ul class="list-group bottom-pad-15">
                        <li class="list-group-item">
                            <div class="row">
                                <div class="col-md-12" id="blockonomics_waiting" style="text-align:center;">
                                    Waiting for payment<br>
                                    <strong><span id="blockonomics_timeout">10m 0s</span></strong> left<br>
                                    <img src="/images/spinner.gif">
                                </div>
                            </div>
                        </li>

                    </ul>  

We can change it to anything that we want.

Second, we are going to place a button on the order confirmation page. This button will redirect the user to the blockonomics page. The order confirmation page is at views\themes\Cloth\payment-complete-blockonomics.hbs.

<div class="col-md-10 offset-md-1 col-sm-12 top-pad-50">
    <div class="row">
        <div class="text-center col-md-10 offset-md-1">
            <h2 class="text-success">Thank you. Order have been received.</h2>
            <div>
                <p><h5>Order will be be processed upon confirmation by the bitcoin network.  Please keep below order details for reference.</h5></p>
                <p><strong>{{ @root.__ "Order ID" }}:</strong> {{result._id}}</p>
                <p><strong>{{ @root.__ "Payment ID" }}:</strong> {{result.orderPaymentId}}</p>
            </div>
            <a href="/" class="btn btn-outline-warning">Home</a>
            <a href="https://www.blockonomics.co" class="btn btn-outline-success">Accept bitcoin payments on your website today</a>
        </div>
    </div>
</div>