diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 435aee8..df23e2e 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -19,6 +19,9 @@ pub enum Error { MissingTimestamp, MissingNonce, MissingSignature, + MissingPayerData, + MissingPayerDataIdentifier, + MissingPayerDataCompliance, } impl fmt::Display for Error { @@ -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"), } } } diff --git a/src/protocol/pay_request.rs b/src/protocol/pay_request.rs index 9a0e8a1..083c4f6 100644 --- a/src/protocol/pay_request.rs +++ b/src/protocol/pay_request.rs @@ -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, - /// 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, + + // 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, + + // RequestedPayeeData is the data that the sender is requesting about the payee. + #[serde(rename = "payeeData")] + pub requested_payee_data: Option, + + // 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, + + // 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 { + pub fn signable_payload(&self) -> Result, 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()) } } diff --git a/src/protocol/payee_data.rs b/src/protocol/payee_data.rs index 0481aec..d4a05a0 100644 --- a/src/protocol/payee_data.rs +++ b/src/protocol/payee_data.rs @@ -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 { diff --git a/src/protocol/payer_data.rs b/src/protocol/payer_data.rs index f2d1126..df2aa94 100644 --- a/src/protocol/payer_data.rs +++ b/src/protocol/payer_data.rs @@ -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, @@ -101,12 +104,31 @@ impl<'de> Deserialize<'de> for PayerDataOptions { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct PayerData { - pub name: Option, - pub email: Option, - 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 { + 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)] diff --git a/src/uma.rs b/src/uma.rs index 1d25664..9b1ffc0 100644 --- a/src/uma.rs +++ b/src/uma.rs @@ -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, ) } @@ -399,11 +405,13 @@ pub fn get_vasp_domain_from_uma_address(uma_address: &str) -> Result, payer_email: Option<&str>, tr_info: Option<&str>, @@ -412,6 +420,8 @@ pub fn get_pay_request( payer_uxtos: &[String], payer_node_pubkey: Option<&str>, utxo_callback: &str, + requested_payee_data: Option, + comment: Option<&str>, ) -> Result { let compliance_data = get_signed_compliance_payer_data( receiver_encryption_pub_key, @@ -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, }) } diff --git a/src/uma_test.rs b/src/uma_test.rs index 46b911e..0cd642a 100644 --- a/src/uma_test.rs +++ b/src/uma_test.rs @@ -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"), @@ -221,6 +223,8 @@ mod tests { &[], None, "/api/lnurl/utxocallback?txid=1234", + None, + None, ) .unwrap(); @@ -234,7 +238,9 @@ mod tests { let cipher_text = hex::decode( payreq .payer_data - .compliance + .unwrap() + .compliance() + .unwrap() .encrypted_travel_rule_info .unwrap(), ) @@ -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"), @@ -262,6 +270,8 @@ mod tests { &[], None, "/api/lnurl/utxocallback?txid=1234", + None, + None, ) .unwrap();