Initial LNURLP Request
The first request from the sending VASP (VASP1) to the receiving VASP (VASP2) is an lnurlp request corresponding to the Lightning Address (LUD-16) specification, but with some additional query parameters.
If $alice@vasp1.com is paying $bob@vasp2.com, the request looks like:
GET vasp2.com/.well-known/lnurlp/$bob?umaVersion=1.0&nonce=1234&vaspDomain=vasp1.com&signature=abcd&isSubjectToTravelRule=true×tamp=12345678
-
umaVersion is the UMA standard version supported by VASP1. See the versioning section below.
-
isSubjectToTravelRule indicates vasp1 is a financial institution that is subject to the Travel Rule.
-
nonce is a random unique string generated by the SDK that is used to prevent replay attacks.
-
timestamp is the unix timestamp in seconds (number of seconds since epoch).
-
signature is vasp1's signature over hash("$bob@vasp2.com" + nonce + timestamp). Signatures are always hex-encoded strings for UMA.
This request can be easily created using the UMA SDK:
import * as uma from "@uma-sdk/core";
const lnurlpRequestUrl = await uma.getSignedLnurlpRequestUrl({
isSubjectToTravelRule: true,
receiverAddress: receiver,
signingPrivateKey: this.config.umaSigningPrivKey(),
senderVaspDomain: hostName(req),
});
The first parameter to this function is your signing private key so that the SDK can sign the request for you. Details on keys and how to generate them can be found in the previous section. Now that you have a signed request URL, you can send the request using your preferred request framework:
let response: globalThis.Response;
try {
response = await fetch(lnurlpRequestUrl);
} catch (e) {
res.status(424).send("Error fetching Lnurlp request.");
return;
}
if (!response.ok) {
res.status(424).send(`Error fetching Lnurlp request. ${response.status}`);
return;
}
The receiving VASP can parse the incoming lnurlp request using the SDK:
let lnurlpRequest: uma.LnurlpRequest;
try {
lnurlpRequest = uma.parseLnurlpRequest(await response.text());
} catch (e) {
console.error("Error parsing lnurlp response.", e);
res.status(424).send("Error parsing Lnurlp response.");
return;
}
The parsed request object has the structure:
/**
* LnurlpRequest is the first request in the UMA protocol. It is sent by the VASP
* that is sending the payment to find out information about the receiver.
*
* Note: Several of these fields are required for UMA requests, but not for regular
* LNURLP requests. They are optional in this type to allow for both types of
* requests to be parsed.
*/
export type LnurlpRequest = {
/** The address of the user at VASP2 that is receiving the payment. */
receiverAddress: string;
/**
* A random string that is used to prevent replay attacks.
* Required for UMA requests.
*/
nonce?: string | undefined;
/**
* The base64-encoded signature of sha256(ReceiverAddress|Nonce|Timestamp)
* Required for UMA requests.
*/
signature?: string | undefined;
/**
* Indicates VASP1 is a financial institution that requires travel rule
* information. Required for UMA requests.
*/
isSubjectToTravelRule?: boolean | undefined;
/**
* The domain of the VASP that is sending the payment. It will be used by VASP2
* to fetch the public keys of VASP1. Required for UMA requests.
*/
vaspDomain?: string | undefined;
/**
* The unix timestamp of when the request was sent. Used in the signature.
* Required for UMA requests.
*/
timestamp?: Date | undefined;
/**
* The version of the UMA protocol that VASP2 has chosen for this transaction
* based on its own support and VASP1's specified preference in the LnurlpRequest.
* For the version negotiation flow, see
* https://static.swimlanes.io/87f5d188e080cb8e0494e46f80f2ae74.png
*/
umaVersion?: string | undefined;
};
Next, the receiving VASP needs to verify the signature of the sending VASP in the
LnurlpRequest
object.