mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 23:55:01 +01:00
Add transactions to database (#686)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use cdk::nuts::nut18::TransportType;
|
||||
use cdk::nuts::{CurrencyUnit, PaymentRequest, PaymentRequestPayload, Token, Transport};
|
||||
use cdk::wallet::MultiMintWallet;
|
||||
use cdk::wallet::{MultiMintWallet, ReceiveOptions};
|
||||
use clap::Args;
|
||||
use nostr_sdk::nips::nip19::Nip19Profile;
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -83,7 +83,7 @@ pub async fn create_request(
|
||||
let token = Token::new(payload.mint, payload.proofs, payload.memo, payload.unit);
|
||||
|
||||
let amount = multi_mint_wallet
|
||||
.receive(&token.to_string(), &[], &[])
|
||||
.receive(&token.to_string(), ReceiveOptions::default())
|
||||
.await?;
|
||||
|
||||
println!("Received {}", amount);
|
||||
|
||||
@@ -7,6 +7,7 @@ use cdk::nuts::{SecretKey, Token};
|
||||
use cdk::util::unix_time;
|
||||
use cdk::wallet::multi_mint_wallet::MultiMintWallet;
|
||||
use cdk::wallet::types::WalletKey;
|
||||
use cdk::wallet::ReceiveOptions;
|
||||
use cdk::Amount;
|
||||
use clap::Args;
|
||||
use nostr_sdk::nips::nip04;
|
||||
@@ -150,7 +151,14 @@ async fn receive_token(
|
||||
}
|
||||
|
||||
let amount = multi_mint_wallet
|
||||
.receive(token_str, signing_keys, preimage)
|
||||
.receive(
|
||||
token_str,
|
||||
ReceiveOptions {
|
||||
p2pk_signing_keys: signing_keys.to_vec(),
|
||||
preimages: preimage.to_vec(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ use crate::mint_url::MintUrl;
|
||||
use crate::nuts::{
|
||||
CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
|
||||
};
|
||||
use crate::wallet;
|
||||
use crate::wallet::MintQuote as WalletMintQuote;
|
||||
use crate::wallet::{
|
||||
self, MintQuote as WalletMintQuote, Transaction, TransactionDirection, TransactionId,
|
||||
};
|
||||
|
||||
/// Wallet Database trait
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -99,4 +100,21 @@ pub trait Database: Debug {
|
||||
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
|
||||
/// Get current Keyset counter
|
||||
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;
|
||||
|
||||
/// Add transaction to storage
|
||||
async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err>;
|
||||
/// Get transaction from storage
|
||||
async fn get_transaction(
|
||||
&self,
|
||||
transaction_id: TransactionId,
|
||||
) -> Result<Option<Transaction>, Self::Err>;
|
||||
/// List transactions from storage
|
||||
async fn list_transactions(
|
||||
&self,
|
||||
mint_url: Option<MintUrl>,
|
||||
direction: Option<TransactionDirection>,
|
||||
unit: Option<CurrencyUnit>,
|
||||
) -> Result<Vec<Transaction>, Self::Err>;
|
||||
/// Remove transaction from storage
|
||||
async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err>;
|
||||
}
|
||||
|
||||
@@ -217,6 +217,12 @@ pub enum Error {
|
||||
/// Invoice Description not supported
|
||||
#[error("Invoice Description not supported")]
|
||||
InvoiceDescriptionUnsupported,
|
||||
/// Invalid transaction direction
|
||||
#[error("Invalid transaction direction")]
|
||||
InvalidTransactionDirection,
|
||||
/// Invalid transaction id
|
||||
#[error("Invalid transaction id")]
|
||||
InvalidTransactionId,
|
||||
/// Custom Error
|
||||
#[error("`{0}`")]
|
||||
Custom(String),
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
//! Wallet Types
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
||||
use cashu::util::hex;
|
||||
use cashu::{nut00, Proofs, PublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey};
|
||||
use crate::Amount;
|
||||
use crate::{Amount, Error};
|
||||
|
||||
/// Wallet Key
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
@@ -107,3 +112,183 @@ impl SendKind {
|
||||
matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wallet Transaction
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Transaction {
|
||||
/// Mint Url
|
||||
pub mint_url: MintUrl,
|
||||
/// Transaction direction
|
||||
pub direction: TransactionDirection,
|
||||
/// Amount
|
||||
pub amount: Amount,
|
||||
/// Fee
|
||||
pub fee: Amount,
|
||||
/// Currency Unit
|
||||
pub unit: CurrencyUnit,
|
||||
/// Proof Ys
|
||||
pub ys: Vec<PublicKey>,
|
||||
/// Unix timestamp
|
||||
pub timestamp: u64,
|
||||
/// Memo
|
||||
pub memo: Option<String>,
|
||||
/// User-defined metadata
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
/// Transaction ID
|
||||
pub fn id(&self) -> TransactionId {
|
||||
TransactionId::new(self.ys.clone())
|
||||
}
|
||||
|
||||
/// Check if transaction matches conditions
|
||||
pub fn matches_conditions(
|
||||
&self,
|
||||
mint_url: &Option<MintUrl>,
|
||||
direction: &Option<TransactionDirection>,
|
||||
unit: &Option<CurrencyUnit>,
|
||||
) -> bool {
|
||||
if let Some(mint_url) = mint_url {
|
||||
if &self.mint_url != mint_url {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(direction) = direction {
|
||||
if &self.direction != direction {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(unit) = unit {
|
||||
if &self.unit != unit {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Transaction {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Transaction {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.timestamp.cmp(&other.timestamp).reverse()
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction Direction
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TransactionDirection {
|
||||
/// Incoming transaction (i.e., receive or mint)
|
||||
Incoming,
|
||||
/// Outgoing transaction (i.e., send or melt)
|
||||
Outgoing,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TransactionDirection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TransactionDirection::Incoming => write!(f, "Incoming"),
|
||||
TransactionDirection::Outgoing => write!(f, "Outgoing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TransactionDirection {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"Incoming" => Ok(Self::Incoming),
|
||||
"Outgoing" => Ok(Self::Outgoing),
|
||||
_ => Err(Error::InvalidTransactionDirection),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction ID
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct TransactionId([u8; 32]);
|
||||
|
||||
impl TransactionId {
|
||||
/// Create new [`TransactionId`]
|
||||
pub fn new(ys: Vec<PublicKey>) -> Self {
|
||||
let mut ys = ys;
|
||||
ys.sort();
|
||||
let mut hasher = sha256::Hash::engine();
|
||||
for y in ys {
|
||||
hasher.input(&y.to_bytes());
|
||||
}
|
||||
let hash = sha256::Hash::from_engine(hasher);
|
||||
Self(hash.to_byte_array())
|
||||
}
|
||||
|
||||
/// From proofs
|
||||
pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
|
||||
let ys = proofs
|
||||
.iter()
|
||||
.map(|proof| proof.y())
|
||||
.collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
|
||||
Ok(Self::new(ys))
|
||||
}
|
||||
|
||||
/// From bytes
|
||||
pub fn from_bytes(bytes: [u8; 32]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
/// From hex string
|
||||
pub fn from_hex(value: &str) -> Result<Self, Error> {
|
||||
let bytes = hex::decode(value)?;
|
||||
let mut array = [0u8; 32];
|
||||
array.copy_from_slice(&bytes);
|
||||
Ok(Self(array))
|
||||
}
|
||||
|
||||
/// From slice
|
||||
pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
|
||||
if slice.len() != 32 {
|
||||
return Err(Error::InvalidTransactionId);
|
||||
}
|
||||
let mut array = [0u8; 32];
|
||||
array.copy_from_slice(slice);
|
||||
Ok(Self(array))
|
||||
}
|
||||
|
||||
/// Get inner value
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get inner value as slice
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TransactionId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TransactionId {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Self::from_hex(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Proofs> for TransactionId {
|
||||
type Error = nut00::Error;
|
||||
|
||||
fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
|
||||
Self::from_proofs(proofs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use bip39::Mnemonic;
|
||||
use cashu::Amount;
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::nuts::nut00::ProofsMethods;
|
||||
use cdk::nuts::{
|
||||
CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, PreMintSecrets, Proofs,
|
||||
SecretKey, State, SwapRequest,
|
||||
};
|
||||
use cdk::wallet::types::TransactionDirection;
|
||||
use cdk::wallet::{HttpClient, MintConnector, Wallet};
|
||||
use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
|
||||
use cdk_integration_tests::{attempt_to_swap_pending, wait_for_mint_to_be_paid};
|
||||
@@ -322,6 +324,72 @@ async fn test_fake_melt_payment_err_paid() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that change outputs in a melt quote are correctly handled
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_melt_change_in_quote() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(memory::empty().await?),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
||||
|
||||
let _mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
let transaction = wallet
|
||||
.list_transactions(Some(TransactionDirection::Incoming))
|
||||
.await?
|
||||
.pop()
|
||||
.expect("No transaction found");
|
||||
assert_eq!(wallet.mint_url, transaction.mint_url);
|
||||
assert_eq!(TransactionDirection::Incoming, transaction.direction);
|
||||
assert_eq!(Amount::from(100), transaction.amount);
|
||||
assert_eq!(Amount::from(0), transaction.fee);
|
||||
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
||||
|
||||
let fake_description = FakeInvoiceDescription::default();
|
||||
|
||||
let invoice = create_fake_invoice(9000, serde_json::to_string(&fake_description).unwrap());
|
||||
|
||||
let proofs = wallet.get_unspent_proofs().await?;
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?;
|
||||
|
||||
let keyset = wallet.get_active_mint_keyset().await?;
|
||||
|
||||
let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?;
|
||||
|
||||
let client = HttpClient::new(MINT_URL.parse()?, None);
|
||||
|
||||
let melt_request = MeltBolt11Request::new(
|
||||
melt_quote.id.clone(),
|
||||
proofs.clone(),
|
||||
Some(premint_secrets.blinded_messages()),
|
||||
);
|
||||
|
||||
let melt_response = client.post_melt(melt_request).await?;
|
||||
|
||||
assert!(melt_response.change.is_some());
|
||||
|
||||
let check = wallet.melt_quote_status(&melt_quote.id).await?;
|
||||
let mut melt_change = melt_response.change.unwrap();
|
||||
melt_change.sort_by(|a, b| a.amount.cmp(&b.amount));
|
||||
|
||||
let mut check = check.change.unwrap();
|
||||
check.sort_by(|a, b| a.amount.cmp(&b.amount));
|
||||
|
||||
assert_eq!(melt_change, check);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that the correct database type is used based on environment variables
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_database_type() -> Result<()> {
|
||||
|
||||
@@ -9,17 +9,18 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::hash::RandomState;
|
||||
use std::str::FromStr;
|
||||
|
||||
use cashu::amount::SplitTarget;
|
||||
use cashu::dhke::construct_proofs;
|
||||
use cashu::mint_url::MintUrl;
|
||||
use cashu::{
|
||||
CurrencyUnit, Id, MeltBolt11Request, NotificationPayload, PreMintSecrets, ProofState,
|
||||
SecretKey, SpendingConditions, State, SwapRequest,
|
||||
};
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::mint::Mint;
|
||||
use cdk::nuts::nut00::ProofsMethods;
|
||||
use cdk::subscription::{IndexableParams, Params};
|
||||
use cdk::wallet::SendOptions;
|
||||
use cdk::wallet::types::{TransactionDirection, TransactionId};
|
||||
use cdk::wallet::{ReceiveOptions, SendMemo, SendOptions};
|
||||
use cdk::Amount;
|
||||
use cdk_fake_wallet::create_fake_invoice;
|
||||
use cdk_integration_tests::init_pure_tests::*;
|
||||
@@ -68,7 +69,10 @@ async fn test_swap_to_send() {
|
||||
)
|
||||
);
|
||||
let token = wallet_alice
|
||||
.send(prepared_send, None)
|
||||
.send(
|
||||
prepared_send,
|
||||
Some(SendMemo::for_token("test_swapt_to_send")),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to send token");
|
||||
assert_eq!(
|
||||
@@ -97,12 +101,30 @@ async fn test_swap_to_send() {
|
||||
)
|
||||
);
|
||||
|
||||
let transaction_id = TransactionId::from_proofs(token.proofs()).expect("Failed to get tx id");
|
||||
|
||||
let transaction = wallet_alice
|
||||
.get_transaction(transaction_id)
|
||||
.await
|
||||
.expect("Failed to get transaction")
|
||||
.expect("Transaction not found");
|
||||
assert_eq!(wallet_alice.mint_url, transaction.mint_url);
|
||||
assert_eq!(TransactionDirection::Outgoing, transaction.direction);
|
||||
assert_eq!(Amount::from(40), transaction.amount);
|
||||
assert_eq!(Amount::from(0), transaction.fee);
|
||||
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
||||
assert_eq!(token.proofs().ys().unwrap(), transaction.ys);
|
||||
|
||||
// Alice sends cashu, Carol receives
|
||||
let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())
|
||||
.await
|
||||
.expect("Failed to create Carol's wallet");
|
||||
let received_amount = wallet_carol
|
||||
.receive_proofs(token.proofs(), SplitTarget::None, &[], &[])
|
||||
.receive_proofs(
|
||||
token.proofs(),
|
||||
ReceiveOptions::default(),
|
||||
token.memo().clone(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to receive proofs");
|
||||
|
||||
@@ -114,6 +136,19 @@ async fn test_swap_to_send() {
|
||||
.await
|
||||
.expect("Failed to get Carol's balance")
|
||||
);
|
||||
|
||||
let transaction = wallet_carol
|
||||
.get_transaction(transaction_id)
|
||||
.await
|
||||
.expect("Failed to get transaction")
|
||||
.expect("Transaction not found");
|
||||
assert_eq!(wallet_carol.mint_url, transaction.mint_url);
|
||||
assert_eq!(TransactionDirection::Incoming, transaction.direction);
|
||||
assert_eq!(Amount::from(40), transaction.amount);
|
||||
assert_eq!(Amount::from(0), transaction.fee);
|
||||
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
||||
assert_eq!(token.proofs().ys().unwrap(), transaction.ys);
|
||||
assert_eq!(token.memo().clone(), transaction.memo);
|
||||
}
|
||||
|
||||
/// Tests the NUT-06 functionality (mint discovery):
|
||||
@@ -141,6 +176,18 @@ async fn test_mint_nut06() {
|
||||
.expect("Failed to get balance");
|
||||
assert_eq!(Amount::from(64), balance_alice);
|
||||
|
||||
let transaction = wallet_alice
|
||||
.list_transactions(None)
|
||||
.await
|
||||
.expect("Failed to list transactions")
|
||||
.pop()
|
||||
.expect("No transactions found");
|
||||
assert_eq!(wallet_alice.mint_url, transaction.mint_url);
|
||||
assert_eq!(TransactionDirection::Incoming, transaction.direction);
|
||||
assert_eq!(Amount::from(64), transaction.amount);
|
||||
assert_eq!(Amount::from(0), transaction.fee);
|
||||
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
||||
|
||||
let initial_mint_url = wallet_alice.mint_url.clone();
|
||||
let mint_info_before = wallet_alice
|
||||
.get_mint_info()
|
||||
|
||||
@@ -11,7 +11,7 @@ use cdk_common::common::ProofInfo;
|
||||
use cdk_common::database::WalletDatabase;
|
||||
use cdk_common::mint_url::MintUrl;
|
||||
use cdk_common::util::unix_time;
|
||||
use cdk_common::wallet::{self, MintQuote};
|
||||
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
||||
use cdk_common::{
|
||||
database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
|
||||
};
|
||||
@@ -40,6 +40,8 @@ const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_
|
||||
const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs");
|
||||
const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
|
||||
const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
|
||||
// <Transaction_id, Transaction>
|
||||
const TRANSACTIONS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("transactions");
|
||||
|
||||
const DATABASE_VERSION: u32 = 2;
|
||||
|
||||
@@ -132,6 +134,7 @@ impl WalletRedbDatabase {
|
||||
let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
|
||||
let _ = write_txn.open_table(PROOFS_TABLE)?;
|
||||
let _ = write_txn.open_table(KEYSET_COUNTER)?;
|
||||
let _ = write_txn.open_table(TRANSACTIONS_TABLE)?;
|
||||
table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
|
||||
}
|
||||
|
||||
@@ -685,4 +688,95 @@ impl WalletDatabase for WalletRedbDatabase {
|
||||
|
||||
Ok(counter.map(|c| c.value()))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err> {
|
||||
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||
|
||||
{
|
||||
let mut table = write_txn
|
||||
.open_table(TRANSACTIONS_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
table
|
||||
.insert(
|
||||
transaction.id().as_slice(),
|
||||
serde_json::to_string(&transaction)
|
||||
.map_err(Error::from)?
|
||||
.as_str(),
|
||||
)
|
||||
.map_err(Error::from)?;
|
||||
}
|
||||
|
||||
write_txn.commit().map_err(Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn get_transaction(
|
||||
&self,
|
||||
transaction_id: TransactionId,
|
||||
) -> Result<Option<Transaction>, Self::Err> {
|
||||
let read_txn = self.db.begin_read().map_err(Error::from)?;
|
||||
let table = read_txn
|
||||
.open_table(TRANSACTIONS_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
if let Some(transaction) = table.get(transaction_id.as_slice()).map_err(Error::from)? {
|
||||
return Ok(serde_json::from_str(transaction.value()).map_err(Error::from)?);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn list_transactions(
|
||||
&self,
|
||||
mint_url: Option<MintUrl>,
|
||||
direction: Option<TransactionDirection>,
|
||||
unit: Option<CurrencyUnit>,
|
||||
) -> Result<Vec<Transaction>, Self::Err> {
|
||||
let read_txn = self.db.begin_read().map_err(Error::from)?;
|
||||
|
||||
let table = read_txn
|
||||
.open_table(TRANSACTIONS_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let transactions: Vec<Transaction> = table
|
||||
.iter()
|
||||
.map_err(Error::from)?
|
||||
.flatten()
|
||||
.filter_map(|(_k, v)| {
|
||||
let mut transaction = None;
|
||||
|
||||
if let Ok(tx) = serde_json::from_str::<Transaction>(v.value()) {
|
||||
if tx.matches_conditions(&mint_url, &direction, &unit) {
|
||||
transaction = Some(tx)
|
||||
}
|
||||
}
|
||||
|
||||
transaction
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err> {
|
||||
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||
|
||||
{
|
||||
let mut table = write_txn
|
||||
.open_table(TRANSACTIONS_TABLE)
|
||||
.map_err(Error::from)?;
|
||||
table
|
||||
.remove(transaction_id.as_slice())
|
||||
.map_err(Error::from)?;
|
||||
}
|
||||
|
||||
write_txn.commit().map_err(Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ pub enum Error {
|
||||
/// Serde Error
|
||||
#[error(transparent)]
|
||||
Serde(#[from] serde_json::Error),
|
||||
/// CDK Error
|
||||
#[error(transparent)]
|
||||
CDK(#[from] cdk_common::Error),
|
||||
/// NUT00 Error
|
||||
#[error(transparent)]
|
||||
CDKNUT00(#[from] cdk_common::nuts::nut00::Error),
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Migration to add transactions table
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id BLOB PRIMARY KEY,
|
||||
mint_url TEXT NOT NULL,
|
||||
direction TEXT CHECK (direction IN ('Incoming', 'Outgoing')) NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
fee INTEGER NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
ys BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
memo TEXT,
|
||||
metadata TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS mint_url_index ON transactions(mint_url);
|
||||
CREATE INDEX IF NOT EXISTS direction_index ON transactions(direction);
|
||||
CREATE INDEX IF NOT EXISTS unit_index ON transactions(unit);
|
||||
CREATE INDEX IF NOT EXISTS timestamp_index ON transactions(timestamp);
|
||||
@@ -10,10 +10,10 @@ use cdk_common::database::WalletDatabase;
|
||||
use cdk_common::mint_url::MintUrl;
|
||||
use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
|
||||
use cdk_common::secret::Secret;
|
||||
use cdk_common::wallet::{self, MintQuote};
|
||||
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
||||
use cdk_common::{
|
||||
database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, ProofDleq, PublicKey,
|
||||
SecretKey, SpendingConditions, State,
|
||||
database, nut01, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
|
||||
PublicKey, SecretKey, SpendingConditions, State,
|
||||
};
|
||||
use error::Error;
|
||||
use sqlx::sqlite::SqliteRow;
|
||||
@@ -778,6 +778,138 @@ WHERE id=?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err> {
|
||||
let mint_url = transaction.mint_url.to_string();
|
||||
let direction = transaction.direction.to_string();
|
||||
let unit = transaction.unit.to_string();
|
||||
let amount = u64::from(transaction.amount) as i64;
|
||||
let fee = u64::from(transaction.fee) as i64;
|
||||
let ys = transaction
|
||||
.ys
|
||||
.iter()
|
||||
.flat_map(|y| y.to_bytes().to_vec())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO transactions
|
||||
(id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
mint_url = excluded.mint_url,
|
||||
direction = excluded.direction,
|
||||
unit = excluded.unit,
|
||||
amount = excluded.amount,
|
||||
fee = excluded.fee,
|
||||
ys = excluded.ys,
|
||||
timestamp = excluded.timestamp,
|
||||
memo = excluded.memo,
|
||||
metadata = excluded.metadata
|
||||
;
|
||||
"#,
|
||||
)
|
||||
.bind(transaction.id().as_slice())
|
||||
.bind(mint_url)
|
||||
.bind(direction)
|
||||
.bind(unit)
|
||||
.bind(amount)
|
||||
.bind(fee)
|
||||
.bind(ys)
|
||||
.bind(transaction.timestamp as i64)
|
||||
.bind(transaction.memo)
|
||||
.bind(serde_json::to_string(&transaction.metadata).map_err(Error::from)?)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn get_transaction(
|
||||
&self,
|
||||
transaction_id: TransactionId,
|
||||
) -> Result<Option<Transaction>, Self::Err> {
|
||||
let rec = sqlx::query(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM transactions
|
||||
WHERE id=?;
|
||||
"#,
|
||||
)
|
||||
.bind(transaction_id.as_slice())
|
||||
.fetch_one(&self.pool)
|
||||
.await;
|
||||
|
||||
let rec = match rec {
|
||||
Ok(rec) => rec,
|
||||
Err(err) => match err {
|
||||
sqlx::Error::RowNotFound => return Ok(None),
|
||||
_ => return Err(Error::SQLX(err).into()),
|
||||
},
|
||||
};
|
||||
|
||||
let transaction = sqlite_row_to_transaction(&rec)?;
|
||||
|
||||
Ok(Some(transaction))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn list_transactions(
|
||||
&self,
|
||||
mint_url: Option<MintUrl>,
|
||||
direction: Option<TransactionDirection>,
|
||||
unit: Option<CurrencyUnit>,
|
||||
) -> Result<Vec<Transaction>, Self::Err> {
|
||||
let recs = sqlx::query(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM transactions;
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await;
|
||||
|
||||
let recs = match recs {
|
||||
Ok(rec) => rec,
|
||||
Err(err) => match err {
|
||||
sqlx::Error::RowNotFound => return Ok(vec![]),
|
||||
_ => return Err(Error::SQLX(err).into()),
|
||||
},
|
||||
};
|
||||
|
||||
let transactions = recs
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let transaction = sqlite_row_to_transaction(p).ok()?;
|
||||
if transaction.matches_conditions(&mint_url, &direction, &unit) {
|
||||
Some(transaction)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM transactions
|
||||
WHERE id=?
|
||||
"#,
|
||||
)
|
||||
.bind(transaction_id.as_slice())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlite_row_to_mint_info(row: &SqliteRow) -> Result<MintInfo, Error> {
|
||||
@@ -926,6 +1058,37 @@ fn sqlite_row_to_proof_info(row: &SqliteRow) -> Result<ProofInfo, Error> {
|
||||
})
|
||||
}
|
||||
|
||||
fn sqlite_row_to_transaction(row: &SqliteRow) -> Result<Transaction, Error> {
|
||||
let mint_url: String = row.try_get("mint_url").map_err(Error::from)?;
|
||||
let direction: String = row.try_get("direction").map_err(Error::from)?;
|
||||
let unit: String = row.try_get("unit").map_err(Error::from)?;
|
||||
let amount: i64 = row.try_get("amount").map_err(Error::from)?;
|
||||
let fee: i64 = row.try_get("fee").map_err(Error::from)?;
|
||||
let ys: Vec<u8> = row.try_get("ys").map_err(Error::from)?;
|
||||
let timestamp: i64 = row.try_get("timestamp").map_err(Error::from)?;
|
||||
let memo: Option<String> = row.try_get("memo").map_err(Error::from)?;
|
||||
let row_metadata: Option<String> = row.try_get("metadata").map_err(Error::from)?;
|
||||
|
||||
let metadata: HashMap<String, String> = row_metadata
|
||||
.and_then(|m| serde_json::from_str(&m).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let ys: Result<Vec<PublicKey>, nut01::Error> =
|
||||
ys.chunks(33).map(PublicKey::from_slice).collect();
|
||||
|
||||
Ok(Transaction {
|
||||
mint_url: MintUrl::from_str(&mint_url)?,
|
||||
direction: TransactionDirection::from_str(&direction)?,
|
||||
unit: CurrencyUnit::from_str(&unit)?,
|
||||
amount: Amount::from(amount as u64),
|
||||
fee: Amount::from(fee as u64),
|
||||
ys: ys?,
|
||||
timestamp: timestamp as u64,
|
||||
memo,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cdk_common::database::WalletDatabase;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::error::Error;
|
||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, SecretKey, SpendingConditions};
|
||||
use cdk::wallet::{SendOptions, Wallet, WalletSubscription};
|
||||
use cdk::wallet::{ReceiveOptions, SendOptions, Wallet, WalletSubscription};
|
||||
use cdk::Amount;
|
||||
use cdk_sqlite::wallet::memory;
|
||||
use rand::random;
|
||||
@@ -94,7 +94,13 @@ async fn main() -> Result<(), Error> {
|
||||
|
||||
// Receive the token using the secret key
|
||||
let amount = wallet
|
||||
.receive(&token.to_string(), SplitTarget::default(), &[secret], &[])
|
||||
.receive(
|
||||
&token.to_string(),
|
||||
ReceiveOptions {
|
||||
p2pk_signing_keys: vec![secret],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Redeemed locked token worth: {}", u64::from(amount));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection};
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -243,6 +245,21 @@ impl Wallet {
|
||||
.update_proofs(change_proof_infos, deleted_ys)
|
||||
.await?;
|
||||
|
||||
// Add transaction to store
|
||||
self.localstore
|
||||
.add_transaction(Transaction {
|
||||
mint_url: self.mint_url.clone(),
|
||||
direction: TransactionDirection::Outgoing,
|
||||
amount: melted.amount,
|
||||
fee: melted.fee_paid,
|
||||
unit: self.unit.clone(),
|
||||
ys: proofs.ys()?,
|
||||
timestamp: unix_time(),
|
||||
memo: None,
|
||||
metadata: HashMap::new(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(melted)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cdk_common::ensure_cdk;
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::MintQuote;
|
||||
@@ -286,6 +289,21 @@ impl Wallet {
|
||||
// Add new proofs to store
|
||||
self.localstore.update_proofs(proof_infos, vec![]).await?;
|
||||
|
||||
// Add transaction to store
|
||||
self.localstore
|
||||
.add_transaction(Transaction {
|
||||
mint_url: self.mint_url.clone(),
|
||||
direction: TransactionDirection::Incoming,
|
||||
amount: proofs.total_amount()?,
|
||||
fee: Amount::ZERO,
|
||||
unit: self.unit.clone(),
|
||||
ys: proofs.ys()?,
|
||||
timestamp: unix_time,
|
||||
memo: None,
|
||||
metadata: HashMap::new(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(proofs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ mod receive;
|
||||
mod send;
|
||||
pub mod subscription;
|
||||
mod swap;
|
||||
mod transactions;
|
||||
pub mod util;
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
@@ -54,6 +55,7 @@ pub use cdk_common::wallet as types;
|
||||
pub use mint_connector::AuthHttpClient;
|
||||
pub use mint_connector::{HttpClient, MintConnector};
|
||||
pub use multi_mint_wallet::MultiMintWallet;
|
||||
pub use receive::ReceiveOptions;
|
||||
pub use send::{PreparedSend, SendMemo, SendOptions};
|
||||
pub use types::{MeltQuote, MintQuote, SendKind};
|
||||
|
||||
|
||||
@@ -10,15 +10,16 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use cdk_common::database;
|
||||
use cdk_common::database::WalletDatabase;
|
||||
use cdk_common::wallet::WalletKey;
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection, WalletKey};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::receive::ReceiveOptions;
|
||||
use super::send::{PreparedSend, SendMemo, SendOptions};
|
||||
use super::Error;
|
||||
use crate::amount::SplitTarget;
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::{CurrencyUnit, MeltOptions, Proof, Proofs, SecretKey, SpendingConditions, Token};
|
||||
use crate::nuts::{CurrencyUnit, MeltOptions, Proof, Proofs, SpendingConditions, Token};
|
||||
use crate::types::Melted;
|
||||
use crate::wallet::types::MintQuote;
|
||||
use crate::{ensure_cdk, Amount, Wallet};
|
||||
@@ -142,6 +143,24 @@ impl MultiMintWallet {
|
||||
Ok(mint_proofs)
|
||||
}
|
||||
|
||||
/// List transactions
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_transactions(
|
||||
&self,
|
||||
direction: Option<TransactionDirection>,
|
||||
) -> Result<Vec<Transaction>, Error> {
|
||||
let mut transactions = Vec::new();
|
||||
|
||||
for (_, wallet) in self.wallets.lock().await.iter() {
|
||||
let wallet_transactions = wallet.list_transactions(direction).await?;
|
||||
transactions.extend(wallet_transactions);
|
||||
}
|
||||
|
||||
transactions.sort();
|
||||
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
/// Prepare to send
|
||||
#[instrument(skip(self))]
|
||||
pub async fn prepare_send(
|
||||
@@ -246,8 +265,7 @@ impl MultiMintWallet {
|
||||
pub async fn receive(
|
||||
&self,
|
||||
encoded_token: &str,
|
||||
p2pk_signing_keys: &[SecretKey],
|
||||
preimages: &[String],
|
||||
opts: ReceiveOptions,
|
||||
) -> Result<Amount, Error> {
|
||||
let token_data = Token::from_str(encoded_token)?;
|
||||
let unit = token_data.unit().unwrap_or_default();
|
||||
@@ -273,7 +291,7 @@ impl MultiMintWallet {
|
||||
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
||||
|
||||
match wallet
|
||||
.receive_proofs(proofs, SplitTarget::default(), p2pk_signing_keys, preimages)
|
||||
.receive_proofs(proofs, opts, token_data.memo().clone())
|
||||
.await
|
||||
{
|
||||
Ok(amount) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use cdk_common::wallet::TransactionId;
|
||||
use cdk_common::Id;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -75,6 +76,8 @@ impl Wallet {
|
||||
pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> {
|
||||
let proof_ys = proofs.ys()?;
|
||||
|
||||
let transaction_id = TransactionId::new(proof_ys.clone());
|
||||
|
||||
let spendable = self
|
||||
.client
|
||||
.post_check_state(CheckStateRequest { ys: proof_ys })
|
||||
@@ -90,6 +93,13 @@ impl Wallet {
|
||||
self.swap(None, SplitTarget::default(), unspent, None, false)
|
||||
.await?;
|
||||
|
||||
match self.localstore.remove_transaction(transaction_id).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to remove transaction: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::str::FromStr;
|
||||
use bitcoin::hashes::sha256::Hash as Sha256Hash;
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::XOnlyPublicKey;
|
||||
use cdk_common::util::unix_time;
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::amount::SplitTarget;
|
||||
@@ -21,9 +23,8 @@ impl Wallet {
|
||||
pub async fn receive_proofs(
|
||||
&self,
|
||||
proofs: Proofs,
|
||||
amount_split_target: SplitTarget,
|
||||
p2pk_signing_keys: &[SecretKey],
|
||||
preimages: &[String],
|
||||
opts: ReceiveOptions,
|
||||
memo: Option<String>,
|
||||
) -> Result<Amount, Error> {
|
||||
let mint_url = &self.mint_url;
|
||||
// Add mint if it does not exist in the store
|
||||
@@ -45,10 +46,14 @@ impl Wallet {
|
||||
|
||||
let mut proofs = proofs;
|
||||
|
||||
let proofs_amount = proofs.total_amount()?;
|
||||
let proofs_ys = proofs.ys()?;
|
||||
|
||||
let mut sig_flag = SigFlag::SigInputs;
|
||||
|
||||
// Map hash of preimage to preimage
|
||||
let hashed_to_preimage: HashMap<String, &String> = preimages
|
||||
let hashed_to_preimage: HashMap<String, &String> = opts
|
||||
.preimages
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let hex_bytes = hex::decode(p)?;
|
||||
@@ -56,7 +61,8 @@ impl Wallet {
|
||||
})
|
||||
.collect::<Result<HashMap<String, &String>, _>>()?;
|
||||
|
||||
let p2pk_signing_keys: HashMap<XOnlyPublicKey, &SecretKey> = p2pk_signing_keys
|
||||
let p2pk_signing_keys: HashMap<XOnlyPublicKey, &SecretKey> = opts
|
||||
.p2pk_signing_keys
|
||||
.iter()
|
||||
.map(|s| (s.x_only_public_key(&SECP256K1).0, s))
|
||||
.collect();
|
||||
@@ -117,7 +123,7 @@ impl Wallet {
|
||||
.await?;
|
||||
|
||||
let mut pre_swap = self
|
||||
.create_swap(None, amount_split_target, proofs, None, false)
|
||||
.create_swap(None, opts.amount_split_target, proofs, None, false)
|
||||
.await?;
|
||||
|
||||
if sig_flag.eq(&SigFlag::SigAll) {
|
||||
@@ -155,6 +161,21 @@ impl Wallet {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add transaction to store
|
||||
self.localstore
|
||||
.add_transaction(Transaction {
|
||||
mint_url: self.mint_url.clone(),
|
||||
direction: TransactionDirection::Incoming,
|
||||
amount: total_amount,
|
||||
fee: proofs_amount - total_amount,
|
||||
unit: self.unit.clone(),
|
||||
ys: proofs_ys,
|
||||
timestamp: unix_time(),
|
||||
memo,
|
||||
metadata: opts.metadata,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(total_amount)
|
||||
}
|
||||
|
||||
@@ -166,7 +187,7 @@ impl Wallet {
|
||||
/// use cdk::amount::SplitTarget;
|
||||
/// use cdk_sqlite::wallet::memory;
|
||||
/// use cdk::nuts::CurrencyUnit;
|
||||
/// use cdk::wallet::Wallet;
|
||||
/// use cdk::wallet::{ReceiveOptions, Wallet};
|
||||
/// use rand::random;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
@@ -178,7 +199,7 @@ impl Wallet {
|
||||
/// let localstore = memory::empty().await?;
|
||||
/// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
|
||||
/// let token = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJhbW91bnQiOjEsInNlY3JldCI6ImI0ZjVlNDAxMDJhMzhiYjg3NDNiOTkwMzU5MTU1MGYyZGEzZTQxNWEzMzU0OTUyN2M2MmM5ZDc5MGVmYjM3MDUiLCJDIjoiMDIzYmU1M2U4YzYwNTMwZWVhOWIzOTQzZmRhMWEyY2U3MWM3YjNmMGNmMGRjNmQ4NDZmYTc2NWFhZjc3OWZhODFkIiwiaWQiOiIwMDlhMWYyOTMyNTNlNDFlIn1dLCJtaW50IjoiaHR0cHM6Ly90ZXN0bnV0LmNhc2h1LnNwYWNlIn1dLCJ1bml0Ijoic2F0In0=";
|
||||
/// let amount_receive = wallet.receive(token, SplitTarget::default(), &[], &[]).await?;
|
||||
/// let amount_receive = wallet.receive(token, ReceiveOptions::default()).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
@@ -186,9 +207,7 @@ impl Wallet {
|
||||
pub async fn receive(
|
||||
&self,
|
||||
encoded_token: &str,
|
||||
amount_split_target: SplitTarget,
|
||||
p2pk_signing_keys: &[SecretKey],
|
||||
preimages: &[String],
|
||||
opts: ReceiveOptions,
|
||||
) -> Result<Amount, Error> {
|
||||
let token = Token::from_str(encoded_token)?;
|
||||
|
||||
@@ -205,7 +224,7 @@ impl Wallet {
|
||||
ensure_cdk!(self.mint_url == token.mint_url()?, Error::IncorrectMint);
|
||||
|
||||
let amount = self
|
||||
.receive_proofs(proofs, amount_split_target, p2pk_signing_keys, preimages)
|
||||
.receive_proofs(proofs, opts, token.memo().clone())
|
||||
.await?;
|
||||
|
||||
Ok(amount)
|
||||
@@ -219,7 +238,7 @@ impl Wallet {
|
||||
/// use cdk::amount::SplitTarget;
|
||||
/// use cdk_sqlite::wallet::memory;
|
||||
/// use cdk::nuts::CurrencyUnit;
|
||||
/// use cdk::wallet::Wallet;
|
||||
/// use cdk::wallet::{ReceiveOptions, Wallet};
|
||||
/// use cdk::util::hex;
|
||||
/// use rand::random;
|
||||
///
|
||||
@@ -232,7 +251,7 @@ impl Wallet {
|
||||
/// let localstore = memory::empty().await?;
|
||||
/// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
|
||||
/// let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
|
||||
/// let amount_receive = wallet.receive_raw(&token_raw, SplitTarget::default(), &[], &[]).await?;
|
||||
/// let amount_receive = wallet.receive_raw(&token_raw, ReceiveOptions::default()).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
@@ -240,17 +259,22 @@ impl Wallet {
|
||||
pub async fn receive_raw(
|
||||
&self,
|
||||
binary_token: &Vec<u8>,
|
||||
amount_split_target: SplitTarget,
|
||||
p2pk_signing_keys: &[SecretKey],
|
||||
preimages: &[String],
|
||||
opts: ReceiveOptions,
|
||||
) -> Result<Amount, Error> {
|
||||
let token_str = Token::try_from(binary_token)?.to_string();
|
||||
self.receive(
|
||||
token_str.as_str(),
|
||||
amount_split_target,
|
||||
p2pk_signing_keys,
|
||||
preimages,
|
||||
)
|
||||
.await
|
||||
self.receive(token_str.as_str(), opts).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive options
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ReceiveOptions {
|
||||
/// Amount split target
|
||||
pub amount_split_target: SplitTarget,
|
||||
/// P2PK signing keys
|
||||
pub p2pk_signing_keys: Vec<SecretKey>,
|
||||
/// Preimages
|
||||
pub preimages: Vec<String>,
|
||||
/// Metadata
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use cdk_common::util::unix_time;
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::SendKind;
|
||||
@@ -202,6 +205,7 @@ impl Wallet {
|
||||
#[instrument(skip(self), err)]
|
||||
pub async fn send(&self, send: PreparedSend, memo: Option<SendMemo>) -> Result<Token, Error> {
|
||||
tracing::info!("Sending prepared send");
|
||||
let total_send_fee = send.fee();
|
||||
let mut proofs_to_send = send.proofs_to_send;
|
||||
|
||||
// Get active keyset ID
|
||||
@@ -273,6 +277,21 @@ impl Wallet {
|
||||
let send_memo = send.options.memo.or(memo);
|
||||
let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None });
|
||||
|
||||
// Add transaction to store
|
||||
self.localstore
|
||||
.add_transaction(Transaction {
|
||||
mint_url: self.mint_url.clone(),
|
||||
direction: TransactionDirection::Outgoing,
|
||||
amount: send.amount,
|
||||
fee: total_send_fee,
|
||||
unit: self.unit.clone(),
|
||||
ys: proofs_to_send.ys()?,
|
||||
timestamp: unix_time(),
|
||||
memo: memo.clone(),
|
||||
metadata: send.options.metadata,
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Create and return token
|
||||
Ok(Token::new(
|
||||
self.mint_url.clone(),
|
||||
@@ -401,6 +420,8 @@ pub struct SendOptions {
|
||||
///
|
||||
/// When this is true the token created will include the amount of fees needed to redeem the token (amount + fee_to_redeem)
|
||||
pub include_fee: bool,
|
||||
/// Metadata
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Send memo
|
||||
@@ -411,3 +432,13 @@ pub struct SendMemo {
|
||||
/// Include memo in token
|
||||
pub include_memo: bool,
|
||||
}
|
||||
|
||||
impl SendMemo {
|
||||
/// Create a new send memo
|
||||
pub fn for_token(memo: &str) -> Self {
|
||||
Self {
|
||||
memo: memo.to_string(),
|
||||
include_memo: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
crates/cdk/src/wallet/transactions.rs
Normal file
31
crates/cdk/src/wallet/transactions.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use cdk_common::wallet::{Transaction, TransactionDirection, TransactionId};
|
||||
|
||||
use crate::{Error, Wallet};
|
||||
|
||||
impl Wallet {
|
||||
/// List transactions
|
||||
pub async fn list_transactions(
|
||||
&self,
|
||||
direction: Option<TransactionDirection>,
|
||||
) -> Result<Vec<Transaction>, Error> {
|
||||
let mut transactions = self
|
||||
.localstore
|
||||
.list_transactions(
|
||||
Some(self.mint_url.clone()),
|
||||
direction,
|
||||
Some(self.unit.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
transactions.sort();
|
||||
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
/// Get transaction by ID
|
||||
pub async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Error> {
|
||||
let transaction = self.localstore.get_transaction(id).await?;
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user