From cc2855f24a8fca0440b1dd2b505335d4fa84ff53 Mon Sep 17 00:00:00 2001 From: Zhen Lu Date: Fri, 27 Oct 2023 16:22:08 -0700 Subject: [PATCH] [WIP] implement part of vasp1. --- examples/uma-demo/Cargo.toml | 6 +- examples/uma-demo/src/main.rs | 76 +++- examples/uma-demo/src/vasp.rs | 395 ++++++++++++++++++-- examples/uma-demo/src/vasp_request_cache.rs | 87 +++++ 4 files changed, 515 insertions(+), 49 deletions(-) create mode 100644 examples/uma-demo/src/vasp_request_cache.rs diff --git a/examples/uma-demo/Cargo.toml b/examples/uma-demo/Cargo.toml index 93a187b..70db466 100644 --- a/examples/uma-demo/Cargo.toml +++ b/examples/uma-demo/Cargo.toml @@ -10,5 +10,9 @@ actix-web = "4.4.0" chrono = "0.4.31" hex = "0.4.3" lightspark = { path = "../../lightspark" } +reqwest = "0.11.22" +serde = "1.0.190" serde_json = "1.0.107" -uma = "0.1.1" +uma = { path = "../../../../uma-rust-sdk"} +url = "2.4.1" +uuid = { version = "1.5.0", features = ["v4"] } diff --git a/examples/uma-demo/src/main.rs b/examples/uma-demo/src/main.rs index a6f9dc3..a29595b 100644 --- a/examples/uma-demo/src/main.rs +++ b/examples/uma-demo/src/main.rs @@ -1,52 +1,87 @@ pub mod config; pub mod vasp; -use std::sync::Arc; +pub mod vasp_request_cache; +use std::sync::{Arc, Mutex}; -use actix_web::{get, post, web, App, HttpServer, Responder}; +use actix_web::{get, post, web, App, HttpRequest, HttpServer, Responder}; +use lightspark::{ + client::LightsparkClient, key::Secp256k1SigningKey, request::auth_provider::AccountAuthProvider, +}; +use serde::Deserialize; +use uma::public_key_cache::InMemoryPublicKeyCache; -use crate::vasp::{VASPReceiving, VASPSending, VASP}; +use crate::vasp::SendingVASP; #[get("/api/umalookup/{receiver}")] -async fn uma_lookup(vasp: web::Data>, receiver: web::Path) -> impl Responder { +async fn uma_lookup( + vasp: web::Data>>>, + receiver: web::Path, +) -> impl Responder { + let mut vasp = vasp.lock().unwrap(); vasp.handle_client_uma_lookup(receiver.as_str()) } +#[derive(Deserialize)] +struct PayReqParam { + amount: i64, + + #[serde(rename = "currencyCode")] + currency_code: String, +} + #[get("/api/umapayreq/{callback_uuid}")] async fn client_payreq( - vasp: web::Data>, + vasp: web::Data>>>, callback_uuid: web::Path, + params: web::Query, ) -> impl Responder { - vasp.handle_client_pay_req(callback_uuid.as_str()) + let mut vasp = vasp.lock().unwrap(); + vasp.handle_client_pay_req( + callback_uuid.as_str(), + params.amount, + params.currency_code.as_str(), + ) + .await } #[get("/api/sendpayment/{callback_uuid}")] async fn send_payment( - vasp: web::Data>, + vasp: web::Data>>>, callback_uuid: web::Path, ) -> impl Responder { + let mut vasp = vasp.lock().unwrap(); vasp.handle_client_payment_confirm(callback_uuid.as_str()) } #[get("/.well-known/lnurlp/{username}")] async fn well_known_lnurlp( - vasp: web::Data>, + req: HttpRequest, + vasp: web::Data>>, username: web::Path, ) -> impl Responder { - vasp.handle_well_known_lnurlp(username.as_str()) + vasp.handle_well_known_lnurlp(&req, username.as_str()) } #[get("/api/uma/payreq/{uuid}")] -async fn lnurl_payreq(vasp: web::Data>, uuid: web::Path) -> impl Responder { +async fn lnurl_payreq( + vasp: web::Data>>, + uuid: web::Path, +) -> impl Responder { vasp.handle_lnurl_payreq(uuid.as_str()) } #[post("/api/uma/payreq/{uuid}")] -async fn uma_payreq(vasp: web::Data>, uuid: web::Path) -> impl Responder { +async fn uma_payreq( + vasp: web::Data>>, + uuid: web::Path, +) -> impl Responder { vasp.handle_uma_payreq(uuid.as_str()) } #[get("/.well-known/lnurlpubkey")] -async fn pubkey_request(vasp: web::Data>) -> impl Responder { +async fn pubkey_request( + vasp: web::Data>>, +) -> impl Responder { vasp.handle_pubkey_request() } @@ -56,9 +91,22 @@ async fn main() -> std::io::Result<()> { .unwrap() .parse::() .unwrap(); - let vasp = Arc::new(VASP { + let pubkey_cache = InMemoryPublicKeyCache::new(); + let req_cache = vasp_request_cache::Vasp1PayReqCache::new(); + let config = config::Config::new_from_env(); + let auth_provider = AccountAuthProvider::new( + config.api_client_id.clone(), + config.api_client_secret.clone(), + ); + let lightspark_client = LightsparkClient::::new(auth_provider).unwrap(); + + let vasp = Arc::new(Mutex::new(SendingVASP { config: config::Config::new_from_env(), - }); + pubkey_cache, + request_cache: req_cache, + client: lightspark_client, + })); + HttpServer::new(move || { let vasp = Arc::clone(&vasp); App::new() diff --git a/examples/uma-demo/src/vasp.rs b/examples/uma-demo/src/vasp.rs index ebab638..5393c81 100644 --- a/examples/uma-demo/src/vasp.rs +++ b/examples/uma-demo/src/vasp.rs @@ -1,11 +1,29 @@ use std::fmt::{self, format}; -use actix_web::{HttpResponse, Responder}; +use actix_web::{HttpRequest, HttpResponse, Responder}; use chrono::{Duration, Utc}; +use lightspark::{ + client::LightsparkClient, + key::Secp256k1SigningKey, + objects::{ + lightspark_node::LightsparkNode, + lightspark_node_with_remote_signing::LightsparkNodeWithRemoteSigning, + }, +}; +use serde::{Deserialize, Serialize}; use serde_json::json; -use uma::protocol::PubKeyResponse; +use uma::{ + payer_data::PayerDataOptions, + protocol::PubKeyResponse, + public_key_cache::PublicKeyCache, + uma::{ + fetch_public_key_for_vasp, get_pay_request, get_signed_lnurlp_request_url, + is_uma_lnurl_query, parse_lnurlp_response, verify_uma_lnurlp_response_signature, + }, +}; +use url::Url; -use crate::config::Config; +use crate::{config::Config, vasp_request_cache::Vasp1PayReqCache}; pub enum Error { SigningKeyParseError, @@ -20,26 +38,26 @@ impl fmt::Display for Error { } } -pub trait VASPSending { - fn handle_client_uma_lookup(&self, receiver: &str) -> String; - fn handle_client_pay_req(&self, callback_uuid: &str) -> String; - fn handle_client_payment_confirm(&self, callback_uuid: &str) -> String; -} - -pub trait VASPReceiving { - fn handle_well_known_lnurlp(&self, username: &str) -> String; - fn handle_lnurl_payreq(&self, uuid: &str) -> String; - fn handle_uma_payreq(&self, uuid: &str) -> String; -} - -#[derive(Debug)] -pub struct VASP { +pub struct SendingVASP { pub config: Config, + pub pubkey_cache: T, + pub request_cache: Vasp1PayReqCache, + pub client: LightsparkClient, } -impl VASP { - pub fn new(config: Config) -> VASP { - VASP { config } +impl SendingVASP { + pub fn new( + config: Config, + pubkey_cache: T, + request_cache: Vasp1PayReqCache, + client: LightsparkClient, + ) -> SendingVASP { + SendingVASP { + config, + pubkey_cache, + request_cache, + client, + } } pub fn handle_pubkey_request(&self) -> impl Responder { @@ -75,32 +93,341 @@ impl VASP { HttpResponse::Ok().json(response) } -} -impl VASPSending for VASP { - fn handle_client_uma_lookup(&self, receiver: &str) -> String { - format(format_args!("Hello {}!", receiver)) + pub fn handle_client_uma_lookup(&mut self, receiver: &str) -> impl Responder { + let address_parts = receiver.split_terminator('@').collect::>(); + if address_parts.len() != 2 { + return HttpResponse::BadRequest().json(json!({ + "status": "ERROR", + "reason": "Invalid receiver address", + })); + } + + let receiver_id = address_parts[0]; + let receiver_vasp = address_parts[1]; + let signing_key = match hex::decode(&self.config.uma_signing_private_key_hex) { + Ok(bytes) => bytes, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error parsing signing key", + })) + } + }; + + let lnurlp_request = match get_signed_lnurlp_request_url( + &signing_key, + receiver, + "localhost:8080", + true, + None, + ) { + Ok(url) => url, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error generating lnurlp request", + })) + } + }; + + let response = match reqwest::blocking::get(lnurlp_request) { + Ok(response) => response, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error fetching lnurlp request", + })) + } + }; + + if response.status() == reqwest::StatusCode::PRECONDITION_FAILED { + todo!("handle unsupported version") + } + + if response.status() != reqwest::StatusCode::OK { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": format!("Failed response from receiver: {}", response.status()), + })); + } + + let response_body = match response.text() { + Ok(body) => body, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error reading response body", + })) + } + }; + + let lnurlp_response = match parse_lnurlp_response(response_body.as_bytes()) { + Ok(response) => response, + Err(e) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": format!("Failed to parse lnurlp response from receiver: {}", e), + })) + } + }; + + let receiving_vasp_pubkey = + match fetch_public_key_for_vasp(receiver_vasp, &mut self.pubkey_cache) { + Ok(pubkey) => pubkey, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Failed to fetch public key for receiving VASP", + })) + } + }; + + let receving_signing_pubkey = receiving_vasp_pubkey.signing_pub_key; + + let result = + verify_uma_lnurlp_response_signature(&lnurlp_response, &receving_signing_pubkey); + + if result.is_err() { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Failed to verify lnurlp response signature from receiver", + })); + } + + let callback_uuid = self.request_cache.save_lnurlp_response_data( + &lnurlp_response, + receiver_id, + receiver_vasp, + ); + + HttpResponse::Ok().json(json!({ + "currencies": lnurlp_response.currencies.clone(), + "minSendSats": lnurlp_response.min_sendable.clone(), + "maxSendSats": lnurlp_response.max_sendable.clone(), + "callbackUuid": callback_uuid, + "receiverKYCStatus": lnurlp_response.compliance.kyc_status.clone(), // You might not actually send this to a client in practice. + })) } - fn handle_client_pay_req(&self, callback_uuid: &str) -> String { - format(format_args!("Hello {}!", callback_uuid)) + pub async fn handle_client_pay_req( + &mut self, + callback_uuid: &str, + amount: i64, + currency_code: &str, + ) -> impl Responder { + let initial_request_data = match self.request_cache.get_lnurlp_response_data(callback_uuid) + { + Some(data) => data, + None => { + return HttpResponse::BadRequest().json(json!({ + "status": "ERROR", + "reason": "Invalid or missing callback UUID", + })) + } + }; + + if amount <= 0 { + return HttpResponse::BadRequest().json(json!({ + "status": "ERROR", + "reason": "Invalid amount", + })); + } + + let mut currency_supported = false; + for currency in initial_request_data.lnurl_response.currencies.iter() { + if currency.code == currency_code { + currency_supported = true; + break; + } + } + + if !currency_supported { + return HttpResponse::BadRequest().json(json!({ + "status": "ERROR", + "reason": "Unsupported currency", + })); + } + + let uma_signing_private_key = match hex::decode(&self.config.uma_signing_private_key_hex) { + Ok(bytes) => bytes, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error parsing signing key", + })) + } + }; + + let vasp2_pubkey = match fetch_public_key_for_vasp( + &initial_request_data.vasp2_domain, + &mut self.pubkey_cache, + ) { + Ok(pubkey) => pubkey, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Failed to fetch public key for receiving VASP", + })) + } + }; + + let payer_info = get_payer_info(&initial_request_data.lnurl_response.required_payer_data); + let tr_info = "Here is some fake travel rul info. It's up to you to implement this."; + let node = match self + .client + .get_entity::(&self.config.node_id) + .await + { + Ok(node) => node, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Failed to fetch node", + })) + } + }; + + let sender_utxos = node.get_uma_prescreening_utxos(); + + let vasp2_encryption_pubkey = vasp2_pubkey.encryption_pub_key; + + // This is the node pub key of the sender's node. In practice, you'd want to get this from the sender's node. + let sender_node_pubkey = "048e45c1f79463468f5824931ac5e34295426a0f954126903e2e3be0aa649e798b708944ba27c0be0a337362bde7f8e474ea8182b2ede5b8980f30e00af5b5df2e"; + + // In practice, you'd probably use some real transaction ID here. + let _tx_id = "1234"; + + let _pay_req = match get_pay_request( + &vasp2_encryption_pubkey, + &uma_signing_private_key, + currency_code, + amount, + &payer_info.identifier, + payer_info.name.as_deref(), + payer_info.email.as_deref(), + Some(&tr_info), + None, + uma::kyc_status::KycStatus::KycStatusVerified, + &sender_utxos, + Some(sender_node_pubkey), + "", // TODO: get the utxo callback url + ) { + Ok(pay_req) => pay_req, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error generating pay request", + })) + } + }; + + unimplemented!() } - fn handle_client_payment_confirm(&self, callback_uuid: &str) -> String { - format(format_args!("Hello {}!", callback_uuid)) + pub fn handle_client_payment_confirm(&mut self, callback_uuid: &str) -> impl Responder { + let pay_req_data = match self.request_cache.get_pay_req_data(callback_uuid) { + Some(data) => data, + None => { + return HttpResponse::BadRequest().json(json!({ + "status": "ERROR", + "reason": "Invalid or missing callback UUID", + })) + } + }; + + if pay_req_data.invoice_data.amount.original_value == 0 { + return HttpResponse::BadRequest().json(json!({ + "status": "ERROR", + "reason": "cannot pay zero-amount invoices via UMA", + })); + } + + let seed_bytes = match hex::decode(&self.config.node_master_seed_hex) { + Ok(bytes) => bytes, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error parsing node master seed", + })) + } + }; + + match self.client.provide_master_seed( + &self.config.node_id, + seed_bytes.to_vec(), + lightspark::objects::bitcoin_network::BitcoinNetwork::Mainnet, + ) { + Ok(_) => (), + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error providing master seed to node", + })) + } + }; + + unimplemented!() } -} -impl VASPReceiving for VASP { - fn handle_well_known_lnurlp(&self, username: &str) -> String { - format(format_args!("Hello {}!", username)) + pub fn handle_well_known_lnurlp( + &self, + request: &HttpRequest, + username: &str, + ) -> impl Responder { + if username != self.config.username { + return HttpResponse::NotFound().json(json!({ + "status": "ERROR", + "reason": format!("User not found: {}", username), + })); + } + + let request_url = match Url::parse(request.uri().to_string().as_str()) { + Ok(url) => url, + Err(_) => { + return HttpResponse::InternalServerError().json(json!({ + "status": "ERROR", + "reason": "Error parsing request URL", + })) + } + }; + + if is_uma_lnurl_query(&request_url) { + return HttpResponse::Ok().into(); + } + unimplemented!() } - fn handle_lnurl_payreq(&self, uuid: &str) -> String { + pub fn handle_lnurl_payreq(&self, uuid: &str) -> String { format(format_args!("Hello {}!", uuid)) } - fn handle_uma_payreq(&self, uuid: &str) -> String { + pub fn handle_uma_payreq(&self, uuid: &str) -> String { format(format_args!("Hello {}!", uuid)) } } + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct PayerInfo { + name: Option, + email: Option, + identifier: String, +} + +fn get_payer_info(option: &PayerDataOptions) -> PayerInfo { + let name = match option.name_required { + true => Some("Alice FakeName".to_string()), + false => None, + }; + + let email = match option.email_required { + true => Some("$alice@vasp2.com".to_string()), + false => None, + }; + + PayerInfo { + name, + email, + identifier: "$alice@vasp1.com".to_string(), + } +} diff --git a/examples/uma-demo/src/vasp_request_cache.rs b/examples/uma-demo/src/vasp_request_cache.rs new file mode 100644 index 0000000..83a1563 --- /dev/null +++ b/examples/uma-demo/src/vasp_request_cache.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; + +use lightspark::objects::invoice_data::InvoiceData; +use uma::protocol::LnurlpResponse; + +#[derive(Clone)] +pub struct Vasp1InitialRequestData { + pub lnurl_response: LnurlpResponse, + pub receiver_id: String, + pub vasp2_domain: String, +} + +#[derive(Clone)] +pub struct Vasp1PayReqData { + pub encoded_invoice: String, + pub utxo_callback: String, + pub invoice_data: InvoiceData, +} + +#[derive(Clone)] +pub struct Vasp1PayReqCache { + pub uma_request_cache: HashMap, + pub pay_req_cache: HashMap, +} + +impl Default for Vasp1PayReqCache { + fn default() -> Self { + Self::new() + } +} + +impl Vasp1PayReqCache { + pub fn new() -> Self { + Vasp1PayReqCache { + uma_request_cache: HashMap::new(), + pay_req_cache: HashMap::new(), + } + } + + pub fn get_lnurlp_response_data(&self, key: &str) -> Option { + self.uma_request_cache.get(key).cloned() + } + + pub fn save_lnurlp_response_data( + &mut self, + lnurlp_response: &LnurlpResponse, + receiver_id: &str, + vasp2_domain: &str, + ) -> String { + let uuid = uuid::Uuid::new_v4().to_string(); + let data = Vasp1InitialRequestData { + lnurl_response: lnurlp_response.clone(), + receiver_id: receiver_id.to_string(), + vasp2_domain: vasp2_domain.to_string(), + }; + self.uma_request_cache.insert(uuid.clone(), data); + uuid + } + + pub fn delete_lnurlp_response_data(&mut self, key: &str) { + self.uma_request_cache.remove(key); + } + + pub fn get_pay_req_data(&self, key: &str) -> Option { + self.pay_req_cache.get(key).cloned() + } + + pub fn save_pay_req_data( + &mut self, + request_uuid: &str, + encoded_invoice: &str, + utxo_callback: &str, + invoice_data: &InvoiceData, + ) -> String { + let data = Vasp1PayReqData { + encoded_invoice: encoded_invoice.to_string(), + utxo_callback: utxo_callback.to_string(), + invoice_data: invoice_data.clone(), + }; + self.pay_req_cache.insert(request_uuid.to_string(), data); + request_uuid.to_string() + } + + pub fn delete_pay_req_data(&mut self, key: &str) { + self.pay_req_cache.remove(key); + } +}