use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::amount::{Amount, CurrencyUnit};
use super::mint::MintUrl;
use super::proof::Proof;
use crate::error::FfiError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum TransportType {
Nostr,
HttpPost,
}
impl From<cdk::nuts::TransportType> for TransportType {
fn from(t: cdk::nuts::TransportType) -> Self {
match t {
cdk::nuts::TransportType::Nostr => Self::Nostr,
cdk::nuts::TransportType::HttpPost => Self::HttpPost,
}
}
}
impl From<TransportType> for cdk::nuts::TransportType {
fn from(t: TransportType) -> Self {
match t {
TransportType::Nostr => cdk::nuts::TransportType::Nostr,
TransportType::HttpPost => cdk::nuts::TransportType::HttpPost,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct Transport {
pub transport_type: TransportType,
pub target: String,
pub tags: Vec<Vec<String>>,
}
impl From<cdk::nuts::Transport> for Transport {
fn from(t: cdk::nuts::Transport) -> Self {
Self {
transport_type: t._type.into(),
target: t.target,
tags: t.tags,
}
}
}
impl From<Transport> for cdk::nuts::Transport {
fn from(t: Transport) -> Self {
Self {
_type: t.transport_type.into(),
target: t.target,
tags: t.tags,
}
}
}
#[derive(Debug, uniffi::Object)]
pub struct PaymentRequest {
inner: cdk::nuts::PaymentRequest,
}
impl PaymentRequest {
pub(crate) fn inner(&self) -> &cdk::nuts::PaymentRequest {
&self.inner
}
pub(crate) fn from_inner(inner: cdk::nuts::PaymentRequest) -> Self {
Self { inner }
}
}
#[uniffi::export]
impl PaymentRequest {
#[uniffi::constructor]
pub fn from_string(encoded: String) -> Result<Arc<Self>, FfiError> {
use std::str::FromStr;
let inner = cdk::nuts::PaymentRequest::from_str(&encoded).map_err(FfiError::internal)?;
Ok(Arc::new(Self { inner }))
}
pub fn to_string_encoded(&self) -> String {
self.inner.to_string()
}
pub fn to_bech32_string(&self) -> Result<String, FfiError> {
self.inner.to_bech32_string().map_err(FfiError::internal)
}
pub fn to_bip321(
&self,
bolt11: Option<String>,
bolt12: Option<String>,
) -> Result<String, FfiError> {
use cdk::wallet::bip321::PaymentRequestBip321Ext;
let builder = self.inner.to_bip321().map_err(FfiError::from)?;
let builder = crate::bip321::apply_optional_lightning_methods(builder, bolt11, bolt12);
Ok(builder.to_string())
}
pub fn payment_id(&self) -> Option<String> {
self.inner.payment_id.clone()
}
pub fn amount(&self) -> Option<Amount> {
self.inner.amount.map(|a| a.into())
}
pub fn unit(&self) -> Option<CurrencyUnit> {
self.inner.unit.clone().map(|u| u.into())
}
pub fn single_use(&self) -> Option<bool> {
self.inner.single_use
}
pub fn mints(&self) -> Vec<String> {
self.inner.mints.iter().map(|m| m.to_string()).collect()
}
pub fn description(&self) -> Option<String> {
self.inner.description.clone()
}
pub fn transports(&self) -> Vec<Transport> {
self.inner
.transports
.iter()
.cloned()
.map(|t| t.into())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct CreateRequestParams {
pub amount: Option<u64>,
pub unit: String,
pub description: Option<String>,
pub pubkeys: Option<Vec<String>>,
pub num_sigs: u64,
pub hash: Option<String>,
pub preimage: Option<String>,
pub transport: String,
pub http_url: Option<String>,
pub nostr_relays: Option<Vec<String>>,
pub mints: Option<Vec<String>>,
}
impl Default for CreateRequestParams {
fn default() -> Self {
Self {
amount: None,
unit: "sat".to_string(),
description: None,
pubkeys: None,
num_sigs: 1,
hash: None,
preimage: None,
transport: "none".to_string(),
http_url: None,
nostr_relays: None,
mints: None,
}
}
}
impl From<CreateRequestParams> for cdk::wallet::payment_request::CreateRequestParams {
fn from(params: CreateRequestParams) -> Self {
Self {
amount: params.amount,
unit: params.unit,
description: params.description,
pubkeys: params.pubkeys,
num_sigs: params.num_sigs,
hash: params.hash,
preimage: params.preimage,
transport: params.transport,
http_url: params.http_url,
nostr_relays: params.nostr_relays,
mints: params.mints,
}
}
}
impl From<cdk::wallet::payment_request::CreateRequestParams> for CreateRequestParams {
fn from(params: cdk::wallet::payment_request::CreateRequestParams) -> Self {
Self {
amount: params.amount,
unit: params.unit,
description: params.description,
pubkeys: params.pubkeys,
num_sigs: params.num_sigs,
hash: params.hash,
preimage: params.preimage,
transport: params.transport,
http_url: params.http_url,
nostr_relays: params.nostr_relays,
mints: params.mints,
}
}
}
#[uniffi::export]
pub fn decode_payment_request(encoded: String) -> Result<Arc<PaymentRequest>, FfiError> {
PaymentRequest::from_string(encoded)
}
#[uniffi::export]
pub fn encode_create_request_params(params: CreateRequestParams) -> Result<String, FfiError> {
Ok(serde_json::to_string(¶ms)?)
}
#[uniffi::export]
pub fn decode_create_request_params(json: String) -> Result<CreateRequestParams, FfiError> {
Ok(serde_json::from_str(&json)?)
}
#[derive(uniffi::Object)]
pub struct NostrWaitInfo {
inner: cdk::wallet::payment_request::NostrWaitInfo,
}
impl NostrWaitInfo {
#[allow(dead_code)]
pub(crate) fn inner(&self) -> &cdk::wallet::payment_request::NostrWaitInfo {
&self.inner
}
}
#[uniffi::export]
impl NostrWaitInfo {
pub fn relays(&self) -> Vec<String> {
self.inner.relays.clone()
}
pub fn pubkey(&self) -> String {
self.inner.pubkey.to_hex()
}
}
#[derive(uniffi::Record)]
pub struct CreateRequestResult {
pub payment_request: Arc<PaymentRequest>,
pub nostr_wait_info: Option<Arc<NostrWaitInfo>>,
}
#[derive(uniffi::Object)]
pub struct PaymentRequestPayload {
inner: cdk::nuts::PaymentRequestPayload,
}
#[uniffi::export]
impl PaymentRequestPayload {
#[uniffi::constructor]
pub fn from_string(json: String) -> Result<Arc<PaymentRequestPayload>, FfiError> {
let inner: cdk::nuts::PaymentRequestPayload = serde_json::from_str(&json)?;
Ok(Arc::new(PaymentRequestPayload { inner }))
}
pub fn id(&self) -> Option<String> {
self.inner.id.clone()
}
pub fn memo(&self) -> Option<String> {
self.inner.memo.clone()
}
pub fn mint(&self) -> MintUrl {
self.inner.mint.clone().into()
}
pub fn unit(&self) -> CurrencyUnit {
self.inner.unit.clone().into()
}
pub fn proofs(&self) -> Vec<Proof> {
self.inner.proofs.iter().map(|p| p.clone().into()).collect()
}
}
impl core::fmt::Display for PaymentRequestPayload {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
serde_json::to_string(&self.inner).map_err(|_| core::fmt::Error)?
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_payment_request_payload() {
use std::str::FromStr;
let mint_url = cdk::mint_url::MintUrl::from_str("https://mint.example.com").unwrap();
let unit = cdk::nuts::CurrencyUnit::Sat;
let proofs = vec![];
let inner = cdk::nuts::PaymentRequestPayload {
id: Some("test-id".to_string()),
memo: Some("test-memo".to_string()),
mint: mint_url.clone(),
unit: unit.clone(),
proofs: proofs.clone(),
};
let payload = PaymentRequestPayload { inner };
assert_eq!(payload.id(), Some("test-id".to_string()));
assert_eq!(payload.memo(), Some("test-memo".to_string()));
assert_eq!(payload.mint().url, "https://mint.example.com");
assert!(matches!(payload.unit(), CurrencyUnit::Sat));
assert!(payload.proofs().is_empty());
}
#[test]
fn test_payment_request_payload_json() {
use std::str::FromStr;
let mint_url = cdk::mint_url::MintUrl::from_str("https://mint.example.com").unwrap();
let unit = cdk::nuts::CurrencyUnit::Sat;
let inner = cdk::nuts::PaymentRequestPayload {
id: Some("test-id".to_string()),
memo: Some("test-memo".to_string()),
mint: mint_url,
unit,
proofs: vec![],
};
let payload = PaymentRequestPayload { inner };
let json = payload.to_string();
let decoded = PaymentRequestPayload::from_string(json).unwrap();
assert_eq!(decoded.id(), payload.id());
assert_eq!(decoded.memo(), payload.memo());
assert_eq!(decoded.mint().url, payload.mint().url);
}
const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
#[test]
fn test_decode_payment_request() {
let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
assert_eq!(req.payment_id().unwrap(), "b7a90176");
assert_eq!(req.amount().unwrap().value, 10);
assert!(matches!(req.unit().unwrap(), CurrencyUnit::Sat));
let mints = req.mints();
assert_eq!(mints.len(), 1);
assert_eq!(mints[0], "https://nofees.testnut.cashu.space");
let transports = req.transports();
assert_eq!(transports.len(), 1);
assert!(matches!(transports[0].transport_type, TransportType::Nostr));
}
#[test]
fn test_roundtrip_payment_request() {
let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
let encoded = req.to_string_encoded();
let decoded = PaymentRequest::from_string(encoded).unwrap();
assert_eq!(req.payment_id(), decoded.payment_id());
assert_eq!(
req.amount().map(|a| a.value),
decoded.amount().map(|a| a.value)
);
}
#[test]
fn test_to_bech32_string() {
let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
let bech32 = req.to_bech32_string().unwrap();
assert!(
bech32.starts_with("CREQB1"),
"Expected bech32 string to start with CREQB1, got: {}",
&bech32[..10.min(bech32.len())]
);
let decoded = PaymentRequest::from_string(bech32).unwrap();
assert_eq!(req.payment_id(), decoded.payment_id());
assert_eq!(
req.amount().map(|a| a.value),
decoded.amount().map(|a| a.value)
);
assert_eq!(req.mints(), decoded.mints());
assert_eq!(req.single_use(), decoded.single_use());
assert_eq!(req.description(), decoded.description());
}
#[test]
fn test_transport_conversion() {
let ffi_transport = Transport {
transport_type: TransportType::Nostr,
target: "nprofile1...".to_string(),
tags: vec![vec!["n".to_string(), "17".to_string()]],
};
let cdk_transport: cdk::nuts::Transport = ffi_transport.clone().into();
let back: Transport = cdk_transport.into();
assert_eq!(ffi_transport.transport_type, back.transport_type);
assert_eq!(ffi_transport.target, back.target);
assert_eq!(ffi_transport.tags, back.tags);
}
#[test]
fn test_create_request_params_default() {
let params = CreateRequestParams::default();
assert_eq!(params.unit, "sat");
assert_eq!(params.num_sigs, 1);
assert_eq!(params.transport, "none");
assert!(params.amount.is_none());
}
#[test]
fn test_create_request_params_serialization() {
let params = CreateRequestParams {
amount: Some(100),
unit: "sat".to_string(),
description: Some("Test payment".to_string()),
transport: "http".to_string(),
http_url: Some("https://example.com/callback".to_string()),
..Default::default()
};
let json = encode_create_request_params(params.clone()).unwrap();
let decoded = decode_create_request_params(json).unwrap();
assert_eq!(params.amount, decoded.amount);
assert_eq!(params.unit, decoded.unit);
assert_eq!(params.description, decoded.description);
}
}