Refactor wallet to accept seed for internal Xpriv

This commit is contained in:
David Caseria
2024-05-14 13:58:25 -04:00
parent 2671c92c00
commit b9234817db
11 changed files with 404 additions and 295 deletions

View File

@@ -28,7 +28,7 @@ cdk = { path = "./crates/cdk", default-features = false }
cdk-rexie = { path = "./crates/cdk-rexie", default-features = false }
tokio = { version = "1.32", default-features = false }
thiserror = "1"
tracing = { version = "0.1", default-features = false }
tracing = { version = "0.1", default-features = false, features = ["attributes"] }
serde = { version = "1", default-features = false, features = ["derive"] }
serde_json = "1"
serde-wasm-bindgen = { version = "0.6.5", default-features = false }

View File

@@ -3,6 +3,7 @@ use wasm_bindgen::prelude::*;
pub mod error;
pub mod nuts;
pub mod types;
#[cfg(all(feature = "wallet", target_arch = "wasm32"))]
pub mod wallet;
#[wasm_bindgen(start)]

View File

@@ -10,6 +10,8 @@ use cdk_rexie::RexieWalletDatabase;
use wasm_bindgen::prelude::*;
use crate::error::{into_err, Result};
use crate::nuts::nut04::JsMintQuoteBolt11Response;
use crate::nuts::nut05::JsMeltQuoteBolt11Response;
use crate::nuts::nut11::JsP2PKSpendingConditions;
use crate::nuts::nut14::JsHTLCSpendingConditions;
use crate::nuts::{JsCurrencyUnit, JsMintInfo, JsProof};
@@ -37,11 +39,11 @@ impl From<Wallet> for JsWallet {
#[wasm_bindgen(js_class = Wallet)]
impl JsWallet {
#[wasm_bindgen(constructor)]
pub async fn new() -> Self {
pub async fn new(seed: Vec<u8>) -> Self {
let client = HttpClient::new();
let db = RexieWalletDatabase::new().await.unwrap();
Wallet::new(client, Arc::new(db), None).await.into()
Wallet::new(client, Arc::new(db), &seed).await.into()
}
#[wasm_bindgen(js_name = totalBalance)]
@@ -95,6 +97,23 @@ impl JsWallet {
Ok(quote.into())
}
#[wasm_bindgen(js_name = mintQuoteStatus)]
pub async fn mint_quote_status(
&self,
mint_url: String,
quote_id: String,
) -> Result<JsMintQuoteBolt11Response> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let quote = self
.inner
.mint_quote_status(mint_url, &quote_id)
.await
.map_err(into_err)?;
Ok(quote.into())
}
#[wasm_bindgen(js_name = mint)]
pub async fn mint(&mut self, mint_url: String, quote_id: String) -> Result<JsAmount> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
@@ -124,6 +143,23 @@ impl JsWallet {
Ok(melt_quote.into())
}
#[wasm_bindgen(js_name = meltQuoteStatus)]
pub async fn melt_quote_status(
&self,
mint_url: String,
quote_id: String,
) -> Result<JsMeltQuoteBolt11Response> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
let quote = self
.inner
.melt_quote_status(mint_url, &quote_id)
.await
.map_err(into_err)?;
Ok(quote.into())
}
#[wasm_bindgen(js_name = melt)]
pub async fn melt(&mut self, mint_url: String, quote_id: String) -> Result<JsMelted> {
let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;

View File

@@ -10,6 +10,7 @@ use cdk::types::{MeltQuote, MintQuote};
use cdk::url::UncheckedUrl;
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
use tokio::sync::Mutex;
use tracing::instrument;
use super::error::Error;
@@ -23,7 +24,7 @@ const PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinitio
const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> =
MultimapTableDefinition::new("pending_proofs");
const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
const KEYSET_COUNTER: TableDefinition<&str, u64> = TableDefinition::new("keyset_counter");
const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
const DATABASE_VERSION: u32 = 0;
@@ -79,6 +80,7 @@ impl RedbWalletDatabase {
impl WalletDatabase for RedbWalletDatabase {
type Err = cdk_database::Error;
#[instrument(skip(self))]
async fn add_mint(
&self,
mint_url: UncheckedUrl,
@@ -104,6 +106,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self))]
async fn get_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Into::<Error>::into)?;
@@ -119,6 +122,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(None)
}
#[instrument(skip(self))]
async fn get_mints(&self) -> Result<HashMap<UncheckedUrl, Option<MintInfo>>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
@@ -138,6 +142,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(mints)
}
#[instrument(skip(self))]
async fn add_mint_keysets(
&self,
mint_url: UncheckedUrl,
@@ -168,6 +173,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self))]
async fn get_mint_keysets(
&self,
mint_url: UncheckedUrl,
@@ -188,6 +194,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(keysets)
}
#[instrument(skip_all)]
async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
@@ -209,6 +216,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip_all)]
async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Into::<Error>::into)?;
@@ -223,6 +231,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(None)
}
#[instrument(skip_all)]
async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
@@ -239,6 +248,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip_all)]
async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
@@ -260,6 +270,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip_all)]
async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
@@ -274,6 +285,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(None)
}
#[instrument(skip_all)]
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
@@ -290,6 +302,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip_all)]
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
@@ -309,6 +322,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self))]
async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
@@ -321,6 +335,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(None)
}
#[instrument(skip(self))]
async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let write_txn = db.begin_write().map_err(Error::from)?;
@@ -336,6 +351,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self, proofs))]
async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Self::Err> {
let db = self.db.lock().await;
@@ -360,6 +376,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self))]
async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
@@ -377,6 +394,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(proofs)
}
#[instrument(skip(self, proofs))]
async fn remove_proofs(
&self,
mint_url: UncheckedUrl,
@@ -405,6 +423,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self, proofs))]
async fn add_pending_proofs(
&self,
mint_url: UncheckedUrl,
@@ -433,6 +452,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
#[instrument(skip(self))]
async fn get_pending_proofs(
&self,
mint_url: UncheckedUrl,
@@ -453,6 +473,7 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(proofs)
}
#[instrument(skip(self, proofs))]
async fn remove_pending_proofs(
&self,
mint_url: UncheckedUrl,
@@ -481,7 +502,8 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err> {
#[instrument(skip(self))]
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
let db = self.db.lock().await;
let current_counter;
@@ -512,7 +534,8 @@ impl WalletDatabase for RedbWalletDatabase {
Ok(())
}
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Self::Err> {
#[instrument(skip(self))]
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
let db = self.db.lock().await;
let read_txn = db.begin_read().map_err(Error::from)?;
let table = read_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;

View File

@@ -592,7 +592,7 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(())
}
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err> {
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
@@ -604,7 +604,7 @@ impl WalletDatabase for RexieWalletDatabase {
let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?;
let current_count = counter_store.get(&keyset_id).await.map_err(Error::from)?;
let current_count: Option<u64> =
let current_count: Option<u32> =
serde_wasm_bindgen::from_value(current_count).map_err(Error::from)?;
let new_count = current_count.unwrap_or_default() + count;
@@ -621,7 +621,7 @@ impl WalletDatabase for RexieWalletDatabase {
Ok(())
}
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Self::Err> {
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
let rexie = self.db.lock().await;
let transaction = rexie
@@ -633,7 +633,7 @@ impl WalletDatabase for RexieWalletDatabase {
let keyset_id = serde_wasm_bindgen::to_value(keyset_id).map_err(Error::from)?;
let current_count = counter_store.get(&keyset_id).await.map_err(Error::from)?;
let current_count: Option<u64> =
let current_count: Option<u32> =
serde_wasm_bindgen::from_value(current_count).map_err(Error::from)?;
Ok(current_count)

View File

@@ -21,25 +21,39 @@ nut13 = ["dep:bip39"]
async-trait = "0.1"
base64 = "0.22" # bitcoin uses v0.13 (optional dep)
bip39 = { version = "2.0", optional = true }
bitcoin = { version = "0.30", features = ["serde", "rand", "rand-std"] } # lightning-invoice uses v0.30
bitcoin = { version = "0.30", features = [
"serde",
"rand",
"rand-std",
] } # lightning-invoice uses v0.30
http = "1.0"
lightning-invoice = { version = "0.30", features = ["serde"] }
once_cell = "1.19"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "socks"], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"]}
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
"socks",
], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0"
serde_with = "3.4"
tracing = { version = "0.1", default-features = false }
tracing = { version = "0.1", default-features = false, features = [
"attributes",
"log",
] }
thiserror = "1.0"
url = "2.3"
uuid = { version = "1.6", features = ["v4"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync"] }
tokio = { workspace = true, features = [
"rt-multi-thread",
"time",
"macros",
"sync",
] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
getrandom = { version = "0.2", features = ["js"] }
instant = { version = "0.1", features = [ "wasm-bindgen", "inaccurate" ] }
instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }

View File

@@ -88,8 +88,8 @@ pub trait WalletDatabase {
proofs: &Proofs,
) -> Result<(), Self::Err>;
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Self::Err>;
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u64>, Self::Err>;
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;
}
#[cfg(feature = "mint")]

View File

@@ -21,7 +21,7 @@ pub struct WalletMemoryDatabase {
mint_keys: Arc<Mutex<HashMap<Id, Keys>>>,
proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
pending_proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
keyset_counter: Arc<Mutex<HashMap<Id, u64>>>,
keyset_counter: Arc<Mutex<HashMap<Id, u32>>>,
}
impl WalletMemoryDatabase {
@@ -29,7 +29,7 @@ impl WalletMemoryDatabase {
mint_quotes: Vec<MintQuote>,
melt_quotes: Vec<MeltQuote>,
mint_keys: Vec<Keys>,
keyset_counter: HashMap<Id, u64>,
keyset_counter: HashMap<Id, u32>,
) -> Self {
Self {
mints: Arc::new(Mutex::new(HashMap::new())),
@@ -215,7 +215,7 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(())
}
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> {
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Error> {
let keyset_counter = self.keyset_counter.lock().await;
let current_counter = keyset_counter.get(keyset_id).unwrap_or(&0);
self.keyset_counter
@@ -225,7 +225,7 @@ impl WalletDatabase for WalletMemoryDatabase {
Ok(())
}
async fn get_keyset_counter(&self, id: &Id) -> Result<Option<u64>, Error> {
async fn get_keyset_counter(&self, id: &Id) -> Result<Option<u32>, Error> {
Ok(self.keyset_counter.lock().await.get(id).cloned())
}
}

View File

@@ -3,6 +3,7 @@
use reqwest::Client;
use serde_json::Value;
use thiserror::Error;
use tracing::instrument;
use url::Url;
use crate::error::ErrorResponse;
@@ -74,6 +75,7 @@ impl HttpClient {
}
/// Get Active Mint Keys [NUT-01]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_mint_keys(&self, mint_url: Url) -> Result<Vec<KeySet>, Error> {
let url = join_url(mint_url, &["v1", "keys"])?;
let keys = self.inner.get(url).send().await?.json::<Value>().await?;
@@ -83,6 +85,7 @@ impl HttpClient {
}
/// Get Keyset Keys [NUT-01]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_mint_keyset(&self, mint_url: Url, keyset_id: Id) -> Result<KeySet, Error> {
let url = join_url(mint_url, &["v1", "keys", &keyset_id.to_string()])?;
let keys = self
@@ -99,6 +102,7 @@ impl HttpClient {
}
/// Get Keysets [NUT-02]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_mint_keysets(&self, mint_url: Url) -> Result<KeysetResponse, Error> {
let url = join_url(mint_url, &["v1", "keysets"])?;
let res = self.inner.get(url).send().await?.json::<Value>().await?;
@@ -113,6 +117,7 @@ impl HttpClient {
}
/// Mint Quote [NUT-04]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn post_mint_quote(
&self,
mint_url: Url,
@@ -136,7 +141,30 @@ impl HttpClient {
}
}
/// Mint Quote status
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_mint_quote_status(
&self,
mint_url: Url,
quote_id: &str,
) -> Result<MintQuoteBolt11Response, Error> {
let url = join_url(mint_url, &["v1", "mint", "quote", "bolt11", quote_id])?;
let res = self.inner.get(url).send().await?;
let status = res.status();
let response: Result<MintQuoteBolt11Response, serde_json::Error> =
serde_json::from_value(res.json().await?);
match response {
Ok(res) => Ok(res),
Err(_) => Err(ErrorResponse::from_json(&status.to_string())?.into()),
}
}
/// Mint Tokens [NUT-04]
#[instrument(skip(self, quote, premint_secrets), fields(mint_url = %mint_url))]
pub async fn post_mint(
&self,
mint_url: Url,
@@ -169,6 +197,7 @@ impl HttpClient {
}
/// Melt Quote [NUT-05]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn post_melt_quote(
&self,
mint_url: Url,
@@ -192,8 +221,31 @@ impl HttpClient {
}
}
/// Melt Quote Status
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_melt_quote_status(
&self,
mint_url: Url,
quote_id: &str,
) -> Result<MeltQuoteBolt11Response, Error> {
let url = join_url(mint_url, &["v1", "melt", "quote", "bolt11", quote_id])?;
let res = self.inner.get(url).send().await?;
let status = res.status();
let response: Result<MeltQuoteBolt11Response, serde_json::Error> =
serde_json::from_value(res.json().await?);
match response {
Ok(res) => Ok(res),
Err(_) => Err(ErrorResponse::from_json(&status.to_string())?.into()),
}
}
/// Melt [NUT-05]
/// [Nut-08] Lightning fee return if outputs defined
#[instrument(skip(self, quote, inputs, outputs), fields(mint_url = %mint_url))]
pub async fn post_melt(
&self,
mint_url: Url,
@@ -222,6 +274,7 @@ impl HttpClient {
}
/// Split Token [NUT-06]
#[instrument(skip(self, swap_request), fields(mint_url = %mint_url))]
pub async fn post_swap(
&self,
mint_url: Url,
@@ -241,6 +294,7 @@ impl HttpClient {
}
/// Get Mint Info [NUT-06]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_mint_info(&self, mint_url: Url) -> Result<MintInfo, Error> {
let url = join_url(mint_url, &["v1", "info"])?;
@@ -255,6 +309,7 @@ impl HttpClient {
}
/// Spendable check [NUT-07]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn post_check_state(
&self,
mint_url: Url,
@@ -281,6 +336,7 @@ impl HttpClient {
}
}
#[instrument(skip(self, request), fields(mint_url = %mint_url))]
pub async fn post_restore(
&self,
mint_url: Url,

View File

@@ -2,11 +2,7 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/13.md>
use core::str::FromStr;
use bip39::Mnemonic;
use bitcoin::bip32::{DerivationPath, ExtendedPrivKey};
use bitcoin::Network;
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey};
use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
use super::nut01::SecretKey;
@@ -18,21 +14,12 @@ use crate::util::hex;
use crate::{Amount, SECP256K1};
impl Secret {
pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result<Self, Error> {
tracing::debug!(
"Deriving secret for {} with count {}",
keyset_id.to_string(),
counter.to_string()
);
let path: DerivationPath = DerivationPath::from_str(&format!(
"m/129372'/0'/{}'/{}'/0",
u64::try_from(keyset_id)?,
counter
))?;
let seed: [u8; 64] = mnemonic.to_seed("");
let bip32_root_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed)?;
let derived_xpriv = bip32_root_key.derive_priv(&SECP256K1, &path)?;
pub fn from_xpriv(xpriv: ExtendedPrivKey, keyset_id: Id, counter: u32) -> Result<Self, Error> {
tracing::debug!("Deriving secret for {} with count {}", keyset_id, counter);
let path = derive_path_from_keyset_id(keyset_id)?
.child(ChildNumber::from_hardened_idx(counter)?)
.child(ChildNumber::from_normal_idx(0)?);
let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
Ok(Self::new(hex::encode(
derived_xpriv.private_key.secret_bytes(),
@@ -41,21 +28,12 @@ impl Secret {
}
impl SecretKey {
pub fn from_seed(mnemonic: &Mnemonic, keyset_id: Id, counter: u64) -> Result<Self, Error> {
tracing::debug!(
"Deriving key for {} with count {}",
keyset_id.to_string(),
counter.to_string()
);
let path = DerivationPath::from_str(&format!(
"m/129372'/0'/{}'/{}'/1",
u64::try_from(keyset_id)?,
counter
))?;
let seed: [u8; 64] = mnemonic.to_seed("");
let bip32_root_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed)?;
let derived_xpriv = bip32_root_key.derive_priv(&SECP256K1, &path)?;
pub fn from_xpriv(xpriv: ExtendedPrivKey, keyset_id: Id, counter: u32) -> Result<Self, Error> {
tracing::debug!("Deriving key for {} with count {}", keyset_id, counter);
let path = derive_path_from_keyset_id(keyset_id)?
.child(ChildNumber::from_hardened_idx(counter)?)
.child(ChildNumber::from_normal_idx(1)?);
let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
Ok(Self::from(derived_xpriv.private_key))
}
@@ -64,10 +42,10 @@ impl SecretKey {
impl PreMintSecrets {
/// Generate blinded messages from predetermined secrets and blindings
/// factor
pub fn from_seed(
pub fn from_xpriv(
keyset_id: Id,
counter: u64,
mnemonic: &Mnemonic,
counter: u32,
xpriv: ExtendedPrivKey,
amount: Amount,
zero_amount: bool,
) -> Result<Self, Error> {
@@ -76,8 +54,8 @@ impl PreMintSecrets {
let mut counter = counter;
for amount in amount.split() {
let secret = Secret::from_seed(mnemonic, keyset_id, counter)?;
let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, counter)?;
let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;
let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
@@ -103,15 +81,15 @@ impl PreMintSecrets {
/// factor
pub fn restore_batch(
keyset_id: Id,
mnemonic: &Mnemonic,
start_count: u64,
end_count: u64,
xpriv: ExtendedPrivKey,
start_count: u32,
end_count: u32,
) -> Result<Self, Error> {
let mut pre_mint_secrets = PreMintSecrets::default();
for i in start_count..=end_count {
let secret = Secret::from_seed(mnemonic, keyset_id, i)?;
let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, i)?;
let secret = Secret::from_xpriv(xpriv, keyset_id, i)?;
let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, i)?;
let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
@@ -131,8 +109,23 @@ impl PreMintSecrets {
}
}
fn derive_path_from_keyset_id(id: Id) -> Result<DerivationPath, Error> {
let index = (u64::try_from(id)? % (2u64.pow(31) - 1)) as u32;
let keyset_child_number = ChildNumber::from_hardened_idx(index)?;
Ok(DerivationPath::from(vec![
ChildNumber::from_hardened_idx(129372)?,
ChildNumber::from_hardened_idx(0)?,
keyset_child_number,
]))
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use bip39::Mnemonic;
use bitcoin::Network;
use super::*;
#[test]
@@ -140,6 +133,8 @@ mod tests {
let seed =
"half depart obvious quality work element tank gorilla view sugar picture humble";
let mnemonic = Mnemonic::from_str(seed).unwrap();
let seed: [u8; 64] = mnemonic.to_seed("");
let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, &seed).unwrap();
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let test_secrets = [
@@ -151,7 +146,7 @@ mod tests {
];
for (i, test_secret) in test_secrets.iter().enumerate() {
let secret = Secret::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap();
let secret = Secret::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
assert_eq!(secret, Secret::from_str(test_secret).unwrap())
}
}
@@ -160,6 +155,8 @@ mod tests {
let seed =
"half depart obvious quality work element tank gorilla view sugar picture humble";
let mnemonic = Mnemonic::from_str(seed).unwrap();
let seed: [u8; 64] = mnemonic.to_seed("");
let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, &seed).unwrap();
let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
let test_rs = [
@@ -171,7 +168,7 @@ mod tests {
];
for (i, test_r) in test_rs.iter().enumerate() {
let r = SecretKey::from_seed(&mnemonic, keyset_id, i.try_into().unwrap()).unwrap();
let r = SecretKey::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
assert_eq!(r, SecretKey::from_hex(test_r).unwrap())
}
}

View File

@@ -5,19 +5,21 @@ use std::num::ParseIntError;
use std::str::FromStr;
use std::sync::Arc;
use bip39::Mnemonic;
use bitcoin::bip32::ExtendedPrivKey;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::Hash;
use bitcoin::Network;
use thiserror::Error;
use tracing::instrument;
use crate::cdk_database::wallet_memory::WalletMemoryDatabase;
use crate::cdk_database::{self, WalletDatabase};
use crate::client::HttpClient;
use crate::dhke::{construct_proofs, hash_to_curve};
use crate::nuts::{
nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind, MintInfo,
PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SigFlag,
SigningKey, SpendingConditions, State, SwapRequest, Token, VerifyingKey,
nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind,
MeltQuoteBolt11Response, MintInfo, MintQuoteBolt11Response, PreMintSecrets, PreSwap, Proof,
ProofState, Proofs, PublicKey, RestoreRequest, SigFlag, SigningKey, SpendingConditions, State,
SwapRequest, Token, VerifyingKey,
};
use crate::types::{MeltQuote, Melted, MintQuote};
use crate::url::UncheckedUrl;
@@ -47,9 +49,6 @@ pub enum Error {
PreimageNotProvided,
#[error("Unknown Key")]
UnknownKey,
/// Mnemonic Required
#[error("Mnemonic Required")]
MnemonicRequired,
/// Spending Locktime not provided
#[error("Spending condition locktime not provided")]
LocktimeNotProvided,
@@ -93,38 +92,25 @@ impl From<Error> for cdk_database::Error {
pub struct Wallet {
pub client: HttpClient,
pub localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync>,
mnemonic: Option<Mnemonic>,
}
impl Default for Wallet {
fn default() -> Self {
Self {
localstore: Arc::new(WalletMemoryDatabase::default()),
client: HttpClient::default(),
mnemonic: None,
}
}
xpriv: ExtendedPrivKey,
}
impl Wallet {
pub async fn new(
client: HttpClient,
pub fn new(
localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync>,
mnemonic: Option<Mnemonic>,
seed: &[u8],
) -> Self {
let xpriv = ExtendedPrivKey::new_master(Network::Bitcoin, seed)
.expect("Could not create master key");
Self {
mnemonic,
client,
client: HttpClient::new(),
localstore,
xpriv,
}
}
/// Back up seed
pub fn mnemonic(&self) -> Option<Mnemonic> {
self.mnemonic.clone()
}
/// Total Balance of wallet
#[instrument(skip(self))]
pub async fn total_balance(&self) -> Result<Amount, Error> {
let mints = self.localstore.get_mints().await?;
let mut balance = Amount::ZERO;
@@ -140,6 +126,7 @@ impl Wallet {
Ok(balance)
}
#[instrument(skip(self))]
pub async fn mint_balances(&self) -> Result<HashMap<UncheckedUrl, Amount>, Error> {
let mints = self.localstore.get_mints().await?;
@@ -158,10 +145,12 @@ impl Wallet {
Ok(balances)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
Ok(self.localstore.get_proofs(mint_url).await?)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn add_mint(&self, mint_url: UncheckedUrl) -> Result<Option<MintInfo>, Error> {
let mint_info = match self
.client
@@ -182,6 +171,7 @@ impl Wallet {
Ok(mint_info)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_keyset_keys(
&self,
mint_url: &UncheckedUrl,
@@ -203,6 +193,7 @@ impl Wallet {
Ok(keys)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_mint_keysets(
&self,
mint_url: &UncheckedUrl,
@@ -217,6 +208,7 @@ impl Wallet {
}
/// Get active mint keyset
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn get_active_mint_keys(
&self,
mint_url: &UncheckedUrl,
@@ -237,6 +229,7 @@ impl Wallet {
}
/// Refresh Mint keys
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn refresh_mint_keys(&self, mint_url: &UncheckedUrl) -> Result<(), Error> {
let current_mint_keysets_info = self
.client
@@ -275,6 +268,7 @@ impl Wallet {
}
/// Check if a proof is spent
#[instrument(skip(self, proofs), fields(mint_url = %mint_url))]
pub async fn check_proofs_spent(
&self,
mint_url: UncheckedUrl,
@@ -296,6 +290,7 @@ impl Wallet {
}
/// Mint Quote
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn mint_quote(
&mut self,
mint_url: UncheckedUrl,
@@ -321,6 +316,34 @@ impl Wallet {
Ok(quote)
}
/// Mint quote status
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
pub async fn mint_quote_status(
&self,
mint_url: UncheckedUrl,
quote_id: &str,
) -> Result<MintQuoteBolt11Response, Error> {
let response = self
.client
.get_mint_quote_status(mint_url.try_into()?, quote_id)
.await?;
match self.localstore.get_mint_quote(quote_id).await? {
Some(quote) => {
let mut quote = quote;
quote.paid = response.paid;
self.localstore.add_mint_quote(quote).await?;
}
None => {
tracing::info!("Quote mint {} unknown", quote_id);
}
}
Ok(response)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
async fn active_mint_keyset(
&mut self,
mint_url: &UncheckedUrl,
@@ -351,6 +374,7 @@ impl Wallet {
Err(Error::NoActiveKeyset)
}
#[instrument(skip(self), fields(mint_url = %mint_url))]
async fn active_keys(
&mut self,
mint_url: &UncheckedUrl,
@@ -376,6 +400,7 @@ impl Wallet {
}
/// Mint
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
pub async fn mint(&mut self, mint_url: UncheckedUrl, quote_id: &str) -> Result<Amount, Error> {
// Check that mint is in store of mints
if self.localstore.get_mint(mint_url.clone()).await?.is_none() {
@@ -396,42 +421,24 @@ impl Wallet {
let active_keyset_id = self.active_mint_keyset(&mint_url, &quote_info.unit).await?;
let mut counter: Option<u64> = None;
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
let premint_secrets;
let count = if let Some(count) = count {
count + 1
} else {
0
};
#[cfg(not(feature = "nut13"))]
{
premint_secrets = PreMintSecrets::random(active_keyset_id, quote_info.amount)?;
}
#[cfg(feature = "nut13")]
{
premint_secrets = match &self.mnemonic {
Some(mnemonic) => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
counter = Some(count);
PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
quote_info.amount,
false,
)?
}
None => PreMintSecrets::random(active_keyset_id, quote_info.amount)?,
};
}
let premint_secrets = PreMintSecrets::from_xpriv(
active_keyset_id,
count,
self.xpriv,
quote_info.amount,
false,
)?;
let mint_res = self
.client
@@ -469,12 +476,9 @@ impl Wallet {
self.localstore.remove_mint_quote(&quote_info.id).await?;
// Update counter for keyset
#[cfg(feature = "nut13")]
if counter.is_some() {
self.localstore
.increment_keyset_counter(&active_keyset_id, proofs.len() as u64)
.await?;
}
self.localstore
.increment_keyset_counter(&active_keyset_id, proofs.len() as u32)
.await?;
// Add new proofs to store
self.localstore.add_proofs(mint_url, proofs).await?;
@@ -483,6 +487,7 @@ impl Wallet {
}
/// Swap
#[instrument(skip(self, input_proofs), fields(mint_url = %mint_url))]
pub async fn swap(
&mut self,
mint_url: &UncheckedUrl,
@@ -516,14 +521,11 @@ impl Wallet {
.ok_or(Error::UnknownKey)?,
)?;
#[cfg(feature = "nut13")]
if self.mnemonic.is_some() {
let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
self.localstore
.increment_keyset_counter(&active_keyset_id, post_swap_proofs.len() as u64)
.await?;
}
self.localstore
.increment_keyset_counter(&active_keyset_id, post_swap_proofs.len() as u32)
.await?;
let mut keep_proofs = Proofs::new();
let proofs_to_send;
@@ -590,6 +592,7 @@ impl Wallet {
}
/// Create Swap Payload
#[instrument(skip(self, proofs), fields(mint_url = %mint_url))]
async fn create_swap(
&mut self,
mint_url: &UncheckedUrl,
@@ -606,105 +609,65 @@ impl Wallet {
let desired_amount = amount.unwrap_or(proofs_total);
let change_amount = proofs_total - desired_amount;
let mut desired_messages;
let change_messages;
let (mut desired_messages, change_messages) = match spending_conditions {
Some(conditions) => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
#[cfg(not(feature = "nut13"))]
{
(desired_messages, change_messages) = match spendig_conditions {
Some(conditions) => (
let count = if let Some(count) = count {
count + 1
} else {
0
};
let change_premint_secrets = PreMintSecrets::from_xpriv(
active_keyset_id,
count,
self.xpriv,
change_amount,
false,
)?;
(
PreMintSecrets::with_conditions(active_keyset_id, desired_amount, conditions)?,
PreMintSecrets::random(active_keyset_id, change_amount),
),
None => (
PreMintSecrets::random(active_keyset_id, proofs_total)?,
PreMintSecrets::default(),
),
};
}
change_premint_secrets,
)
}
None => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
#[cfg(feature = "nut13")]
{
(desired_messages, change_messages) = match &self.mnemonic {
Some(mnemonic) => match spending_conditions {
Some(conditions) => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
let count = if let Some(count) = count {
count + 1
} else {
0
};
let premint_secrets = PreMintSecrets::from_xpriv(
active_keyset_id,
count,
self.xpriv,
desired_amount,
false,
)?;
let change_premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
change_amount,
false,
)?;
let count = count + premint_secrets.len() as u32;
(
PreMintSecrets::with_conditions(
active_keyset_id,
desired_amount,
conditions,
)?,
change_premint_secrets,
)
}
None => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
let change_premint_secrets = PreMintSecrets::from_xpriv(
active_keyset_id,
count,
self.xpriv,
change_amount,
false,
)?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
let premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
desired_amount,
false,
)?;
let count = count + premint_secrets.len() as u64;
let change_premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
change_amount,
false,
)?;
(premint_secrets, change_premint_secrets)
}
},
None => match spending_conditions {
Some(conditions) => (
PreMintSecrets::with_conditions(
active_keyset_id,
desired_amount,
conditions,
)?,
PreMintSecrets::random(active_keyset_id, change_amount)?,
),
None => (
PreMintSecrets::random(active_keyset_id, desired_amount)?,
PreMintSecrets::random(active_keyset_id, change_amount)?,
),
},
};
}
(premint_secrets, change_premint_secrets)
}
};
// Combine the BlindedMessages totoalling the desired amount with change
desired_messages.combine(change_messages);
@@ -720,6 +683,7 @@ impl Wallet {
}
/// Send
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn send(
&mut self,
mint_url: &UncheckedUrl,
@@ -730,14 +694,16 @@ impl Wallet {
) -> Result<String, Error> {
let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
let send_proofs = match input_proofs
.iter()
.map(|p| p.amount)
.sum::<Amount>()
.eq(&amount)
{
true => Some(input_proofs),
false => {
let send_proofs = match (
input_proofs
.iter()
.map(|p| p.amount)
.sum::<Amount>()
.eq(&amount),
&conditions,
) {
(true, None) => Some(input_proofs),
_ => {
self.swap(mint_url, unit, Some(amount), input_proofs, conditions)
.await?
}
@@ -754,6 +720,7 @@ impl Wallet {
}
/// Melt Quote
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn melt_quote(
&mut self,
mint_url: UncheckedUrl,
@@ -784,7 +751,35 @@ impl Wallet {
Ok(quote)
}
/// Melt quote status
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
pub async fn melt_quote_status(
&self,
mint_url: UncheckedUrl,
quote_id: &str,
) -> Result<MeltQuoteBolt11Response, Error> {
let response = self
.client
.get_melt_quote_status(mint_url.try_into()?, quote_id)
.await?;
match self.localstore.get_melt_quote(quote_id).await? {
Some(quote) => {
let mut quote = quote;
quote.paid = response.paid;
self.localstore.add_melt_quote(quote).await?;
}
None => {
tracing::info!("Quote melt {} unknown", quote_id);
}
}
Ok(response)
}
// Select proofs
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn select_proofs(
&self,
mint_url: UncheckedUrl,
@@ -845,6 +840,7 @@ impl Wallet {
}
/// Melt
#[instrument(skip(self, quote_id), fields(mint_url = %mint_url))]
pub async fn melt(&mut self, mint_url: &UncheckedUrl, quote_id: &str) -> Result<Melted, Error> {
let quote_info = self.localstore.get_melt_quote(quote_id).await?;
@@ -864,44 +860,21 @@ impl Wallet {
let proofs_amount = proofs.iter().map(|p| p.amount).sum();
let mut counter: Option<u64> = None;
let active_keyset_id = self.active_mint_keyset(mint_url, &quote_info.unit).await?;
let premint_secrets;
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
#[cfg(not(feature = "nut13"))]
{
premint_secrets = PreMintSecrets::blank(active_keyset_id, proofs_amount)?;
}
let count = if let Some(count) = count {
count + 1
} else {
0
};
#[cfg(feature = "nut13")]
{
premint_secrets = match &self.mnemonic {
Some(mnemonic) => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
counter = Some(count);
PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
proofs_amount,
true,
)?
}
None => PreMintSecrets::blank(active_keyset_id, proofs_amount)?,
};
}
let premint_secrets =
PreMintSecrets::from_xpriv(active_keyset_id, count, self.xpriv, proofs_amount, true)?;
let melt_response = self
.client
@@ -939,12 +912,9 @@ impl Wallet {
);
// Update counter for keyset
#[cfg(feature = "nut13")]
if counter.is_some() {
self.localstore
.increment_keyset_counter(&active_keyset_id, change_proofs.len() as u64)
.await?;
}
self.localstore
.increment_keyset_counter(&active_keyset_id, change_proofs.len() as u32)
.await?;
self.localstore
.add_proofs(mint_url.clone(), change_proofs)
@@ -961,6 +931,7 @@ impl Wallet {
}
/// Receive
#[instrument(skip_all)]
pub async fn receive(
&mut self,
encoded_token: &str,
@@ -977,6 +948,16 @@ impl Wallet {
continue;
}
// Add mint if it does not exist in the store
if self
.localstore
.get_mint(token.mint.clone())
.await?
.is_none()
{
self.add_mint(token.mint.clone()).await?;
}
let active_keyset_id = self.active_mint_keyset(&token.mint, &unit).await?;
let keys = self.get_keyset_keys(&token.mint, active_keyset_id).await?;
@@ -1078,12 +1059,9 @@ impl Wallet {
)?;
let mint_proofs = received_proofs.entry(token.mint).or_default();
#[cfg(feature = "nut13")]
if self.mnemonic.is_some() {
self.localstore
.increment_keyset_counter(&active_keyset_id, p.len() as u64)
.await?;
}
self.localstore
.increment_keyset_counter(&active_keyset_id, p.len() as u32)
.await?;
mint_proofs.extend(p);
}
@@ -1095,6 +1073,7 @@ impl Wallet {
Ok(())
}
#[instrument(skip(self, proofs), fields(mint_url = %mint_url))]
pub fn proofs_to_token(
&self,
mint_url: UncheckedUrl,
@@ -1106,6 +1085,7 @@ impl Wallet {
}
#[cfg(feature = "nut13")]
#[instrument(skip(self), fields(mint_url = %mint_url))]
pub async fn restore(&mut self, mint_url: UncheckedUrl) -> Result<Amount, Error> {
// Check that mint is in store of mints
if self.localstore.get_mint(mint_url.clone()).await?.is_none() {
@@ -1124,7 +1104,7 @@ impl Wallet {
while empty_batch.lt(&3) {
let premint_secrets = PreMintSecrets::restore_batch(
keyset.id,
&self.mnemonic.clone().ok_or(Error::MnemonicRequired)?,
self.xpriv,
start_counter,
start_counter + 100,
)?;
@@ -1178,7 +1158,7 @@ impl Wallet {
#[cfg(feature = "nut13")]
self.localstore
.increment_keyset_counter(&keyset.id, proofs.len() as u64)
.increment_keyset_counter(&keyset.id, proofs.len() as u32)
.await?;
let states = self
@@ -1209,6 +1189,7 @@ impl Wallet {
/// Verify all proofs in token have meet the required spend
/// Can be used to allow a wallet to accept payments offline while reducing
/// the risk of claiming back to the limits let by the spending_conditions
#[instrument(skip(self, token))]
pub fn verify_token_p2pk(
&self,
token: &Token,
@@ -1326,6 +1307,7 @@ impl Wallet {
}
/// Verify all proofs in token have a valid DLEQ proof
#[instrument(skip(self, token))]
pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
let mut keys_cache: HashMap<Id, Keys> = HashMap::new();