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!
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.
}