Fungible tokens: payment flows

Published:



Introduction

In the previous article, Fungible tokens 101, I introduced the concept of a ledger and various extensions that can help us solve practical problems. In this article, we shall analyze a few payment flows—protocols built on top of a ledger allowing clients to exchange tokens for a service—in the context of the Internet Computer.

Prerequisites

The payment scenario

Abstract protocols can be dull and hard to comprehend, so let us model a specific payment scenario: me buying a new laptop online and paying for it in wxdr (wrapped SDR) tokens locked in a ledger hosted on the Internet Computer.

I open the website of the hardware vendor I trust, select the configuration (the memory capacity, the number of cores, etc.) that suits my needs, fill in the shipment details, and go to the payment page. I choose an option to pay in wxdr.

In the rest of the article, we will fantasize about what the payment page can look like and how it can interact with the shop.

Participants

Each flow will involve the following participants:

me Me: a merry human sitting in front of a computer and ordering a new laptop.
shop Shop: an Internet Computer smart contract accepting orders.
webpage Web page: a spaghetti of markup, styling, and scripts serving the shop UI.
wallet Wallet: a trusty hardware wallet device, such as Ledger or Trezor, with a corresponding UI for interacting with the ledger, such as Ledger Live. A more sophisticated wallet can smoothen the UX, but the ideas remain the same.
ledger Ledger: an Internet Computer smart contract processing payments.

Payment phases

All the payment flows we will analyze have three phases:

  1. The negotiation phase. After I place my order and fill in the shipment details, the shop creates a unique order identifier, Invoice ID. The web page displays the payment details (e.g., as a QR code of the request I need to sign) and instructions on how to proceed with the order.
  2. The payment phase. I use my wallet to execute the transaction as instructed on the web page. This phase is essentially the same in all flows; only the transaction type varies.
  3. The notification phase. The shop receives a payment notification for the Invoice ID, validates the payment, and updates the order status. The web page displays an upbeat message, completing the flow.

Invoice account

The first payment flow we will analyze relies on the subaccounts ledger feature. The idea behind the flow is quite clever: the shop can use its subaccount identified by the Invoice ID as a temporary “cell” for the payment. I can transfer my tokens to this cell, and the shop can move tokens out because the cell belongs to the shop.

The happy case of the flow needs only one primitive from the ledger, the transfer method specified below.

service : {
  // Transfers token amount from the account of the (implicit) caller
  // to the account specified by the principal and the subaccount.
  // Arguments:
  //   amount - the token amount to transfer.
  //   from_subaccount - the subaccount of the caller to transfer tokens from.
  //   to - the receiver of the tokens.
  //   to_subaccount - which subaccount of the receiver the tokens will land on.
  transfer(record {
    amount : nat;
    from_subaccount : opt blob;
    to : principal;
    to_subaccount : opt blob;
  }) -> (TxReceipt);
}

The flow proceeds as follows:

  1. In the negotiation phase, the webpage instructs me to transfer tokens to the shop's Invoice ID subaccount and displays a big green “Done” button that I need to press after the payment succeeds.
  2. In the payment phase, I use my wallet to execute the transfer({ amount = Price, to = Shop, to_subaccount = InvoiceId}) call on the ledger.
  3. In the notification phase, I click on the “Done” button dispatching a notification to the shop indicating that I paid the invoice (the webpage can remember the Invoice ID on the client side, so I do not have to type it in). Upon receiving the notification, the shop attempts to transfer the amount from its Invoice ID subaccount to its default account, calling transfer({ amount = Price - Fee, from_subaccount = InvoiceID, to = Shop }) on the ledger. If that final transfer succeeds, the order is complete.
A sequence diagram for the invoice account payment flow.

The invoice account flow has a few interesting properties:

What happens if I transfer my wxdrs but never click the “Done” button? Or what if my browser loses network connection right before it sends the shop notification? The shop will not receive any notifications, likely never making progress with my order. One strategy that the shop could use to improve the user experience in such cases is to monitor balances for unpaid invoices and complete transactions automatically if the notification does not arrive in a reasonable amount of time.

Approve-transfer-from

The approve-transfer-from pattern relies on the approvals ledger feature, first appearing in the ERC-20 token standard. The flow uses two new ledger primitives, approve and transfer_from, and involves three parties:

  1. The owner holds tokens on the ledger. The owner can approve transfers from its account to a delegate.
  2. The delegate can transfer tokens from the owner's account within the approved cap.
  3. The beneficiary receives tokens from the delegate as if the owner sent them.

In our scenario, the delegate and the beneficiary are the same entity — the shop.

We can capture the required ledger primitives in the following Candid interface:

service : {
  // Entitles the delegate to spend at most the specified token amount on behalf
  // of the (implicit) caller.
  // Arguments:
  //   amount - the cap on the amount the delegate can transfer from the caller's account.
  //   delegate - the actor entitled to make payments on behalf of the caller.
  approve(record {
    amount : nat;
    delegate : principal;
  }) -> ();

  // Transfers the specified token amount from the owner account to the
  // specified account.
  // Arguments:
  //   amount - the token amount to transfer.
  //   owner - the account to transfer tokens from.
  //   to - the receiver of the tokens (the beneficiary).
  //
  // PRECONDITION: the owner has approved at least the amount to the (implicit) caller.
  // POSTCONDITION: the caller's allowance decreases by the amount.
  transfer_from(record {
    amount : nat;
    owner : principal;
    to : principal;
  }) -> (nat) query;
}

The flow proceeds as follows:

  1. In the negotiation phase, the webpage instructs me to approve a transfer to the shop, displaying the shop's account. One difference from the invoice account flow is that the shop needs to know my wallet's address on the ledger to make a transfer on my behalf. The webpage displays a text field for my account and the familiar “Done” button.
  2. In the payment phase, I use my wallet to execute the approve({to = Shop, amount = Price}) call on the ledger.
  3. In the notification phase, I paste my ledger address into the text field and press the button. Once the shop receives the notification with my address and the Invoice ID, it executes transfer_from({ amount = Price; owner = Wallet; to = Shop }) call on the ledger. If that transfer is successful, the order is complete.
A sequence diagram for the approve-transfer-from payment flow.

Let us see how this flow compares to the invoice account flow:

One strong side of the approve-transfer-from flow is that it supports recurring payments. For example, if I were buying a subscription with monthly installments, I could have approved transfers for the entire year, allowing the shop to transfer from my account once a month. Of course, I must trust the shop not to charge the whole yearly amount in one go.

Transfer-notify

Note that the failure of the frontend to send a notification is a prevalent error in the previous flows. What if the ledger automatically delivered the notification to the receiver over the reliable channel that the Internet Computer provides? That is the idea behind the transfer-notify flow.

There is one issue we need to sort out, however. When we relied on the webpage to send the notification, we could include the Invoice ID into the payload, making it possible for the shop to identify the relevant order. If we ask the ledger to send the payment notification, we must pass the Invoice ID in that message. The common way to address this issue is to add the memo argument to the transfer arguments, allowing the caller to attach an arbitrary payload to the transaction details.

service : {
  // Transfers token amount from the account of the (implicit) caller
  // to the account specified by the principal.
  // If the transfer is successful, sends a notification to the receiver.
  // Arguments:
  //   amount - the token amount to transfer.
  //   to - the receiver of the tokens.
  //   memo - an opaque identifier attached to the notification.
  transfer_notify(record {
    amount : nat;
    to : principal;
    memo : opt blob;
  }) -> (TxReceipt);
}

The flow proceeds as follows:

  1. In the negotiation phase, the webpage displays the payment details and starts polling the shop for payment confirmation.
  2. In the payment phase, I use my wallet to execute the transfer_notify({to = Shop, amount = Price, memo = InvoiceID}) call on the ledger.
  3. Once the transfer succeeds, the ledger notifies the shop about the payment, providing the amount and the memo containing the Invoice ID. The shop consumes the notification and changes the order status. The next time the webpage polls the shop, the shop replies with a confirmation, and I see a positive message.
A sequence diagram for the transfer-notify payment flow.

Let us check how this flow compares to the previous ones:

Transfer-fetch

The transfer-fetch flow relies on the ability to request details of past transactions from the ledger. After I transfer tokens to the shop, specifying the Invoice ID as the transaction memo, the ledger issues a unique transaction identifier. I can then pass this identifier to the shop as proof of my payment. The shop can fetch transaction details directly from the ledger to validate the payment. Below is the interface we expect from the ledger.

service : {
  // Transfers token amount from the account of the (implicit) caller
  // to the account specified by the principal.
  // Returns a unique transaction identifier.
  // Arguments:
  //   amount - the token amount to transfer.
  //   to - the receiver of the tokens.
  //   memo - an opaque identifier attached to the transaction.
  transfer(record {
    amount : nat;
    to : principal;
    memo : opt blob;
  }) -> (nat);

  // Retrieves details of the transaction with the specified identifier.
  // Arguments:
  //   txid - a unique transaction identifier.
  fetch(txid : nat) -> (opt record {
    from : principal;
    to : principal;
    amount : nat;
    memo : opt blob;
  });
}

The flow proceeds as follows:

  1. In the negotiation phase, the webpage displays the payment details, a text field for the transaction identifier, and a big green “Done” button.
  2. In the payment phase, I use my wallet to execute the transfer({to = Shop, amount = Price, memo = InvoiceID}) call on the ledger. If the transfer is successful, the transaction receipt contains a unique transaction identifier.
  3. I paste the transaction identifier into the text field and press the green button. Once the shop receives the notification with the transaction identifier, it fetches the transaction from the ledger and validates the amount and the memo. If the validation passes, the order is complete.
A sequence diagram for the transfer-fetch payment flow.

Conclusion

We analyzed several payment flows for ledgers hosted on the Internet Computer. All the flows we discussed had three phases: negotiation, payment, and notification.

Below is a table comparing the payment flows.

invoice account approve-transfer-from transfer-notify transfer-fetch
Ledger messages 2 2 2 2
Fees 2 2 1
Ledger memory per flow 70 68 70 0
Concurrent payments
Recurrent payments
Ledger complexity simple moderate complex simple
Failure recovery not easy ok hard but rare hard

So which flow is the best one? None of them is a clear winner on all fronts. You might prefer different flows based on your design goals and the application needs.