Payreq Response
The receiving VASP needs to construct an invoice for the sending VASP for the correct amount and
then return a valid
PayReqResponse
. The SDK can help with this process. First, we'll need an
implementation of the UmaInvoiceCreator
interface.export interface UmaInvoiceCreator {
/**
* Creates an invoice with the given amount and encoded LNURL metadata.
*
* @param amountMsats The amount of the invoice in millisatoshis.
* @param metadata The metadata that will be added to the invoice's metadata hash
* field.
* @param receiverIdentifier Optional identifier of the receiver.
* @return The encoded BOLT-11 invoice that should be returned to the sender for
* the given [PayRequest] wrapped in a [Promise].
*/
createUmaInvoice(amountMsats: number, metadata: string, receiverIdentifier: string | undefined): Promise<string>;
}
You can use any node implementation to create the invoice and wrap the invoice creation in an
implementation of this interface. Consult your node's documentation for how to create an invoice
with the correct amount and metadata hash.
From there, we can use this invoice creator and the SDK to construct the
PayReqResponse
.// These two functions are implementation-specific and need to be written by you:
// The receiver's utxos might be used by the sending VASP to pre-screen the
// transaction with their compliance provider.
const receiverChannelUtxos = await this.getReceiverChannelUtxos(receiverUma);
// This callback is the URL path that the sending VASP will call to send UTXOs of
// the channel that the sender used to receive the payment once it completes. See
// the "Sending the Payment & Post-transaction Hooks" section for more info.
const utxoCallback = "https://vasp2.com/api/uma/utxocallback?txid=" + transactionId;
// If known, the public key of the receiver's node. If supported by the sending
// VASP's compliance provider, this will be used to pre-screen the sender's UTXOs
// for compliance purposes.
const receiverNodePubKey = await this.getReceiverNodePubKey(identifier);
// You can populate payee data based on what was requested in the pay request.
const payeeData = {
"name": "Alice",
"email": "alice@mailcompany.org",
"identifier": "$alice@vasp2.com"
};
const isUmaRequest = payreq.isUma();
let response: uma.PayReqResponse;
try {
response = await uma.getPayReqResponse({
request: payreq,
conversionRate: exchangeRate,
receivingCurrencyCode: "USD",
receivingCurrencyDecimals: 2,
invoiceCreator: umaInvoiceCreator,
metadata: this.getEncodedMetadata(requestUrl, user),
receiverChannelUtxos: isUmaRequest
? receiverChannelUtxos
: undefined,
receiverFeesMillisats: 0,
receiverNodePubKey: isUmaRequest
? receiverNodePubKey
: undefined,
utxoCallback: isUmaRequest
? utxoCallback
: undefined,
payeeData,
receivingVaspPrivateKey: isUmaRequest
? this.config.umaSigningPrivKey()
: undefined,
payeeIdentifier: payeeData.identifier,
});
res.send(response.toJsonSchemaObject());
} catch (e) {
console.error(e);
res.status(500).send("Failed to generate UMA response.");
}
A note on exchange rates and expiration: The conversion rate returned in the initial
LnurlpResponse
is just considered an estimate. However, in the PayReqResponse
, you
are meant to be providing an exchange rate quote that is valid for the transaction until
the invoice you've created expires, so that the sender can ensure the end recipient receives
the intended amount by sending enough to cover any exchange costs and other fees charged by
the receiving VASP. Therefore, you should approach these rate quotations and invoice expiries
consistent with your current customer agreements and practices for these transactions.In this example, we're using a 10 minute expiration, but you should set this based on the
considerations above. Note that if the sending VASP's user takes a while to confirm and
send the payment, the invoice may expire. Some potential UX solutions are outlined in the
next section, “Sending the Payment & Post-transaction Hooks”.
There is also a
receiverFeesMillisats
field in which you can include any exchange fees
separately from the exchange rate. This gives both the sender and receiver more clarity around
transaction costs. Both the exchange rate and fees will be included in the created invoice amount.The response can now be serialized to JSON and sent back to the sending VASP in the response body.
When the sending VASP receives the response, it needs to first deserialize the response body to a
PayReqResponse
object:const payResponse = await uma.PayReqResponse.fromJson(await response.text());
The full
PayReqResponse
structure is:export class PayReqResponse {
constructor(
/** The BOLT11 invoice that the sender will pay. */
public readonly pr: string,
/**
* The data about the receiver that the sending VASP requested in the payreq
* request. Required for UMA.
*/
public readonly payeeData: PayeeData | undefined,
/**
* Information about the payment that the receiver will receive. Includes
* Final currency-related information for the payment. Required for UMA.
*/
public readonly converted: PayReqResponsePaymentInfo | undefined,
/**
* This field may be used by a WALLET to decide whether the initial LNURL link
* will be stored locally for later reuse or erased. If disposable is null, it
* should be interpreted as true, so if SERVICE intends its LNURL links to be
* stored it must return `disposable: false`. UMA should always return
* `disposable: false`. See LUD-11.
*/
public readonly disposable: boolean | undefined,
/**
* Defines a struct which can be stored and shown to the user on payment
* success. See LUD-09.
*/
public readonly successAction: Record<string, string> | undefined,
/**
* The major version of the UMA protocol that this currency adheres to. This is
* not serialized to JSON.
*/
public readonly umaMajorVersion: number,
/**
* Usually just an empty list from legacy LNURL, which was replaced by route
* hints in the BOLT11 invoice.
*/
public readonly routes: Route[] = [],
) {}
// ... functions to serialize and deserialize
};
export type PayReqResponseCompliance = {
/** nodePubKey is the public key of the receiver's node if known. */
nodePubKey?: string|undefined|null;
/**
* utxos is a list of UTXOs of channels over which the receiver will likely
* receive the payment.
*/
utxos: string[];
/**
* utxoCallback is the URL that the sender VASP will call to send UTXOs of
* the channel that the sender used to send the payment once it completes.
*/
utxoCallback?: string|undefined|null,
/**
* Signature is the base64-encoded signature of
* sha256(SenderAddress|ReceiverAddress|Nonce|Timestamp).
*
* Note: This field is optional for UMA v0.X backwards-compatibility. It is
* required for UMA v1.X.
*/
signature?: string|undefined|null,
/**
* Nonce is a random string that is used to prevent replay attacks.
*
* Note: This field is optional for UMA v0.X backwards-compatibility. It is
* required for UMA v1.X.
*/
signatureNonce?: string|undefined|null,
/**
* Timestamp is the unix timestamp (seconds since epoch) of when the request
* was sent. Used in the signature.
*
* Note: This field is optional for UMA v0.X backwards-compatibility. It is
* required for UMA v1.X.
*/
signatureTimestamp?: number|undefined|null,
};
export type PayReqResponsePaymentInfo = {
/**
* The amount that the receiver will receive in the receiving currency not
* including fees. The amount is specified in the smallest unit of the currency
* (eg. cents for USD).
*/
amount?: number | undefined;
/**
* currencyCode is the ISO 3-digit currency code that the receiver will receive
* for this payment.
*/
currencyCode: string;
/**
* Number of digits after the decimal point for the receiving currency. For
* example, in USD, by convention, there are 2 digits for cents - $5.95. In
* this case, `decimals` would be 2. This should align with the currency's
* `decimals` field in the LNURLP response. It is included here for convenience.
* See [UMAD-04](https://github.com/uma-universal-money-address/protocol/blob/main/umad-04-lnurlp-response.md)
* for details, edge cases, and examples.
*/
decimals: number;
/**
* The conversion rate. It is the number of millisatoshis that the receiver will
* receive for 1 unit of the specified currency.
*/
multiplier: number;
/**
* The fees charged (in millisats) by the receiving VASP for this transaction.
* This is separate from the multiplier.
*/
fee: number;
};
Like other UMA responses, the
PayReqResponse
includes signature which can be verified as follows:const verified = await verifyPayReqResponseSignature(
parsedPayreqResponse,
"$alice@vasp1.com",
"$bob@vasp2.com",
receiverPubkeyResponse,
nonceCache,
);
// If verified is false, the signature is invalid.
Now, if desired, the sending VASP can pre-screen the compliance info in the response with their
Compliance Provider by sending either
payreqResponse.Compliance.Utxos
or
payreqResponse.Compliance.NodePubKey
to the Compliance Provider.There's also a good chance that you'll want to save important
payReqResponse
data somewhere
persistent, so that when your user confirms that they want to send the payment, you still have
the encoded invoice, conversion rate, and any other identifiers you need to track the transaction
internally. If you also plan to show the sending user the conversion rate from their preferred
currency to bitcoin (or straight to the receiving currency), you should return it to your client
here. Note that the UMA standard describes only the interaction between VASPs, but the interaction
between you (a VASP) and your end-user devices is up to you. For an example implementation, see the
full example VASP implementation here.At this point, you can show the user a confirmation screen, including conversion rates, etc.