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"
sender_name = "Alice FakeName" if payer_data_options.name_required else None
sender_email = "alicefakename123@vasp1.com" if payer_data_options.email_required else None
if payer_data_options["compliance"]["mandatory"]:
# 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 need 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. This function is node-implementation-specific
# and needs 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. This function is implementation-specific and
# needs 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,
)
else:
sender_compliance_data = None
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(
{
"compliance": True,
"identifier": True,
"email": False,
"name": False,
}
)
pay_request = uma.create_pay_request(
receiving_currency_code=receiving_currency_code,
is_amount_in_receiving_currency=is_amount_in_receiving_currency,
amount=amount,
payer_identifier=sender_identifier,
payer_name=sender_name,
payer_email=sender_email,
payer_compliance=payer_compliance,
requested_payee_data=requested_payee_data,
uma_major_version=uma_version if uma_version is not None else 1,
)
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.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.