mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-18 13:14:59 +01:00
token: add spending-condition inspection helpers and token_secrets() (#1124)
* token: add Token::token_secrets() and spending-condition helpers - New helpers on Token that do not require mint keysets: - spending_conditions() - p2pk_pubkeys() - p2pk_refund_pubkeys() - htlc_hashes() - locktimes() - Introduce token_secrets() to unify V3/V4 proof traversal and avoid duplication - Bypass short->long keyset-id mapping since only Secret is needed for conditions - Use &Secret for TryFrom to fix compile error
This commit is contained in:
@@ -2,18 +2,20 @@
|
||||
//!
|
||||
//! <https://github.com/cashubtc/nuts/blob/main/00.md>
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
|
||||
use bitcoin::base64::{alphabet, Engine as _};
|
||||
use bitcoin::hashes::sha256;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Error, Proof, ProofV3, ProofV4, Proofs};
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nut02::ShortKeysetId;
|
||||
use crate::nuts::{CurrencyUnit, Id};
|
||||
use crate::nuts::nut11::SpendingConditions;
|
||||
use crate::nuts::{CurrencyUnit, Id, Kind, PublicKey};
|
||||
use crate::{ensure_cdk, Amount, KeySetInfo};
|
||||
|
||||
/// Token Enum
|
||||
@@ -128,6 +130,90 @@ impl Token {
|
||||
Self::TokenV4(token) => token.to_raw_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all proof secrets in this token without keyset-id mapping, across V3/V4
|
||||
/// This is intended for spending-condition inspection where only the secret matters.
|
||||
pub fn token_secrets(&self) -> Vec<&crate::secret::Secret> {
|
||||
match self {
|
||||
Token::TokenV3(t) => t
|
||||
.token
|
||||
.iter()
|
||||
.flat_map(|kt| kt.proofs.iter().map(|p| &p.secret))
|
||||
.collect(),
|
||||
Token::TokenV4(t) => t
|
||||
.token
|
||||
.iter()
|
||||
.flat_map(|kt| kt.proofs.iter().map(|p| &p.secret))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract unique spending conditions across all proofs
|
||||
pub fn spending_conditions(&self) -> Result<HashSet<SpendingConditions>, Error> {
|
||||
let mut set = HashSet::new();
|
||||
for secret in self.token_secrets().into_iter() {
|
||||
if let Ok(cond) = SpendingConditions::try_from(secret) {
|
||||
set.insert(cond);
|
||||
}
|
||||
}
|
||||
Ok(set)
|
||||
}
|
||||
|
||||
/// Collect pubkeys for P2PK-locked ecash
|
||||
pub fn p2pk_pubkeys(&self) -> Result<HashSet<PublicKey>, Error> {
|
||||
let mut keys: HashSet<PublicKey> = HashSet::new();
|
||||
for secret in self.token_secrets().into_iter() {
|
||||
if let Ok(cond) = SpendingConditions::try_from(secret) {
|
||||
if cond.kind() == Kind::P2PK {
|
||||
if let Some(ps) = cond.pubkeys() {
|
||||
keys.extend(ps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Collect refund pubkeys from P2PK conditions
|
||||
pub fn p2pk_refund_pubkeys(&self) -> Result<HashSet<PublicKey>, Error> {
|
||||
let mut keys: HashSet<PublicKey> = HashSet::new();
|
||||
for secret in self.token_secrets().into_iter() {
|
||||
if let Ok(cond) = SpendingConditions::try_from(secret) {
|
||||
if cond.kind() == Kind::P2PK {
|
||||
if let Some(ps) = cond.refund_keys() {
|
||||
keys.extend(ps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Collect HTLC hashes
|
||||
pub fn htlc_hashes(&self) -> Result<HashSet<sha256::Hash>, Error> {
|
||||
let mut hashes: HashSet<sha256::Hash> = HashSet::new();
|
||||
for secret in self.token_secrets().into_iter() {
|
||||
if let Ok(SpendingConditions::HTLCConditions { data, .. }) =
|
||||
SpendingConditions::try_from(secret)
|
||||
{
|
||||
hashes.insert(data);
|
||||
}
|
||||
}
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
/// Collect unique locktimes from spending conditions
|
||||
pub fn locktimes(&self) -> Result<BTreeSet<u64>, Error> {
|
||||
let mut set: BTreeSet<u64> = BTreeSet::new();
|
||||
for secret in self.token_secrets().into_iter() {
|
||||
if let Ok(cond) = SpendingConditions::try_from(secret) {
|
||||
if let Some(lt) = cond.locktime() {
|
||||
set.insert(lt);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(set)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Token {
|
||||
@@ -535,10 +621,13 @@ mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use bip39::rand::{self, RngCore};
|
||||
use bitcoin::hashes::sha256::Hash as Sha256Hash;
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
use super::*;
|
||||
use crate::dhke::hash_to_curve;
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::nut11::{Conditions, SigFlag, SpendingConditions};
|
||||
use crate::secret::Secret;
|
||||
use crate::util::hex;
|
||||
|
||||
@@ -826,4 +915,155 @@ mod tests {
|
||||
let proofs1 = token1.unwrap().proofs(&keysets_info);
|
||||
assert!(proofs1.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn test_token_spending_condition_helpers_p2pk_htlc_v4() {
|
||||
let mint_url = MintUrl::from_str("https://example.com").unwrap();
|
||||
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
|
||||
|
||||
// P2PK: base pubkey plus an extra pubkey via tags, refund key, and locktime
|
||||
let sk1 = crate::nuts::SecretKey::generate();
|
||||
let pk1 = sk1.public_key();
|
||||
let sk2 = crate::nuts::SecretKey::generate();
|
||||
let pk2 = sk2.public_key();
|
||||
let refund_sk = crate::nuts::SecretKey::generate();
|
||||
let refund_pk = refund_sk.public_key();
|
||||
|
||||
let cond_p2pk = Conditions {
|
||||
locktime: Some(1_700_000_000),
|
||||
pubkeys: Some(vec![pk2]),
|
||||
refund_keys: Some(vec![refund_pk]),
|
||||
num_sigs: Some(1),
|
||||
sig_flag: SigFlag::SigInputs,
|
||||
num_sigs_refund: None,
|
||||
};
|
||||
|
||||
let nut10_p2pk = crate::nuts::Nut10Secret::new(
|
||||
crate::nuts::Kind::P2PK,
|
||||
pk1.to_string(),
|
||||
Some(cond_p2pk.clone()),
|
||||
);
|
||||
let secret_p2pk: Secret = nut10_p2pk.try_into().unwrap();
|
||||
|
||||
// HTLC: use a known preimage hash and its own locktime
|
||||
let preimage = b"cdk-test-preimage";
|
||||
let htlc_hash = Sha256Hash::hash(preimage);
|
||||
let cond_htlc = Conditions {
|
||||
locktime: Some(1_800_000_000),
|
||||
..Default::default()
|
||||
};
|
||||
let nut10_htlc = crate::nuts::Nut10Secret::new(
|
||||
crate::nuts::Kind::HTLC,
|
||||
htlc_hash.to_string(),
|
||||
Some(cond_htlc.clone()),
|
||||
);
|
||||
let secret_htlc: Secret = nut10_htlc.try_into().unwrap();
|
||||
|
||||
// Build two proofs (one P2PK, one HTLC)
|
||||
let proof_p2pk = Proof::new(Amount::from(1), keyset_id, secret_p2pk.clone(), pk1);
|
||||
let proof_htlc = Proof::new(Amount::from(2), keyset_id, secret_htlc.clone(), pk2);
|
||||
let token = Token::new(
|
||||
mint_url,
|
||||
vec![proof_p2pk, proof_htlc].into_iter().collect(),
|
||||
None,
|
||||
CurrencyUnit::Sat,
|
||||
);
|
||||
|
||||
// token_secrets should see both
|
||||
assert_eq!(token.token_secrets().len(), 2);
|
||||
|
||||
// spending_conditions should contain both kinds with their conditions
|
||||
let sc = token.spending_conditions().unwrap();
|
||||
assert!(sc.contains(&SpendingConditions::P2PKConditions {
|
||||
data: pk1,
|
||||
conditions: Some(cond_p2pk.clone())
|
||||
}));
|
||||
assert!(sc.contains(&SpendingConditions::HTLCConditions {
|
||||
data: htlc_hash,
|
||||
conditions: Some(cond_htlc.clone())
|
||||
}));
|
||||
|
||||
// p2pk_pubkeys should include base pk1 and extra pk2 from tags (deduped)
|
||||
let pks = token.p2pk_pubkeys().unwrap();
|
||||
assert!(pks.contains(&pk1));
|
||||
assert!(pks.contains(&pk2));
|
||||
assert_eq!(pks.len(), 2);
|
||||
|
||||
// p2pk_refund_pubkeys should include refund_pk only
|
||||
let refund = token.p2pk_refund_pubkeys().unwrap();
|
||||
assert!(refund.contains(&refund_pk));
|
||||
assert_eq!(refund.len(), 1);
|
||||
|
||||
// htlc_hashes should include exactly our hash
|
||||
let hashes = token.htlc_hashes().unwrap();
|
||||
assert!(hashes.contains(&htlc_hash));
|
||||
assert_eq!(hashes.len(), 1);
|
||||
|
||||
// locktimes should include both unique locktimes
|
||||
let lts = token.locktimes().unwrap();
|
||||
assert!(lts.contains(&1_700_000_000));
|
||||
assert!(lts.contains(&1_800_000_000));
|
||||
assert_eq!(lts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_spending_condition_helpers_dedup_and_v3() {
|
||||
let mint_url = MintUrl::from_str("https://example.org").unwrap();
|
||||
let id = Id::from_str("00ad268c4d1f5826").unwrap();
|
||||
|
||||
// Same P2PK conditions duplicated across two proofs
|
||||
let sk = crate::nuts::SecretKey::generate();
|
||||
let pk = sk.public_key();
|
||||
|
||||
let cond = Conditions {
|
||||
locktime: Some(1_650_000_000),
|
||||
pubkeys: Some(vec![pk]), // include itself to test dedup inside pubkeys()
|
||||
refund_keys: Some(vec![pk]), // deliberate duplicate
|
||||
num_sigs: Some(1),
|
||||
sig_flag: SigFlag::SigInputs,
|
||||
num_sigs_refund: None,
|
||||
};
|
||||
|
||||
let nut10 = crate::nuts::Nut10Secret::new(
|
||||
crate::nuts::Kind::P2PK,
|
||||
pk.to_string(),
|
||||
Some(cond.clone()),
|
||||
);
|
||||
let secret: Secret = nut10.try_into().unwrap();
|
||||
|
||||
let p1 = Proof::new(Amount::from(1), id, secret.clone(), pk);
|
||||
let p2 = Proof::new(Amount::from(2), id, secret.clone(), pk);
|
||||
|
||||
// Build a V3 token explicitly and wrap into Token::TokenV3
|
||||
let token_v3 = TokenV3::new(
|
||||
mint_url,
|
||||
vec![p1, p2].into_iter().collect(),
|
||||
None,
|
||||
Some(CurrencyUnit::Sat),
|
||||
)
|
||||
.unwrap();
|
||||
let token = Token::TokenV3(token_v3);
|
||||
|
||||
// Helpers should dedup
|
||||
let sc = token.spending_conditions().unwrap();
|
||||
assert_eq!(sc.len(), 1); // identical conditions across proofs
|
||||
|
||||
let pks = token.p2pk_pubkeys().unwrap();
|
||||
assert!(pks.contains(&pk));
|
||||
assert_eq!(pks.len(), 1); // duplicates removed
|
||||
|
||||
let refunds = token.p2pk_refund_pubkeys().unwrap();
|
||||
assert!(refunds.contains(&pk));
|
||||
assert_eq!(refunds.len(), 1);
|
||||
|
||||
let lts = token.locktimes().unwrap();
|
||||
assert!(lts.contains(&1_650_000_000));
|
||||
assert_eq!(lts.len(), 1);
|
||||
|
||||
// No HTLC here
|
||||
let hashes = token.htlc_hashes().unwrap();
|
||||
assert!(hashes.is_empty());
|
||||
|
||||
// token_secrets length equals number of proofs even if conditions identical
|
||||
assert_eq!(token.token_secrets().len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
pub mod database;
|
||||
pub mod error;
|
||||
pub mod multi_mint_wallet;
|
||||
pub mod token;
|
||||
pub mod types;
|
||||
pub mod wallet;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use cdk::wallet::multi_mint_wallet::{
|
||||
};
|
||||
|
||||
use crate::error::FfiError;
|
||||
use crate::token::Token;
|
||||
use crate::types::*;
|
||||
|
||||
/// FFI-compatible MultiMintWallet
|
||||
|
||||
158
crates/cdk-ffi/src/token.rs
Normal file
158
crates/cdk-ffi/src/token.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
//! FFI token bindings
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::error::FfiError;
|
||||
use crate::{Amount, CurrencyUnit, MintUrl, Proofs};
|
||||
|
||||
/// FFI-compatible Token
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
pub struct Token {
|
||||
pub(crate) inner: cdk::nuts::Token,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Token {
|
||||
type Err = FfiError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let token = cdk::nuts::Token::from_str(s)
|
||||
.map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?;
|
||||
Ok(Token { inner: token })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<cdk::nuts::Token> for Token {
|
||||
fn from(token: cdk::nuts::Token) -> Self {
|
||||
Self { inner: token }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Token> for cdk::nuts::Token {
|
||||
fn from(token: Token) -> Self {
|
||||
token.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Token {
|
||||
/// Create a new Token from string
|
||||
#[uniffi::constructor]
|
||||
pub fn from_string(encoded_token: String) -> Result<Token, FfiError> {
|
||||
let token = cdk::nuts::Token::from_str(&encoded_token)
|
||||
.map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?;
|
||||
Ok(Token { inner: token })
|
||||
}
|
||||
|
||||
/// Get the total value of the token
|
||||
pub fn value(&self) -> Result<Amount, FfiError> {
|
||||
Ok(self.inner.value()?.into())
|
||||
}
|
||||
|
||||
/// Get the memo from the token
|
||||
pub fn memo(&self) -> Option<String> {
|
||||
self.inner.memo().clone()
|
||||
}
|
||||
|
||||
/// Get the currency unit
|
||||
pub fn unit(&self) -> Option<CurrencyUnit> {
|
||||
self.inner.unit().map(Into::into)
|
||||
}
|
||||
|
||||
/// Get the mint URL
|
||||
pub fn mint_url(&self) -> Result<MintUrl, FfiError> {
|
||||
Ok(self.inner.mint_url()?.into())
|
||||
}
|
||||
|
||||
/// Get proofs from the token (simplified - no keyset filtering for now)
|
||||
pub fn proofs_simple(&self) -> Result<Proofs, FfiError> {
|
||||
// For now, return empty keysets to get all proofs
|
||||
let empty_keysets = vec![];
|
||||
let proofs = self.inner.proofs(&empty_keysets)?;
|
||||
Ok(proofs
|
||||
.into_iter()
|
||||
.map(|p| std::sync::Arc::new(p.into()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Convert token to raw bytes
|
||||
pub fn to_raw_bytes(&self) -> Result<Vec<u8>, FfiError> {
|
||||
Ok(self.inner.to_raw_bytes()?)
|
||||
}
|
||||
|
||||
/// Encode token to string representation
|
||||
pub fn encode(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
|
||||
/// Decode token from string representation
|
||||
#[uniffi::constructor]
|
||||
pub fn decode(encoded_token: String) -> Result<Token, FfiError> {
|
||||
encoded_token.parse()
|
||||
}
|
||||
|
||||
/// Return unique spending conditions across all proofs in this token
|
||||
pub fn spending_conditions(&self) -> Vec<crate::types::SpendingConditions> {
|
||||
self.inner
|
||||
.spending_conditions()
|
||||
.map(|set| set.into_iter().map(Into::into).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Return all P2PK pubkeys referenced by this token's spending conditions
|
||||
pub fn p2pk_pubkeys(&self) -> Vec<String> {
|
||||
let set = self
|
||||
.inner
|
||||
.p2pk_pubkeys()
|
||||
.map(|keys| {
|
||||
keys.into_iter()
|
||||
.map(|k| k.to_string())
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
set.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Return all refund pubkeys from P2PK spending conditions
|
||||
pub fn p2pk_refund_pubkeys(&self) -> Vec<String> {
|
||||
let set = self
|
||||
.inner
|
||||
.p2pk_refund_pubkeys()
|
||||
.map(|keys| {
|
||||
keys.into_iter()
|
||||
.map(|k| k.to_string())
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
set.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Return all HTLC hashes from spending conditions
|
||||
pub fn htlc_hashes(&self) -> Vec<String> {
|
||||
let set = self
|
||||
.inner
|
||||
.htlc_hashes()
|
||||
.map(|hashes| {
|
||||
hashes
|
||||
.into_iter()
|
||||
.map(|h| h.to_string())
|
||||
.collect::<BTreeSet<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
set.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Return all locktimes from spending conditions (sorted ascending)
|
||||
pub fn locktimes(&self) -> Vec<u64> {
|
||||
self.inner
|
||||
.locktimes()
|
||||
.map(|s| s.into_iter().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use cdk::Amount as CdkAmount;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::FfiError;
|
||||
use crate::token::Token;
|
||||
|
||||
/// FFI-compatible Amount type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
|
||||
@@ -200,98 +201,6 @@ impl From<ProofState> for CdkState {
|
||||
}
|
||||
}
|
||||
|
||||
/// FFI-compatible Token
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
pub struct Token {
|
||||
pub(crate) inner: cdk::nuts::Token,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Token {
|
||||
type Err = FfiError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let token = cdk::nuts::Token::from_str(s)
|
||||
.map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?;
|
||||
Ok(Token { inner: token })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<cdk::nuts::Token> for Token {
|
||||
fn from(token: cdk::nuts::Token) -> Self {
|
||||
Self { inner: token }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Token> for cdk::nuts::Token {
|
||||
fn from(token: Token) -> Self {
|
||||
token.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Token {
|
||||
/// Create a new Token from string
|
||||
#[uniffi::constructor]
|
||||
pub fn from_string(encoded_token: String) -> Result<Token, FfiError> {
|
||||
let token = cdk::nuts::Token::from_str(&encoded_token)
|
||||
.map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?;
|
||||
Ok(Token { inner: token })
|
||||
}
|
||||
|
||||
/// Get the total value of the token
|
||||
pub fn value(&self) -> Result<Amount, FfiError> {
|
||||
Ok(self.inner.value()?.into())
|
||||
}
|
||||
|
||||
/// Get the memo from the token
|
||||
pub fn memo(&self) -> Option<String> {
|
||||
self.inner.memo().clone()
|
||||
}
|
||||
|
||||
/// Get the currency unit
|
||||
pub fn unit(&self) -> Option<CurrencyUnit> {
|
||||
self.inner.unit().map(Into::into)
|
||||
}
|
||||
|
||||
/// Get the mint URL
|
||||
pub fn mint_url(&self) -> Result<MintUrl, FfiError> {
|
||||
Ok(self.inner.mint_url()?.into())
|
||||
}
|
||||
|
||||
/// Get proofs from the token (simplified - no keyset filtering for now)
|
||||
pub fn proofs_simple(&self) -> Result<Proofs, FfiError> {
|
||||
// For now, return empty keysets to get all proofs
|
||||
let empty_keysets = vec![];
|
||||
let proofs = self.inner.proofs(&empty_keysets)?;
|
||||
Ok(proofs
|
||||
.into_iter()
|
||||
.map(|p| std::sync::Arc::new(p.into()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Convert token to raw bytes
|
||||
pub fn to_raw_bytes(&self) -> Result<Vec<u8>, FfiError> {
|
||||
Ok(self.inner.to_raw_bytes()?)
|
||||
}
|
||||
|
||||
/// Encode token to string representation
|
||||
pub fn encode(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
|
||||
/// Decode token from string representation
|
||||
#[uniffi::constructor]
|
||||
pub fn decode(encoded_token: String) -> Result<Token, FfiError> {
|
||||
encoded_token.parse()
|
||||
}
|
||||
}
|
||||
|
||||
/// FFI-compatible SendMemo
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct SendMemo {
|
||||
|
||||
@@ -7,6 +7,7 @@ use bip39::Mnemonic;
|
||||
use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
|
||||
|
||||
use crate::error::FfiError;
|
||||
use crate::token::Token;
|
||||
use crate::types::*;
|
||||
|
||||
/// FFI-compatible Wallet
|
||||
|
||||
@@ -214,7 +214,7 @@ async fn test_ffi_mint_quote_creation() {
|
||||
let quote = wallet
|
||||
.mint_quote(amount, Some(description.clone()))
|
||||
.await
|
||||
.expect(&format!("Failed to create quote for {} sats", amount_value));
|
||||
.unwrap_or_else(|_| panic!("Failed to create quote for {} sats", amount_value));
|
||||
|
||||
// Verify quote properties
|
||||
assert_eq!(quote.amount, Some(amount));
|
||||
|
||||
@@ -335,10 +335,9 @@ mod test {
|
||||
|
||||
let db_url = format!("{db_url} schema={test_id}");
|
||||
|
||||
let db = MintPgDatabase::new(db_url.as_str())
|
||||
MintPgDatabase::new(db_url.as_str())
|
||||
.await
|
||||
.expect("database");
|
||||
db
|
||||
.expect("database")
|
||||
}
|
||||
|
||||
mint_db_test!(provide_db);
|
||||
|
||||
Reference in New Issue
Block a user