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.
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.