Lightspark
Lightspark

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.

Sending VASP

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.
vasp2_pubkeys = uma.fetch_public_key_for_vasp(initial_request_data.vasp_domain, pubkey_cache)
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.
payer_data_options = lnurlp_response.required_payer_data

# 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.
sender_identifier = "$alice@vasp1.com"

# You may optionally provide information if CounterpartyDataOption.mandatory is false
name_option = payer_data_options.get(CounterpartyDataKeys.NAME.value)
sender_name = "Alice FakeName" if name_option else None

email_option = payer_data_options.get(CounterpartyDataKeys.EMAIL.value)
sender_email = "alicefakename123@vasp1.com" if email_option and email_option.mandatory else None

# 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. This function is implementation-specific and
# needs to be written by you.
if lnurl_response.compliance.is_subject_to_travel_rule:
  travel_rule_info = generate_travel_rule_info(sender_identifier, receiver_address)
else:
  travel_rule_info = None

# If you are using a standardized travel rule format, you can set
# this to something like "IVMS@101.2023".
travel_rule_format = None

# The sender's utxos might be used by the receiver to pre-screen
# the transaction with their compliance provider. See example below
# for retrieving using the lightspark-sdk. This function is
# implementation-specific and need to be written by you.
sender_utxos = get_node_utxos(sender_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. See example
# below for retrieving using the lightspark-sdk. This function is
# implementation-specific and need to be written by you.
payer_node_pubkey = get_node_pubkey(sender_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.
utxo_callback = "https://vasp1.com/api/uma/utxocallback?txid=" + transaction_id

sender_compliance_data = uma.create_compliance_payer_data(
  receiver_encryption_pubkey=vasp2_encryption_pubkey,
  signing_private_key=signing_private_key,
  payer_identifier=sender_identifier,
  travel_rule_info=travel_rule_info,
  travel_rule_format=travel_rule_format,
  payer_kyc_status=uma.KycStatus.VERIFIED,
  payer_utxos=sender_utxos,
  payer_node_pubkey=sender_node_pubkey,
  utxo_callback=utxo_callback,
)
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.
is_amount_in_receiving_currency = True

# If the lnurlpResponse agreed on a different UMA version, you can parse
# it here and use it to create the pay request.
uma_version = initial_request_data.lnurlp_response.uma_version
if uma_version is not None:
    uma_version = ParsedVersion.load(uma_version).major

requested_payee_data = create_counterparty_data_options(
    {
        CounterpartyDataKeys.COMPLIANCE.value: True,
        CounterpartyDataKeys.IDENTIFIER.value: True,
        CounterpartyDataKeys.EMAIL.value: False,
        CounterpartyDataKeys.NAME.value: False,
    }
)

# compliance and identifier are mandatory fields and are added
# automatically
payer_data: PayerData = {
  CounterpartyDataKeys.NAME.value: sender_name,
}

pay_request = uma.create_pay_request_with_payer_data(
  receiving_currency_code=receiving_currency_code,
  is_amount_in_receiving_currency=is_amount_in_receiving_currency,
  amount=amount,
  payer_identifier=sender_identifier,
  payer_compliance=payer_compliance,
  requested_payee_data=requested_payee_data,
  uma_major_version=uma_version if uma_version is not None else 1,
  payer_data=payer_data,
)
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. However, here are examples of how to fetch this data assuming you are using the lightspark-sdk with a single node. To retrieve the nodePubKey, you need to fetch the node:
entity = lightspark_client.get_entity(config.node_id, lightspark.LightsparkNode)
node_pubkey = node.public_key
Next, let's retrieve the sending channel UTXOs. Assuming we already have the node fetched from above, we can directly grab its pre-screening UTXOs:
node_utxos = node.uma_prescreening_utxos
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.
import requests

pay_request_response = requests.post(
  url=lnurlp_response.callback,
  json=pay_request.to_dict(),
  timeout=20,
)
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:
pay_request = uma.parse_pay_request(http_request_body)
The parsed PayRequest object has the structure:
@dataclass
class PayRequest:
    sending_amount_currency_code: Optional[str]
    """
    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.
    """

    receiving_currency_code: Optional[str]
    """
    The currency code for the currency that the receiver will receive
    for this payment.
    """

    amount: int
    """
    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).
    """

    payer_data: Optional[PayerData]
    """
    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.
    """

    requested_payee_data: Optional[CounterpartyDataOptions]
    """
    The data about the receiver that the sending VASP would like to
    know from the receiver.
    See LUD-22.
    """

    comment: Optional[str] = None
    """
    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`.
    """

    uma_major_version: Optional[int] = MAJOR_VERSION
    """
    The major version of the UMA protocol that this currency adheres
    to. This is not serialized to JSON.
    """
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:
sending_vasp_domain = uma.get_vasp_domain_from_uma_address(pay_request.payer_data.identifier)
if pay_request.is_uma_request():
    pubkey = uma.fetch_public_key_for_vasp(sending_vasp_domain, pubkey_cache)
    uma.verify_pay_request_signature(pay_request, sending_vasp_pubkey_response, nonce_cache)
    # Successfully verified the signature!
Next, the receiving VASP will construct and respond with the payreq response.