feat(wallet): token v4

feat(wallet): receive is single mint and unit
This commit is contained in:
David Caseria
2024-06-06 17:16:06 +01:00
committed by thesimplekid
parent 4637b050d6
commit 22e7c41491
14 changed files with 878 additions and 353 deletions

View File

@@ -1 +1,53 @@
# Changelog
<!-- All notable changes to this project will be documented in this file. -->
<!-- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -->
<!-- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -->
<!-- Template
## [Unreleased]
### Summary
### Changed
### Added
### Fixed
### Removed
-->
## [Unreleased]
### Summary
### Changed
cdk(wallet): `wallet:receive` will not claim `proofs` from a mint other then the wallet's mint ([thesimplekid]).
cdk(NUT00): `Token` is changed from a struct to enum of either `TokenV4` or `Tokenv3` ([thesimplekid]).
cdk(NUT00): Rename `MintProofs` to `TokenV3Token` ([thesimplekid]).
### Added
cdk: TokenV4 CBOR ([davidcaseria]/[thesimplekid]).
cdk(wallet): `wallet::receive_proof` functions to claim specific proofs instead of encoded token ([thesimplekid]).
cdk-cli: Flag on `send` to print v3 token, default is v4 ([thesimplekid]).
## [v0.1.1]
### Summary
### Changed
cdk(wallet): `wallet::total_pending_balance` does not include reserced proofs ([thesimplekid]).
### Added
cdk(wallet): Added get reserved proofs [thesimplekid](https://github.com/thesimplekid).
<!-- Contributors -->
[thesimplekid]: https://github.com/thesimplekid
[davidcaseria]: https://github.com/davidcaseria

View File

@@ -39,6 +39,7 @@ bitcoin = { version = "0.30", features = [
"rand",
"rand-std",
] } # lightning-invoice uses v0.30
anyhow = "1"
[profile]

View File

@@ -1,22 +0,0 @@
use std::ops::Deref;
use cdk::nuts::MintProofs;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(js_name = MintProofs)]
pub struct JsMintProofs {
inner: MintProofs,
}
impl Deref for JsMintProofs {
type Target = MintProofs;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl From<MintProofs> for JsMintProofs {
fn from(inner: MintProofs) -> JsMintProofs {
JsMintProofs { inner }
}
}

View File

@@ -1,7 +1,6 @@
pub mod blind_signature;
pub mod blinded_message;
pub mod currency_unit;
pub mod mint_proofs;
pub mod premint;
pub mod proof;
pub mod token;

View File

@@ -127,7 +127,7 @@ async fn receive_token(
preimage: &[String],
) -> Result<Amount> {
let token = Token::from_str(token_str)?;
let mint_url = token.token.first().unwrap().mint.clone();
let mint_url = token.proofs().iter().next().unwrap().0.clone();
let wallet = match wallets.get(&mint_url) {
Some(wallet) => wallet.clone(),

View File

@@ -5,7 +5,7 @@ use std::str::FromStr;
use anyhow::{bail, Result};
use cdk::amount::SplitTarget;
use cdk::nuts::{Conditions, PublicKey, SpendingConditions};
use cdk::nuts::{Conditions, PublicKey, SpendingConditions, Token};
use cdk::wallet::Wallet;
use cdk::{Amount, UncheckedUrl};
use clap::Args;
@@ -32,6 +32,9 @@ pub struct SendSubCommand {
/// Refund keys that can be used after locktime
#[arg(long, action = clap::ArgAction::Append)]
refund_keys: Vec<String>,
/// Token as V3 token
#[arg(short, long)]
v3: bool,
}
pub async fn send(
@@ -152,7 +155,16 @@ pub async fn send(
)
.await?;
println!("{}", token);
match sub_command_args.v3 {
true => {
let token = Token::from_str(&token)?;
println!("{}", token.to_v3_string());
}
false => {
println!("{}", token);
}
}
Ok(())
}

View File

@@ -19,12 +19,13 @@ wallet = ["dep:reqwest"]
[dependencies]
async-trait.workspace = true
base64 = "0.22" # bitcoin uses v0.13 (optional dep)
http = "1.0"
bitcoin = { workspace = true, features = [
"serde",
"rand",
"rand-std",
] }
] } # lightning-invoice uses v0.30
ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
http = "1.0"
lightning-invoice = { version = "0.31", features = ["serde"] }
once_cell = "1.19"
reqwest = { version = "0.12", default-features = false, features = [
@@ -68,3 +69,4 @@ required-features = ["wallet"]
[dev-dependencies]
rand = "0.8.5"
bip39.workspace = true
anyhow.workspace = true

View File

@@ -20,8 +20,8 @@ pub mod nut14;
pub mod nut15;
pub use nut00::{
BlindSignature, BlindedMessage, CurrencyUnit, MintProofs, PaymentMethod, PreMint,
PreMintSecrets, Proof, Proofs, Token, Witness,
BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
Proofs, Token, TokenV3, TokenV4, Witness,
};
pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey};
#[cfg(feature = "mint")]

View File

@@ -5,14 +5,10 @@
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::string::FromUtf8Error;
use base64::engine::{general_purpose, GeneralPurpose};
use base64::{alphabet, Engine as _};
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
use url::Url;
use super::nut10;
use super::nut11::SpendingConditions;
@@ -24,9 +20,11 @@ use crate::nuts::nut12::BlindSignatureDleq;
use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness};
use crate::nuts::{Id, ProofDleq};
use crate::secret::Secret;
use crate::url::UncheckedUrl;
use crate::Amount;
pub mod token;
pub use token::{Token, TokenV3, TokenV4};
/// List of [Proof]
pub type Proofs = Vec<Proof>;
@@ -54,6 +52,9 @@ pub enum Error {
/// Parse Url Error
#[error(transparent)]
UrlParseError(#[from] url::ParseError),
/// Ciborium error
#[error(transparent)]
CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
/// CDK error
#[error(transparent)]
Cdk(#[from] crate::error::Error),
@@ -233,6 +234,79 @@ impl PartialOrd for Proof {
}
}
/// Proof V4
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofV4 {
/// Amount in satoshi
#[serde(rename = "a")]
pub amount: Amount,
/// Secret message
#[serde(rename = "s")]
pub secret: Secret,
/// Unblinded signature
#[serde(
serialize_with = "serialize_v4_pubkey",
deserialize_with = "deserialize_v4_pubkey"
)]
pub c: PublicKey,
/// Witness
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub witness: Option<Witness>,
/// DLEQ Proof
#[serde(rename = "d")]
pub dleq: Option<ProofDleq>,
}
impl ProofV4 {
/// [`ProofV4`] into [`Proof`]
pub fn into_proof(&self, keyset_id: &Id) -> Proof {
Proof {
amount: self.amount,
keyset_id: *keyset_id,
secret: self.secret.clone(),
c: self.c,
witness: self.witness.clone(),
dleq: self.dleq.clone(),
}
}
}
impl From<Proof> for ProofV4 {
fn from(proof: Proof) -> ProofV4 {
let Proof {
amount,
keyset_id: _,
secret,
c,
witness,
dleq,
} = proof;
ProofV4 {
amount,
secret,
c,
witness,
dleq,
}
}
}
fn serialize_v4_pubkey<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&key.to_bytes())
}
fn deserialize_v4_pubkey<'de, D>(deserializer: D) -> Result<PublicKey, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = Vec::<u8>::deserialize(deserializer)?;
PublicKey::from_slice(&bytes).map_err(serde::de::Error::custom)
}
/// Currency Unit
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub enum CurrencyUnit {
@@ -563,102 +637,6 @@ impl PartialOrd for PreMintSecrets {
}
}
/// Token
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Token {
/// Proofs in [`Token`] by mint
pub token: Vec<MintProofs>,
/// Memo for token
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
/// Token Unit
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<CurrencyUnit>,
}
impl Token {
/// Create new [`Token`]
pub fn new(
mint_url: UncheckedUrl,
proofs: Proofs,
memo: Option<String>,
unit: Option<CurrencyUnit>,
) -> Result<Self, Error> {
if proofs.is_empty() {
return Err(Error::ProofsRequired);
}
// Check Url is valid
let _: Url = (&mint_url).try_into().map_err(|_| Error::InvalidUrl)?;
Ok(Self {
token: vec![MintProofs::new(mint_url, proofs)],
memo,
unit,
})
}
/// Token Info
/// Assumes only one mint in [`Token`]
pub fn token_info(&self) -> (Amount, String) {
let mut amount = Amount::ZERO;
for proofs in &self.token {
for proof in &proofs.proofs {
amount += proof.amount;
}
}
(amount, self.token[0].mint.to_string())
}
}
impl FromStr for Token {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = if s.starts_with("cashuA") {
s.replace("cashuA", "")
} else {
return Err(Error::UnsupportedToken);
};
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::STANDARD, decode_config).decode(s)?;
let decoded_str = String::from_utf8(decoded)?;
let token: Token = serde_json::from_str(&decoded_str)?;
Ok(token)
}
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
let encoded = general_purpose::STANDARD.encode(json_string);
write!(f, "cashuA{}", encoded)
}
}
/// Mint Proofs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintProofs {
/// Url of mint
pub mint: UncheckedUrl,
/// [`Proofs`]
pub proofs: Proofs,
}
impl MintProofs {
/// Create new [`MintProofs`]
pub fn new(mint_url: UncheckedUrl, proofs: Proofs) -> Self {
Self {
mint: mint_url,
proofs,
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
@@ -678,31 +656,8 @@ mod tests {
assert_eq!(proof.len(), 2);
}
#[test]
fn test_token_str_round_trip() {
let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let token = Token::from_str(token_str).unwrap();
assert_eq!(
token.token[0].mint,
UncheckedUrl::from_str("https://8333.space:3338").unwrap()
);
assert_eq!(
token.token[0].proofs[0].clone().keyset_id,
Id::from_str("009a1f293253e41e").unwrap()
);
assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
let encoded = &token.to_string();
let token_data = Token::from_str(encoded).unwrap();
assert_eq!(token_data, token);
}
#[test]
fn test_blank_blinded_messages() {
// TODO: Need to update id to new type in proof
let b = PreMintSecrets::blank(
Id::from_str("009a1f293253e41e").unwrap(),
Amount::from(1000),
@@ -710,30 +665,8 @@ mod tests {
.unwrap();
assert_eq!(b.len(), 10);
// TODO: Need to update id to new type in proof
let b = PreMintSecrets::blank(Id::from_str("009a1f293253e41e").unwrap(), Amount::from(1))
.unwrap();
assert_eq!(b.len(), 1);
}
#[test]
fn incorrect_tokens() {
let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let incorrect_prefix_token = Token::from_str(incorrect_prefix);
assert!(incorrect_prefix_token.is_err());
let no_prefix = "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let no_prefix_token = Token::from_str(no_prefix);
assert!(no_prefix_token.is_err());
let correct_token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let correct_token = Token::from_str(correct_token);
assert!(correct_token.is_ok());
}
}

View File

@@ -0,0 +1,527 @@
//! Cashu Token
//!
//! <https://github.com/cashubtc/nuts/blob/main/00.md>
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use base64::engine::{general_purpose, GeneralPurpose};
use base64::{alphabet, Engine as _};
use serde::{Deserialize, Serialize};
use url::Url;
use super::{Error, Proof, ProofV4, Proofs};
use crate::nuts::{CurrencyUnit, Id};
use crate::url::UncheckedUrl;
use crate::Amount;
/// Token Enum
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Token {
/// Token V3
TokenV3(TokenV3),
/// Token V4
TokenV4(TokenV4),
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let token = match self {
Self::TokenV3(token) => token.to_string(),
Self::TokenV4(token) => token.to_string(),
};
write!(f, "{}", token)
}
}
impl Token {
/// Create new [`Token`]
pub fn new(
mint_url: UncheckedUrl,
proofs: Proofs,
memo: Option<String>,
unit: Option<CurrencyUnit>,
) -> Self {
let proofs = proofs
.into_iter()
.fold(HashMap::new(), |mut acc, val| {
acc.entry(val.keyset_id)
.and_modify(|p: &mut Vec<Proof>| p.push(val.clone()))
.or_insert(vec![val.clone()]);
acc
})
.into_iter()
.map(|(id, proofs)| TokenV4Token::new(id, proofs))
.collect();
Token::TokenV4(TokenV4 {
mint_url,
unit,
memo,
token: proofs,
})
}
/// Proofs in [`Token`]
pub fn proofs(&self) -> HashMap<UncheckedUrl, Proofs> {
match self {
Self::TokenV3(token) => token.proofs(),
Self::TokenV4(token) => token.proofs(),
}
}
/// Total value of [`Token`]
pub fn value(&self) -> Amount {
match self {
Self::TokenV3(token) => token.value(),
Self::TokenV4(token) => token.value(),
}
}
/// [`Token`] memo
pub fn memo(&self) -> &Option<String> {
match self {
Self::TokenV3(token) => token.memo(),
Self::TokenV4(token) => token.memo(),
}
}
/// Unit
pub fn unit(&self) -> &Option<CurrencyUnit> {
match self {
Self::TokenV3(token) => token.unit(),
Self::TokenV4(token) => token.unit(),
}
}
/// To v3 string
pub fn to_v3_string(&self) -> String {
let v3_token = match self {
Self::TokenV3(token) => token.clone(),
Self::TokenV4(token) => token.clone().into(),
};
v3_token.to_string()
}
}
impl FromStr for Token {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (is_v3, s) = match (s.strip_prefix("cashuA"), s.strip_prefix("cashuB")) {
(Some(s), None) => (true, s),
(None, Some(s)) => (false, s),
_ => return Err(Error::UnsupportedToken),
};
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
match is_v3 {
true => {
let decoded_str = String::from_utf8(decoded)?;
let token: TokenV3 = serde_json::from_str(&decoded_str)?;
Ok(Token::TokenV3(token))
}
false => {
let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
Ok(Token::TokenV4(token))
}
}
}
}
/// Token V3 Token
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenV3Token {
/// Url of mint
pub mint: UncheckedUrl,
/// [`Proofs`]
pub proofs: Proofs,
}
impl TokenV3Token {
/// Create new [`TokenV3Token`]
pub fn new(mint_url: UncheckedUrl, proofs: Proofs) -> Self {
Self {
mint: mint_url,
proofs,
}
}
}
/// Token
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenV3 {
/// Proofs in [`Token`] by mint
pub token: Vec<TokenV3Token>,
/// Memo for token
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
/// Token Unit
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<CurrencyUnit>,
}
impl TokenV3 {
/// Create new [`Token`]
pub fn new(
mint_url: UncheckedUrl,
proofs: Proofs,
memo: Option<String>,
unit: Option<CurrencyUnit>,
) -> Result<Self, Error> {
if proofs.is_empty() {
return Err(Error::ProofsRequired);
}
// Check Url is valid
let _: Url = (&mint_url).try_into().map_err(|_| Error::InvalidUrl)?;
Ok(Self {
token: vec![TokenV3Token::new(mint_url, proofs)],
memo,
unit,
})
}
fn proofs(&self) -> HashMap<UncheckedUrl, Proofs> {
let mut proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
for token in self.token.clone() {
let mint_url = token.mint;
let mut mint_proofs = token.proofs;
proofs
.entry(mint_url)
.and_modify(|p| p.append(&mut mint_proofs))
.or_insert(mint_proofs);
}
proofs
}
#[inline]
fn value(&self) -> Amount {
self.token
.iter()
.map(|t| t.proofs.iter().map(|p| p.amount).sum())
.sum()
}
#[inline]
fn memo(&self) -> &Option<String> {
&self.memo
}
#[inline]
fn unit(&self) -> &Option<CurrencyUnit> {
&self.unit
}
}
impl FromStr for TokenV3 {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.strip_prefix("cashuA").ok_or(Error::UnsupportedToken)?;
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
let decoded_str = String::from_utf8(decoded)?;
let token: TokenV3 = serde_json::from_str(&decoded_str)?;
Ok(token)
}
}
impl fmt::Display for TokenV3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
let encoded = general_purpose::URL_SAFE.encode(json_string);
write!(f, "cashuA{}", encoded)
}
}
impl From<TokenV4> for TokenV3 {
fn from(token: TokenV4) -> Self {
let (mint_url, proofs) = token
.proofs()
.into_iter()
.next()
.expect("Token has no proofs");
TokenV3 {
token: vec![TokenV3Token::new(mint_url, proofs)],
memo: token.memo,
unit: token.unit,
}
}
}
/// Token V4
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenV4 {
/// Mint Url
#[serde(rename = "m")]
pub mint_url: UncheckedUrl,
/// Token Unit
#[serde(rename = "u", skip_serializing_if = "Option::is_none")]
pub unit: Option<CurrencyUnit>,
/// Memo for token
#[serde(rename = "d", skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
/// Proofs
///
/// Proofs separated by keyset_id
#[serde(rename = "t")]
pub token: Vec<TokenV4Token>,
}
impl TokenV4 {
/// Proofs from token
pub fn proofs(&self) -> HashMap<UncheckedUrl, Proofs> {
let mint_url = &self.mint_url;
let mut proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
for token in self.token.clone() {
let mut mint_proofs = token
.proofs
.iter()
.map(|p| p.into_proof(&token.keyset_id))
.collect();
proofs
.entry(mint_url.clone())
.and_modify(|p| p.append(&mut mint_proofs))
.or_insert(mint_proofs);
}
proofs
}
#[inline]
fn value(&self) -> Amount {
self.token
.iter()
.map(|t| t.proofs.iter().map(|p| p.amount).sum())
.sum()
}
#[inline]
fn memo(&self) -> &Option<String> {
&self.memo
}
#[inline]
fn unit(&self) -> &Option<CurrencyUnit> {
&self.unit
}
}
impl fmt::Display for TokenV4 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use serde::ser::Error;
let mut data = Vec::new();
ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
let encoded = general_purpose::URL_SAFE.encode(data);
write!(f, "cashuB{}", encoded)
}
}
impl FromStr for TokenV4 {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.strip_prefix("cashuB").ok_or(Error::UnsupportedToken)?;
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
Ok(token)
}
}
impl TryFrom<TokenV3> for TokenV4 {
type Error = Error;
fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
let proofs = token.proofs();
if proofs.len() != 1 {
return Err(Error::UnsupportedToken);
}
let (mint_url, mint_proofs) = proofs.iter().next().expect("No proofs");
let proofs = mint_proofs
.iter()
.fold(HashMap::new(), |mut acc, val| {
acc.entry(val.keyset_id)
.and_modify(|p: &mut Vec<Proof>| p.push(val.clone()))
.or_insert(vec![val.clone()]);
acc
})
.into_iter()
.map(|(id, proofs)| TokenV4Token::new(id, proofs))
.collect();
Ok(TokenV4 {
mint_url: mint_url.to_owned(),
token: proofs,
memo: token.memo,
unit: token.unit,
})
}
}
/// Token V4 Token
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenV4Token {
/// `Keyset id`
#[serde(
rename = "i",
serialize_with = "serialize_v4_keyset_id",
deserialize_with = "deserialize_v4_keyset_id"
)]
pub keyset_id: Id,
/// Proofs
#[serde(rename = "p")]
pub proofs: Vec<ProofV4>,
}
fn serialize_v4_keyset_id<S>(keyset_id: &Id, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&keyset_id.to_bytes())
}
fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<Id, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = Vec::<u8>::deserialize(deserializer)?;
Id::from_bytes(&bytes).map_err(serde::de::Error::custom)
}
impl TokenV4Token {
/// Create new [`TokenV4Token`]
pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
Self {
keyset_id,
proofs: proofs.into_iter().map(|p| p.into()).collect(),
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::UncheckedUrl;
#[test]
fn test_token_v4_str_round_trip() {
let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
let token = TokenV4::from_str(token_str).unwrap();
assert_eq!(
token.mint_url,
UncheckedUrl::from_str("http://localhost:3338").unwrap()
);
assert_eq!(
token.token[0].keyset_id,
Id::from_str("00ad268c4d1f5826").unwrap()
);
let token: TokenV4 = token.try_into().unwrap();
let encoded = &token.to_string();
let token_data = TokenV4::from_str(encoded).unwrap();
assert_eq!(token_data, token);
}
#[test]
fn test_token_v4_multi_keyset() -> anyhow::Result<()> {
let token_str_multi_keysets = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA==";
let token = Token::from_str(token_str_multi_keysets).unwrap();
let amount = token.value();
assert_eq!(amount, Amount::from(4));
let unit = token.unit().clone().unwrap();
assert_eq!(CurrencyUnit::Sat, unit);
match token {
Token::TokenV4(token) => {
let tokens: Vec<Id> = token.token.iter().map(|t| t.keyset_id).collect();
assert_eq!(tokens.len(), 2);
assert!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap()));
assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap()));
let mint_url = token.mint_url;
assert_eq!("http://localhost:3338", &mint_url.to_string());
}
_ => {
anyhow::bail!("Token should be a v4 token")
}
}
Ok(())
}
#[test]
fn test_token_str_round_trip() {
let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let token = TokenV3::from_str(token_str).unwrap();
assert_eq!(
token.token[0].mint,
UncheckedUrl::from_str("https://8333.space:3338").unwrap()
);
assert_eq!(
token.token[0].proofs[0].clone().keyset_id,
Id::from_str("009a1f293253e41e").unwrap()
);
assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
let encoded = &token.to_string();
let token_data = TokenV3::from_str(encoded).unwrap();
assert_eq!(token_data, token);
}
#[test]
fn incorrect_tokens() {
let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let incorrect_prefix_token = TokenV3::from_str(incorrect_prefix);
assert!(incorrect_prefix_token.is_err());
let no_prefix = "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let no_prefix_token = TokenV3::from_str(no_prefix);
assert!(no_prefix_token.is_err());
let correct_token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
let correct_token = TokenV3::from_str(correct_token);
assert!(correct_token.is_ok());
}
}

View File

@@ -61,6 +61,13 @@ pub enum Error {
/// Keyset Not Found
#[error("Keyset Not Found")]
KeysetNotFound,
/// Receive can only be used with tokens from single mint
#[error("Multiple mint tokens not supported by receive. Please deconstruct the token and use receive with_proof")]
MultiMintTokenNotSupported,
/// Incorrect Mint
/// Token does not match wallet mint
#[error("Token does not match wallet mint")]
IncorrectMint,
/// From hex error
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),

View File

@@ -18,11 +18,12 @@ use url::Url;
use crate::amount::SplitTarget;
use crate::cdk_database::{self, WalletDatabase};
use crate::dhke::{construct_proofs, hash_to_curve};
use crate::nuts::nut00::token::Token;
use crate::nuts::{
nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind,
MeltQuoteBolt11Response, MeltQuoteState, MintInfo, MintQuoteBolt11Response, MintQuoteState,
PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey,
SigFlag, SpendingConditions, State, SwapRequest, Token,
SigFlag, SpendingConditions, State, SwapRequest,
};
use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo};
use crate::url::UncheckedUrl;
@@ -891,10 +892,7 @@ impl Wallet {
.await?;
}
Ok(
util::proof_to_token(mint_url.clone(), send_proofs, memo, Some(unit.clone()))?
.to_string(),
)
Ok(Token::new(mint_url.clone(), send_proofs, memo, Some(unit.clone())).to_string())
}
/// Melt Quote
@@ -1214,6 +1212,144 @@ impl Wallet {
Ok((condition_selected_proofs, selected_proofs))
}
/// Receive proofs
#[instrument(skip_all)]
pub async fn receive_proofs(
&self,
proofs: Proofs,
amount_split_target: &SplitTarget,
p2pk_signing_keys: &[SecretKey],
preimages: &[String],
) -> Result<Amount, Error> {
let mut received_proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
let mint_url = &self.mint_url;
// Add mint if it does not exist in the store
if self
.localstore
.get_mint(self.mint_url.clone())
.await?
.is_none()
{
self.get_mint_info().await?;
}
let active_keyset_id = self.active_mint_keyset().await?;
let keys = self.get_keyset_keys(active_keyset_id).await?;
// Sum amount of all proofs
let amount: Amount = proofs.iter().map(|p| p.amount).sum();
let mut proofs = proofs;
let mut sig_flag = SigFlag::SigInputs;
// Map hash of preimage to preimage
let hashed_to_preimage: HashMap<String, &String> = preimages
.iter()
.flat_map(|p| match hex::decode(p) {
Ok(hex_bytes) => Some((Sha256Hash::hash(&hex_bytes).to_string(), p)),
Err(_) => None,
})
.collect();
let p2pk_signing_keys: HashMap<XOnlyPublicKey, &SecretKey> = p2pk_signing_keys
.iter()
.map(|s| (s.x_only_public_key(&SECP256K1).0, s))
.collect();
for proof in &mut proofs {
// Verify that proof DLEQ is valid
if proof.dleq.is_some() {
let keys = self.get_keyset_keys(proof.keyset_id).await?;
let key = keys.amount_key(proof.amount).ok_or(Error::UnknownKey)?;
proof.verify_dleq(key)?;
}
if let Ok(secret) =
<crate::secret::Secret as TryInto<crate::nuts::nut10::Secret>>::try_into(
proof.secret.clone(),
)
{
let conditions: Result<Conditions, _> =
secret.secret_data.tags.unwrap_or_default().try_into();
if let Ok(conditions) = conditions {
let mut pubkeys = conditions.pubkeys.unwrap_or_default();
match secret.kind {
Kind::P2PK => {
let data_key = PublicKey::from_str(&secret.secret_data.data)?;
pubkeys.push(data_key);
}
Kind::HTLC => {
let hashed_preimage = &secret.secret_data.data;
let preimage = hashed_to_preimage
.get(hashed_preimage)
.ok_or(Error::PreimageNotProvided)?;
proof.add_preimage(preimage.to_string());
}
}
for pubkey in pubkeys {
if let Some(signing) = p2pk_signing_keys.get(&pubkey.x_only_public_key()) {
proof.sign_p2pk(signing.to_owned().clone())?;
}
}
if conditions.sig_flag.eq(&SigFlag::SigAll) {
sig_flag = SigFlag::SigAll;
}
}
}
}
let mut pre_swap = self
.create_swap(Some(amount), amount_split_target, proofs, None)
.await?;
if sig_flag.eq(&SigFlag::SigAll) {
for blinded_message in &mut pre_swap.swap_request.outputs {
for signing_key in p2pk_signing_keys.values() {
blinded_message.sign_p2pk(signing_key.to_owned().clone())?
}
}
}
let swap_response = self
.client
.post_swap(mint_url.clone().try_into()?, pre_swap.swap_request)
.await?;
// Proof to keep
let p = construct_proofs(
swap_response.signatures,
pre_swap.pre_mint_secrets.rs(),
pre_swap.pre_mint_secrets.secrets(),
&keys,
)?;
let mint_proofs = received_proofs.entry(mint_url.clone()).or_default();
self.localstore
.increment_keyset_counter(&active_keyset_id, p.len() as u32)
.await?;
mint_proofs.extend(p);
let mut total_amount = Amount::ZERO;
for (mint, proofs) in received_proofs {
total_amount += proofs.iter().map(|p| p.amount).sum();
let proofs = proofs
.into_iter()
.flat_map(|proof| {
ProofInfo::new(proof, mint.clone(), State::Unspent, self.unit.clone())
})
.collect();
self.localstore.add_proofs(proofs).await?;
}
Ok(total_amount)
}
/// Receive
#[instrument(skip_all)]
pub async fn receive(
@@ -1223,143 +1359,30 @@ impl Wallet {
p2pk_signing_keys: &[SecretKey],
preimages: &[String],
) -> Result<Amount, Error> {
//TODO: check token is for this mint
let token_data = Token::from_str(encoded_token)?;
let unit = token_data.unit.unwrap_or_default();
let unit = token_data.unit().clone().unwrap_or_default();
let mut received_proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
for token in token_data.token {
if token.proofs.is_empty() {
continue;
}
// Add mint if it does not exist in the store
if self
.localstore
.get_mint(token.mint.clone())
.await?
.is_none()
{
self.get_mint_info().await?;
}
let active_keyset_id = self.active_mint_keyset().await?;
let keys = self.get_keyset_keys(active_keyset_id).await?;
// Sum amount of all proofs
let amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
let mut proofs = token.proofs;
let mut sig_flag = SigFlag::SigInputs;
// Map hash of preimage to preimage
let hashed_to_preimage: HashMap<String, &String> = preimages
.iter()
.flat_map(|p| match hex::decode(p) {
Ok(hex_bytes) => Some((Sha256Hash::hash(&hex_bytes).to_string(), p)),
Err(_) => None,
})
.collect();
let p2pk_signing_keys: HashMap<XOnlyPublicKey, &SecretKey> = p2pk_signing_keys
.iter()
.map(|s| (s.x_only_public_key(&SECP256K1).0, s))
.collect();
for proof in &mut proofs {
// Verify that proof DLEQ is valid
if proof.dleq.is_some() {
let keys = self.get_keyset_keys(proof.keyset_id).await?;
let key = keys.amount_key(proof.amount).ok_or(Error::UnknownKey)?;
proof.verify_dleq(key)?;
}
if let Ok(secret) =
<crate::secret::Secret as TryInto<crate::nuts::nut10::Secret>>::try_into(
proof.secret.clone(),
)
{
let conditions: Result<Conditions, _> =
secret.secret_data.tags.unwrap_or_default().try_into();
if let Ok(conditions) = conditions {
let mut pubkeys = conditions.pubkeys.unwrap_or_default();
match secret.kind {
Kind::P2PK => {
let data_key = PublicKey::from_str(&secret.secret_data.data)?;
pubkeys.push(data_key);
}
Kind::HTLC => {
let hashed_preimage = &secret.secret_data.data;
let preimage = hashed_to_preimage
.get(hashed_preimage)
.ok_or(Error::PreimageNotProvided)?;
proof.add_preimage(preimage.to_string());
}
}
for pubkey in pubkeys {
if let Some(signing) =
p2pk_signing_keys.get(&pubkey.x_only_public_key())
{
proof.sign_p2pk(signing.to_owned().clone())?;
}
}
if conditions.sig_flag.eq(&SigFlag::SigAll) {
sig_flag = SigFlag::SigAll;
}
}
}
}
let mut pre_swap = self
.create_swap(Some(amount), amount_split_target, proofs, None)
.await?;
if sig_flag.eq(&SigFlag::SigAll) {
for blinded_message in &mut pre_swap.swap_request.outputs {
for signing_key in p2pk_signing_keys.values() {
blinded_message.sign_p2pk(signing_key.to_owned().clone())?
}
}
}
let swap_response = self
.client
.post_swap(token.mint.clone().try_into()?, pre_swap.swap_request)
.await?;
// Proof to keep
let p = construct_proofs(
swap_response.signatures,
pre_swap.pre_mint_secrets.rs(),
pre_swap.pre_mint_secrets.secrets(),
&keys,
)?;
let mint_proofs = received_proofs.entry(token.mint).or_default();
self.localstore
.increment_keyset_counter(&active_keyset_id, p.len() as u32)
.await?;
mint_proofs.extend(p);
if unit != self.unit {
return Err(Error::UnitNotSupported);
}
let mut total_amount = Amount::ZERO;
for (mint, proofs) in received_proofs {
total_amount += proofs.iter().map(|p| p.amount).sum();
let proofs = proofs
.into_iter()
.flat_map(|proof| ProofInfo::new(proof, mint.clone(), State::Unspent, unit.clone()))
.collect();
self.localstore.add_proofs(proofs).await?;
let proofs = token_data.proofs();
if proofs.len() != 1 {
return Err(Error::MultiMintTokenNotSupported);
}
Ok(total_amount)
let (mint_url, proofs) = proofs.into_iter().next().expect("Token has proofs");
if self.mint_url != mint_url {
return Err(Error::IncorrectMint);
}
let amount = self
.receive_proofs(proofs, amount_split_target, p2pk_signing_keys, preimages)
.await?;
Ok(amount)
}
/// Restore
@@ -1526,14 +1549,14 @@ impl Wallet {
));
}
for mint_proof in &token.token {
if mint_proof.mint != self.mint_url {
for (mint_url, proofs) in &token.proofs() {
if mint_url != &self.mint_url {
return Err(Error::IncorrectWallet(format!(
"Should be {} not {}",
self.mint_url, mint_proof.mint
self.mint_url, mint_url
)));
}
for proof in &mint_proof.proofs {
for proof in proofs {
let secret: nut10::Secret = (&proof.secret).try_into()?;
let proof_conditions: SpendingConditions = secret.try_into()?;
@@ -1618,14 +1641,14 @@ impl Wallet {
pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
let mut keys_cache: HashMap<Id, Keys> = HashMap::new();
for mint_proof in &token.token {
if mint_proof.mint != self.mint_url {
for (mint_url, proofs) in &token.proofs() {
if mint_url != &self.mint_url {
return Err(Error::IncorrectWallet(format!(
"Should be {} not {}",
self.mint_url, mint_proof.mint
self.mint_url, mint_url
)));
}
for proof in &mint_proof.proofs {
for proof in proofs {
let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
Some(keys) => keys.amount_key(proof.amount),
None => {

View File

@@ -2,7 +2,7 @@
//!
//! Wrapper around core [`Wallet`] that enables the use of multiple mint unit pairs
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
@@ -207,33 +207,38 @@ impl MultiMintWallet {
preimages: &[String],
) -> Result<Amount, Error> {
let token_data = Token::from_str(encoded_token)?;
let unit = token_data.unit.unwrap_or_default();
let mint_url = token_data.token.first().unwrap().mint.clone();
let unit = token_data.unit().clone().unwrap_or_default();
let mints: HashSet<&UncheckedUrl> = token_data.token.iter().map(|d| &d.mint).collect();
let mint_proofs = token_data.proofs();
let mut amount_received = Amount::ZERO;
// Check that all mints in tokes have wallets
for mint in mints {
let wallet_key = WalletKey::new(mint.clone(), unit.clone());
for (mint_url, proofs) in mint_proofs {
let wallet_key = WalletKey::new(mint_url.clone(), unit.clone());
if !self.has(&wallet_key).await {
return Err(Error::UnknownWallet(wallet_key.to_string()));
}
let wallet_key = WalletKey::new(mint_url, unit.clone());
let wallet = self
.get_wallet(&wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
let amount = wallet
.receive_proofs(
proofs,
&SplitTarget::default(),
p2pk_signing_keys,
preimages,
)
.await?;
amount_received += amount;
}
let wallet_key = WalletKey::new(mint_url, unit);
let wallet = self
.get_wallet(&wallet_key)
.await
.ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
wallet
.receive(
encoded_token,
&SplitTarget::default(),
p2pk_signing_keys,
preimages,
)
.await
Ok(amount_received)
}
/// Pay an bolt11 invoice from specific wallet

View File

@@ -1,9 +1,5 @@
//! Wallet Utility Functions
use super::Error;
use crate::nuts::{CurrencyUnit, Proofs, Token};
use crate::UncheckedUrl;
/// Extract token from text
pub fn token_from_text(text: &str) -> Option<&str> {
let text = text.trim();
@@ -17,16 +13,6 @@ pub fn token_from_text(text: &str) -> Option<&str> {
None
}
/// Convert proofs to token
pub fn proof_to_token(
mint_url: UncheckedUrl,
proofs: Proofs,
memo: Option<String>,
unit: Option<CurrencyUnit>,
) -> Result<Token, Error> {
Ok(Token::new(mint_url, proofs, memo, unit)?)
}
#[cfg(test)]
mod tests {