|
@@ -1,262 +1,8 @@
|
|
|
//! Types for `cashu-crab`
|
|
|
|
|
|
-use std::str::FromStr;
|
|
|
+use serde::{Deserialize, Serialize};
|
|
|
|
|
|
-use base64::{engine::general_purpose, Engine as _};
|
|
|
-use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
|
-use url::Url;
|
|
|
-
|
|
|
-use crate::keyset::{self, PublicKey};
|
|
|
-use crate::utils::generate_secret;
|
|
|
-use crate::Amount;
|
|
|
-pub use crate::Invoice;
|
|
|
-use crate::{dhke::blind_message, error::Error, mint, serde_utils::serde_url, utils::split_amount};
|
|
|
-
|
|
|
-/// Blinded Message [NUT-00]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct BlindedMessage {
|
|
|
- /// Amount in satoshi
|
|
|
- pub amount: Amount,
|
|
|
- /// encrypted secret message (B_)
|
|
|
- #[serde(rename = "B_")]
|
|
|
- pub b: PublicKey,
|
|
|
-}
|
|
|
-
|
|
|
-/// Blinded Messages [NUT-00]
|
|
|
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct BlindedMessages {
|
|
|
- /// Blinded messages
|
|
|
- pub blinded_messages: Vec<BlindedMessage>,
|
|
|
- /// Secrets
|
|
|
- pub secrets: Vec<String>,
|
|
|
- /// Rs
|
|
|
- pub rs: Vec<keyset::SecretKey>,
|
|
|
- /// Amounts
|
|
|
- pub amounts: Vec<Amount>,
|
|
|
-}
|
|
|
-
|
|
|
-impl BlindedMessages {
|
|
|
- /// Outputs for speceifed amount with random secret
|
|
|
- pub fn random(amount: Amount) -> Result<Self, Error> {
|
|
|
- let mut blinded_messages = BlindedMessages::default();
|
|
|
-
|
|
|
- for amount in split_amount(amount) {
|
|
|
- let secret = generate_secret();
|
|
|
- let (blinded, r) = blind_message(secret.as_bytes(), None)?;
|
|
|
-
|
|
|
- let blinded_message = BlindedMessage { amount, b: blinded };
|
|
|
-
|
|
|
- blinded_messages.secrets.push(secret);
|
|
|
- blinded_messages.blinded_messages.push(blinded_message);
|
|
|
- blinded_messages.rs.push(r.into());
|
|
|
- blinded_messages.amounts.push(amount);
|
|
|
- }
|
|
|
-
|
|
|
- Ok(blinded_messages)
|
|
|
- }
|
|
|
-
|
|
|
- /// Blank Outputs used for NUT-08 change
|
|
|
- pub fn blank(fee_reserve: Amount) -> Result<Self, Error> {
|
|
|
- let mut blinded_messages = BlindedMessages::default();
|
|
|
-
|
|
|
- let fee_reserve = bitcoin::Amount::from_sat(fee_reserve.to_sat());
|
|
|
-
|
|
|
- let count = (fee_reserve
|
|
|
- .to_float_in(bitcoin::Denomination::Satoshi)
|
|
|
- .log2()
|
|
|
- .ceil() as u64)
|
|
|
- .max(1);
|
|
|
-
|
|
|
- for _i in 0..count {
|
|
|
- let secret = generate_secret();
|
|
|
- let (blinded, r) = blind_message(secret.as_bytes(), None)?;
|
|
|
-
|
|
|
- let blinded_message = BlindedMessage {
|
|
|
- amount: Amount::ZERO,
|
|
|
- b: blinded,
|
|
|
- };
|
|
|
-
|
|
|
- blinded_messages.secrets.push(secret);
|
|
|
- blinded_messages.blinded_messages.push(blinded_message);
|
|
|
- blinded_messages.rs.push(r.into());
|
|
|
- blinded_messages.amounts.push(Amount::ZERO);
|
|
|
- }
|
|
|
-
|
|
|
- Ok(blinded_messages)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct SplitPayload {
|
|
|
- pub keep_blinded_messages: BlindedMessages,
|
|
|
- pub send_blinded_messages: BlindedMessages,
|
|
|
- pub split_payload: SplitRequest,
|
|
|
-}
|
|
|
-
|
|
|
-/// Promise (BlindedSignature) [NUT-00]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct Promise {
|
|
|
- pub id: String,
|
|
|
- pub amount: Amount,
|
|
|
- /// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
|
|
|
- #[serde(rename = "C_")]
|
|
|
- pub c: PublicKey,
|
|
|
-}
|
|
|
-
|
|
|
-/// Proofs [NUT-00]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct Proof {
|
|
|
- /// Amount in satoshi
|
|
|
- pub amount: Amount,
|
|
|
- /// Secret message
|
|
|
- // #[serde(with = "crate::serde_utils::bytes_base64")]
|
|
|
- pub secret: String,
|
|
|
- /// Unblinded signature
|
|
|
- #[serde(rename = "C")]
|
|
|
- pub c: PublicKey,
|
|
|
- /// `Keyset id`
|
|
|
- pub id: Option<String>,
|
|
|
- #[serde(skip_serializing_if = "Option::is_none")]
|
|
|
- /// P2SHScript that specifies the spending condition for this Proof
|
|
|
- pub script: Option<String>,
|
|
|
-}
|
|
|
-
|
|
|
-/// List of proofs
|
|
|
-pub type Proofs = Vec<Proof>;
|
|
|
-
|
|
|
-/// Mint request response [NUT-03]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct RequestMintResponse {
|
|
|
- /// Bolt11 payment request
|
|
|
- pub pr: Invoice,
|
|
|
- /// Random hash MUST not be the hash of invoice
|
|
|
- pub hash: String,
|
|
|
-}
|
|
|
-
|
|
|
-/// Post Mint Request [NUT-04]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct MintRequest {
|
|
|
- pub outputs: Vec<BlindedMessage>,
|
|
|
-}
|
|
|
-
|
|
|
-impl MintRequest {
|
|
|
- pub fn total_amount(&self) -> Amount {
|
|
|
- self.outputs
|
|
|
- .iter()
|
|
|
- .map(|BlindedMessage { amount, .. }| *amount)
|
|
|
- .sum()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Post Mint Response [NUT-05]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct PostMintResponse {
|
|
|
- pub promises: Vec<Promise>,
|
|
|
-}
|
|
|
-
|
|
|
-/// Check Fees Response [NUT-05]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct CheckFeesResponse {
|
|
|
- /// Expected Mac Fee in satoshis
|
|
|
- pub fee: Amount,
|
|
|
-}
|
|
|
-
|
|
|
-/// Check Fees request [NUT-05]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct CheckFeesRequest {
|
|
|
- /// Lighting Invoice
|
|
|
- pub pr: Invoice,
|
|
|
-}
|
|
|
-
|
|
|
-/// Melt Request [NUT-05]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct MeltRequest {
|
|
|
- pub proofs: Proofs,
|
|
|
- /// bollt11
|
|
|
- pub pr: Invoice,
|
|
|
- /// Blinded Message that can be used to return change [NUT-08]
|
|
|
- /// Amount field of blindedMessages `SHOULD` be set to zero
|
|
|
- pub outputs: Option<Vec<BlindedMessage>>,
|
|
|
-}
|
|
|
-
|
|
|
-impl MeltRequest {
|
|
|
- pub fn proofs_amount(&self) -> Amount {
|
|
|
- self.proofs.iter().map(|proof| proof.amount).sum()
|
|
|
- }
|
|
|
-
|
|
|
- pub fn invoice_amount(&self) -> Result<Amount, Error> {
|
|
|
- match self.pr.amount_milli_satoshis() {
|
|
|
- Some(value) => Ok(Amount::from_sat(value)),
|
|
|
- None => Err(Error::InvoiceAmountUndefined),
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Melt Response [NUT-05]
|
|
|
-/// Lightning fee return [NUT-08] if change is defined
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct MeltResponse {
|
|
|
- pub paid: bool,
|
|
|
- pub preimage: Option<String>,
|
|
|
- pub change: Option<Vec<Promise>>,
|
|
|
-}
|
|
|
-
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct Melted {
|
|
|
- pub paid: bool,
|
|
|
- pub preimage: Option<String>,
|
|
|
- pub change: Option<Proofs>,
|
|
|
-}
|
|
|
-
|
|
|
-/// Split Request [NUT-06]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct SplitRequest {
|
|
|
- pub amount: Amount,
|
|
|
- pub proofs: Proofs,
|
|
|
- pub outputs: Vec<BlindedMessage>,
|
|
|
-}
|
|
|
-
|
|
|
-impl SplitRequest {
|
|
|
- pub fn proofs_amount(&self) -> Amount {
|
|
|
- self.proofs.iter().map(|proof| proof.amount).sum()
|
|
|
- }
|
|
|
- pub fn output_amount(&self) -> Amount {
|
|
|
- self.outputs.iter().map(|proof| proof.amount).sum()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Split Response [NUT-06]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct SplitResponse {
|
|
|
- /// Promises to keep
|
|
|
- pub fst: Vec<Promise>,
|
|
|
- /// Promises to send
|
|
|
- pub snd: Vec<Promise>,
|
|
|
-}
|
|
|
-
|
|
|
-impl SplitResponse {
|
|
|
- pub fn change_amount(&self) -> Amount {
|
|
|
- self.fst.iter().map(|Promise { amount, .. }| *amount).sum()
|
|
|
- }
|
|
|
-
|
|
|
- pub fn target_amount(&self) -> Amount {
|
|
|
- self.snd.iter().map(|Promise { amount, .. }| *amount).sum()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Check spendabale request [NUT-07]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct CheckSpendableRequest {
|
|
|
- pub proofs: mint::Proofs,
|
|
|
-}
|
|
|
-
|
|
|
-/// Check Spendable Response [NUT-07]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct CheckSpendableResponse {
|
|
|
- /// booleans indicating whether the provided Proof is still spendable.
|
|
|
- /// In same order as provided proofs
|
|
|
- pub spendable: Vec<bool>,
|
|
|
-}
|
|
|
+use crate::nuts::nut00::{mint, Proofs};
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
pub struct ProofsStatus {
|
|
@@ -269,166 +15,3 @@ pub struct SendProofs {
|
|
|
pub change_proofs: Proofs,
|
|
|
pub send_proofs: Proofs,
|
|
|
}
|
|
|
-
|
|
|
-/// Mint Version
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
-pub struct MintVersion {
|
|
|
- pub name: String,
|
|
|
- pub version: String,
|
|
|
-}
|
|
|
-
|
|
|
-impl Serialize for MintVersion {
|
|
|
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
- where
|
|
|
- S: Serializer,
|
|
|
- {
|
|
|
- let combined = format!("{}/{}", self.name, self.version);
|
|
|
- serializer.serialize_str(&combined)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl<'de> Deserialize<'de> for MintVersion {
|
|
|
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
- where
|
|
|
- D: Deserializer<'de>,
|
|
|
- {
|
|
|
- let combined = String::deserialize(deserializer)?;
|
|
|
- let parts: Vec<&str> = combined.split('/').collect();
|
|
|
- if parts.len() != 2 {
|
|
|
- return Err(serde::de::Error::custom("Invalid input string"));
|
|
|
- }
|
|
|
- Ok(MintVersion {
|
|
|
- name: parts[0].to_string(),
|
|
|
- version: parts[1].to_string(),
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/// Mint Info [NIP-09]
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct MintInfo {
|
|
|
- /// name of the mint and should be recognizable
|
|
|
- pub name: Option<String>,
|
|
|
- /// hex pubkey of the mint
|
|
|
- pub pubkey: Option<PublicKey>,
|
|
|
- /// implementation name and the version running
|
|
|
- pub version: Option<MintVersion>,
|
|
|
- /// short description of the mint
|
|
|
- pub description: Option<String>,
|
|
|
- /// long description
|
|
|
- pub description_long: Option<String>,
|
|
|
- /// contact methods to reach the mint operator
|
|
|
- pub contact: Vec<Vec<String>>,
|
|
|
- /// shows which NUTs the mint supports
|
|
|
- pub nuts: Vec<String>,
|
|
|
- /// message of the day that the wallet must display to the user
|
|
|
- pub motd: Option<String>,
|
|
|
-}
|
|
|
-
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct MintProofs {
|
|
|
- #[serde(with = "serde_url")]
|
|
|
- pub mint: Url,
|
|
|
- pub proofs: Proofs,
|
|
|
-}
|
|
|
-
|
|
|
-impl MintProofs {
|
|
|
- fn new(mint_url: Url, proofs: Proofs) -> Self {
|
|
|
- Self {
|
|
|
- mint: mint_url,
|
|
|
- proofs,
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
-pub struct Token {
|
|
|
- pub token: Vec<MintProofs>,
|
|
|
- pub memo: Option<String>,
|
|
|
-}
|
|
|
-
|
|
|
-impl Token {
|
|
|
- pub fn new(mint_url: Url, proofs: Proofs, memo: Option<String>) -> Self {
|
|
|
- Self {
|
|
|
- token: vec![MintProofs::new(mint_url, proofs)],
|
|
|
- memo,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- pub fn token_info(&self) -> (u64, String) {
|
|
|
- let mut amount = Amount::ZERO;
|
|
|
-
|
|
|
- for proofs in &self.token {
|
|
|
- for proof in &proofs.proofs {
|
|
|
- amount += proof.amount;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- (amount.to_sat(), self.token[0].mint.to_string())
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl FromStr for Token {
|
|
|
- type Err = Error;
|
|
|
-
|
|
|
- fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
- if !s.starts_with("cashuA") {
|
|
|
- return Err(Error::UnsupportedToken);
|
|
|
- }
|
|
|
-
|
|
|
- let s = s.replace("cashuA", "");
|
|
|
- let decoded = general_purpose::STANDARD.decode(s)?;
|
|
|
- let decoded_str = String::from_utf8(decoded)?;
|
|
|
- println!("decode: {:?}", decoded_str);
|
|
|
- let token: Token = serde_json::from_str(&decoded_str)?;
|
|
|
- Ok(token)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl Token {
|
|
|
- pub fn convert_to_string(&self) -> Result<String, Error> {
|
|
|
- let json_string = serde_json::to_string(self)?;
|
|
|
- let encoded = general_purpose::STANDARD.encode(json_string);
|
|
|
- Ok(format!("cashuA{}", encoded))
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#[cfg(test)]
|
|
|
-mod tests {
|
|
|
- use super::*;
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn test_proof_seralize() {
|
|
|
- let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]";
|
|
|
- let proof: Proofs = serde_json::from_str(proof).unwrap();
|
|
|
-
|
|
|
- assert_eq!(proof[0].clone().id.unwrap(), "DSAl9nvvyfva");
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn test_token_str_round_trip() {
|
|
|
- let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmt5b3UuIn0=";
|
|
|
- let token = Token::from_str(token_str).unwrap();
|
|
|
-
|
|
|
- assert_eq!(
|
|
|
- token.token[0].mint,
|
|
|
- Url::from_str("https://8333.space:3338").unwrap()
|
|
|
- );
|
|
|
- assert_eq!(token.token[0].proofs[0].clone().id.unwrap(), "DSAl9nvvyfva");
|
|
|
-
|
|
|
- let encoded = &token.convert_to_string().unwrap();
|
|
|
-
|
|
|
- let token_data = Token::from_str(encoded).unwrap();
|
|
|
-
|
|
|
- assert_eq!(token_data, token);
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn test_blank_blinded_messages() {
|
|
|
- let b = BlindedMessages::blank(Amount::from_sat(1000)).unwrap();
|
|
|
- assert_eq!(b.blinded_messages.len(), 10);
|
|
|
-
|
|
|
- let b = BlindedMessages::blank(Amount::from_sat(1)).unwrap();
|
|
|
- assert_eq!(b.blinded_messages.len(), 1);
|
|
|
- }
|
|
|
-}
|