diff --git a/src/bitcoin/fee.rs b/src/bitcoin/fee.rs index 749fa18a..ce9861dc 100644 --- a/src/bitcoin/fee.rs +++ b/src/bitcoin/fee.rs @@ -22,13 +22,15 @@ use bitcoin::util::amount::Denomination; use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::Amount; +use crate::bitcoin::{transaction, Bitcoin, Strategy}; use crate::blockchain::{Fee, FeePriority, FeeStrategy, FeeStrategyError}; use crate::consensus::{self, CanonicalBytes}; -use crate::bitcoin::{transaction, Bitcoin, Strategy}; - use std::str::FromStr; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde::{de, Deserialize, Deserializer}; + /// An amount of Bitcoin (internally in satoshis) representing the number of satoshis per virtual /// byte a transaction must use for its fee. A [`FeeStrategy`] can use one of more of this type /// depending of its complexity (fixed, range, etc). @@ -67,6 +69,27 @@ impl SatPerVByte { } } +impl Serialize for SatPerVByte { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(format!("{}", self).as_ref()) + } +} + +impl<'de> Deserialize<'de> for SatPerVByte { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok( + SatPerVByte::from_str(&String::deserialize(deserializer)?) + .map_err(de::Error::custom)?, + ) + } +} + impl CanonicalBytes for SatPerVByte { fn as_canonical_bytes(&self) -> Vec { bitcoin::consensus::encode::serialize(&self.0.as_sat()) @@ -195,6 +218,11 @@ impl Fee for PartiallySignedTransaction { mod tests { use super::*; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + struct SerdeTest { + fee: SatPerVByte, + } + #[test] fn parse_sats_per_vbyte() { for s in [ @@ -221,4 +249,25 @@ mod tests { let fee_rate = SatPerVByte::from_sat(100); assert_eq!(format!("{}", fee_rate), "100 satoshi/vByte".to_string()); } + + #[test] + fn serialize_fee_rate_in_yaml() { + let fee_rate = SerdeTest { + fee: SatPerVByte::from_sat(10), + }; + let s = serde_yaml::to_string(&fee_rate).expect("Encode fee rate in yaml"); + assert_eq!("---\nfee: 10 satoshi/vByte\n", s); + } + + #[test] + fn deserialize_fee_rate_in_yaml() { + let s = "---\nfee: 10 satoshi/vByte\n"; + let fee_rate = serde_yaml::from_str(&s).expect("Decode fee rate from yaml"); + assert_eq!( + SerdeTest { + fee: SatPerVByte::from_sat(10) + }, + fee_rate + ); + } } diff --git a/src/bitcoin/timelock.rs b/src/bitcoin/timelock.rs index 7fed9494..741053fb 100644 --- a/src/bitcoin/timelock.rs +++ b/src/bitcoin/timelock.rs @@ -17,7 +17,7 @@ impl FromStr for CSVTimelock { } /// An `OP_CSV` value (32-bits integer) to use in transactions and scripts. -#[derive(PartialEq, Eq, PartialOrd, Clone, Debug, Copy, Display)] +#[derive(PartialEq, Eq, PartialOrd, Clone, Debug, Copy, Display, Serialize, Deserialize)] #[display("{0} blocks")] pub struct CSVTimelock(u32); diff --git a/src/blockchain.rs b/src/blockchain.rs index 1b432251..d0cbde48 100644 --- a/src/blockchain.rs +++ b/src/blockchain.rs @@ -16,7 +16,7 @@ use crate::consensus::{self, deserialize, serialize, CanonicalBytes, Decodable, use crate::crypto::Signatures; use crate::transaction::{Buyable, Cancelable, Fundable, Lockable, Punishable, Refundable}; -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Display)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Display, Serialize, Deserialize)] #[display(Debug)] pub enum Blockchain { Bitcoin, diff --git a/src/negotiation.rs b/src/negotiation.rs index d3c0b67d..4033082c 100644 --- a/src/negotiation.rs +++ b/src/negotiation.rs @@ -21,7 +21,9 @@ use bitcoin::secp256k1::PublicKey; use inet2_addr::InetSocketAddr; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde::{de, Deserialize, Deserializer}; +use std::fmt::Display; use std::str::FromStr; use thiserror::Error; use tiny_keccak::{Hasher, Keccak}; @@ -118,7 +120,12 @@ impl<'de> Deserialize<'de> for OfferId { /// perspective. The daemon start when the maker is ready to finalize his offer, transforming the /// offer into a [`PublicOffer`] which contains the data needed to a taker to connect to the /// maker's daemon. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +/// +/// ## Serde implementation +/// Amount types may have multiple serialization representation, e.g. btc and sat for bitcoin or +/// xmr and pico for monero. Using [`Display`] and [`FromStr`] unifies the interface to +/// de/serialize generic amounts. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct Offer { /// Type of offer and network to use. pub network: Network, @@ -127,8 +134,14 @@ pub struct Offer { /// The chosen accordant blockchain. pub accordant_blockchain: Blockchain, /// Amount of arbitrating assets to exchanged. + #[serde(with = "string")] + #[serde(bound(serialize = "Amt: Display"))] + #[serde(bound(deserialize = "Amt: FromStr, Amt::Err: Display"))] pub arbitrating_amount: Amt, /// Amount of accordant assets to exchanged. + #[serde(with = "string")] + #[serde(bound(serialize = "Bmt: Display"))] + #[serde(bound(deserialize = "Bmt: FromStr, Bmt::Err: Display"))] pub accordant_amount: Bmt, /// The cancel timelock parameter of the arbitrating blockchain. pub cancel_timelock: Ti, @@ -140,12 +153,38 @@ pub struct Offer { pub maker_role: SwapRole, } -impl fmt::Display for Offer +mod string { + use std::fmt::Display; + use std::str::FromStr; + + use serde::{de, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &T, serializer: S) -> Result + where + T: Display, + S: Serializer, + { + serializer.collect_str(value) + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: FromStr, + T::Err: Display, + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(de::Error::custom) + } +} + +impl Display for Offer where - Amt: fmt::Display, - Bmt: fmt::Display, - Ti: fmt::Display, - F: fmt::Display, + Amt: Display, + Bmt: Display, + Ti: Display, + F: Display, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Network: {}", self.network)?; @@ -486,11 +525,15 @@ impl_strict_encoding!(PublicOfferId); /// A public offer is shared across [`TradeRole::Maker`]'s prefered network to signal is willing of /// trading some assets at some conditions. The assets and condition are defined in the [`Offer`], /// maker peer connection information are contained in the public offer. -#[derive(Debug, Clone, Eq, Hash, PartialEq)] +#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct PublicOffer { /// The public offer version. pub version: Version, /// The content of the offer. + #[serde(bound(serialize = "Amt: Display, Bmt: Display, Ti: Serialize, F: Serialize"))] + #[serde(bound( + deserialize = "Amt: FromStr, Amt::Err: Display, Bmt: FromStr, Bmt::Err: Display, Ti: Deserialize<'de>, F: Deserialize<'de>" + ))] pub offer: Offer, /// Node public key, used both as an ID and encryption key for per-session ECDH. pub node_id: PublicKey, @@ -542,7 +585,7 @@ impl PublicOffer { } } -impl std::fmt::Display for PublicOffer +impl Display for PublicOffer where Self: Encodable, { @@ -553,7 +596,7 @@ where } } -impl std::str::FromStr for PublicOffer +impl FromStr for PublicOffer where Amt: CanonicalBytes, Bmt: CanonicalBytes, @@ -572,41 +615,6 @@ where } } -// TODO: implement properly without encoding in base58 first -impl Serialize for PublicOffer -where - Amt: CanonicalBytes, - Bmt: CanonicalBytes, - Ti: CanonicalBytes, - F: CanonicalBytes, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} - -// TODO: implement properly without decoding from base58 -impl<'de, Amt, Bmt, Ti, F> Deserialize<'de> for PublicOffer -where - Amt: CanonicalBytes, - Bmt: CanonicalBytes, - Ti: CanonicalBytes, - F: CanonicalBytes, -{ - fn deserialize(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - Ok( - PublicOffer::from_str(&deserializer.deserialize_string(OfferString)?) - .map_err(de::Error::custom)?, - ) - } -} - impl Encodable for PublicOffer where Amt: CanonicalBytes, @@ -738,6 +746,46 @@ mod tests { assert_eq!(&format!("{}", pub_offer), S); } + #[test] + fn serialize_offer_in_yaml() { + let offer: Offer = Offer { + network: Network::Testnet, + arbitrating_blockchain: Blockchain::Bitcoin, + accordant_blockchain: Blockchain::Monero, + arbitrating_amount: bitcoin::Amount::from_sat(5), + accordant_amount: monero::Amount::from_pico(6), + cancel_timelock: CSVTimelock::new(7), + punish_timelock: CSVTimelock::new(8), + fee_strategy: FeeStrategy::Fixed(SatPerVByte::from_sat(9)), + maker_role: SwapRole::Bob, + }; + let s = serde_yaml::to_string(&offer).expect("Encode public offer in yaml"); + assert_eq!( + "---\nnetwork: Testnet\narbitrating_blockchain: Bitcoin\naccordant_blockchain: Monero\narbitrating_amount: 0.00000005 BTC\naccordant_amount: 0.000000000006 XMR\ncancel_timelock: 7\npunish_timelock: 8\nfee_strategy:\n Fixed: 9 satoshi/vByte\nmaker_role: Bob\n", + s + ); + } + + #[test] + fn deserialize_offer_from_yaml() { + let s = "---\nnetwork: Testnet\narbitrating_blockchain: Bitcoin\naccordant_blockchain: Monero\narbitrating_amount: 0.00000005 BTC\naccordant_amount: 0.000000000006 XMR\ncancel_timelock: 7\npunish_timelock: 8\nfee_strategy:\n Fixed: 9 satoshi/vByte\nmaker_role: Bob\n"; + let offer = serde_yaml::from_str(&s).expect("Decode offer from yaml"); + assert_eq!( + Offer { + network: Network::Testnet, + arbitrating_blockchain: Blockchain::Bitcoin, + accordant_blockchain: Blockchain::Monero, + arbitrating_amount: bitcoin::Amount::from_sat(5), + accordant_amount: monero::Amount::from_pico(6), + cancel_timelock: CSVTimelock::new(7), + punish_timelock: CSVTimelock::new(8), + fee_strategy: FeeStrategy::Fixed(SatPerVByte::from_sat(9)), + maker_role: SwapRole::Bob, + }, + offer + ); + } + #[test] fn serialize_public_offer_in_yaml() { let public_offer = @@ -745,14 +793,14 @@ mod tests { .expect("Valid public offer"); let s = serde_yaml::to_string(&public_offer).expect("Encode public offer in yaml"); assert_eq!( - "---\n\"Offer:Cke4ftrP5A71W723UjzEWsNR4gmBqNCsR11111uMFubBevJ2E5fp6ZR11111TBALTh113GTvtvqfD1111114A4TTfifktDH7QZD71vpdfo6EVo2ds7KviHz7vYbLZDkgsMNb11111111111111111111111111111111111111111AfZ113XRBum3er3R\"\n", + "---\nversion: 1\noffer:\n network: Local\n arbitrating_blockchain: Bitcoin\n accordant_blockchain: Monero\n arbitrating_amount: 0.00001350 BTC\n accordant_amount: 1000000.001000000000 XMR\n cancel_timelock: 4\n punish_timelock: 6\n fee_strategy:\n Fixed: 1 satoshi/vByte\n maker_role: Bob\nnode_id: 02e77b779cdc2c713823f7a19147a67e4209c74d77e2cb5045bce0584a6be064d4\npeer_address:\n address:\n IPv4: 127.0.0.1\n port: 9735\n", s ); } #[test] fn deserialize_public_offer_from_yaml() { - let s = "---\nOffer:Cke4ftrP5A71W723UjzEWsNR4gmBqNCsR11111uMFubBevJ2E5fp6ZR11111TBALTh113GTvtvqfD1111114A4TTfifktDH7QZD71vpdfo6EVo2ds7KviHz7vYbLZDkgsMNb11111111111111111111111111111111111111111AfZ113XRBum3er3R\n"; + let s = "---\nversion: 1\noffer:\n network: Local\n arbitrating_blockchain: Bitcoin\n accordant_blockchain: Monero\n arbitrating_amount: 0.00001350 BTC\n accordant_amount: 1000000.001000000000 XMR\n cancel_timelock: 4\n punish_timelock: 6\n fee_strategy:\n Fixed: 1 satoshi/vByte\n maker_role: Bob\nnode_id: 02e77b779cdc2c713823f7a19147a67e4209c74d77e2cb5045bce0584a6be064d4\npeer_address:\n address:\n IPv4: 127.0.0.1\n port: 9735\n"; let public_offer = serde_yaml::from_str(&s).expect("Decode public offer from yaml"); assert_eq!( PublicOffer::::from_str("Offer:Cke4ftrP5A71W723UjzEWsNR4gmBqNCsR11111uMFubBevJ2E5fp6ZR11111TBALTh113GTvtvqfD1111114A4TTfifktDH7QZD71vpdfo6EVo2ds7KviHz7vYbLZDkgsMNb11111111111111111111111111111111111111111AfZ113XRBum3er3R") diff --git a/src/protocol.rs b/src/protocol.rs index be3824d7..cdb6f2d1 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -31,7 +31,7 @@ struct ValidatedCoreTransactions { punish_lock: DataPunishableLock, } -#[derive(Debug, Clone, Copy, Hash)] +#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)] pub struct CoreArbitratingTransactions { /// Partial transaction raw type representing the lock. pub lock: Px, @@ -57,7 +57,7 @@ impl CoreArbitratingTransactions { } } -#[derive(Debug, Clone, Copy, Hash)] +#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)] pub struct ArbitratingParameters { pub arbitrating_amount: Amt, pub cancel_timelock: Ti, @@ -65,19 +65,19 @@ pub struct ArbitratingParameters { pub fee_strategy: FeeStrategy, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)] pub struct TxSignatures { pub sig: Sig, pub adapted_sig: Sig, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)] pub struct FullySignedPunish { pub punish: Px, pub punish_sig: Sig, } -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, Hash, Serialize, Deserialize)] pub struct Parameters { pub buy: Pk, pub cancel: Pk, diff --git a/src/protocol/message.rs b/src/protocol/message.rs index 0bfcead3..0efe4f84 100644 --- a/src/protocol/message.rs +++ b/src/protocol/message.rs @@ -778,7 +778,7 @@ impl Strategy for BuyProcedureSignature { /// that they have aborted the swap with an `OPTIONAL` message body to provide the reason. /// /// [`SwapRole`]: crate::role::SwapRole -#[derive(Clone, Debug, Hash, Display)] +#[derive(Clone, Debug, Hash, Display, Serialize, Deserialize)] #[display(Debug)] pub struct Abort { /// The swap identifier related to this message. diff --git a/src/transaction.rs b/src/transaction.rs index 637143ad..881693a8 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -133,7 +133,7 @@ pub trait Transaction { } /// Defines the transaction Farcaster IDs for serialization and network communication. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, Serialize, Deserialize)] pub enum TxLabel { /// Represents the first transaction created outside of the system by an external wallet to /// fund the swap on the arbitrating blockchain.