Introduction
This article describes a jQuery widget for making payments using Bitcoin.
Paying with Bitcoin
So what exactly is needed to be able to pay with Bitcoin?
Unlike the other traditional payment systems, Bitcoin requires each transaction to be presigned by a secret key which is stored in payer's wallet.
This secret key can't be exposed to the public and that's why the only secure way to pay with Bitcoin is to initiate a payment from a payer's wallet using the address and amount provided by merchant.
Payer scans this information from widget in format of QR code. After information was scanned and payment initiated, the payer should notify the widget to start monitoring for transactions.
After widget receives the transaction from Bitcoin network, payment is considered successful.
Auto Update
Bitcoin exchange rate changes very frequently and is currently very unpredictable.
That's why the address and amount of the payment (in format of QR code) should constantly auto update and reflect the most recent price for the service.
Note: address usually changes along with amount because it's a more secure approach.
Widget Requirements
Generalizing all of the requirements, widget should be able to do the following:
- Show a payer information about the most recent merchant address and amount in a format of QR code
- Autoupdate the QR code every 5 minutes. Notify the payer with a timer when the QR code will be updated at least 1 minute before expiration.
- Provide a button for the payer to notify that the payment has been submitted. Start monitoring for a new transaction after button was clicked.
- When the transaction has been received, execute necessary callbacks and notify the payer that the payment was successful.
Using this widget
Widget requires jQuery and jquery-qrcode libraries.
Here is the example of usage:
HTML:
<div class="bitcoin-payment-widget" id="widget">
<div class="border-cover">
<div class="header-logo"><img src="~/Content/bitcoin.png" /></div>
<div class="header-label">Bitcoin Payment</div>
<div class="qr-image-container">
<div class="qr-image" data-role="qr-image"></div>
</div>
<div class="update-label" data-role="update-label"></div>
<div class="check-payment-button-container">
<button class="check-payment-button"
data-role="check-payment-button"></button>
</div>
</div>
</div>
JavaScript:
$(document).ready(function () {
$("#widget").bitcoinpayment({
getNewLinkUrl: "/Payment/GetNewLink",
getPaymentStatusUrl: "/Payment/GetPaymentStatus",
postPaymentUrl: "/Payment/PostPayment",
postPaymentCallback: function (result) {
alert("Payment received at " + result.paidAt + ",
transaction id: " + result.transactionId);
}
});
})
Pre-requirement for this widget is a REST service which should communicate to Bitcoin (or upperlayer) network and be able to generate new address/amount pairs and check their corresponding transaction status.
Here is the description of the param object:
getNewLinkUrl
- URL of REST service method returning Bitcoin link
and unique paymentRequestId
corresponding to it. Bitcoin link should be in URI format (see Bitcoin URI) and should contain address and amount. getPaymentStatusUrl
- URL of REST service method accepting paymentRequestId
string as a parameter and returning paymentSuccessful
boolean as response. This service method should make requests to Bitcoin (or upperlayer) network and check if there are any transactions related to specific paymentRequestId
. If true
, the method should return paymentSuccessful
as true
. Optionally, can also return paidAt
and transactionId
. postPaymentUrl
- URL of REST service to postback on successful payment. Should take paymentRequestId
as a parameter. postPaymentCallback
- Callback to trigger on successful payment.
Below is a possible server side implementation (in C# and ASP.NET):
[HttpPost]
public ActionResult GetNewLink()
{
var paymentInfo = new PaymentInfo()
{
Amount = 0.25M,
Currency = "USD"
};
var paymentRequest = BitcoinNetworkService.GenerateNewPaymentRequest(paymentInfo);
return Content("{ \"link\": \"" +
paymentRequest.BitcoinUri + "\", \"paymentRequestId\": \"" +
paymentRequest.PaymentRequestId + "\" }");
}
[HttpPost]
public ActionResult GetPaymentStatus(string paymentRequestId)
{
var paymentResult = BitcoinNetworkService.GetPaymentStatus(paymentRequestId);
return Content("{ \"paymentSuccessful\": " +
paymentResult.IsSuccessful.ToString().ToLowerInvariant() +
", \"paidAt\": \"" +
paymentResult.PaidAt + "\", \"transactionId\": \"" +
paymentResult.TransactionId + "\" }");
}
[HttpPost]
public ActionResult PostPayment(string paymentRequestId)
{
return new EmptyResult();
}
Demo Project
Demo project implements server logic using Coinbase
. To run demo, create a Merchant
account in Coinbase
and specify credentials (CoinbaseApiKey
and CoinBaseApiSecret
) in Web.Config file.
Widget Implementation Details
Let's start observing the code with initialization.
function init($container) {
$qrImage = $container.find("[data-role='qr-image']");
$checkPaymentButton = $container.find("[data-role='check-payment-button']");
$updateLabel = $container.find("[data-role='update-label']");
$checkPaymentButton.text("Check Payment");
$updateLabel.text("Pay QR in your wallet");
$checkPaymentButton.click(function () {
if (isMonitoringPayments) {
$updateLabel.text("Pay QR in your wallet");
$checkPaymentButton.text("Check Payment");
stopMonitoringPayments();
startAutoUpdater();
}
else {
startMonitoringPayments();
stopAutoUpdater();
$checkPaymentButton.text("Cancel");
$updateLabel.text("Checking payment...");
}
})
startAutoUpdater();
}
In initialization, we should do a few things: initialize variables, start auto updater and hook logic of the "Check Payment" button depending on its current state (which can be either "Check Payment" or "Cancel Checking Payment").
First, the parameters $qrImage
, $checkPaymentButton
and $updateLabel
are initialized with HTML elements having specific data-role
attributes. Using these attributes allows to make HTML robust and use different markup for the elements if necessary
Then $checkPaymentButton
is assigned a click
event which has two conditions. If this button was clicked when widget was not monitoring payments, it should mean that the user clicked Check Payment button and widget should start monitoring payments and stop auto updating of the QR code. Otherwise, it means that user clicked Cancel button and in this case as opposed to the previous behavior, widget should interrupt monitoring payments and continue auto update of the QR code. $updateLabel
and $checkPaymentButton
are updated accordingly.
In the end of the method, startAutoUpdater
is executed to start auto updater and perform the initial update of the QR code.
Now let's look at the main functions in more detail:
function checkForUpdate() {
var minimumNotificationTime = 60000;
var currentTime = Date.now();
if (!timeToNextUpdate || timeToNextUpdate.getTime() < currentTime) {
timeToNextUpdate = new Date(currentTime + updateIntervalInMinutes * 60000)
$qrImage.empty();
$.post(options.getNewLinkUrl)
.done(function (r) {
var result = JSON.parse(r);
console.log("Received link: " + result.link);
paymentRequestId = result.paymentRequestId;
$qrImage.empty();
$qrImage.qrcode({
text: result.link,
width: $qrImage.width(),
height: $qrImage.height()
});
$updateLabel.text("Pay QR in your wallet");
})
.fail(function () {
console.error("Service " + options.getNewLinkUrl + " is not accessible.");
});
}
else if (timeToNextUpdate.getTime() - currentTime <= minimumNotificationTime) {
$updateLabel.text("Time before update: " +
fmtMMSS(timeToNextUpdate.getTime() - currentTime));
}
}
This function is executed in scope of the auto updater loop. Its main responsibility is to update the QR code with most recent amount and payment address.
timeToNextUpdate
is a time generated each time when QR is generated. It is assigned in this method using a sum of currentTime
and updateIntervalInMinutes
multiplied by 60000 since all these values are in milliseconds. When timeToNextUpdate
is not set (initial state) or less than currentTime
, it means that the time has come and QR code needs to be updated. On this update, widget does the POST
request to getNewLinkUrl
provided in options which must return a JSON string
with paymentRequestId
and link
in the Bitcoin
URI format containing information about payment and merchant address. QR code is updated with this link using the qrcode
function which is a part of the jquery.qrcode
library (see GitHub). minimumNotificationTime
variable stores the time when user needs to be notified about future expiring of the QR code. When that time has come, the user will see a notification in $updateLabel
that the QR code will expire soon.
function checkPayment() {
$.post(options.getPaymentStatusUrl,
{
paymentRequestId: paymentRequestId
})
.done(function (r) {
var result = JSON.parse(r);
if (result.paymentSuccessful) {
console.log("Payment received")
stopMonitoringPayments();
stopAutoUpdater();
$updateLabel.text("Payment Succeeded!");
$checkPaymentButton.attr("disabled", "disabled")
$.post(options.postPaymentUrl, {
paymentRequestId: paymentRequestId
})
.done(function () {
if (options.postPaymentCallback) {
options.postPaymentCallback(result);
}
});
}
}).fail(function () {
console.error("Service " + options.getPaymentStatusUrl + " is not accessible.");
});
}
The main purpose of this function is to check if payment has been received by the server.
System does request with paymentRequestId
to getPaymentStatusUrl
and receives JSON response with the payment status. When payment is successful, the system stops the processes and invokes callbacks. First system stops monitoring and auto updating. $updateLabel
is updated to notify the payer about the successful transaction. Then $checkPaymentButton
is disabled to prevent any further actions by the user since all operations on widgets were stopped. In the end, postback is done on postPaymentUrl
and postPaymentCallback
to notify the server and client correspondingly about the successful result.
Points of Interest
Edge cases when user has paid more or less than the required amount are not covered in this widget. However, they can be added easily assuming the appropriate functionality is implemented on server side.