use bitcoin::secp256k1::PublicKey;
use inet2_addr::InetSocketAddr;
use serde::ser::{Serialize, Serializer};
use serde::{de, Deserialize, Deserializer};
use std::fmt::Display;
use std::str::FromStr;
use strict_encoding::{StrictDecode, StrictEncode};
use thiserror::Error;
use tiny_keccak::{Hasher, Keccak};
use std::fmt;
use std::io;
use crate::blockchain::{Blockchain, FeeStrategy, Network};
use crate::consensus::{self, serialize, serialize_hex, CanonicalBytes, Decodable, Encodable};
use crate::hash::HashString;
use crate::protocol::ArbitratingParameters;
use crate::role::{SwapRole, TradeRole};
use crate::Uuid;
pub const DEAL_MAGIC_BYTES: &[u8; 6] = b"FCSWAP";
pub const DEAL_PREFIX: &str = "Deal:";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, Serialize, Deserialize)]
#[display("v{0}")]
pub struct Version(u16);
impl Version {
pub fn new_v1() -> Self {
Self::new(1)
}
pub fn new(version: u16) -> Self {
Version(version)
}
pub fn to_u16(&self) -> u16 {
self.0
}
}
impl Encodable for Version {
fn consensus_encode<W: io::Write>(&self, s: &mut W) -> Result<usize, io::Error> {
self.to_u16().consensus_encode(s)
}
}
impl Decodable for Version {
fn consensus_decode<D: io::Read>(d: &mut D) -> Result<Self, consensus::Error> {
Ok(Self::new(Decodable::consensus_decode(d)?))
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("Unsupported version")]
UnsupportedVersion,
#[error("Invalid signature")]
InvalidSignature,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Display,
Serialize,
Deserialize,
StrictEncode,
StrictDecode,
)]
#[serde(transparent)]
#[display(inner)]
pub struct DealId(pub Uuid);
impl From<Uuid> for DealId {
fn from(u: Uuid) -> Self {
DealId(u)
}
}
impl From<uuid::Uuid> for DealId {
fn from(u: uuid::Uuid) -> Self {
DealId(u.into())
}
}
impl FromStr for DealId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::from_str(s)?))
}
}
fixed_hash::construct_fixed_hash!(
pub struct DealFingerprint(32);
);
impl Serialize for DealFingerprint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(format!("{:#x}", self).as_ref())
}
}
impl<'de> Deserialize<'de> for DealFingerprint {
fn deserialize<D>(deserializer: D) -> Result<DealFingerprint, D::Error>
where
D: Deserializer<'de>,
{
DealFingerprint::from_str(&deserializer.deserialize_string(HashString)?)
.map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct DealParameters<Amt, Bmt, Ti, F> {
pub uuid: DealId,
pub network: Network,
pub arbitrating_blockchain: Blockchain,
pub accordant_blockchain: Blockchain,
#[serde(with = "string")]
#[serde(bound(serialize = "Amt: Display"))]
#[serde(bound(deserialize = "Amt: FromStr, Amt::Err: Display"))]
pub arbitrating_amount: Amt,
#[serde(with = "string")]
#[serde(bound(serialize = "Bmt: Display"))]
#[serde(bound(deserialize = "Bmt: FromStr, Bmt::Err: Display"))]
pub accordant_amount: Bmt,
pub cancel_timelock: Ti,
pub punish_timelock: Ti,
pub fee_strategy: FeeStrategy<F>,
pub maker_role: SwapRole,
}
mod string {
use std::fmt::Display;
use std::str::FromStr;
use serde::{de, Deserialize, Deserializer, Serializer};
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: Display,
S: Serializer,
{
serializer.collect_str(value)
}
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: FromStr,
T::Err: Display,
D: Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(de::Error::custom)
}
}
impl<Amt, Bmt, Ti, F> Display for DealParameters<Amt, Bmt, Ti, F>
where
Self: Encodable,
Amt: Display,
Bmt: Display,
Ti: Display,
F: Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Uuid: {}", self.uuid)?;
writeln!(f, "Fingerprint: {:?}", self.fingerprint())?;
writeln!(f, "Network: {}", self.network)?;
writeln!(f, "Blockchain: {}", self.arbitrating_blockchain)?;
writeln!(f, "- amount: {}", self.arbitrating_amount)?;
writeln!(f, "Blockchain: {}", self.accordant_blockchain)?;
writeln!(f, "- amount: {}", self.accordant_amount)?;
writeln!(f, "Timelocks")?;
writeln!(f, "- cancel: {}", self.cancel_timelock)?;
writeln!(f, "- punish: {}", self.punish_timelock)?;
writeln!(f, "Fee strategy: {}", self.fee_strategy)?;
writeln!(f, "Maker swap role: {}", self.maker_role)
}
}
impl<Amt, Bmt, Ti, F> DealParameters<Amt, Bmt, Ti, F> {
pub fn to_v1(self, node_id: PublicKey, peer_address: InetSocketAddr) -> Deal<Amt, Bmt, Ti, F> {
Deal {
version: Version::new_v1(),
parameters: self,
node_id,
peer_address,
}
}
pub fn swap_role(&self, trade_role: &TradeRole) -> SwapRole {
match trade_role {
TradeRole::Maker => self.maker_role,
TradeRole::Taker => self.maker_role.other(),
}
}
}
impl<Amt, Bmt, Ti, F> DealParameters<Amt, Bmt, Ti, F> {
pub fn id(&self) -> DealId {
self.uuid()
}
pub fn uuid(&self) -> DealId {
self.uuid
}
pub fn randomize_uuid(&mut self) {
self.uuid = DealId(Uuid::new());
}
}
impl<Amt, Bmt, Ti, F> DealParameters<Amt, Bmt, Ti, F>
where
Self: Encodable,
{
pub fn fingerprint(&self) -> DealFingerprint {
let mut keccak = Keccak::v256();
let mut out = [0u8; 32];
keccak.update(&serialize(self)[16..]);
keccak.finalize(&mut out);
DealFingerprint(out)
}
}
impl<Amt, Bmt, Ti, F> Encodable for DealParameters<Amt, Bmt, Ti, F>
where
Amt: CanonicalBytes,
Bmt: CanonicalBytes,
Ti: CanonicalBytes,
F: CanonicalBytes,
{
fn consensus_encode<W: io::Write>(&self, s: &mut W) -> Result<usize, io::Error> {
let mut len = self.uuid.0.consensus_encode(s)?;
len += self.network.consensus_encode(s)?;
len += self.arbitrating_blockchain.consensus_encode(s)?;
len += self.accordant_blockchain.consensus_encode(s)?;
len += self
.arbitrating_amount
.as_canonical_bytes()
.consensus_encode(s)?;
len += self
.accordant_amount
.as_canonical_bytes()
.consensus_encode(s)?;
len += self
.cancel_timelock
.as_canonical_bytes()
.consensus_encode(s)?;
len += self
.punish_timelock
.as_canonical_bytes()
.consensus_encode(s)?;
len += self.fee_strategy.consensus_encode(s)?;
Ok(len + self.maker_role.consensus_encode(s)?)
}
}
impl<Amt, Bmt, Ti, F> Decodable for DealParameters<Amt, Bmt, Ti, F>
where
Amt: CanonicalBytes,
Bmt: CanonicalBytes,
Ti: CanonicalBytes,
F: CanonicalBytes,
{
fn consensus_decode<D: io::Read>(d: &mut D) -> Result<Self, consensus::Error> {
Ok(DealParameters {
uuid: DealId(Decodable::consensus_decode(d)?),
network: Decodable::consensus_decode(d)?,
arbitrating_blockchain: Decodable::consensus_decode(d)?,
accordant_blockchain: Decodable::consensus_decode(d)?,
arbitrating_amount: Amt::from_canonical_bytes(unwrap_vec_ref!(d).as_ref())?,
accordant_amount: Bmt::from_canonical_bytes(unwrap_vec_ref!(d).as_ref())?,
cancel_timelock: Ti::from_canonical_bytes(unwrap_vec_ref!(d).as_ref())?,
punish_timelock: Ti::from_canonical_bytes(unwrap_vec_ref!(d).as_ref())?,
fee_strategy: Decodable::consensus_decode(d)?,
maker_role: Decodable::consensus_decode(d)?,
})
}
}
impl_strict_encoding!(DealParameters<Amt, Bmt, Ti, F>, Amt: CanonicalBytes, Bmt: CanonicalBytes, Ti: CanonicalBytes, F: CanonicalBytes,);
#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Deal<Amt, Bmt, Ti, F> {
pub version: Version,
#[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 parameters: DealParameters<Amt, Bmt, Ti, F>,
pub node_id: PublicKey,
pub peer_address: InetSocketAddr,
}
impl<Amt, Bmt, Ti, F> Deal<Amt, Bmt, Ti, F>
where
Amt: Copy,
Ti: Copy,
F: Copy,
{
pub fn to_arbitrating_params(&self) -> ArbitratingParameters<Amt, Ti, F> {
ArbitratingParameters {
arbitrating_amount: self.parameters.arbitrating_amount,
cancel_timelock: self.parameters.cancel_timelock,
punish_timelock: self.parameters.punish_timelock,
fee_strategy: self.parameters.fee_strategy,
}
}
}
impl<Amt, Bmt, Ti, F> Deal<Amt, Bmt, Ti, F>
where
Self: Encodable,
{
pub fn fingerprint(&self) -> DealFingerprint {
let mut keccak = Keccak::v256();
let mut out = [0u8; 32];
let ser = serialize(self);
keccak.update(&ser[..8]);
keccak.update(&ser[24..]);
keccak.finalize(&mut out);
DealFingerprint(out)
}
pub fn to_hex(&self) -> String {
serialize_hex(self)
}
}
impl<Amt, Bmt, Ti, F> Deal<Amt, Bmt, Ti, F> {
pub fn id(&self) -> DealId {
self.uuid()
}
pub fn uuid(&self) -> DealId {
self.parameters.uuid()
}
pub fn randomize_uuid(&mut self) {
self.parameters.randomize_uuid();
}
pub fn swap_role(&self, trade_role: &TradeRole) -> SwapRole {
self.parameters.swap_role(trade_role)
}
}
impl<Amt, Bmt, Ti, F> Display for Deal<Amt, Bmt, Ti, F>
where
Self: Encodable,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let encoded = base58_monero::encode_check(consensus::serialize(self).as_ref())
.expect("Encoding in base58 check works");
write!(f, "{}{}", DEAL_PREFIX, encoded)
}
}
impl<Amt, Bmt, Ti, F> FromStr for Deal<Amt, Bmt, Ti, F>
where
Amt: CanonicalBytes,
Bmt: CanonicalBytes,
Ti: CanonicalBytes,
F: CanonicalBytes,
{
type Err = consensus::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() < 6 {
return Err(consensus::Error::UnknownType);
}
if &s[..5] != DEAL_PREFIX {
return Err(consensus::Error::IncorrectMagicBytes);
}
let decoded = base58_monero::decode_check(&s[5..]).map_err(consensus::Error::new)?;
let mut res = std::io::Cursor::new(decoded);
Decodable::consensus_decode(&mut res)
}
}
impl<Amt, Bmt, Ti, F> Encodable for Deal<Amt, Bmt, Ti, F>
where
Amt: CanonicalBytes,
Bmt: CanonicalBytes,
Ti: CanonicalBytes,
F: CanonicalBytes,
{
fn consensus_encode<W: io::Write>(&self, s: &mut W) -> Result<usize, io::Error> {
let mut len = DEAL_MAGIC_BYTES.consensus_encode(s)?;
len += self.version.consensus_encode(s)?;
len += self.parameters.consensus_encode(s)?;
len += self.node_id.as_canonical_bytes().consensus_encode(s)?;
len +=
strict_encoding::StrictEncode::strict_encode(&self.peer_address, s).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"Failed to encode InetSocketAddr",
)
})?;
Ok(len)
}
}
impl<Amt, Bmt, Ti, F> Decodable for Deal<Amt, Bmt, Ti, F>
where
Amt: CanonicalBytes,
Bmt: CanonicalBytes,
Ti: CanonicalBytes,
F: CanonicalBytes,
{
fn consensus_decode<D: io::Read>(d: &mut D) -> Result<Self, consensus::Error> {
let magic_bytes: [u8; 6] = Decodable::consensus_decode(d)?;
if magic_bytes != *DEAL_MAGIC_BYTES {
return Err(consensus::Error::IncorrectMagicBytes);
}
Ok(Deal {
version: Decodable::consensus_decode(d)?,
parameters: Decodable::consensus_decode(d)?,
node_id: PublicKey::from_canonical_bytes(unwrap_vec_ref!(d).as_ref())?,
peer_address: strict_encoding::StrictDecode::strict_decode(d)
.map_err(consensus::Error::new)?,
})
}
}
impl_strict_encoding!(Deal<Amt, Bmt, Ti, F>, Amt: CanonicalBytes, Bmt: CanonicalBytes, Ti: CanonicalBytes, F: CanonicalBytes,);
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::{
bitcoin::{fee::SatPerKvB, timelock::CSVTimelock},
blockchain::Blockchain,
consensus,
role::SwapRole,
};
use inet2_addr::InetSocketAddr;
use secp256k1::PublicKey;
use uuid::uuid;
const S: &str = "Deal:Cke4ftrP5A7CRkYdGNd87TRU6sUP1kBKM1LQM2fvVdFMNR4gmBqNCsR11111uMM4pF11111112Lvo11111TBALTh113GTvtvqfD1111114A4TUWxWeBc1WxwGBKaUssrb6pnijjhnb6RAs1HBr1CaX7o1a1111111111111111111111111111111111111111115T1WG8uDoZeAW1q";
lazy_static::lazy_static! {
pub static ref NODE_ID: PublicKey = {
let sk =
bitcoin::util::key::PrivateKey::from_wif("L1HKVVLHXiUhecWnwFYF6L3shkf1E12HUmuZTESvBXUdx3yqVP1D")
.unwrap()
.inner;
secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk)
};
pub static ref PEER_ADDRESS: InetSocketAddr = {
InetSocketAddr::socket(
FromStr::from_str("1.2.3.4").unwrap(),
FromStr::from_str("9735").unwrap(),
)
};
pub static ref DEAL_PARAMS: DealParameters<bitcoin::Amount, monero::Amount, CSVTimelock, SatPerKvB> = {
DealParameters {
uuid: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(),
network: Network::Testnet,
arbitrating_blockchain: Blockchain::Bitcoin,
accordant_blockchain: Blockchain::Monero,
arbitrating_amount: bitcoin::Amount::from_sat(1350),
accordant_amount: monero::Amount::from_pico(10000),
cancel_timelock: CSVTimelock::new(4),
punish_timelock: CSVTimelock::new(6),
fee_strategy: FeeStrategy::Fixed(SatPerKvB::from_sat(1)),
maker_role: SwapRole::Bob,
}
};
}
#[test]
fn parse_invalid_deal() {
for deal in ["", "D", "Deal", "Deal:", "Deal:a", "Deal:aa"] {
assert!(
Deal::<bitcoin::Amount, monero::Amount, CSVTimelock, SatPerKvB>::from_str(deal)
.is_err()
);
}
}
#[test]
fn parse_deal() {
let deal = Deal::<bitcoin::Amount, monero::Amount, CSVTimelock, SatPerKvB>::from_str(S);
assert!(deal.is_ok());
let deal = deal.unwrap();
assert_eq!(deal.version, Version::new_v1());
assert_eq!(deal.parameters, DEAL_PARAMS.clone());
assert_eq!(deal.node_id, *NODE_ID);
assert_eq!(deal.peer_address, *PEER_ADDRESS);
}
#[test]
fn parse_deal_fail_without_prefix() {
let deal =
Deal::<bitcoin::Amount, monero::Amount, CSVTimelock, SatPerKvB>::from_str(&S[5..]);
match deal {
Err(consensus::Error::IncorrectMagicBytes) => (),
_ => panic!("Should have return an error IncorrectMagicBytes"),
}
}
#[test]
fn display_deal_params() {
assert_eq!(&format!("{}", *DEAL_PARAMS), "Uuid: 67e55044-10b1-426f-9247-bb680e5fe0c8\nFingerprint: 0xd68b1483de11001050026ca012a2b440818dac23341384c60680f668b52697b0\nNetwork: Testnet\nBlockchain: Bitcoin\n- amount: 0.00001350 BTC\nBlockchain: Monero\n- amount: 0.000000010000 XMR\nTimelocks\n- cancel: 4 blocks\n- punish: 6 blocks\nFee strategy: 1 satoshi/kvB\nMaker swap role: Bob\n");
}
#[test]
fn display_deal() {
let deal = DEAL_PARAMS.clone().to_v1(*NODE_ID, *PEER_ADDRESS);
assert_eq!(&format!("{}", deal), S);
}
#[test]
fn serialize_deal_params_in_yaml() {
let deal_params: DealParameters<bitcoin::Amount, monero::Amount, CSVTimelock, SatPerKvB> =
DealParameters {
uuid: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(),
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(SatPerKvB::from_sat(9)),
maker_role: SwapRole::Bob,
};
let s = serde_yaml::to_string(&deal_params).expect("Encode deal in yaml");
assert_eq!(
"---\nuuid: 67e55044-10b1-426f-9247-bb680e5fe0c8\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/kvB\nmaker_role: Bob\n",
s
);
}
#[test]
fn deserialize_deal_params_from_yaml() {
let s = "---\nuuid: 67e55044-10b1-426f-9247-bb680e5fe0c8\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/kvB\nmaker_role: Bob\n";
let deal_params = serde_yaml::from_str(&s).expect("Decode deal from yaml");
assert_eq!(
DealParameters {
uuid: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(),
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(SatPerKvB::from_sat(9)),
maker_role: SwapRole::Bob,
},
deal_params
);
}
#[test]
fn serialize_deal_in_yaml() {
let deal =
Deal::<bitcoin::Amount, monero::Amount, CSVTimelock, SatPerKvB>::from_str("Deal:Cke4ftrP5A7CRkYdGNd87TRU6sUP1kBKM1W723UjzEWsNR4gmBqNCsR11111uMFubBevJ2E5fp6ZR11111TBALTh113GTvtvqfD1111114A4TTfifktDH7QZD71vpdfo6EVo2ds7KviHz7vYbLZDkgsMNb11111111111111111111111111111111111111111AfZ113XRBuL3QS1m")
.expect("Valid deal");
let s = serde_yaml::to_string(&deal).expect("Encode deal in yaml");
assert_eq!(
"---\nversion: 1\nparameters:\n uuid: 67e55044-10b1-426f-9247-bb680e5fe0c8\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/kvB\n maker_role: Bob\nnode_id: 02e77b779cdc2c713823f7a19147a67e4209c74d77e2cb5045bce0584a6be064d4\npeer_address:\n IPv4: \"127.0.0.1:9735\"\n",
s
);
}
#[test]
fn deserialize_deal_from_yaml() {
let s = "---\nversion: 1\nparameters:\n uuid: 67e55044-10b1-426f-9247-bb680e5fe0c8\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/kvB\n maker_role: Bob\nnode_id: 02e77b779cdc2c713823f7a19147a67e4209c74d77e2cb5045bce0584a6be064d4\npeer_address:\n IPv4: \"127.0.0.1:9735\"\n";
let deal = serde_yaml::from_str(&s).expect("Decode deal from yaml");
assert_eq!(
Deal::<bitcoin::Amount, monero::Amount, CSVTimelock, SatPerKvB>::from_str("Deal:Cke4ftrP5A7CRkYdGNd87TRU6sUP1kBKM1W723UjzEWsNR4gmBqNCsR11111uMFubBevJ2E5fp6ZR11111TBALTh113GTvtvqfD1111114A4TTfifktDH7QZD71vpdfo6EVo2ds7KviHz7vYbLZDkgsMNb11111111111111111111111111111111111111111AfZ113XRBuL3QS1m")
.expect("Valid deal"),
deal
);
}
}