Payreq Request
The next request in the protocol is the payreq request, which corresponds to the second request of
LUD-06, but with some minor changes. This request exchanges compliance data and
tells the receiving VASP to generate an invoice for a specified amount on behalf of the receiving user.
First, the sending VASP will need to retrieve the receiving VASP's public
keys as shown in the “Fetching Public Keys and Verifying Signatures”
section. The encryption public key will be used to encrypt Travel Rule
information, if the sending VASP is sending that information.
let receivingVaspPubKeys: uma.PubKeyResponse;
try {
receivingVaspPubKeys = await uma.fetchPublicKeyForVasp({
cache: this.pubKeyCache,
vaspDomain: receivingVaspDomain,
});
} catch (e) {
console.error("Error fetching public key.", e);
res.status(424).send("Error fetching public key.");
}
Then, the sending VASP needs to get the payer info corresponding to the
required payer data specified in the
LnurlpResponse
received from the receiving VASP.const payerDataOptions = lnurlpResponse.payerData;
// NOTE: In a real application, you'd want to use the authentication context to
// pull out this information. It's not actually always Alice sending the money ;-).
// The identifier is the sender's UMA.
const identifier = "$alice@vasp1.com";
const name = payerDataOptions?.name?.mandatory ? "Alice FakeName" : undefined;
const email = payerDataOptions.email?.mandatory ? "alicefakename123@vasp1.com" : undefined;
Now let's build the request object:
// If you'd instead like to lock the amount to msats so that the sending amount
// is fixed set this to False and specify the amount in msats rather than the
// receiving currency.
isAmountInReceivingCurrency = true
// If the lnurlpResponse agreed on a different UMA version, you can parse it here
// and use it to create the pay request.
const umaMajorVersion = initialRequestData.lnurlpResponse.umaVersion
? getMajorVersion(initialRequestData.lnurlpResponse.umaVersion)
: 1;
// These three functions are implementation-specific and need to be written by you:
// The travel rule info should be a json-encoded string. Its contents depend on your
// geographic region and legal requirements. If this transaction is not subject to
// the travel rule, this can be null or an empty string.
const trInfo = generateTravelRuleInfo(identifier, receiverAddress);
// If you are using a standardized travel rule format, you can set this to something
// like "IVMS@101.2023".
const trFormat = null;
// The payer's utxos might be used by the receiver to pre-screen the transaction
// with their compliance provider.
const payerUtxos = getPayerChannelUtxos(identifier);
// If known, the public key of the sender's node. If supported by the receiving
// VASP's compliance provider, this will be used to pre-screen the sender's UTXOs
// for compliance purposes. This function is node-implementation-specific and needs
// to be written by you.
const payerNodePubKey = getPayerNodePubKey(identifier);
// This callback is the URL that the receiver will call to send UTXOs of the channel
// that the receiver used to receive the payment once it completes. See the "Sending
// the Payment & Post-transaction Hooks" section for more info. The transactionId
// here is an optional example in case you might generate if you want to track the
// transaction which has completed.
const utxoCallback = "https://vasp1.com/api/uma/utxocallback?txid=" + transactionId;
let payReq: uma.PayReq;
try {
payReq = await uma.getPayRequest({
receiverEncryptionPubKey: hexToBytes(pubKeys.encryptionPubKey),
sendingVaspPrivateKey: this.config.umaSigningPrivKey(),
receivingCurrencyCode: currencyCode,
isAmountInReceivingCurrency,
amount,
payerIdentifier: identifier,
payerKycStatus: uma.KycStatus.Verified,
utxoCallback,
trInfo,
travelRuleFormat: trFormat,
payerNodePubKey,
payerUtxos,
payerName: name,
payerEmail: email,
requestedPayeeData: {
// You can request any fields here.
// Compliance and Identifier are mandatory fields added automatically for UMA.
name: { mandatory: false },
email: { mandatory: false },
},
umaMajorVersion,
});
} catch (e) {
console.error("Error creating pay request.", e);
res.status(500).send("Error creating pay request.");
}
The
travelRuleInfo
passed here will be encrypted using the receiving VASP's
encryptionPubKey
. That way, the receiving VASP can store the encrypted
travel rule info blob and only decrypt it when needed.Retrieving the
nodePubKey
and sendingChannelUtxos
is node-implementation-specific.
See your node or Lightning provider's documentation for more information on how
to retrieve these values.We've now created a signed
PayRequest
object that can be serialized to JSON
and sent as the request body to the URL specified in the callback
field of
the LnurlpResponse
previously received from the receiving VASP.let response: globalThis.Response;
try {
response = await fetch(lnurlpResponse.callback, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: payReq.toJsonString(),
});
} catch (e) {
res.status(500).send("Error sending payreq.");
return;
}
if (!response.ok) {
res.status(424).send(`Payreq failed. ${response.status}`);
return;
}
IMPORTANT NOTE: This request is a POST request rather than a GET like in the LNURL
specification. UMA requires this to be a POST to allow for a longer request body than what is
supported with query parameters. Specifically, the Travel Rule data might be of arbitrary length.
The receiving VASP can parse the incoming payreq request by handling a route for POST requests
at whatever
callback
URL was chosen and returned in the LnurlpResponse
. Normally, the callback
URL will include either a receiving user ID or a transaction ID so that the receiving VASP can
construct the response. To parse the request object from the raw request body:let payreq: uma.PayRequest;
try {
payreq = uma.PayRequest.fromJson(req.body);
} catch (e) {
return res.status(400).send("Failed to parse pay request.");
}
The parsed
PayRequest
object has the structure:/**
* A class which wraps the `PayRequestSchema` and provides a more convenient
* interface for creating and parsing PayRequests.
*
* NOTE: The `fromJson` and `toJsonString` methods are used to convert to and from
* JSON strings. This is necessary because `JSON.stringify` will not include the
* correct field names.
*/
export class PayRequest {
constructor(
/**
* The amount of the payment in the currency specified by `currency_code`.
* This amount is in the smallest unit of the specified currency (e.g.
* cents for USD).
*/
public readonly amount: number,
/**
* The 3-character currency code that the receiver will receive for this
* payment.
*/
public readonly receivingCurrencyCode: string | undefined,
/**
* The currency code of the `amount` field. `None` indicates that `amount` is
* in millisatoshis as in LNURL without LUD-21. If this is not `None`, then
* `amount` is in the smallest unit of the specified currency (e.g. cents for
* USD). This currency code can be any currency which the receiver can quote.
* However, there are two most common scenarios for UMA:
*
* 1. If the sender wants the receiver wants to receive a specific amount in
* their receiving currency, then this field should be the same as
* `receiving_currency_code`. This is useful for cases where the sender wants
* to ensure that the receiver receives a specific amount in that destination
* currency, regardless of the exchange rate, for example, when paying for
* some goods or services in a foreign currency.
*
* 2. If the sender has a specific amount in their own currency that they
* would like to send, then this field should be left as `None` to indicate
* that the amount is in millisatoshis. This will lock the sent amount on the
* sender side, and the receiver will receive the equivalent amount in their
* receiving currency. NOTE: In this scenario, the sending VASP *should not*
* pass the sending currency code here, as it is not relevant to the receiver.
* Rather, by specifying an invoice amount in msats, the sending VASP can
* ensure that their user will be sending a fixed amount, regardless of the
* exchange rate on the receiving side.
*/
public readonly sendingAmountCurrencyCode: string | undefined,
/**
* The major version of the UMA protocol that this currency adheres to.
* This is not serialized to JSON.
*/
readonly umaMajorVersion: number,
/**
* The data about the payer that the sending VASP must provide in order to
* send a payment. This was requested by the receiver in the lnulp response.
* See LUD-18.
*/
public readonly payerData?: z.infer<typeof PayerDataSchema> | undefined,
/**
* The data about the receiver that the sending VASP would like to know from
* the receiver. See LUD-22.
*/
public readonly requestedPayeeData?: CounterPartyDataOptions | undefined,
/**
* A comment that the sender would like to include with the payment. This can
* only be included if the receiver included the `commentAllowed` field in the
* lnurlp response. The length of the comment must be less than or equal to the
* value of `commentAllowed`.
*/
public readonly comment?: string | undefined,
) {}
};
PayerData
includes sender metadata, along with the compliance info provided by the sending VASP
(KYC status, encrypted Travel Rule info, utxo data, the sending node public key, and a signature).
Note that the “KYC status” is meant to permit an UMA Participant that offers services that may not
require KYC for all customers to reveal that its user has a KYC credential (KycStatus=VERIFIED
)
or not (KycStatus=NOT_VERIFIED
). For VASPs offering custodial services only, this field may not
always be relevant.You can then fetch the sending VASP's public key from the cache and verify the signature on the
request:
// No need to verify non-UMA requests.
if (!payreq.isUma()) {
return;
}
let pubKeys: uma.PubKeyResponse;
try {
pubKeys = await uma.fetchPublicKeyForVasp({
cache: this.pubKeyCache,
vaspDomain: uma.getVaspDomainFromUmaAddress(
payreq.payerData.identifier,
),
});
} catch (e) {
console.error(e);
response.status(400).send("Failed to fetch public key.");
return;
}
try {
const isSignatureValid = await uma.verifyPayReqSignature(
payreq,
pubKeys,
nonceCache,
);
if (!isSignatureValid) {
response.status(400).send("Invalid payreq signature.");
return;
}
} catch (e) {
console.error(e);
response.status(400).send("Invalid payreq signature.");
return;
}
Next, the receiving VASP will construct and respond with the payreq response.