mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 13:44:55 +01:00
Keysets V2 (#702)
--------- Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use std::string::FromUtf8Error;
|
|||||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::nut02::ShortKeysetId;
|
||||||
#[cfg(feature = "wallet")]
|
#[cfg(feature = "wallet")]
|
||||||
use super::nut10;
|
use super::nut10;
|
||||||
#[cfg(feature = "wallet")]
|
#[cfg(feature = "wallet")]
|
||||||
@@ -183,6 +184,9 @@ pub enum Error {
|
|||||||
/// NUT11 error
|
/// NUT11 error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NUT11(#[from] crate::nuts::nut11::Error),
|
NUT11(#[from] crate::nuts::nut11::Error),
|
||||||
|
/// Short keyset id -> id error
|
||||||
|
#[error(transparent)]
|
||||||
|
NUT02(#[from] crate::nuts::nut02::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blinded Message (also called `output`)
|
/// Blinded Message (also called `output`)
|
||||||
@@ -434,6 +438,12 @@ impl ProofV4 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hash for ProofV4 {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.secret.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Proof> for ProofV4 {
|
impl From<Proof> for ProofV4 {
|
||||||
fn from(proof: Proof) -> ProofV4 {
|
fn from(proof: Proof) -> ProofV4 {
|
||||||
let Proof {
|
let Proof {
|
||||||
@@ -454,6 +464,80 @@ impl From<Proof> for ProofV4 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ProofV3> for ProofV4 {
|
||||||
|
fn from(proof: ProofV3) -> Self {
|
||||||
|
Self {
|
||||||
|
amount: proof.amount,
|
||||||
|
secret: proof.secret,
|
||||||
|
c: proof.c,
|
||||||
|
witness: proof.witness,
|
||||||
|
dleq: proof.dleq,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proof v3 with short keyset id
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProofV3 {
|
||||||
|
/// Amount
|
||||||
|
pub amount: Amount,
|
||||||
|
/// Short keyset id
|
||||||
|
#[serde(rename = "id")]
|
||||||
|
pub keyset_id: ShortKeysetId,
|
||||||
|
/// Secret message
|
||||||
|
pub secret: Secret,
|
||||||
|
/// Unblinded signature
|
||||||
|
#[serde(rename = "C")]
|
||||||
|
pub c: PublicKey,
|
||||||
|
/// Witness
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub witness: Option<Witness>,
|
||||||
|
/// DLEQ Proof
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub dleq: Option<ProofDleq>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProofV3 {
|
||||||
|
/// [`ProofV3`] 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 ProofV3 {
|
||||||
|
fn from(proof: Proof) -> ProofV3 {
|
||||||
|
let Proof {
|
||||||
|
amount,
|
||||||
|
keyset_id,
|
||||||
|
secret,
|
||||||
|
c,
|
||||||
|
witness,
|
||||||
|
dleq,
|
||||||
|
} = proof;
|
||||||
|
ProofV3 {
|
||||||
|
amount,
|
||||||
|
secret,
|
||||||
|
c,
|
||||||
|
witness,
|
||||||
|
dleq,
|
||||||
|
keyset_id: keyset_id.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for ProofV3 {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.secret.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_v4_pubkey<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize_v4_pubkey<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
S: serde::Serializer,
|
S: serde::Serializer,
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
|
|||||||
use bitcoin::base64::{alphabet, Engine as _};
|
use bitcoin::base64::{alphabet, Engine as _};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{Error, Proof, ProofV4, Proofs};
|
use super::{Error, Proof, ProofV3, ProofV4, Proofs};
|
||||||
use crate::mint_url::MintUrl;
|
use crate::mint_url::MintUrl;
|
||||||
use crate::nuts::nut00::ProofsMethods;
|
use crate::nut02::ShortKeysetId;
|
||||||
use crate::nuts::{CurrencyUnit, Id};
|
use crate::nuts::{CurrencyUnit, Id};
|
||||||
use crate::{ensure_cdk, Amount};
|
use crate::{ensure_cdk, Amount, KeySetInfo};
|
||||||
|
|
||||||
/// Token Enum
|
/// Token Enum
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -66,10 +66,10 @@ impl Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Proofs in [`Token`]
|
/// Proofs in [`Token`]
|
||||||
pub fn proofs(&self) -> Proofs {
|
pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::TokenV3(token) => token.proofs(),
|
Self::TokenV3(token) => token.proofs(mint_keysets),
|
||||||
Self::TokenV4(token) => token.proofs(),
|
Self::TokenV4(token) => token.proofs(mint_keysets),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +181,8 @@ impl TryFrom<&Vec<u8>> for Token {
|
|||||||
pub struct TokenV3Token {
|
pub struct TokenV3Token {
|
||||||
/// Url of mint
|
/// Url of mint
|
||||||
pub mint: MintUrl,
|
pub mint: MintUrl,
|
||||||
/// [`Proofs`]
|
/// [`Vec<ProofV3>`]
|
||||||
pub proofs: Proofs,
|
pub proofs: Vec<ProofV3>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TokenV3Token {
|
impl TokenV3Token {
|
||||||
@@ -190,7 +190,7 @@ impl TokenV3Token {
|
|||||||
pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
|
pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mint: mint_url,
|
mint: mint_url,
|
||||||
proofs,
|
proofs: proofs.into_iter().map(ProofV3::from).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,17 +226,21 @@ impl TokenV3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Proofs
|
/// Proofs
|
||||||
pub fn proofs(&self) -> Proofs {
|
pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
|
||||||
self.token
|
let mut proofs: Proofs = vec![];
|
||||||
.iter()
|
for t in self.token.iter() {
|
||||||
.flat_map(|token| token.proofs.clone())
|
for p in t.proofs.iter() {
|
||||||
.collect()
|
let long_id = Id::from_short_keyset_id(&p.keyset_id, mint_keysets)?;
|
||||||
|
proofs.push(p.into_proof(&long_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(proofs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Value - errors if duplicate proofs are found
|
/// Value - errors if duplicate proofs are found
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn value(&self) -> Result<Amount, Error> {
|
pub fn value(&self) -> Result<Amount, Error> {
|
||||||
let proofs = self.proofs();
|
let proofs: Vec<ProofV3> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
|
||||||
let unique_count = proofs
|
let unique_count = proofs
|
||||||
.iter()
|
.iter()
|
||||||
.collect::<std::collections::HashSet<_>>()
|
.collect::<std::collections::HashSet<_>>()
|
||||||
@@ -247,7 +251,12 @@ impl TokenV3 {
|
|||||||
return Err(Error::DuplicateProofs);
|
return Err(Error::DuplicateProofs);
|
||||||
}
|
}
|
||||||
|
|
||||||
proofs.total_amount()
|
Ok(Amount::try_sum(
|
||||||
|
self.token
|
||||||
|
.iter()
|
||||||
|
.map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
|
||||||
|
.collect::<Result<Vec<Amount>, _>>()?,
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Memo
|
/// Memo
|
||||||
@@ -306,10 +315,27 @@ impl fmt::Display for TokenV3 {
|
|||||||
|
|
||||||
impl From<TokenV4> for TokenV3 {
|
impl From<TokenV4> for TokenV3 {
|
||||||
fn from(token: TokenV4) -> Self {
|
fn from(token: TokenV4) -> Self {
|
||||||
let proofs = token.proofs();
|
let proofs: Vec<ProofV3> = token
|
||||||
|
.token
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|token| {
|
||||||
|
token.proofs.into_iter().map(move |p| ProofV3 {
|
||||||
|
amount: p.amount,
|
||||||
|
keyset_id: token.keyset_id.clone(),
|
||||||
|
secret: p.secret,
|
||||||
|
c: p.c,
|
||||||
|
witness: p.witness,
|
||||||
|
dleq: p.dleq,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let token_v3_token = TokenV3Token {
|
||||||
|
mint: token.mint_url,
|
||||||
|
proofs,
|
||||||
|
};
|
||||||
TokenV3 {
|
TokenV3 {
|
||||||
token: vec![TokenV3Token::new(token.mint_url, proofs)],
|
token: vec![token_v3_token],
|
||||||
memo: token.memo,
|
memo: token.memo,
|
||||||
unit: Some(token.unit),
|
unit: Some(token.unit),
|
||||||
}
|
}
|
||||||
@@ -335,17 +361,19 @@ pub struct TokenV4 {
|
|||||||
|
|
||||||
impl TokenV4 {
|
impl TokenV4 {
|
||||||
/// Proofs from token
|
/// Proofs from token
|
||||||
pub fn proofs(&self) -> Proofs {
|
pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
|
||||||
self.token
|
let mut proofs: Proofs = vec![];
|
||||||
.iter()
|
for t in self.token.iter() {
|
||||||
.flat_map(|token| token.proofs.iter().map(|p| p.into_proof(&token.keyset_id)))
|
let long_id = Id::from_short_keyset_id(&t.keyset_id, mint_keysets)?;
|
||||||
.collect()
|
proofs.extend(t.proofs.iter().map(|p| p.into_proof(&long_id)));
|
||||||
|
}
|
||||||
|
Ok(proofs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Value - errors if duplicate proofs are found
|
/// Value - errors if duplicate proofs are found
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn value(&self) -> Result<Amount, Error> {
|
pub fn value(&self) -> Result<Amount, Error> {
|
||||||
let proofs = self.proofs();
|
let proofs: Vec<ProofV4> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
|
||||||
let unique_count = proofs
|
let unique_count = proofs
|
||||||
.iter()
|
.iter()
|
||||||
.collect::<std::collections::HashSet<_>>()
|
.collect::<std::collections::HashSet<_>>()
|
||||||
@@ -356,7 +384,12 @@ impl TokenV4 {
|
|||||||
return Err(Error::DuplicateProofs);
|
return Err(Error::DuplicateProofs);
|
||||||
}
|
}
|
||||||
|
|
||||||
proofs.total_amount()
|
Ok(Amount::try_sum(
|
||||||
|
self.token
|
||||||
|
.iter()
|
||||||
|
.map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
|
||||||
|
.collect::<Result<Vec<Amount>, _>>()?,
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Memo
|
/// Memo
|
||||||
@@ -421,23 +454,29 @@ impl TryFrom<&Vec<u8>> for TokenV4 {
|
|||||||
impl TryFrom<TokenV3> for TokenV4 {
|
impl TryFrom<TokenV3> for TokenV4 {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
|
fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
|
||||||
let proofs = token.proofs();
|
|
||||||
let mint_urls = token.mint_urls();
|
let mint_urls = token.mint_urls();
|
||||||
|
let proofs: Vec<ProofV3> = token.token.into_iter().flat_map(|t| t.proofs).collect();
|
||||||
|
|
||||||
ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
|
ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
|
||||||
|
|
||||||
let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
|
let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
|
||||||
|
|
||||||
let proofs = proofs
|
let proofs = 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()
|
.into_iter()
|
||||||
.map(|(id, proofs)| TokenV4Token::new(id, proofs))
|
.fold(
|
||||||
|
HashMap::<ShortKeysetId, Vec<ProofV4>>::new(),
|
||||||
|
|mut acc, val| {
|
||||||
|
acc.entry(val.keyset_id.clone())
|
||||||
|
.and_modify(|p: &mut Vec<ProofV4>| p.push(val.clone().into()))
|
||||||
|
.or_insert(vec![val.clone().into()]);
|
||||||
|
acc
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, proofs)| TokenV4Token {
|
||||||
|
keyset_id: id,
|
||||||
|
proofs,
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(TokenV4 {
|
Ok(TokenV4 {
|
||||||
@@ -458,32 +497,34 @@ pub struct TokenV4Token {
|
|||||||
serialize_with = "serialize_v4_keyset_id",
|
serialize_with = "serialize_v4_keyset_id",
|
||||||
deserialize_with = "deserialize_v4_keyset_id"
|
deserialize_with = "deserialize_v4_keyset_id"
|
||||||
)]
|
)]
|
||||||
pub keyset_id: Id,
|
pub keyset_id: ShortKeysetId,
|
||||||
/// Proofs
|
/// Proofs
|
||||||
#[serde(rename = "p")]
|
#[serde(rename = "p")]
|
||||||
pub proofs: Vec<ProofV4>,
|
pub proofs: Vec<ProofV4>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_v4_keyset_id<S>(keyset_id: &Id, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize_v4_keyset_id<S>(keyset_id: &ShortKeysetId, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
S: serde::Serializer,
|
S: serde::Serializer,
|
||||||
{
|
{
|
||||||
serializer.serialize_bytes(&keyset_id.to_bytes())
|
serializer.serialize_bytes(&keyset_id.to_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<Id, D::Error>
|
fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<ShortKeysetId, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let bytes = Vec::<u8>::deserialize(deserializer)?;
|
let bytes = Vec::<u8>::deserialize(deserializer)?;
|
||||||
Id::from_bytes(&bytes).map_err(serde::de::Error::custom)
|
ShortKeysetId::from_bytes(&bytes).map_err(serde::de::Error::custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TokenV4Token {
|
impl TokenV4Token {
|
||||||
/// Create new [`TokenV4Token`]
|
/// Create new [`TokenV4Token`]
|
||||||
pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
|
pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
|
||||||
|
// Create a short keyset id from id
|
||||||
|
let short_id = ShortKeysetId::from(keyset_id);
|
||||||
Self {
|
Self {
|
||||||
keyset_id,
|
keyset_id: short_id,
|
||||||
proofs: proofs.into_iter().map(|p| p.into()).collect(),
|
proofs: proofs.into_iter().map(|p| p.into()).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,7 +534,10 @@ impl TokenV4Token {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use bip39::rand::{self, RngCore};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::dhke::hash_to_curve;
|
||||||
use crate::mint_url::MintUrl;
|
use crate::mint_url::MintUrl;
|
||||||
use crate::secret::Secret;
|
use crate::secret::Secret;
|
||||||
use crate::util::hex;
|
use crate::util::hex;
|
||||||
@@ -522,7 +566,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
token.token[0].keyset_id,
|
token.token[0].keyset_id,
|
||||||
Id::from_str("00ad268c4d1f5826").unwrap()
|
ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
let encoded = &token.to_string();
|
let encoded = &token.to_string();
|
||||||
@@ -546,12 +590,13 @@ mod tests {
|
|||||||
|
|
||||||
match token {
|
match token {
|
||||||
Token::TokenV4(token) => {
|
Token::TokenV4(token) => {
|
||||||
let tokens: Vec<Id> = token.token.iter().map(|t| t.keyset_id).collect();
|
let tokens: Vec<ShortKeysetId> =
|
||||||
|
token.token.iter().map(|t| t.keyset_id.clone()).collect();
|
||||||
|
|
||||||
assert_eq!(tokens.len(), 2);
|
assert_eq!(tokens.len(), 2);
|
||||||
|
|
||||||
assert!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap()));
|
assert!(tokens.contains(&ShortKeysetId::from_str("00ffd48b8f5ecf80").unwrap()));
|
||||||
assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap()));
|
assert!(tokens.contains(&ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()));
|
||||||
|
|
||||||
let mint_url = token.mint_url;
|
let mint_url = token.mint_url;
|
||||||
|
|
||||||
@@ -584,7 +629,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
token.token[0].proofs[0].clone().keyset_id,
|
token.token[0].proofs[0].clone().keyset_id,
|
||||||
Id::from_str("009a1f293253e41e").unwrap()
|
ShortKeysetId::from_str("009a1f293253e41e").unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
|
assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
|
||||||
|
|
||||||
@@ -684,4 +729,101 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), Amount::from(20));
|
assert_eq!(result.unwrap(), Amount::from(20));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_token_from_proofs_with_idv2_round_trip() {
|
||||||
|
let mint_url = MintUrl::from_str("https://example.com").unwrap();
|
||||||
|
|
||||||
|
let keysets_info: Vec<KeySetInfo> = (0..10)
|
||||||
|
.map(|_| {
|
||||||
|
let mut bytes: [u8; 33] = [0u8; 33];
|
||||||
|
bytes[0] = 1u8;
|
||||||
|
rand::thread_rng().fill_bytes(&mut bytes[1..]);
|
||||||
|
let id = Id::from_bytes(&bytes).unwrap();
|
||||||
|
KeySetInfo {
|
||||||
|
id,
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
active: true,
|
||||||
|
input_fee_ppk: 0,
|
||||||
|
final_expiry: None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let chosen_keyset_id = keysets_info[0].id;
|
||||||
|
// Make up a bunch of fake proofs
|
||||||
|
let proofs = (0..5)
|
||||||
|
.map(|_| {
|
||||||
|
let mut c_preimage: [u8; 33] = [0u8; 33];
|
||||||
|
c_preimage[0] = 1u8;
|
||||||
|
rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
|
||||||
|
Proof::new(
|
||||||
|
Amount::from(1),
|
||||||
|
chosen_keyset_id,
|
||||||
|
Secret::generate(),
|
||||||
|
hash_to_curve(&c_preimage).unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
|
||||||
|
let token_str = token.to_string();
|
||||||
|
|
||||||
|
let token1 = Token::from_str(&token_str);
|
||||||
|
assert!(token1.is_ok());
|
||||||
|
|
||||||
|
let proofs1 = token1.unwrap().proofs(&keysets_info);
|
||||||
|
assert!(proofs1.is_ok());
|
||||||
|
|
||||||
|
//println!("{:?}", proofs1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_token_proofs_with_unknown_short_keyset_id() {
|
||||||
|
let mint_url = MintUrl::from_str("https://example.com").unwrap();
|
||||||
|
|
||||||
|
let keysets_info: Vec<KeySetInfo> = (0..10)
|
||||||
|
.map(|_| {
|
||||||
|
let mut bytes: [u8; 33] = [0u8; 33];
|
||||||
|
bytes[0] = 1u8;
|
||||||
|
rand::thread_rng().fill_bytes(&mut bytes[1..]);
|
||||||
|
let id = Id::from_bytes(&bytes).unwrap();
|
||||||
|
KeySetInfo {
|
||||||
|
id,
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
active: true,
|
||||||
|
input_fee_ppk: 0,
|
||||||
|
final_expiry: None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let chosen_keyset_id =
|
||||||
|
Id::from_str("01c352c0b47d42edb764bddf8c53d77b85f057157d92084d9d05e876251ecd8422")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Make up a bunch of fake proofs
|
||||||
|
let proofs = (0..5)
|
||||||
|
.map(|_| {
|
||||||
|
let mut c_preimage: [u8; 33] = [0u8; 33];
|
||||||
|
c_preimage[0] = 1u8;
|
||||||
|
rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
|
||||||
|
Proof::new(
|
||||||
|
Amount::from(1),
|
||||||
|
chosen_keyset_id,
|
||||||
|
Secret::generate(),
|
||||||
|
hash_to_curve(&c_preimage).unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
|
||||||
|
let token_str = token.to_string();
|
||||||
|
|
||||||
|
let token1 = Token::from_str(&token_str);
|
||||||
|
assert!(token1.is_ok());
|
||||||
|
|
||||||
|
let proofs1 = token1.unwrap().proofs(&keysets_info);
|
||||||
|
assert!(proofs1.is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ pub enum Error {
|
|||||||
/// Keyset id does not match
|
/// Keyset id does not match
|
||||||
#[error("Keyset id incorrect")]
|
#[error("Keyset id incorrect")]
|
||||||
IncorrectKeysetId,
|
IncorrectKeysetId,
|
||||||
|
/// Short keyset id does not match any of the provided IDv2s
|
||||||
|
#[error("Short keyset id does not match any of the provided IDv2s")]
|
||||||
|
UnknownShortKeysetId,
|
||||||
|
/// Short keyset id is ill-formed
|
||||||
|
#[error("Short keyset id is ill-formed")]
|
||||||
|
MalformedShortKeysetId,
|
||||||
/// Slice Error
|
/// Slice Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Slice(#[from] TryFromSliceError),
|
Slice(#[from] TryFromSliceError),
|
||||||
@@ -51,8 +57,10 @@ pub enum Error {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
pub enum KeySetVersion {
|
pub enum KeySetVersion {
|
||||||
/// Current Version 00
|
/// Version 00
|
||||||
Version00,
|
Version00,
|
||||||
|
/// Version 01
|
||||||
|
Version01,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeySetVersion {
|
impl KeySetVersion {
|
||||||
@@ -60,6 +68,7 @@ impl KeySetVersion {
|
|||||||
pub fn to_byte(&self) -> u8 {
|
pub fn to_byte(&self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
Self::Version00 => 0,
|
Self::Version00 => 0,
|
||||||
|
Self::Version01 => 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +76,7 @@ impl KeySetVersion {
|
|||||||
pub fn from_byte(byte: &u8) -> Result<Self, Error> {
|
pub fn from_byte(byte: &u8) -> Result<Self, Error> {
|
||||||
match byte {
|
match byte {
|
||||||
0 => Ok(Self::Version00),
|
0 => Ok(Self::Version00),
|
||||||
|
1 => Ok(Self::Version01),
|
||||||
_ => Err(Error::UnknownVersion),
|
_ => Err(Error::UnknownVersion),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +86,27 @@ impl fmt::Display for KeySetVersion {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
KeySetVersion::Version00 => f.write_str("00"),
|
KeySetVersion::Version00 => f.write_str("00"),
|
||||||
|
KeySetVersion::Version01 => f.write_str("01"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keyset ID bytes
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
pub enum IdBytes {
|
||||||
|
/// Bytes for v1
|
||||||
|
V1([u8; 7]),
|
||||||
|
/// Bytes for v2
|
||||||
|
V2([u8; 32]),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdBytes {
|
||||||
|
/// Convert [`IdBytes`] to [`Vec<u8>`]
|
||||||
|
pub fn to_vec(&self) -> Vec<u8> {
|
||||||
|
match self {
|
||||||
|
IdBytes::V1(bytes) => bytes.to_vec(),
|
||||||
|
IdBytes::V2(bytes) => bytes.to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,12 +120,14 @@ impl fmt::Display for KeySetVersion {
|
|||||||
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
pub struct Id {
|
pub struct Id {
|
||||||
version: KeySetVersion,
|
version: KeySetVersion,
|
||||||
id: [u8; Self::BYTELEN],
|
id: IdBytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Id {
|
impl Id {
|
||||||
const STRLEN: usize = 14;
|
const STRLEN_V1: usize = 14;
|
||||||
const BYTELEN: usize = 7;
|
const BYTELEN_V1: usize = 7;
|
||||||
|
const STRLEN_V2: usize = 64;
|
||||||
|
const BYTELEN_V2: usize = 32;
|
||||||
|
|
||||||
/// [`Id`] to bytes
|
/// [`Id`] to bytes
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
@@ -103,18 +136,122 @@ impl Id {
|
|||||||
|
|
||||||
/// [`Id`] from bytes
|
/// [`Id`] from bytes
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
let version = KeySetVersion::from_byte(&bytes[0])?;
|
||||||
version: KeySetVersion::from_byte(&bytes[0])?,
|
let id = match version {
|
||||||
id: bytes[1..].try_into()?,
|
KeySetVersion::Version00 => IdBytes::V1(bytes[1..].try_into()?),
|
||||||
})
|
KeySetVersion::Version01 => IdBytes::V2(bytes[1..].try_into()?),
|
||||||
|
};
|
||||||
|
Ok(Self { version, id })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Id`] as bytes
|
/// Get the version of the keyset
|
||||||
pub fn as_bytes(&self) -> [u8; Self::BYTELEN + 1] {
|
pub fn get_version(&self) -> KeySetVersion {
|
||||||
let mut bytes = [0u8; Self::BYTELEN + 1];
|
self.version
|
||||||
bytes[0] = self.version.to_byte();
|
}
|
||||||
bytes[1..].copy_from_slice(&self.id);
|
|
||||||
bytes
|
/// *** V2 KEYSET ***
|
||||||
|
/// create [`Id`] v2 from keys, unit and (optionally) expiry
|
||||||
|
/// 1 - sort public keys by their amount in ascending order
|
||||||
|
/// 2 - concatenate all public keys to one byte array
|
||||||
|
/// 3 - concatenate the lowercase unit string to the byte array (e.g. "unit:sat")
|
||||||
|
/// 4 - If a final expiration is specified, convert it into a radix-10 string and concatenate it (e.g "final_expiry:1896187313")
|
||||||
|
/// 5 - HASH_SHA256 the concatenated byte array and take the first 31 bytes
|
||||||
|
/// 6 - prefix it with a keyset ID version byte
|
||||||
|
pub fn v2_from_data(map: &Keys, unit: &CurrencyUnit, expiry: Option<u64>) -> Self {
|
||||||
|
let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
|
||||||
|
keys.sort_by_key(|(amt, _v)| *amt);
|
||||||
|
|
||||||
|
let mut pubkeys_concat: Vec<u8> = keys
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pubkey)| pubkey.to_bytes())
|
||||||
|
.collect::<Vec<[u8; 33]>>()
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
// Add the unit
|
||||||
|
pubkeys_concat.extend(b"unit:");
|
||||||
|
pubkeys_concat.extend(unit.to_string().to_lowercase().as_bytes());
|
||||||
|
|
||||||
|
// Add the expiration
|
||||||
|
if let Some(expiry) = expiry {
|
||||||
|
pubkeys_concat.extend(b"final_expiry:");
|
||||||
|
pubkeys_concat.extend(expiry.to_string().as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = Sha256::hash(&pubkeys_concat);
|
||||||
|
let hex_of_hash = hex::encode(hash.to_byte_array());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
version: KeySetVersion::Version01,
|
||||||
|
id: IdBytes::V2(
|
||||||
|
hex::decode(&hex_of_hash[0..Self::STRLEN_V2])
|
||||||
|
.expect("Keys hash could not be hex decoded")
|
||||||
|
.try_into()
|
||||||
|
.expect("Invalid length of hex id"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// *** V1 VERSION ***
|
||||||
|
/// As per NUT-02:
|
||||||
|
/// 1. sort public keys by their amount in ascending order
|
||||||
|
/// 2. concatenate all public keys to one string
|
||||||
|
/// 3. HASH_SHA256 the concatenated public keys
|
||||||
|
/// 4. take the first 14 characters of the hex-encoded hash
|
||||||
|
/// 5. prefix it with a keyset ID version byte
|
||||||
|
pub fn v1_from_keys(map: &Keys) -> Self {
|
||||||
|
let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
|
||||||
|
keys.sort_by_key(|(amt, _v)| *amt);
|
||||||
|
|
||||||
|
let pubkeys_concat: Vec<u8> = keys
|
||||||
|
.iter()
|
||||||
|
.map(|(_, pubkey)| pubkey.to_bytes())
|
||||||
|
.collect::<Vec<[u8; 33]>>()
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
let hash = Sha256::hash(&pubkeys_concat);
|
||||||
|
let hex_of_hash = hex::encode(hash.to_byte_array());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
version: KeySetVersion::Version00,
|
||||||
|
id: IdBytes::V1(
|
||||||
|
hex::decode(&hex_of_hash[0..Self::STRLEN_V1])
|
||||||
|
.expect("Keys hash could not be hex decoded")
|
||||||
|
.try_into()
|
||||||
|
.expect("Invalid length of hex id"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the correct IDv2 from a list of keysets and the given short-id
|
||||||
|
/// or returns the short-id in the case of v1.
|
||||||
|
pub fn from_short_keyset_id(
|
||||||
|
short_id: &ShortKeysetId,
|
||||||
|
keysets_info: &[KeySetInfo],
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
// Check prefix length
|
||||||
|
if short_id.prefix.len() < Self::BYTELEN_V1 || short_id.prefix.len() > Self::BYTELEN_V2 {
|
||||||
|
return Err(Error::MalformedShortKeysetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
match short_id.version {
|
||||||
|
KeySetVersion::Version00 => {
|
||||||
|
let mut idbytes: [u8; Self::BYTELEN_V1] = [0u8; Self::BYTELEN_V1];
|
||||||
|
idbytes.copy_from_slice(&short_id.prefix[..Self::BYTELEN_V1]);
|
||||||
|
Ok(Self {
|
||||||
|
version: short_id.version,
|
||||||
|
id: IdBytes::V1(idbytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
KeySetVersion::Version01 => {
|
||||||
|
// We return the first match or error
|
||||||
|
for keyset_info in keysets_info.iter() {
|
||||||
|
if keyset_info.id.id.to_vec()[..short_id.prefix.len()] == short_id.prefix {
|
||||||
|
return Ok(keyset_info.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::UnknownShortKeysetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +259,9 @@ impl Id {
|
|||||||
// This is a one-way function
|
// This is a one-way function
|
||||||
impl From<Id> for u32 {
|
impl From<Id> for u32 {
|
||||||
fn from(value: Id) -> Self {
|
fn from(value: Id) -> Self {
|
||||||
let hex_bytes: [u8; 8] = value.as_bytes();
|
let id_bytes = value.to_bytes();
|
||||||
|
let mut hex_bytes: [u8; 8] = [0; 8];
|
||||||
|
hex_bytes.copy_from_slice(&id_bytes[..8]);
|
||||||
|
|
||||||
let int = u64::from_be_bytes(hex_bytes);
|
let int = u64::from_be_bytes(hex_bytes);
|
||||||
|
|
||||||
@@ -132,13 +271,21 @@ impl From<Id> for u32 {
|
|||||||
|
|
||||||
impl fmt::Display for Id {
|
impl fmt::Display for Id {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(&format!("{}{}", self.version, hex::encode(self.id)))
|
let hex_id = match self.id {
|
||||||
|
IdBytes::V1(id) => hex::encode(id),
|
||||||
|
IdBytes::V2(id) => hex::encode(id),
|
||||||
|
};
|
||||||
|
f.write_str(&format!("{}{}", self.version, hex_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Id {
|
impl fmt::Debug for Id {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
f.write_str(&format!("{}{}", self.version, hex::encode(self.id)))
|
let hex_id = match self.id {
|
||||||
|
IdBytes::V1(id) => hex::encode(id),
|
||||||
|
IdBytes::V2(id) => hex::encode(id),
|
||||||
|
};
|
||||||
|
f.write_str(&format!("{}{}", self.version, hex_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +293,26 @@ impl TryFrom<String> for Id {
|
|||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
ensure_cdk!(s.len() == 16, Error::Length);
|
ensure_cdk!(
|
||||||
|
s.len() == Self::STRLEN_V1 + 2 || s.len() == Self::STRLEN_V2 + 2,
|
||||||
|
Error::Length
|
||||||
|
);
|
||||||
|
|
||||||
Ok(Self {
|
let version: KeySetVersion = KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?;
|
||||||
version: KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?,
|
let id = match version {
|
||||||
id: hex::decode(&s[2..])?
|
KeySetVersion::Version00 => IdBytes::V1(
|
||||||
|
hex::decode(&s[2..])?
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| Error::Length)?,
|
.map_err(|_| Error::Length)?,
|
||||||
})
|
),
|
||||||
|
KeySetVersion::Version01 => IdBytes::V2(
|
||||||
|
hex::decode(&s[2..])?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Length)?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { version, id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,33 +330,88 @@ impl From<Id> for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Keys> for Id {
|
/// Improper prefix of the keyset ID. In case of v1, this is the whole ID.
|
||||||
/// As per NUT-02:
|
/// In case of v2, this is the 8-byte prefix
|
||||||
/// 1. sort public keys by their amount in ascending order
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
/// 2. concatenate all public keys to one string
|
#[serde(into = "String", try_from = "String")]
|
||||||
/// 3. HASH_SHA256 the concatenated public keys
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
/// 4. take the first 14 characters of the hex-encoded hash
|
pub struct ShortKeysetId {
|
||||||
/// 5. prefix it with a keyset ID version byte
|
/// The version of the short keyset
|
||||||
fn from(map: &Keys) -> Self {
|
version: KeySetVersion,
|
||||||
let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
|
/// The improper prefix of the keyset ID bytes
|
||||||
keys.sort_by_key(|(amt, _v)| *amt);
|
prefix: Vec<u8>,
|
||||||
|
|
||||||
let pubkeys_concat: Vec<u8> = keys
|
|
||||||
.iter()
|
|
||||||
.map(|(_, pubkey)| pubkey.to_bytes())
|
|
||||||
.collect::<Vec<[u8; 33]>>()
|
|
||||||
.concat();
|
|
||||||
|
|
||||||
let hash = Sha256::hash(&pubkeys_concat);
|
|
||||||
let hex_of_hash = hex::encode(hash.to_byte_array());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
version: KeySetVersion::Version00,
|
|
||||||
id: hex::decode(&hex_of_hash[0..Self::STRLEN])
|
|
||||||
.expect("Keys hash could not be hex decoded")
|
|
||||||
.try_into()
|
|
||||||
.expect("Invalid length of hex id"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ShortKeysetId {
|
||||||
|
/// [`ShortKeysetId`] to bytes
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
[vec![self.version.to_byte()], self.prefix.clone()].concat()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`ShortKeysetId`] from bytes
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||||
|
let version = KeySetVersion::from_byte(&bytes[0])?;
|
||||||
|
let prefix = bytes[1..].to_vec();
|
||||||
|
Ok(Self { version, prefix })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Id> for ShortKeysetId {
|
||||||
|
fn from(id: Id) -> Self {
|
||||||
|
let version = id.version;
|
||||||
|
let prefix: Vec<u8> = match id.version {
|
||||||
|
KeySetVersion::Version00 => match id.id {
|
||||||
|
IdBytes::V1(idbytes) => Vec::from(&idbytes),
|
||||||
|
_ => panic!("Unexpected IdBytes length"),
|
||||||
|
},
|
||||||
|
KeySetVersion::Version01 => match id.id {
|
||||||
|
IdBytes::V2(idbytes) => Vec::from(&idbytes[..7]),
|
||||||
|
_ => panic!("Unexpected IdBytes length"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Self { version, prefix }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ShortKeysetId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let hex_id = hex::encode(&self.prefix);
|
||||||
|
f.write_str(&format!("{}{}", self.version, hex_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ShortKeysetId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let hex_id = hex::encode(&self.prefix);
|
||||||
|
f.write_str(&format!("{}{}", self.version, hex_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for ShortKeysetId {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
ensure_cdk!(s.len() == 16, Error::Length);
|
||||||
|
|
||||||
|
let version: KeySetVersion = KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?;
|
||||||
|
let prefix = hex::decode(&s[2..])?;
|
||||||
|
|
||||||
|
Ok(Self { version, prefix })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ShortKeysetId {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::try_from(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShortKeysetId> for String {
|
||||||
|
fn from(value: ShortKeysetId) -> Self {
|
||||||
|
value.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,14 +437,26 @@ pub struct KeySet {
|
|||||||
pub unit: CurrencyUnit,
|
pub unit: CurrencyUnit,
|
||||||
/// Keyset [`Keys`]
|
/// Keyset [`Keys`]
|
||||||
pub keys: Keys,
|
pub keys: Keys,
|
||||||
|
/// Expiry
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub final_expiry: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeySet {
|
impl KeySet {
|
||||||
/// Verify the keyset is matches keys
|
/// Verify the keyset id matches keys
|
||||||
pub fn verify_id(&self) -> Result<(), Error> {
|
pub fn verify_id(&self) -> Result<(), Error> {
|
||||||
let keys_id: Id = (&self.keys).into();
|
match self.id.version {
|
||||||
|
KeySetVersion::Version00 => {
|
||||||
|
let keys_id: Id = Id::v1_from_keys(&self.keys);
|
||||||
|
|
||||||
ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
|
ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
|
||||||
|
}
|
||||||
|
KeySetVersion::Version01 => {
|
||||||
|
let keys_id: Id = Id::v2_from_data(&self.keys, &self.unit, self.final_expiry);
|
||||||
|
|
||||||
|
ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -243,6 +469,7 @@ impl From<MintKeySet> for KeySet {
|
|||||||
id: keyset.id,
|
id: keyset.id,
|
||||||
unit: keyset.unit,
|
unit: keyset.unit,
|
||||||
keys: Keys::from(keyset.keys),
|
keys: Keys::from(keyset.keys),
|
||||||
|
final_expiry: keyset.final_expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,6 +492,9 @@ pub struct KeySetInfo {
|
|||||||
default = "default_input_fee_ppk"
|
default = "default_input_fee_ppk"
|
||||||
)]
|
)]
|
||||||
pub input_fee_ppk: u64,
|
pub input_fee_ppk: u64,
|
||||||
|
/// Expiry of the keyset
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub final_expiry: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||||
@@ -290,6 +520,9 @@ pub struct MintKeySet {
|
|||||||
pub unit: CurrencyUnit,
|
pub unit: CurrencyUnit,
|
||||||
/// Keyset [`MintKeys`]
|
/// Keyset [`MintKeys`]
|
||||||
pub keys: MintKeys,
|
pub keys: MintKeys,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Expiry [`Option<u64>`]
|
||||||
|
pub final_expiry: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
@@ -300,6 +533,8 @@ impl MintKeySet {
|
|||||||
xpriv: Xpriv,
|
xpriv: Xpriv,
|
||||||
unit: CurrencyUnit,
|
unit: CurrencyUnit,
|
||||||
max_order: u8,
|
max_order: u8,
|
||||||
|
final_expiry: Option<u64>,
|
||||||
|
version: KeySetVersion,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut map = BTreeMap::new();
|
let mut map = BTreeMap::new();
|
||||||
for i in 0..max_order {
|
for i in 0..max_order {
|
||||||
@@ -322,10 +557,15 @@ impl MintKeySet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let keys = MintKeys::new(map);
|
let keys = MintKeys::new(map);
|
||||||
|
let id = match version {
|
||||||
|
KeySetVersion::Version00 => Id::v1_from_keys(&keys.clone().into()),
|
||||||
|
KeySetVersion::Version01 => Id::v2_from_data(&keys.clone().into(), &unit, final_expiry),
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
id: (&keys).into(),
|
id,
|
||||||
unit,
|
unit,
|
||||||
keys,
|
keys,
|
||||||
|
final_expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +576,8 @@ impl MintKeySet {
|
|||||||
max_order: u8,
|
max_order: u8,
|
||||||
currency_unit: CurrencyUnit,
|
currency_unit: CurrencyUnit,
|
||||||
derivation_path: DerivationPath,
|
derivation_path: DerivationPath,
|
||||||
|
final_expiry: Option<u64>,
|
||||||
|
version: KeySetVersion,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
|
let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
|
||||||
Self::generate(
|
Self::generate(
|
||||||
@@ -345,6 +587,8 @@ impl MintKeySet {
|
|||||||
.expect("RNG busted"),
|
.expect("RNG busted"),
|
||||||
currency_unit,
|
currency_unit,
|
||||||
max_order,
|
max_order,
|
||||||
|
final_expiry,
|
||||||
|
version,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +599,8 @@ impl MintKeySet {
|
|||||||
max_order: u8,
|
max_order: u8,
|
||||||
currency_unit: CurrencyUnit,
|
currency_unit: CurrencyUnit,
|
||||||
derivation_path: DerivationPath,
|
derivation_path: DerivationPath,
|
||||||
|
final_expiry: Option<u64>,
|
||||||
|
version: KeySetVersion,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::generate(
|
Self::generate(
|
||||||
secp,
|
secp,
|
||||||
@@ -363,6 +609,8 @@ impl MintKeySet {
|
|||||||
.expect("RNG busted"),
|
.expect("RNG busted"),
|
||||||
currency_unit,
|
currency_unit,
|
||||||
max_order,
|
max_order,
|
||||||
|
final_expiry,
|
||||||
|
version,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,8 +619,10 @@ impl MintKeySet {
|
|||||||
impl From<MintKeySet> for Id {
|
impl From<MintKeySet> for Id {
|
||||||
fn from(keyset: MintKeySet) -> Id {
|
fn from(keyset: MintKeySet) -> Id {
|
||||||
let keys: super::KeySet = keyset.into();
|
let keys: super::KeySet = keyset.into();
|
||||||
|
match keys.id.version {
|
||||||
Id::from(&keys.keys)
|
KeySetVersion::Version00 => Id::v1_from_keys(&keys.keys),
|
||||||
|
KeySetVersion::Version01 => Id::v2_from_data(&keys.keys, &keys.unit, keys.final_expiry),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +631,7 @@ impl From<&MintKeys> for Id {
|
|||||||
fn from(map: &MintKeys) -> Self {
|
fn from(map: &MintKeys) -> Self {
|
||||||
let keys: super::Keys = map.clone().into();
|
let keys: super::Keys = map.clone().into();
|
||||||
|
|
||||||
Id::from(&keys)
|
Id::v1_from_keys(&keys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,10 +641,11 @@ mod test {
|
|||||||
|
|
||||||
use bitcoin::secp256k1::rand::{self, RngCore};
|
use bitcoin::secp256k1::rand::{self, RngCore};
|
||||||
|
|
||||||
use super::{KeySetInfo, Keys, KeysetResponse};
|
use super::{KeySetInfo, KeySetVersion, Keys, KeysetResponse, ShortKeysetId};
|
||||||
use crate::nuts::nut02::{Error, Id};
|
use crate::nuts::nut02::{Error, Id};
|
||||||
use crate::nuts::KeysResponse;
|
use crate::nuts::KeysResponse;
|
||||||
use crate::util::hex;
|
use crate::util::hex;
|
||||||
|
use crate::CurrencyUnit;
|
||||||
|
|
||||||
const SHORT_KEYSET_ID: &str = "00456a94ab4e1c46";
|
const SHORT_KEYSET_ID: &str = "00456a94ab4e1c46";
|
||||||
const SHORT_KEYSET: &str = r#"
|
const SHORT_KEYSET: &str = r#"
|
||||||
@@ -482,17 +733,43 @@ mod test {
|
|||||||
|
|
||||||
let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
|
let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
|
||||||
|
|
||||||
let id: Id = (&keys).into();
|
let id: Id = Id::v1_from_keys(&keys);
|
||||||
|
|
||||||
assert_eq!(id, Id::from_str(SHORT_KEYSET_ID).unwrap());
|
assert_eq!(id, Id::from_str(SHORT_KEYSET_ID).unwrap());
|
||||||
|
|
||||||
let keys: Keys = serde_json::from_str(KEYSET).unwrap();
|
let keys: Keys = serde_json::from_str(KEYSET).unwrap();
|
||||||
|
|
||||||
let id: Id = (&keys).into();
|
let id: Id = Id::v1_from_keys(&keys);
|
||||||
|
|
||||||
assert_eq!(id, Id::from_str(KEYSET_ID).unwrap());
|
assert_eq!(id, Id::from_str(KEYSET_ID).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_deserialization_and_id_generation() {
|
||||||
|
let unit: CurrencyUnit = CurrencyUnit::from_str("sat").unwrap();
|
||||||
|
let expiry: u64 = 2059210353; // +10 years from now
|
||||||
|
|
||||||
|
let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
|
||||||
|
let id_from_str =
|
||||||
|
Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
|
||||||
|
.unwrap();
|
||||||
|
let id = Id::v2_from_data(&keys, &unit, Some(expiry));
|
||||||
|
assert_eq!(id, id_from_str);
|
||||||
|
|
||||||
|
let keys: Keys = serde_json::from_str(KEYSET).unwrap();
|
||||||
|
let id_from_str =
|
||||||
|
Id::from_str("0125bc634e270ad7e937af5b957f8396bb627d73f6e1fd2ffe4294c26b57daf9e0")
|
||||||
|
.unwrap();
|
||||||
|
let id = Id::v2_from_data(&keys, &unit, Some(expiry));
|
||||||
|
assert_eq!(id, id_from_str);
|
||||||
|
|
||||||
|
let id = Id::v2_from_data(&keys, &unit, None);
|
||||||
|
let id_from_str =
|
||||||
|
Id::from_str("016d72f27c8d22808ad66d1959b3dab83af17e2510db7ffd57d2365d9eec3ced75")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(id, id_from_str);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialization_keyset_info() {
|
fn test_deserialization_keyset_info() {
|
||||||
let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
|
let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
|
||||||
@@ -519,6 +796,15 @@ mod test {
|
|||||||
assert_eq!(864559728, id_int)
|
assert_eq!(864559728, id_int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_to_int() {
|
||||||
|
let id = Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let id_int = u32::from(id);
|
||||||
|
assert_eq!(2113471806, id_int);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_id_from_invalid_byte_length() {
|
fn test_id_from_invalid_byte_length() {
|
||||||
let three_bytes = [0x01, 0x02, 0x03];
|
let three_bytes = [0x01, 0x02, 0x03];
|
||||||
@@ -548,16 +834,28 @@ mod test {
|
|||||||
assert_eq!(keys_response.keysets.len(), 2);
|
assert_eq!(keys_response.keysets.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_random_id() -> Id {
|
fn generate_random_id(version: KeySetVersion) -> Id {
|
||||||
|
match version {
|
||||||
|
KeySetVersion::Version00 => {
|
||||||
let mut rand_bytes = vec![0u8; 8];
|
let mut rand_bytes = vec![0u8; 8];
|
||||||
rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
|
rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
|
||||||
Id::from_bytes(&rand_bytes)
|
Id::from_bytes(&rand_bytes).unwrap_or_else(|e| {
|
||||||
.unwrap_or_else(|e| panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes)))
|
panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
KeySetVersion::Version01 => {
|
||||||
|
let mut rand_bytes = vec![1u8; 33];
|
||||||
|
rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
|
||||||
|
Id::from_bytes(&rand_bytes).unwrap_or_else(|e| {
|
||||||
|
panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_id_serialization() {
|
fn test_id_serialization() {
|
||||||
let id = generate_random_id();
|
let id = generate_random_id(KeySetVersion::Version00);
|
||||||
let id_str = id.to_string();
|
let id_str = id.to_string();
|
||||||
|
|
||||||
assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
|
assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
|
||||||
@@ -565,6 +863,16 @@ mod test {
|
|||||||
assert_eq!(id_str.to_lowercase(), id_str);
|
assert_eq!(id_str.to_lowercase(), id_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_id_v2_serialization() {
|
||||||
|
let id = generate_random_id(KeySetVersion::Version01);
|
||||||
|
let id_str = id.to_string();
|
||||||
|
|
||||||
|
assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
|
||||||
|
assert_eq!(66, id_str.len());
|
||||||
|
assert_eq!(id_str.to_lowercase(), id_str);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_id_deserialization() {
|
fn test_id_deserialization() {
|
||||||
let id_from_short_str = Id::from_str("00123");
|
let id_from_short_str = Id::from_str("00123");
|
||||||
@@ -579,4 +887,18 @@ mod test {
|
|||||||
let id_from_uppercase = Id::from_str(&SHORT_KEYSET_ID.to_uppercase());
|
let id_from_uppercase = Id::from_str(&SHORT_KEYSET_ID.to_uppercase());
|
||||||
assert!(id_from_uppercase.is_ok());
|
assert!(id_from_uppercase.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_short_keyset_id_from_id() {
|
||||||
|
let idv1 = Id::from_str("009a1f293253e41e").unwrap();
|
||||||
|
let idv2 =
|
||||||
|
Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let short_id_1: ShortKeysetId = idv1.into();
|
||||||
|
let short_id_2: ShortKeysetId = idv2.into();
|
||||||
|
|
||||||
|
assert!(short_id_1.to_string() == "009a1f293253e41e");
|
||||||
|
assert!(short_id_2.to_string() == "01adc013fa9d8517");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,19 @@ pub async fn pay_request(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
|
|
||||||
|
let token = matching_wallet.send(prepared_send, None).await?;
|
||||||
|
|
||||||
|
// We need the keysets information to properly convert from token proof to proof
|
||||||
|
let keysets_info = match matching_wallet
|
||||||
|
.localstore
|
||||||
|
.get_mint_keysets(token.mint_url()?)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(keysets_info) => keysets_info,
|
||||||
|
None => matching_wallet.get_mint_keysets().await?, // Hit the keysets endpoint if we don't have the keysets for this Mint
|
||||||
|
};
|
||||||
|
let proofs = token.proofs(&keysets_info)?;
|
||||||
|
|
||||||
if let Some(transport) = transport {
|
if let Some(transport) = transport {
|
||||||
let payload = PaymentRequestPayload {
|
let payload = PaymentRequestPayload {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async fn setup_keyset<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: &DB
|
|||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
active: true,
|
active: true,
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
valid_to: None,
|
final_expiry: None,
|
||||||
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||||
derivation_path_index: Some(0),
|
derivation_path_index: Some(0),
|
||||||
max_order: 32,
|
max_order: 32,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::collections::HashMap;
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use cashu::KeySet;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::common::ProofInfo;
|
use crate::common::ProofInfo;
|
||||||
@@ -72,7 +73,7 @@ pub trait Database: Debug {
|
|||||||
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
|
async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
|
||||||
|
|
||||||
/// Add [`Keys`] to storage
|
/// Add [`Keys`] to storage
|
||||||
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err>;
|
async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err>;
|
||||||
/// Get [`Keys`] from storage
|
/// Get [`Keys`] from storage
|
||||||
async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
|
async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
|
||||||
/// Remove [`Keys`] from storage
|
/// Remove [`Keys`] from storage
|
||||||
|
|||||||
@@ -139,9 +139,6 @@ pub struct MintKeySetInfo {
|
|||||||
pub active: bool,
|
pub active: bool,
|
||||||
/// Starting unix time Keyset is valid from
|
/// Starting unix time Keyset is valid from
|
||||||
pub valid_from: u64,
|
pub valid_from: u64,
|
||||||
/// When the Keyset is valid to
|
|
||||||
/// This is not shown to the wallet and can only be used internally
|
|
||||||
pub valid_to: Option<u64>,
|
|
||||||
/// [`DerivationPath`] keyset
|
/// [`DerivationPath`] keyset
|
||||||
pub derivation_path: DerivationPath,
|
pub derivation_path: DerivationPath,
|
||||||
/// DerivationPath index of Keyset
|
/// DerivationPath index of Keyset
|
||||||
@@ -151,6 +148,8 @@ pub struct MintKeySetInfo {
|
|||||||
/// Input Fee ppk
|
/// Input Fee ppk
|
||||||
#[serde(default = "default_fee")]
|
#[serde(default = "default_fee")]
|
||||||
pub input_fee_ppk: u64,
|
pub input_fee_ppk: u64,
|
||||||
|
/// Final expiry
|
||||||
|
pub final_expiry: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default fee
|
/// Default fee
|
||||||
@@ -165,6 +164,7 @@ impl From<MintKeySetInfo> for KeySetInfo {
|
|||||||
unit: keyset_info.unit,
|
unit: keyset_info.unit,
|
||||||
active: keyset_info.active,
|
active: keyset_info.active,
|
||||||
input_fee_ppk: keyset_info.input_fee_ppk,
|
input_fee_ppk: keyset_info.input_fee_ppk,
|
||||||
|
final_expiry: keyset_info.final_expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,10 +77,11 @@ async fn test_swap_to_send() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to send token");
|
.expect("Failed to send token");
|
||||||
|
let keysets_info = wallet_alice.get_mint_keysets().await.unwrap();
|
||||||
|
let token_proofs = token.proofs(&keysets_info).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Amount::from(40),
|
Amount::from(40),
|
||||||
token
|
token_proofs
|
||||||
.proofs()
|
|
||||||
.total_amount()
|
.total_amount()
|
||||||
.expect("Failed to get total amount")
|
.expect("Failed to get total amount")
|
||||||
);
|
);
|
||||||
@@ -92,7 +93,7 @@ async fn test_swap_to_send() {
|
|||||||
.expect("Failed to get balance")
|
.expect("Failed to get balance")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
HashSet::<_, RandomState>::from_iter(token.proofs().ys().expect("Failed to get ys")),
|
HashSet::<_, RandomState>::from_iter(token_proofs.ys().expect("Failed to get ys")),
|
||||||
HashSet::from_iter(
|
HashSet::from_iter(
|
||||||
wallet_alice
|
wallet_alice
|
||||||
.get_pending_spent_proofs()
|
.get_pending_spent_proofs()
|
||||||
@@ -103,7 +104,8 @@ async fn test_swap_to_send() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let transaction_id = TransactionId::from_proofs(token.proofs()).expect("Failed to get tx id");
|
let transaction_id =
|
||||||
|
TransactionId::from_proofs(token_proofs.clone()).expect("Failed to get tx id");
|
||||||
|
|
||||||
let transaction = wallet_alice
|
let transaction = wallet_alice
|
||||||
.get_transaction(transaction_id)
|
.get_transaction(transaction_id)
|
||||||
@@ -115,7 +117,7 @@ async fn test_swap_to_send() {
|
|||||||
assert_eq!(Amount::from(40), transaction.amount);
|
assert_eq!(Amount::from(40), transaction.amount);
|
||||||
assert_eq!(Amount::from(0), transaction.fee);
|
assert_eq!(Amount::from(0), transaction.fee);
|
||||||
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
||||||
assert_eq!(token.proofs().ys().unwrap(), transaction.ys);
|
assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
|
||||||
|
|
||||||
// Alice sends cashu, Carol receives
|
// Alice sends cashu, Carol receives
|
||||||
let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())
|
let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())
|
||||||
@@ -123,7 +125,7 @@ async fn test_swap_to_send() {
|
|||||||
.expect("Failed to create Carol's wallet");
|
.expect("Failed to create Carol's wallet");
|
||||||
let received_amount = wallet_carol
|
let received_amount = wallet_carol
|
||||||
.receive_proofs(
|
.receive_proofs(
|
||||||
token.proofs(),
|
token_proofs.clone(),
|
||||||
ReceiveOptions::default(),
|
ReceiveOptions::default(),
|
||||||
token.memo().clone(),
|
token.memo().clone(),
|
||||||
)
|
)
|
||||||
@@ -149,7 +151,7 @@ async fn test_swap_to_send() {
|
|||||||
assert_eq!(Amount::from(40), transaction.amount);
|
assert_eq!(Amount::from(40), transaction.amount);
|
||||||
assert_eq!(Amount::from(0), transaction.fee);
|
assert_eq!(Amount::from(0), transaction.fee);
|
||||||
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
assert_eq!(CurrencyUnit::Sat, transaction.unit);
|
||||||
assert_eq!(token.proofs().ys().unwrap(), transaction.ys);
|
assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
|
||||||
assert_eq!(token.memo().clone(), transaction.memo);
|
assert_eq!(token.memo().clone(), transaction.memo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +239,8 @@ async fn test_mint_double_spend() {
|
|||||||
.await
|
.await
|
||||||
.expect("Could not get proofs");
|
.expect("Could not get proofs");
|
||||||
|
|
||||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
|
||||||
let keyset_id = Id::from(&keys);
|
let keyset_id = keys.id;
|
||||||
|
|
||||||
let preswap = PreMintSecrets::random(
|
let preswap = PreMintSecrets::random(
|
||||||
keyset_id,
|
keyset_id,
|
||||||
@@ -294,8 +296,8 @@ async fn test_attempt_to_swap_by_overflowing() {
|
|||||||
|
|
||||||
let amount = 2_u64.pow(63);
|
let amount = 2_u64.pow(63);
|
||||||
|
|
||||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
|
||||||
let keyset_id = Id::from(&keys);
|
let keyset_id = keys.id;
|
||||||
|
|
||||||
let pre_mint_amount =
|
let pre_mint_amount =
|
||||||
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
|
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
|
||||||
@@ -532,7 +534,7 @@ async fn test_swap_overpay_underpay_fee() {
|
|||||||
.expect("Could not get proofs");
|
.expect("Could not get proofs");
|
||||||
|
|
||||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
||||||
let keyset_id = Id::from(&keys);
|
let keyset_id = Id::v1_from_keys(&keys);
|
||||||
|
|
||||||
let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap();
|
let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap();
|
||||||
|
|
||||||
@@ -597,8 +599,8 @@ async fn test_mint_enforce_fee() {
|
|||||||
.await
|
.await
|
||||||
.expect("Could not get proofs");
|
.expect("Could not get proofs");
|
||||||
|
|
||||||
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
|
let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
|
||||||
let keyset_id = Id::from(&keys);
|
let keyset_id = keys.id;
|
||||||
|
|
||||||
let five_proofs: Vec<_> = proofs.drain(..5).collect();
|
let five_proofs: Vec<_> = proofs.drain(..5).collect();
|
||||||
|
|
||||||
@@ -884,6 +886,8 @@ async fn test_concurrent_double_spend_melt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_keyset_id(mint: &Mint) -> Id {
|
async fn get_keyset_id(mint: &Mint) -> Id {
|
||||||
let keys = mint.pubkeys().keysets.first().unwrap().clone().keys;
|
let keys = mint.pubkeys().keysets.first().unwrap().clone();
|
||||||
Id::from(&keys)
|
keys.verify_id()
|
||||||
|
.expect("Keyset ID generation is successful");
|
||||||
|
keys.id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use cdk_common::mint_url::MintUrl;
|
|||||||
use cdk_common::util::unix_time;
|
use cdk_common::util::unix_time;
|
||||||
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
||||||
use cdk_common::{
|
use cdk_common::{
|
||||||
database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
|
database, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions,
|
||||||
|
State,
|
||||||
};
|
};
|
||||||
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
|
use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -493,15 +494,19 @@ impl WalletDatabase for WalletRedbDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
|
async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
|
||||||
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||||
|
|
||||||
|
keyset.verify_id()?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
|
let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
|
||||||
table
|
table
|
||||||
.insert(
|
.insert(
|
||||||
Id::from(&keys).to_string().as_str(),
|
keyset.id.to_string().as_str(),
|
||||||
serde_json::to_string(&keys).map_err(Error::from)?.as_str(),
|
serde_json::to_string(&keyset.keys)
|
||||||
|
.map_err(Error::from)?
|
||||||
|
.as_str(),
|
||||||
)
|
)
|
||||||
.map_err(Error::from)?;
|
.map_err(Error::from)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -547,7 +547,10 @@ impl WalletDatabase for WalletRexieDatabase {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
|
async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
|
||||||
|
// Verify ID by recomputing id
|
||||||
|
keyset.verify_id()?;
|
||||||
|
|
||||||
let rexie = self.db.lock().await;
|
let rexie = self.db.lock().await;
|
||||||
|
|
||||||
let transaction = rexie
|
let transaction = rexie
|
||||||
@@ -556,7 +559,7 @@ impl WalletDatabase for WalletRexieDatabase {
|
|||||||
|
|
||||||
let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?;
|
let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?;
|
||||||
|
|
||||||
let keyset_id = serde_wasm_bindgen::to_value(&Id::from(&keys)).map_err(Error::from)?;
|
let keyset_id = serde_wasm_bindgen::to_value(&keyset.id).map_err(Error::from)?;
|
||||||
let keys = serde_wasm_bindgen::to_value(&keys).map_err(Error::from)?;
|
let keys = serde_wasm_bindgen::to_value(&keys).map_err(Error::from)?;
|
||||||
|
|
||||||
keys_store
|
keys_store
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ pub async fn init_keysets(
|
|||||||
highest_index_keyset.max_order,
|
highest_index_keyset.max_order,
|
||||||
highest_index_keyset.unit.clone(),
|
highest_index_keyset.unit.clone(),
|
||||||
highest_index_keyset.derivation_path.clone(),
|
highest_index_keyset.derivation_path.clone(),
|
||||||
|
highest_index_keyset.final_expiry,
|
||||||
|
cdk_common::nut02::KeySetVersion::Version00,
|
||||||
);
|
);
|
||||||
active_keysets.insert(id, keyset);
|
active_keysets.insert(id, keyset);
|
||||||
let mut keyset_info = highest_index_keyset;
|
let mut keyset_info = highest_index_keyset;
|
||||||
@@ -97,6 +99,8 @@ pub async fn init_keysets(
|
|||||||
unit.clone(),
|
unit.clone(),
|
||||||
*max_order,
|
*max_order,
|
||||||
*input_fee_ppk,
|
*input_fee_ppk,
|
||||||
|
// TODO: add Mint settings for a final expiry of newly generated keysets
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let id = keyset_info.id;
|
let id = keyset_info.id;
|
||||||
@@ -114,6 +118,7 @@ pub async fn init_keysets(
|
|||||||
|
|
||||||
/// Generate new [`MintKeySetInfo`] from path
|
/// Generate new [`MintKeySetInfo`] from path
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn create_new_keyset<C: secp256k1::Signing>(
|
pub fn create_new_keyset<C: secp256k1::Signing>(
|
||||||
secp: &secp256k1::Secp256k1<C>,
|
secp: &secp256k1::Secp256k1<C>,
|
||||||
xpriv: Xpriv,
|
xpriv: Xpriv,
|
||||||
@@ -122,6 +127,7 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
|
|||||||
unit: CurrencyUnit,
|
unit: CurrencyUnit,
|
||||||
max_order: u8,
|
max_order: u8,
|
||||||
input_fee_ppk: u64,
|
input_fee_ppk: u64,
|
||||||
|
final_expiry: Option<u64>,
|
||||||
) -> (MintKeySet, MintKeySetInfo) {
|
) -> (MintKeySet, MintKeySetInfo) {
|
||||||
let keyset = MintKeySet::generate(
|
let keyset = MintKeySet::generate(
|
||||||
secp,
|
secp,
|
||||||
@@ -130,13 +136,16 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
|
|||||||
.expect("RNG busted"),
|
.expect("RNG busted"),
|
||||||
unit,
|
unit,
|
||||||
max_order,
|
max_order,
|
||||||
|
final_expiry,
|
||||||
|
// TODO: change this to Version01 to generate keysets v2
|
||||||
|
cdk_common::nut02::KeySetVersion::Version00,
|
||||||
);
|
);
|
||||||
let keyset_info = MintKeySetInfo {
|
let keyset_info = MintKeySetInfo {
|
||||||
id: keyset.id,
|
id: keyset.id,
|
||||||
unit: keyset.unit.clone(),
|
unit: keyset.unit.clone(),
|
||||||
active: true,
|
active: true,
|
||||||
valid_from: unix_time(),
|
valid_from: unix_time(),
|
||||||
valid_to: None,
|
final_expiry: keyset.final_expiry,
|
||||||
derivation_path,
|
derivation_path,
|
||||||
derivation_path_index,
|
derivation_path_index,
|
||||||
max_order,
|
max_order,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ impl DbSignatory {
|
|||||||
unit.clone(),
|
unit.clone(),
|
||||||
max_order,
|
max_order,
|
||||||
fee,
|
fee,
|
||||||
|
// TODO: add and connect settings for this
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let id = keyset_info.id;
|
let id = keyset_info.id;
|
||||||
@@ -130,6 +132,8 @@ impl DbSignatory {
|
|||||||
keyset_info.max_order,
|
keyset_info.max_order,
|
||||||
keyset_info.unit.clone(),
|
keyset_info.unit.clone(),
|
||||||
keyset_info.derivation_path.clone(),
|
keyset_info.derivation_path.clone(),
|
||||||
|
keyset_info.final_expiry,
|
||||||
|
keyset_info.id.get_version(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,6 +240,8 @@ impl Signatory for DbSignatory {
|
|||||||
args.unit.clone(),
|
args.unit.clone(),
|
||||||
args.max_order,
|
args.max_order,
|
||||||
args.input_fee_ppk,
|
args.input_fee_ppk,
|
||||||
|
// TODO: add and connect settings for this
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
let id = info.id;
|
let id = info.id;
|
||||||
self.localstore.add_keyset_info(info.clone()).await?;
|
self.localstore.add_keyset_info(info.clone()).await?;
|
||||||
@@ -266,6 +272,8 @@ mod test {
|
|||||||
2,
|
2,
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
|
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
|
||||||
|
None,
|
||||||
|
cdk_common::nut02::KeySetVersion::Version00,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(keyset.unit, CurrencyUnit::Sat);
|
assert_eq!(keyset.unit, CurrencyUnit::Sat);
|
||||||
@@ -310,6 +318,8 @@ mod test {
|
|||||||
2,
|
2,
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
|
derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
|
||||||
|
None,
|
||||||
|
cdk_common::nut02::KeySetVersion::Version00,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(keyset.unit, CurrencyUnit::Sat);
|
assert_eq!(keyset.unit, CurrencyUnit::Sat);
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ impl TryInto<crate::signatory::SignatoryKeySet> for KeySet {
|
|||||||
.map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk)))
|
.map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk)))
|
||||||
.collect::<Result<BTreeMap<Amount, _>, _>>()?,
|
.collect::<Result<BTreeMap<Amount, _>, _>>()?,
|
||||||
),
|
),
|
||||||
|
final_expiry: self.final_expiry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +79,7 @@ impl From<crate::signatory::SignatoryKeySet> for KeySet {
|
|||||||
.map(|(key, value)| ((*key).into(), value.to_bytes().to_vec()))
|
.map(|(key, value)| ((*key).into(), value.to_bytes().to_vec()))
|
||||||
.collect(),
|
.collect(),
|
||||||
}),
|
}),
|
||||||
|
final_expiry: keyset.final_expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,6 +395,7 @@ impl TryInto<cdk_common::KeySet> for KeySet {
|
|||||||
.map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk)))
|
.map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk)))
|
||||||
.collect::<Result<BTreeMap<cdk_common::Amount, cdk_common::PublicKey>, _>>()?,
|
.collect::<Result<BTreeMap<cdk_common::Amount, cdk_common::PublicKey>, _>>()?,
|
||||||
),
|
),
|
||||||
|
final_expiry: self.final_expiry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,6 +436,7 @@ impl From<cdk_common::KeySetInfo> for KeySet {
|
|||||||
active: value.active,
|
active: value.active,
|
||||||
input_fee_ppk: value.input_fee_ppk,
|
input_fee_ppk: value.input_fee_ppk,
|
||||||
keys: Default::default(),
|
keys: Default::default(),
|
||||||
|
final_expiry: value.final_expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,6 +454,7 @@ impl TryInto<cdk_common::KeySetInfo> for KeySet {
|
|||||||
.map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
|
.map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
|
||||||
active: self.active,
|
active: self.active,
|
||||||
input_fee_ppk: self.input_fee_ppk,
|
input_fee_ppk: self.input_fee_ppk,
|
||||||
|
final_expiry: self.final_expiry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ message KeySet {
|
|||||||
bool active = 3;
|
bool active = 3;
|
||||||
uint64 input_fee_ppk = 4;
|
uint64 input_fee_ppk = 4;
|
||||||
Keys keys = 5;
|
Keys keys = 5;
|
||||||
|
optional uint64 final_expiry = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Keys {
|
message Keys {
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ pub struct SignatoryKeySet {
|
|||||||
pub keys: Keys,
|
pub keys: Keys,
|
||||||
/// Information about the fee per public key
|
/// Information about the fee per public key
|
||||||
pub input_fee_ppk: u64,
|
pub input_fee_ppk: u64,
|
||||||
|
/// Final expiry of the keyset (unix timestamp in the future)
|
||||||
|
pub final_expiry: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&SignatoryKeySet> for KeySet {
|
impl From<&SignatoryKeySet> for KeySet {
|
||||||
@@ -87,6 +89,7 @@ impl From<SignatoryKeySet> for KeySet {
|
|||||||
id: val.id,
|
id: val.id,
|
||||||
unit: val.unit,
|
unit: val.unit,
|
||||||
keys: val.keys,
|
keys: val.keys,
|
||||||
|
final_expiry: val.final_expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +110,7 @@ impl From<SignatoryKeySet> for MintKeySetInfo {
|
|||||||
derivation_path: Default::default(),
|
derivation_path: Default::default(),
|
||||||
derivation_path_index: Default::default(),
|
derivation_path_index: Default::default(),
|
||||||
max_order: 0,
|
max_order: 0,
|
||||||
valid_to: None,
|
final_expiry: val.final_expiry,
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,6 +124,7 @@ impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
|
|||||||
active: info.active,
|
active: info.active,
|
||||||
input_fee_ppk: info.input_fee_ppk,
|
input_fee_ppk: info.input_fee_ppk,
|
||||||
keys: key.keys.clone().into(),
|
keys: key.keys.clone().into(),
|
||||||
|
final_expiry: key.final_expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
|
|||||||
.bind(":unit", keyset.unit.to_string())
|
.bind(":unit", keyset.unit.to_string())
|
||||||
.bind(":active", keyset.active)
|
.bind(":active", keyset.active)
|
||||||
.bind(":valid_from", keyset.valid_from as i64)
|
.bind(":valid_from", keyset.valid_from as i64)
|
||||||
.bind(":valid_to", keyset.valid_to.map(|v| v as i64))
|
.bind(":valid_to", keyset.final_expiry.map(|v| v as i64))
|
||||||
.bind(":derivation_path", keyset.derivation_path.to_string())
|
.bind(":derivation_path", keyset.derivation_path.to_string())
|
||||||
.bind(":max_order", keyset.max_order)
|
.bind(":max_order", keyset.max_order)
|
||||||
.bind(":derivation_path_index", keyset.derivation_path_index)
|
.bind(":derivation_path_index", keyset.derivation_path_index)
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ impl MintKeysDatabase for MintSqliteDatabase {
|
|||||||
.bind(":unit", keyset.unit.to_string())
|
.bind(":unit", keyset.unit.to_string())
|
||||||
.bind(":active", keyset.active)
|
.bind(":active", keyset.active)
|
||||||
.bind(":valid_from", keyset.valid_from as i64)
|
.bind(":valid_from", keyset.valid_from as i64)
|
||||||
.bind(":valid_to", keyset.valid_to.map(|v| v as i64))
|
.bind(":valid_to", keyset.final_expiry.map(|v| v as i64))
|
||||||
.bind(":derivation_path", keyset.derivation_path.to_string())
|
.bind(":derivation_path", keyset.derivation_path.to_string())
|
||||||
.bind(":max_order", keyset.max_order)
|
.bind(":max_order", keyset.max_order)
|
||||||
.bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
|
.bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
|
||||||
@@ -1134,11 +1134,11 @@ fn sqlite_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo, Error>
|
|||||||
unit: column_as_string!(unit, CurrencyUnit::from_str),
|
unit: column_as_string!(unit, CurrencyUnit::from_str),
|
||||||
active: matches!(active, Column::Integer(1)),
|
active: matches!(active, Column::Integer(1)),
|
||||||
valid_from: column_as_number!(valid_from),
|
valid_from: column_as_number!(valid_from),
|
||||||
valid_to: column_as_nullable_number!(valid_to),
|
|
||||||
derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
|
derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
|
||||||
derivation_path_index: column_as_nullable_number!(derivation_path_index),
|
derivation_path_index: column_as_nullable_number!(derivation_path_index),
|
||||||
max_order: column_as_number!(max_order),
|
max_order: column_as_number!(max_order),
|
||||||
input_fee_ppk: column_as_number!(row_keyset_ppk),
|
input_fee_ppk: column_as_number!(row_keyset_ppk),
|
||||||
|
final_expiry: column_as_nullable_number!(valid_to),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1319,11 +1319,11 @@ mod tests {
|
|||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
active: true,
|
active: true,
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
valid_to: None,
|
|
||||||
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||||
derivation_path_index: Some(0),
|
derivation_path_index: Some(0),
|
||||||
max_order: 32,
|
max_order: 32,
|
||||||
input_fee_ppk: 0,
|
input_fee_ppk: 0,
|
||||||
|
final_expiry: None,
|
||||||
};
|
};
|
||||||
db.add_keyset_info(keyset_info).await.unwrap();
|
db.add_keyset_info(keyset_info).await.unwrap();
|
||||||
|
|
||||||
@@ -1387,11 +1387,11 @@ mod tests {
|
|||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
active: true,
|
active: true,
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
valid_to: None,
|
|
||||||
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||||
derivation_path_index: Some(0),
|
derivation_path_index: Some(0),
|
||||||
max_order: 32,
|
max_order: 32,
|
||||||
input_fee_ppk: 0,
|
input_fee_ppk: 0,
|
||||||
|
final_expiry: None,
|
||||||
};
|
};
|
||||||
db.add_keyset_info(keyset_info).await.unwrap();
|
db.add_keyset_info(keyset_info).await.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
|
|||||||
("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/20250314082116_allow_pending_spent.sql"#)),
|
("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/20250314082116_allow_pending_spent.sql"#)),
|
||||||
("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
|
("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
|
||||||
("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
|
("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
|
||||||
|
("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE keyset ADD COLUMN final_expiry INTEGER DEFAULT NULL;
|
||||||
@@ -14,8 +14,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
|
|||||||
use cdk_common::secret::Secret;
|
use cdk_common::secret::Secret;
|
||||||
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
||||||
use cdk_common::{
|
use cdk_common::{
|
||||||
database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, ProofDleq, PublicKey,
|
database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
|
||||||
SecretKey, SpendingConditions, State,
|
PublicKey, SecretKey, SpendingConditions, State,
|
||||||
};
|
};
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -294,14 +294,15 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
|||||||
Statement::new(
|
Statement::new(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO keyset
|
INSERT INTO keyset
|
||||||
(mint_url, id, unit, active, input_fee_ppk)
|
(mint_url, id, unit, active, input_fee_ppk, final_expiry)
|
||||||
VALUES
|
VALUES
|
||||||
(:mint_url, :id, :unit, :active, :input_fee_ppk)
|
(:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
mint_url = excluded.mint_url,
|
mint_url = excluded.mint_url,
|
||||||
unit = excluded.unit,
|
unit = excluded.unit,
|
||||||
active = excluded.active,
|
active = excluded.active,
|
||||||
input_fee_ppk = excluded.input_fee_ppk;
|
input_fee_ppk = excluded.input_fee_ppk,
|
||||||
|
final_expiry = excluded.final_expiry;
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(":mint_url", mint_url.to_string())
|
.bind(":mint_url", mint_url.to_string())
|
||||||
@@ -309,6 +310,7 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
|||||||
.bind(":unit", keyset.unit.to_string())
|
.bind(":unit", keyset.unit.to_string())
|
||||||
.bind(":active", keyset.active)
|
.bind(":active", keyset.active)
|
||||||
.bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
|
.bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
|
||||||
|
.bind(":final_expiry", keyset.final_expiry.map(|v| v as i64))
|
||||||
.execute(&conn)
|
.execute(&conn)
|
||||||
.map_err(Error::Sqlite)?;
|
.map_err(Error::Sqlite)?;
|
||||||
}
|
}
|
||||||
@@ -327,7 +329,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
|||||||
id,
|
id,
|
||||||
unit,
|
unit,
|
||||||
active,
|
active,
|
||||||
input_fee_ppk
|
input_fee_ppk,
|
||||||
|
final_expiry
|
||||||
FROM
|
FROM
|
||||||
keyset
|
keyset
|
||||||
WHERE mint_url = :mint_url
|
WHERE mint_url = :mint_url
|
||||||
@@ -354,7 +357,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
|||||||
id,
|
id,
|
||||||
unit,
|
unit,
|
||||||
active,
|
active,
|
||||||
input_fee_ppk
|
input_fee_ppk,
|
||||||
|
final_expiry
|
||||||
FROM
|
FROM
|
||||||
keyset
|
keyset
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
@@ -528,7 +532,10 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
|
async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
|
||||||
|
// Recompute ID for verification
|
||||||
|
keyset.verify_id()?;
|
||||||
|
|
||||||
Statement::new(
|
Statement::new(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO key
|
INSERT INTO key
|
||||||
@@ -539,8 +546,11 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
keys = excluded.keys
|
keys = excluded.keys
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(":id", Id::from(&keys).to_string())
|
.bind(":id", keyset.id.to_string())
|
||||||
.bind(":keys", serde_json::to_string(&keys).map_err(Error::from)?)
|
.bind(
|
||||||
|
":keys",
|
||||||
|
serde_json::to_string(&keyset.keys).map_err(Error::from)?,
|
||||||
|
)
|
||||||
.execute(&self.pool.get().map_err(Error::Pool)?)
|
.execute(&self.pool.get().map_err(Error::Pool)?)
|
||||||
.map_err(Error::Sqlite)?;
|
.map_err(Error::Sqlite)?;
|
||||||
|
|
||||||
@@ -909,13 +919,15 @@ fn sqlite_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
|
fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
|
||||||
unpack_into!(
|
unpack_into!(
|
||||||
let (
|
let (
|
||||||
id,
|
id,
|
||||||
unit,
|
unit,
|
||||||
active,
|
active,
|
||||||
input_fee_ppk
|
input_fee_ppk,
|
||||||
|
final_expiry
|
||||||
) = row
|
) = row
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -924,6 +936,7 @@ fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
|
|||||||
unit: column_as_string!(unit, CurrencyUnit::from_str),
|
unit: column_as_string!(unit, CurrencyUnit::from_str),
|
||||||
active: matches!(active, Column::Integer(1)),
|
active: matches!(active, Column::Integer(1)),
|
||||||
input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(),
|
input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(),
|
||||||
|
final_expiry: column_as_nullable_number!(final_expiry),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ impl Mint {
|
|||||||
unit: key.unit.clone(),
|
unit: key.unit.clone(),
|
||||||
active: key.active,
|
active: key.active,
|
||||||
input_fee_ppk: key.input_fee_ppk,
|
input_fee_ppk: key.input_fee_ppk,
|
||||||
|
final_expiry: key.final_expiry,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ impl Mint {
|
|||||||
unit: k.unit.clone(),
|
unit: k.unit.clone(),
|
||||||
active: k.active,
|
active: k.active,
|
||||||
input_fee_ppk: k.input_fee_ppk,
|
input_fee_ppk: k.input_fee_ppk,
|
||||||
|
final_expiry: k.final_expiry,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ impl AuthWallet {
|
|||||||
|
|
||||||
keys.verify_id()?;
|
keys.verify_id()?;
|
||||||
|
|
||||||
self.localstore.add_keys(keys.keys.clone()).await?;
|
self.localstore.add_keys(keys.clone()).await?;
|
||||||
|
|
||||||
keys.keys
|
keys.keys
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ impl Wallet {
|
|||||||
|
|
||||||
keys.verify_id()?;
|
keys.verify_id()?;
|
||||||
|
|
||||||
self.localstore.add_keys(keys.keys.clone()).await?;
|
self.localstore.add_keys(keys.clone()).await?;
|
||||||
|
|
||||||
keys.keys
|
keys.keys
|
||||||
};
|
};
|
||||||
@@ -27,7 +27,23 @@ impl Wallet {
|
|||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get keysets for mint
|
/// Get keysets from DB or fetch them
|
||||||
|
///
|
||||||
|
/// Checks the database for keysets and queries the Mint if
|
||||||
|
/// it can't find any.
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
|
||||||
|
match self
|
||||||
|
.localstore
|
||||||
|
.get_mint_keysets(self.mint_url.clone())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(keysets_info) => Ok(keysets_info),
|
||||||
|
None => self.get_mint_keysets().await, // Hit the keysets endpoint if we don't have the keysets for this Mint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get keysets for wallet's mint
|
||||||
///
|
///
|
||||||
/// Queries mint for all keysets
|
/// Queries mint for all keysets
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
|
|||||||
@@ -474,7 +474,7 @@ impl Wallet {
|
|||||||
/// Can be used to allow a wallet to accept payments offline while reducing
|
/// 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
|
/// the risk of claiming back to the limits let by the spending_conditions
|
||||||
#[instrument(skip(self, token))]
|
#[instrument(skip(self, token))]
|
||||||
pub fn verify_token_p2pk(
|
pub async fn verify_token_p2pk(
|
||||||
&self,
|
&self,
|
||||||
token: &Token,
|
token: &Token,
|
||||||
spending_conditions: SpendingConditions,
|
spending_conditions: SpendingConditions,
|
||||||
@@ -526,8 +526,10 @@ impl Wallet {
|
|||||||
token.mint_url()?
|
token.mint_url()?
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
// We need the keysets information to properly convert from token proof to proof
|
||||||
|
let keysets_info = self.load_mint_keysets().await?;
|
||||||
|
let proofs = token.proofs(&keysets_info)?;
|
||||||
|
|
||||||
let proofs = token.proofs();
|
|
||||||
for proof in proofs {
|
for proof in proofs {
|
||||||
let secret: nut10::Secret = (&proof.secret).try_into()?;
|
let secret: nut10::Secret = (&proof.secret).try_into()?;
|
||||||
|
|
||||||
@@ -620,7 +622,9 @@ impl Wallet {
|
|||||||
// )));
|
// )));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
let proofs = token.proofs();
|
// We need the keysets information to properly convert from token proof to proof
|
||||||
|
let keysets_info = self.load_mint_keysets().await?;
|
||||||
|
let proofs = token.proofs(&keysets_info)?;
|
||||||
for proof in proofs {
|
for proof in proofs {
|
||||||
let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
|
let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
|
||||||
Some(keys) => keys.amount_key(proof.amount),
|
Some(keys) => keys.amount_key(proof.amount),
|
||||||
|
|||||||
@@ -271,12 +271,6 @@ impl MultiMintWallet {
|
|||||||
let token_data = Token::from_str(encoded_token)?;
|
let token_data = Token::from_str(encoded_token)?;
|
||||||
let unit = token_data.unit().unwrap_or_default();
|
let unit = token_data.unit().unwrap_or_default();
|
||||||
|
|
||||||
let proofs = token_data.proofs();
|
|
||||||
|
|
||||||
let mut amount_received = Amount::ZERO;
|
|
||||||
|
|
||||||
let mut mint_errors = None;
|
|
||||||
|
|
||||||
let mint_url = token_data.mint_url()?;
|
let mint_url = token_data.mint_url()?;
|
||||||
|
|
||||||
// Check that all mints in tokes have wallets
|
// Check that all mints in tokes have wallets
|
||||||
@@ -291,6 +285,22 @@ impl MultiMintWallet {
|
|||||||
.get(&wallet_key)
|
.get(&wallet_key)
|
||||||
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
||||||
|
|
||||||
|
// We need the keysets information to properly convert from token proof to proof
|
||||||
|
let keysets_info = match self
|
||||||
|
.localstore
|
||||||
|
.get_mint_keysets(token_data.mint_url()?)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(keysets_info) => keysets_info,
|
||||||
|
// Hit the keysets endpoint if we don't have the keysets for this Mint
|
||||||
|
None => wallet.get_mint_keysets().await?,
|
||||||
|
};
|
||||||
|
let proofs = token_data.proofs(&keysets_info)?;
|
||||||
|
|
||||||
|
let mut amount_received = Amount::ZERO;
|
||||||
|
|
||||||
|
let mut mint_errors = None;
|
||||||
|
|
||||||
match wallet
|
match wallet
|
||||||
.receive_proofs(proofs, opts, token_data.memo().clone())
|
.receive_proofs(proofs, opts, token_data.memo().clone())
|
||||||
.await
|
.await
|
||||||
@@ -356,7 +366,7 @@ impl MultiMintWallet {
|
|||||||
.get(wallet_key)
|
.get(wallet_key)
|
||||||
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
||||||
|
|
||||||
wallet.verify_token_p2pk(token, conditions)
|
wallet.verify_token_p2pk(token, conditions).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifys all proofs in token have valid dleq proof
|
/// Verifys all proofs in token have valid dleq proof
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ impl Wallet {
|
|||||||
|
|
||||||
ensure_cdk!(unit == self.unit, Error::UnsupportedUnit);
|
ensure_cdk!(unit == self.unit, Error::UnsupportedUnit);
|
||||||
|
|
||||||
let proofs = token.proofs();
|
let keysets_info = self.load_mint_keysets().await?;
|
||||||
|
let proofs = token.proofs(&keysets_info)?;
|
||||||
|
|
||||||
if let Token::TokenV3(token) = &token {
|
if let Token::TokenV3(token) = &token {
|
||||||
ensure_cdk!(!token.is_multi_mint(), Error::MultiMintTokenNotSupported);
|
ensure_cdk!(!token.is_multi_mint(), Error::MultiMintTokenNotSupported);
|
||||||
|
|||||||
Reference in New Issue
Block a user