Sending the Payment & Post-transaction Hooks

All that's left is to actually send the payment by paying the invoice! However, this payment needs to go from the sender's preferred currency → bitcoin → the receiver's preferred currency. On the sending VASP side, when the user confirms they want to send the payment, you first need to convert from their preferred payment currency to bitcoin so that it can travel over the Lightning Network. This actual conversion is out of scope of the UMA standard and can be done with whatever mechanism is available to you with whatever fees are normally applied to such a conversion for your users.
Once the funds are converted to bitcoin and in a channel on your Lightning node, you just need to pay the invoice that was created and sent in the PayReqResponse. The payment is node-implementation-specific. Consult your node implementation or Lightning provider's documentation for details on how to pay an invoice.
What if the invoice expires before the user confirms the payment?
As mentioned in the previous section, UMA invoices may have short expiration periods. This means that if the sending VASP's user takes too long between the payreq and confirmation steps, the invoice may expire! As a sending VASP, you have some UX options to address this concern:
  • If the invoice has expired before the user confirms payment, provide a landing experience which clearly informs the user the exchange rate has expired, and give the user a way to get an updated exchange rate to trigger a fresh invoice, like a primary CTA at the bottom of the screen which says "Update". Be sure to show the new exchange rate in the UI.
  • Automatically refresh the invoice before it expires from your client application. For example, if my invoices have a 5-minute expiration time, maybe after 4 minutes, I automatically issue a new payreq request to update the invoice and exchange rate shown in the UI. You can even show a timer in the UI to indicate how long the current exchange rate is valid for. This is the recommended approach.
As a receiving VASP, you'll need to listen for a completed incoming transaction. When your user receives a payment, the UMA flow contemplates that you will convert the received bitcoin to the receiver's preferred currency at the agreed-upon conversion rate, but the actual currency conversion is out of scope of the UMA standard.
At this point, the transaction is complete, and we've successfully gone from the sender's preferred currency to the receiver's preferred currency over the Lightning Network ⚡🎉! Go ahead and show the success screen - you deserve it!
UMA Success
But wait, one more thing! What about those utxoCallback fields and post-transaction hooks we mentioned earlier? We'll need to complete the post-transaction hook process to register transactions with the Compliance Provider for transaction monitoring and any other compliance purposes.
Note: You only need to receive post-transaction hooks from your counterparty VASP for the UTXO-based compliance flow for cases where your Compliance Provider does not support lookups and registration via node public key. If your compliance provider supports using the node public key instead, you can simply pass nil for the utxoCallback field where needed. However, you do still need to send post-transaction hooks if the other VASP has provided a utxoCallback.
On the sending VASP side, you'll need to first wait for the payment to complete. When the payment has completed, you can use the payment result to retrieve the UTXOs of channels used to send the payment, along with the amounts sent over each channel. The way you retrieve this information is node-implementation-specific. Once you have it, create a list of UtxoWithAmount objects, which is a simple data class with a UTXO and amount in milli-satoshis. For example:
const utxos: uma.UtxoWithAmount[] =
  payment.umaPostTransactionData?.map((d) => {
    return {
      utxo: d.utxo,
      amount: convertCurrencyAmount(d.amount, CurrencyUnit.MILLISATOSHI)
        .preferredCurrencyValueRounded,
    };
  }) ?? [];
Then, using the utxoCallback field returned in the PayReqResponse, you can send the UTXOs used to complete the payment.
try {
  const callback = await getPostTransactionCallback({
    utxos,
    vaspDomain: "vasp1.com",
    signingPrivateKey,
  });
  const postTxResponse = await fetch(payReqData.utxoCallback, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(callback),
  });
  if (!postTxResponse.ok) {
    console.error(
      `Error sending post transaction callback. ${postTxResponse.status}`,
    );
  }
} catch (e) {
  console.error("Error sending post transaction callback.", e);
}
This informs the receiving VASP of the sending UTXOs used to send the transaction. It can register these with its Compliance Provider to monitor the transaction or take any other compliance steps as needed.
As the receiving VASP, you'll need to listen for completion of an incoming transaction. Again, this depends on your node implementation. Once the transaction is complete, you'll need to retrieve the UTXOs and amounts used per UTXO as above.
Sending the UTXOs to the callback URL can be implemented with any networking library. For example:
const sendUtxosToCallback = async (
  utxoCallback: string,
  utxos: uma.UtxoWithAmount[],
): Promise<void> => {
  try {
    const callback = await getPostTransactionCallback({
      utxos,
      vaspDomain: "vasp2.com",
      signingPrivateKey,
    });
    await fetch(utxoCallback, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(callback),
    });
  } catch (e) {
    console.error("Error sending post transaction callback.", e);
  }
};
Now the sending VASP can register the incoming HTLC UTXOs with its Compliance Provider for transaction monitoring. With that, the full protocol is completed!
Like previous requests, the post-transaction hook requests are signed and can be verified by the counterparty VASP.
try {
  const parsedPostTransacationCallback =
    uma.parsePostTransactionCallback(callbackJson);
  const verified = await uma.verifyPostTransactionCallbackSignature(
    parsedPostTransacationCallback,
    otherVaspPubKeyResponse,
    nonceCache,
  );
  // Check if verified is true.
} catch (e) {
  // Handle error.
}