LNURLP Response

After retrieving the sending VASP's public key in the previous section, we can use it to verify the signature on the LnurlpRequest.
// Before verifying a signature, make sure the request is an UMA request
// (rather than a regular lnurl request).
if (!uma.isLnurlpRequestForUma(umaQuery)) {
  // Handle the lnurl request if supported.
  return;
}

// First create an instance of NonceValidator to prevent replay attacks.
// Here, we'll use an InMemoryNonceValidator.
const twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const nonceCache = new uma.InMemoryNonceValidator(twoDaysAgo.getTime());

try {
  const isSignatureValid = await uma.verifyUmaLnurlpQuerySignature(
    umaQuery,
    pubKeyResponse,
    nonceCache
  );
  if (!isSignatureValid) {
    res.status(400).send("Invalid lnurlp signature.");
    return;
  }
} catch (e) {
  console.error(e);
  res.status(400).send("Invalid lnurlp signature.");
}
Once the sending VASP's signature is verified, the receiving VASP can create and send the LnurlpResponse.
try {
  const response = await uma.getLnurlpResponse({
    request: umaQuery,
    callback: this.getLnurlpCallback(req),
    requiresTravelRuleInfo: true,
    encodedMetadata: this.getEncodedMetadata(),
    minSendableSats: 1000,
    maxSendableSats: 10000000,
    privateKeyBytes: this.config.umaSigningPrivKey(),
    receiverKycStatus: uma.KycStatus.Verified,
    payerDataOptions: {
      identifier: { mandatory: true },
      name: { mandatory: false },
      email: { mandatory: false },
      compliance: { mandatory: true },
    },
    currencyOptions: [
      new Currency(
        /* code */ "USD",
        /* name */ "US Dollar",
        /* symbol */ "$",
        /* multiplier */ 34_150,
        /* maxSendable */ 10_000_000,
        /* minSendable */ 1,
        /* decimals */ 2,
      ),
      new Currency(
        /* code */ "SAT",
        /* name */ "Satoshi",
        /* symbol */ "sat",
        /* multiplier */ 1000,
        /* maxSendable */ 10_000_000_000,
        /* minSendable */ 1,
        /* decimals */ 0,
      ),
      new Currency(
        /* code */ "MXN",
        /* name */ "Mexican Pesos",
        /* symbol */ "Mex$",
        /* multiplier */ 1820,
        /* maxSendable */ 10_000_000,
        /* minSendable */ 1,
        /* decimals */ 2,
      ),
    ],
  });
  res.send(response);
} catch (e) {
  console.error(e);
  res.status(400).send("Failed to create lnurlp response.");
}
The encodedMetadata param is the json-encoded metadata like with standard lnurl-pay. For example, it might look something like:
[
  ["text/plain", "Pay someone on Vasp2"],
  ["text/identifier", "someone@vasp2.org"],
]
The callback param is the same as in lnurl-pay - it's the URL which the sending VASP should call to complete the pay request. If you'd like, you can include query params in this callback URL. For example, maybe you want to include some tracking request identifier between calls to save and retrieve the encoded metadata when completing the pay request later.
The payerDataOptions param indicates what information, such as payment or compliance information, the receiving VASP requires from the sending VASP in the next request (payreq). This follows the LUD-18 spec, but with an added “compliance” option, which should be set to “true” for UMA requests involving one or more VASPs.
Each entry in the currencies parameter array is an extension of the proposed LUD-21 spec:
/**
 * NOTE: When serializing and deserializing, the Currency.parse() and toJsonString()
 * functions should be used instead of JSON.stringify() and JSON.parse().
 */
export class Currency {
  constructor(
    /**
     * The currency code, eg. "USD".
     */
    public readonly code: string,

    /**
     * The full currency name in plural form, eg. "US Dollars".
     */
    public readonly name: string,

    /**
     * The symbol of the currency, eg. "$".
     */
    public readonly symbol: string,

    /**
     * Estimated millisats per smallest "unit" of this currency (eg. 1 cent in USD).
     */
    public readonly multiplier: number,

    /**
     * Minimum amount that can be sent in this currency. This is in the smallest
     * unit of the currency (eg. cents for USD).
     */
    public readonly minSendable: number,

    /**
     * Maximum amount that can be sent in this currency. This is in the smallest
     * unit of the currency (eg. cents for USD).
     */
    public readonly maxSendable: number,

    /**
     * The number of digits after the decimal point for display on the sender side,
     * and to add clarity around what the "smallest unit" of the currency is. For
     * example, in USD, by convention, there are 2 digits for cents - $5.95. In this
     * case, `decimals` would be 2. Note that the multiplier is still always in the
     * smallest unit (cents). In addition to display purposes, this field can be
     * used to resolve ambiguity in what the multiplier means. For example, if the
     * currency is "BTC" and the multiplier is 1000, really we're exchanging in SATs,
     * so `decimals` would be 8. For details on edge cases and examples, see
     * https://github.com/uma-universal-money-address/protocol/blob/main/umad-04-lnurlp-response.md.
     */
    public readonly decimals: number,

    /**
     * The major version of the UMA protocol that this currency adheres to. This is
     * not serialized to JSON.
     */
    public readonly umaVersion: number = MAJOR_VERSION,
  ) {}
};
The currencies param might be user-dependent based on the recipient's preferred currency and the receiving VASP's capabilities. At this stage, this can be viewed as an indication of the conversion rate the VASP would provide for the transaction - not a binding quote for the conversion. The conversion rate will come from your own calculations, and can be updated later to provide a binding quotation in the payreq step.
Now that you have a response object, you can respond to the LnurlpRequest with the json-serialized response object.
When the sending VASP receives the response, it can parse the response body as JSON using the SDK's helper function:
let lnurlpResponse: uma.LnurlpResponse;
try {
  lnurlpResponse = uma.parseLnurlpResponse(await response.text());
} catch (e) {
  console.error("Error parsing lnurlp response.", e);
  res.status(424).send("Error parsing Lnurlp response.");
  return;
}
The response received is decoded to an object that looks like:
export class LnurlpResponse {
  public tag: string = "payRequest";

  constructor(
    /** The URL that the sender will call for the payreq request. */
    public callback: string,
    /** The minimum amount that the sender can send in millisatoshis. */
    public minSendable: number,
    /** The maximum amount that the sender can send in millisatoshis. */
    public maxSendable: number,
    /**
     * JSON-encoded metadata that the sender can use to display information
     * to the user.
     */
    public metadata: string,
    /** Compliance-related data from the receiving VASP. Required for UMA. */
    public compliance?: LnurlComplianceResponse,
    /**
     * 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
     */
    public umaVersion?: string,
    /** The list of currencies that the receiver accepts in order of preference. */
    public currencies?: Currency[],
    /**
     * The data about the payer that the sending VASP must provide in order to
     * send a payment.
     */
    public payerData?: CounterPartyDataOptions,
    /**
     * The number of characters that the sender can include in the comment field
     * of the pay request.
     */
    public commentAllowed?: number,
    /**
     * An optional nostr pubkey used for nostr zaps (NIP-57). If set, it should
     * be a valid BIP-340 public key in hex format.
     */
    public nostrPubkey?: string,
    /**
     * Should be set to true if the receiving VASP allows nostr zaps (NIP-57).
     */
    public allowsNostr?: boolean,
  ) {}
};
The payerData field of the LnurlpResponse indicates what metadata VASP2 requires from VASP1 about the sender in the next request. Its structure is JSON encoded as specified by LUD-18 as:
{
  // Note that the sender identifier is always required in UMA.
  "identifier": { "mandatory": true },
  "name": { "mandatory": boolean },
  "email": { "mandatory": boolean },
  // New! Indicates we want uma compliance data.
  "compliance":{ "mandatory": boolean },
}
The compliance field of the LnurlpResponse contains UMA-specific metadata from VASP2 related to compliance. Its structure is:
export type LnurlComplianceResponse = {
  /** Indicates whether VASP2 has KYC information about the receiver. **/
  kycStatus: KycStatus;
  /**
   * Indicates whether VASP2 is a financial institution that requires
   * travel rule information.
   */
  isSubjectToTravelRule: boolean;
  /** The identifier of the receiver at VASP2. **/
  receiverIdentifier: string;
  /** The signature of the receiving VASP on the signable payload. **/
  signature: string;
  /** The nonce used in the signature. **/
  signatureNonce: string;
  /** The timestamp used in the signature. **/
  signatureTimestamp: number;
};
At this point, the sending VASP knows the receiver is a verified user at the receiving VASP. It knows the receiver's preferred currencies and an indication or estimate of the corresponding exchange rates. The sending VASP's wallet application might then move to the amount entry screen, showing the preferred receiving currency and exchange rate from the sender's preferred currency.
UMA Amount Entered
When the user enters an amount and confirms, the sending VASP needs to send the payreq request to get an invoice that the sender can pay.