Skip to content

Commit

Permalink
Payer data should be a dictionary instead of a fixed struct.
Browse files Browse the repository at this point in the history
  • Loading branch information
zhenlu committed May 3, 2024
1 parent 748bfcf commit 3d6d562
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 35 deletions.
6 changes: 6 additions & 0 deletions src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub enum Error {
MissingTimestamp,
MissingNonce,
MissingSignature,
MissingPayerData,
MissingPayerDataIdentifier,
MissingPayerDataCompliance,
}

impl fmt::Display for Error {
Expand All @@ -29,6 +32,9 @@ impl fmt::Display for Error {
Self::MissingNonce => write!(f, "Missing nonce"),
Self::MissingTimestamp => write!(f, "Missing timestamp"),
Self::MissingSignature => write!(f, "Missing signature"),
Self::MissingPayerData => write!(f, "Missing payer data"),
Self::MissingPayerDataIdentifier => write!(f, "Missing payer data identifier"),
Self::MissingPayerDataCompliance => write!(f, "Missing payer data compliance"),
}
}
}
Expand Down
69 changes: 55 additions & 14 deletions src/protocol/pay_request.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,73 @@
use serde::{Deserialize, Serialize};

use super::payer_data::PayerData;
use super::{counter_party_data::CounterPartyDataOptions, payer_data::PayerData, Error};

/// PayRequest is the request sent by the sender to the receiver to retrieve an invoice.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PayRequest {
/// currency_code is the ISO 3-digit currency code that the receiver will receive for this
/// payment.
#[serde(rename = "currencyCode")]
pub currency_code: String,
// SendingAmountCurrencyCode is the currency code of the `amount` field. `nil` indicates that `amount` is in
// millisatoshis as in LNURL without LUD-21. If this is not `nil`, 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.
#[serde(rename = "sendingAmountCurrencyCode")]
pub sending_amount_currency_code: Option<String>,

/// amount is the amount that the receiver will receive for this payment in the smallest unit of
/// the specified currency (i.e. cents for USD).
// ReceivingCurrencyCode is the ISO 3-digit currency code that the receiver will receive for this payment. Defaults
// to amount being specified in msats if this is not provided.
#[serde(rename = "receivingCurrencyCode")]
pub receiving_currency_code: Option<String>,

// Amount is the amount that the receiver will receive for this payment in the smallest unit of the specified
// currency (i.e. cents for USD) if `SendingAmountCurrencyCode` is not `nil`. Otherwise, it is the amount in
// millisatoshis.
pub amount: i64,

/// PayerData is the data that the sender will send to the receiver to identify themselves.
// PayerData is the data that the sender will send to the receiver to identify themselves. Required for UMA, as is
// the `compliance` field in the `payerData` object.
#[serde(rename = "payerData")]
pub payer_data: PayerData,
pub payer_data: Option<PayerData>,

// RequestedPayeeData is the data that the sender is requesting about the payee.
#[serde(rename = "payeeData")]
pub requested_payee_data: Option<CounterPartyDataOptions>,

// Comment is 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`.
pub comment: Option<String>,

// UmaMajorVersion is the major version of the UMA protocol that the VASP supports for this currency. This is used
// for serialization, but is not serialized itself.
pub uma_major_version: i32,
}

impl PayRequest {
pub fn signable_payload(&self) -> Vec<u8> {
pub fn signable_payload(&self) -> Result<Vec<u8>, Error> {
let payer_data = self.payer_data.clone().ok_or(Error::MissingNonce)?;
let sender_address = payer_data
.identifier()
.ok_or(Error::MissingPayerDataIdentifier)?;

let compliance_data = payer_data.compliance()?;

let payload_string = format!(
"{}|{}|{}",
self.payer_data.identifier,
self.payer_data.compliance.signature_nonce,
self.payer_data.compliance.signature_timestamp,
sender_address, compliance_data.signature_nonce, compliance_data.signature_timestamp,
);
payload_string.into_bytes()
Ok(payload_string.into_bytes())
}
}
3 changes: 2 additions & 1 deletion src/protocol/payee_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use serde_json::Value;

use super::Error;

pub type PayeeData = Value;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PayeeData(pub Value);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompliancePayeeData {
Expand Down
34 changes: 28 additions & 6 deletions src/protocol/payer_data.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;

use crate::protocol::kyc_status::KycStatus;

use super::Error;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PayerDataOptions {
pub name_required: bool,
Expand Down Expand Up @@ -101,12 +104,31 @@ impl<'de> Deserialize<'de> for PayerDataOptions {
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PayerData {
pub name: Option<String>,
pub email: Option<String>,
pub identifier: String,
pub compliance: CompliancePayerData,
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PayerData(pub Value);

impl PayerData {
pub fn identifier(&self) -> Option<&str> {
self.0.get("identifier").and_then(|v| v.as_str())
}

pub fn name(&self) -> Option<&str> {
self.0.get("name").and_then(|v| v.as_str())
}

pub fn email(&self) -> Option<&str> {
self.0.get("email").and_then(|v| v.as_str())
}

pub fn compliance(&self) -> Result<CompliancePayerData, Error> {
let compliance = self
.0
.get("compliance")
.ok_or(Error::MissingPayerDataCompliance)?;
let result: CompliancePayerData = serde_json::from_value(compliance.clone())
.map_err(|_| Error::MissingPayerDataCompliance)?;
Ok(result)
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
Expand Down
44 changes: 33 additions & 11 deletions src/uma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,16 @@ pub fn verify_pay_req_signature(
pay_req: &PayRequest,
other_vasp_pub_key: &[u8],
) -> Result<(), Error> {
let payload = pay_req.signable_payload();
let payload = pay_req.signable_payload().map_err(Error::ProtocolError)?;
verify_ecdsa(
&payload,
&pay_req.payer_data.compliance.signature,
&pay_req
.clone()
.payer_data
.ok_or(Error::InvalidSignature)?
.compliance()
.map_err(Error::ProtocolError)?
.signature,
other_vasp_pub_key,
)
}
Expand Down Expand Up @@ -399,11 +405,13 @@ pub fn get_vasp_domain_from_uma_address(uma_address: &str) -> Result<String, Err
/// * `utxo_callback` - the URL that the receiver will use to fetch the sender's UTXOs.
#[allow(clippy::too_many_arguments)]
pub fn get_pay_request(
amount: i64,
receiver_encryption_pub_key: &[u8],
sending_vasp_private_key: &[u8],
currency_code: &str,
amount: i64,
receving_currency_code: &str,
is_amount_in_receving_currency_code: bool,
payer_identifier: &str,
uma_major_version: i32,
payer_name: Option<&str>,
payer_email: Option<&str>,
tr_info: Option<&str>,
Expand All @@ -412,6 +420,8 @@ pub fn get_pay_request(
payer_uxtos: &[String],
payer_node_pubkey: Option<&str>,
utxo_callback: &str,
requested_payee_data: Option<CounterPartyDataOptions>,
comment: Option<&str>,
) -> Result<PayRequest, Error> {
let compliance_data = get_signed_compliance_payer_data(
receiver_encryption_pub_key,
Expand All @@ -424,15 +434,27 @@ pub fn get_pay_request(
payer_node_pubkey,
utxo_callback,
)?;

let sending_amount_currency_code = if is_amount_in_receving_currency_code {
Some(receving_currency_code.to_string())
} else {
None
};

let payer_data = PayerData(serde_json::json!({
"identifier": payer_identifier,
"name": payer_name,
"email": payer_email,
"compliance": compliance_data,
}));
Ok(PayRequest {
currency_code: currency_code.to_string(),
sending_amount_currency_code,
receiving_currency_code: Some(receving_currency_code.to_string()),
payer_data: Some(payer_data),
comment: comment.map(|s| s.to_string()),
uma_major_version,
amount,
payer_data: PayerData {
name: payer_name.map(|s| s.to_string()),
email: payer_email.map(|s| s.to_string()),
identifier: payer_identifier.to_string(),
compliance: compliance_data,
},
requested_payee_data,
})
}

Expand Down
16 changes: 13 additions & 3 deletions src/uma_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,13 @@ mod tests {
let (sk2, pk2) = generate_keypair();

let payreq = get_pay_request(
1000,
&pk1.serialize(),
&sk2.serialize(),
"USD",
1000,
true,
"$alice@vasp1.com",
1,
None,
None,
Some("some TR info for VASP2"),
Expand All @@ -221,6 +223,8 @@ mod tests {
&[],
None,
"/api/lnurl/utxocallback?txid=1234",
None,
None,
)
.unwrap();

Expand All @@ -234,7 +238,9 @@ mod tests {
let cipher_text = hex::decode(
payreq
.payer_data
.compliance
.unwrap()
.compliance()
.unwrap()
.encrypted_travel_rule_info
.unwrap(),
)
Expand All @@ -249,11 +255,13 @@ mod tests {
let (sk2, _) = generate_keypair();

let payreq = get_pay_request(
1000,
&pk1.serialize(),
&sk2.serialize(),
"USD",
1000,
true,
"$alice@vasp1.com",
1,
None,
None,
Some("some TR info for VASP2"),
Expand All @@ -262,6 +270,8 @@ mod tests {
&[],
None,
"/api/lnurl/utxocallback?txid=1234",
None,
None,
)
.unwrap();

Expand Down

0 comments on commit 3d6d562

Please sign in to comment.