mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-17 09:34:16 -05:00
refactor: swap-core / swap-machine (#530)
* progress * fix thread safety * move monero types from swap into swap_core * just fmt * move non test code above test code * revert removed tracing in bitcoin-wallet/src/primitives.rs * Use existing private_key_from_secp256k1_scalar * remove unused monero chose code * fix some clippy warnings due to imports * move state machine types into the new `swap-machine` crate * remove monero_c orphan submodule * rm bdk_test and sqlx_test from ci * move proptest.rs into swap-proptest * increase stack size to 12mb * properly increase stack size * fix merge conflict in ci.yml * don't increase stack size on mac * fix infinite recursion * fix integration tests * fix some compile errors * fix compilation errors * rustfmt * ignore unstaged patches we applied to monero submodule when running git status * fix some test compilation errors * use BitcoinWallet trait instead of concrete type everywhere * add just test command to run integration tests * remove test_utils features from bdk in swap-core --------- Co-authored-by: einliterflasche <einliterflasche@pm.me> Co-authored-by: binarybaron <binarybaron@mail.mail>
This commit is contained in:
parent
908308366b
commit
4ae47e57f9
86 changed files with 3651 additions and 3007 deletions
1
.gitmodules
vendored
1
.gitmodules
vendored
|
|
@ -1,6 +1,7 @@
|
|||
[submodule "monero-sys/monero"]
|
||||
path = monero-sys/monero
|
||||
url = https://github.com/mrcyjanek/monero
|
||||
ignore = dirty
|
||||
[submodule "monero-sys/monero-depends"]
|
||||
path = monero-sys/monero-depends
|
||||
url = https://github.com/eigenwallet/monero-depends.git
|
||||
|
|
|
|||
983
Cargo.lock
generated
983
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
|
@ -1,6 +1,7 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"bitcoin-wallet",
|
||||
"electrum-pool",
|
||||
"libp2p-rendezvous-server",
|
||||
"libp2p-tor",
|
||||
|
|
@ -13,9 +14,11 @@ members = [
|
|||
"swap-asb",
|
||||
"swap-controller",
|
||||
"swap-controller-api",
|
||||
"swap-core",
|
||||
"swap-env",
|
||||
"swap-feed",
|
||||
"swap-fs",
|
||||
"swap-machine",
|
||||
"swap-orchestrator",
|
||||
"swap-serde",
|
||||
"throttle",
|
||||
|
|
@ -30,6 +33,21 @@ bdk_electrum = { version = "0.23.0", default-features = false }
|
|||
bdk_wallet = "2.0.0"
|
||||
bitcoin = { version = "0.32", features = ["rand", "serde"] }
|
||||
|
||||
# Cryptography
|
||||
curve25519-dalek = { version = "4", package = "curve25519-dalek-ng" }
|
||||
ecdsa_fun = { version = "0.10", default-features = false, features = ["libsecp_compat", "serde", "adaptor"] }
|
||||
rand = "0.8"
|
||||
# Randomness
|
||||
rand_chacha = "0.3"
|
||||
sha2 = "0.10"
|
||||
sigma_fun = { version = "0.7", default-features = false }
|
||||
|
||||
# Async
|
||||
async-trait = "0.1"
|
||||
|
||||
# Serialization
|
||||
serde_cbor = "0.11"
|
||||
|
||||
anyhow = "1"
|
||||
backoff = { version = "0.4", features = ["futures", "tokio"] }
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
|
|
@ -38,7 +56,6 @@ jsonrpsee = { version = "0.25", default-features = false }
|
|||
libp2p = { version = "0.53.2" }
|
||||
monero = { version = "0.12", features = ["serde_support"] }
|
||||
once_cell = "1.19"
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
||||
rust_decimal = { version = "1", features = ["serde-float"] }
|
||||
rust_decimal_macros = "1"
|
||||
|
|
|
|||
12
bitcoin-wallet/Cargo.toml
Normal file
12
bitcoin-wallet/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "bitcoin-wallet"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bdk_wallet = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
65
bitcoin-wallet/src/lib.rs
Normal file
65
bitcoin-wallet/src/lib.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
pub mod primitives;
|
||||
|
||||
pub use crate::primitives::{ScriptStatus, Subscription, Watchable};
|
||||
use anyhow::Result;
|
||||
use bdk_wallet::{export::FullyNodedExport, Balance};
|
||||
use bitcoin::{Address, Amount, Network, Psbt, Txid, Weight};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait BitcoinWallet: Send + Sync {
|
||||
async fn balance(&self) -> Result<Amount>;
|
||||
|
||||
async fn balance_info(&self) -> Result<Balance>;
|
||||
|
||||
async fn new_address(&self) -> Result<Address>;
|
||||
|
||||
async fn send_to_address(
|
||||
&self,
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
spending_fee: Amount,
|
||||
change_override: Option<Address>,
|
||||
) -> Result<Psbt>;
|
||||
|
||||
async fn send_to_address_dynamic_fee(
|
||||
&self,
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
change_override: Option<Address>,
|
||||
) -> Result<bitcoin::psbt::Psbt>;
|
||||
|
||||
async fn sweep_balance_to_address_dynamic_fee(
|
||||
&self,
|
||||
address: Address,
|
||||
) -> Result<bitcoin::psbt::Psbt>;
|
||||
|
||||
async fn sign_and_finalize(&self, psbt: bitcoin::psbt::Psbt) -> Result<bitcoin::Transaction>;
|
||||
|
||||
async fn broadcast(
|
||||
&self,
|
||||
transaction: bitcoin::Transaction,
|
||||
kind: &str,
|
||||
) -> Result<(Txid, Subscription)>;
|
||||
|
||||
async fn sync(&self) -> Result<()>;
|
||||
|
||||
async fn subscribe_to(&self, tx: Box<dyn Watchable>) -> Subscription;
|
||||
|
||||
async fn status_of_script(&self, tx: &dyn Watchable) -> Result<ScriptStatus>;
|
||||
|
||||
async fn get_raw_transaction(
|
||||
&self,
|
||||
txid: Txid,
|
||||
) -> Result<Option<std::sync::Arc<bitcoin::Transaction>>>;
|
||||
|
||||
async fn max_giveable(&self, locking_script_size: usize) -> Result<(Amount, Amount)>;
|
||||
|
||||
async fn estimate_fee(&self, weight: Weight, transfer_amount: Option<Amount>)
|
||||
-> Result<Amount>;
|
||||
|
||||
fn network(&self) -> Network;
|
||||
|
||||
fn finality_confirmations(&self) -> u32;
|
||||
|
||||
async fn wallet_export(&self, role: &str) -> Result<FullyNodedExport>;
|
||||
}
|
||||
255
bitcoin-wallet/src/primitives.rs
Normal file
255
bitcoin-wallet/src/primitives.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
use anyhow::Result;
|
||||
use bitcoin::{FeeRate, ScriptBuf, Txid};
|
||||
|
||||
/// An object that can estimate fee rates and minimum relay fees.
|
||||
pub trait EstimateFeeRate {
|
||||
/// Estimate the fee rate for a given target block.
|
||||
fn estimate_feerate(
|
||||
&self,
|
||||
target_block: u32,
|
||||
) -> impl std::future::Future<Output = Result<FeeRate>> + Send;
|
||||
/// Get the minimum relay fee.
|
||||
fn min_relay_fee(&self) -> impl std::future::Future<Output = Result<FeeRate>> + Send;
|
||||
}
|
||||
|
||||
/// A subscription to the status of a given transaction
|
||||
/// that can be used to wait for the transaction to be confirmed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Subscription {
|
||||
/// A receiver used to await updates to the status of the transaction.
|
||||
pub receiver: tokio::sync::watch::Receiver<ScriptStatus>,
|
||||
/// The number of confirmations we require for a transaction to be considered final.
|
||||
pub finality_confirmations: u32,
|
||||
/// The transaction ID we are subscribing to.
|
||||
pub txid: Txid,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub fn new(
|
||||
receiver: tokio::sync::watch::Receiver<ScriptStatus>,
|
||||
finality_confirmations: u32,
|
||||
txid: Txid,
|
||||
) -> Self {
|
||||
Self {
|
||||
receiver,
|
||||
finality_confirmations,
|
||||
txid,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_until_final(&self) -> anyhow::Result<()> {
|
||||
let conf_target = self.finality_confirmations;
|
||||
let txid = self.txid;
|
||||
|
||||
tracing::info!(%txid, required_confirmation=%conf_target, "Waiting for Bitcoin transaction finality");
|
||||
|
||||
let mut seen_confirmations = 0;
|
||||
|
||||
self.wait_until(|status| match status {
|
||||
ScriptStatus::Confirmed(inner) => {
|
||||
let confirmations = inner.confirmations();
|
||||
|
||||
if confirmations > seen_confirmations {
|
||||
tracing::info!(%txid,
|
||||
seen_confirmations = %confirmations,
|
||||
needed_confirmations = %conf_target,
|
||||
"Waiting for Bitcoin transaction finality");
|
||||
seen_confirmations = confirmations;
|
||||
}
|
||||
|
||||
inner.meets_target(conf_target)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_until_seen(&self) -> anyhow::Result<()> {
|
||||
self.wait_until(ScriptStatus::has_been_seen).await
|
||||
}
|
||||
|
||||
pub async fn wait_until_confirmed_with<T>(&self, target: T) -> anyhow::Result<()>
|
||||
where
|
||||
T: Into<u32>,
|
||||
T: Copy,
|
||||
{
|
||||
self.wait_until(|status| status.is_confirmed_with(target))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_until(
|
||||
&self,
|
||||
mut predicate: impl FnMut(&ScriptStatus) -> bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut receiver = self.receiver.clone();
|
||||
|
||||
while !predicate(&receiver.borrow()) {
|
||||
receiver
|
||||
.changed()
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Failed while waiting for next status update"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible statuses of a script.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ScriptStatus {
|
||||
Unseen,
|
||||
InMempool,
|
||||
Confirmed(Confirmed),
|
||||
Retrying,
|
||||
}
|
||||
|
||||
/// The status of a confirmed transaction.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Confirmed {
|
||||
/// The depth of this transaction within the blockchain.
|
||||
///
|
||||
/// Zero if the transaction is included in the latest block.
|
||||
pub depth: u32,
|
||||
}
|
||||
|
||||
impl Confirmed {
|
||||
pub fn new(depth: u32) -> Self {
|
||||
Self { depth }
|
||||
}
|
||||
|
||||
/// Compute the depth of a transaction based on its inclusion height and the
|
||||
/// latest known block.
|
||||
///
|
||||
/// Our information about the latest block might be outdated. To avoid an
|
||||
/// overflow, we make sure the depth is 0 in case the inclusion height
|
||||
/// exceeds our latest known block,
|
||||
pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self {
|
||||
let depth = latest_block.saturating_sub(inclusion_height);
|
||||
|
||||
Self { depth }
|
||||
}
|
||||
|
||||
pub fn confirmations(&self) -> u32 {
|
||||
self.depth + 1
|
||||
}
|
||||
|
||||
pub fn meets_target<T>(&self, target: T) -> bool
|
||||
where
|
||||
T: Into<u32>,
|
||||
{
|
||||
self.confirmations() >= target.into()
|
||||
}
|
||||
|
||||
pub fn blocks_left_until<T>(&self, target: T) -> u32
|
||||
where
|
||||
T: Into<u32> + Copy,
|
||||
{
|
||||
if self.meets_target(target) {
|
||||
0
|
||||
} else {
|
||||
target.into() - self.confirmations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ScriptStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ScriptStatus::Unseen => write!(f, "unseen"),
|
||||
ScriptStatus::InMempool => write!(f, "in mempool"),
|
||||
ScriptStatus::Retrying => write!(f, "retrying"),
|
||||
ScriptStatus::Confirmed(inner) => {
|
||||
write!(f, "confirmed with {} blocks", inner.confirmations())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines a watchable transaction.
|
||||
///
|
||||
/// For a transaction to be watchable, we need to know two things: Its
|
||||
/// transaction ID and the specific output script that is going to change.
|
||||
/// A transaction can obviously have multiple outputs but our protocol purposes,
|
||||
/// we are usually interested in a specific one.
|
||||
pub trait Watchable: Send + Sync {
|
||||
/// The transaction ID.
|
||||
fn id(&self) -> Txid;
|
||||
/// The script of the output we are interested in.
|
||||
fn script(&self) -> ScriptBuf;
|
||||
/// Convenience method to get both the script and the txid.
|
||||
fn script_and_txid(&self) -> (ScriptBuf, Txid) {
|
||||
(self.script(), self.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl Watchable for (Txid, ScriptBuf) {
|
||||
fn id(&self) -> Txid {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn script(&self) -> ScriptBuf {
|
||||
self.1.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Watchable for &dyn Watchable {
|
||||
fn id(&self) -> Txid {
|
||||
(*self).id()
|
||||
}
|
||||
|
||||
fn script(&self) -> ScriptBuf {
|
||||
(*self).script()
|
||||
}
|
||||
}
|
||||
|
||||
impl Watchable for Box<dyn Watchable> {
|
||||
fn id(&self) -> Txid {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn script(&self) -> ScriptBuf {
|
||||
(**self).script()
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptStatus {
|
||||
pub fn from_confirmations(confirmations: u32) -> Self {
|
||||
match confirmations {
|
||||
0 => Self::InMempool,
|
||||
confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptStatus {
|
||||
/// Check if the script has any confirmations.
|
||||
pub fn is_confirmed(&self) -> bool {
|
||||
matches!(self, ScriptStatus::Confirmed(_))
|
||||
}
|
||||
|
||||
/// Check if the script has met the given confirmation target.
|
||||
pub fn is_confirmed_with<T>(&self, target: T) -> bool
|
||||
where
|
||||
T: Into<u32>,
|
||||
{
|
||||
match self {
|
||||
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the number of blocks left until the target is met.
|
||||
pub fn blocks_left_until<T>(&self, target: T) -> u32
|
||||
where
|
||||
T: Into<u32> + Copy,
|
||||
{
|
||||
match self {
|
||||
ScriptStatus::Confirmed(inner) => inner.blocks_left_until(target),
|
||||
_ => target.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_been_seen(&self) -> bool {
|
||||
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
|
||||
}
|
||||
}
|
||||
3
justfile
3
justfile
|
|
@ -105,6 +105,9 @@ check_gui_eslint:
|
|||
check_gui_tsc:
|
||||
cd src-gui && yarn run tsc --noEmit
|
||||
|
||||
test test_name:
|
||||
cargo test --test {{test_name}} -- --nocapture
|
||||
|
||||
# Run the checks for the GUI frontend
|
||||
check_gui:
|
||||
just check_gui_eslint || true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use arti_client::{config::StreamTimeoutConfig, TorClient, TorClientConfig};
|
||||
use arti_client::{TorClient, TorClientConfig};
|
||||
use clap::Parser;
|
||||
use cuprate_epee_encoding::{epee_object, from_bytes, to_bytes};
|
||||
use futures::stream::{self, StreamExt};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ use tokio::{
|
|||
};
|
||||
|
||||
use tokio_rustls::rustls::{
|
||||
self,
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
pki_types::{CertificateDer, ServerName, UnixTime},
|
||||
DigitallySignedStruct, Error as TlsError, SignatureScheme,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use zeroize::Zeroizing;
|
|||
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
|
||||
use monero_primitives::keccak256;
|
||||
use monero_oxide::primitives::keccak256;
|
||||
|
||||
use crate::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -177,6 +177,14 @@ impl TryFrom<String> for Daemon {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Daemon {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(address: &'a str) -> Result<Self, Self::Error> {
|
||||
address.to_string().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Daemon {
|
||||
/// Try to convert the daemon configuration to a URL
|
||||
pub fn to_url_string(&self) -> String {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ async fn test_sign_message() {
|
|||
.init();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let daemon = Daemon {
|
||||
address: PLACEHOLDER_NODE.into(),
|
||||
ssl: false,
|
||||
};
|
||||
let daemon = Daemon::try_from(PLACEHOLDER_NODE).unwrap();
|
||||
|
||||
let wallet_name = "test_signing_wallet";
|
||||
let wallet_path = temp_dir.path().join(wallet_name).display().to_string();
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ async fn main() {
|
|||
.init();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let daemon = Daemon {
|
||||
address: STAGENET_REMOTE_NODE.into(),
|
||||
ssl: true,
|
||||
};
|
||||
let daemon = Daemon::try_from(STAGENET_REMOTE_NODE).unwrap();
|
||||
|
||||
let wallet_name = "recovered_wallet";
|
||||
let wallet_path = temp_dir.path().join(wallet_name).display().to_string();
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ async fn test_wallet_with_special_paths() {
|
|||
"path-with-hyphen",
|
||||
];
|
||||
|
||||
let daemon = Daemon {
|
||||
address: "https://moneronode.org:18081".into(),
|
||||
ssl: true,
|
||||
};
|
||||
let daemon = Daemon::try_from("https://moneronode.org:18081").unwrap();
|
||||
|
||||
let futures = special_paths
|
||||
.into_iter()
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ async fn main() {
|
|||
.init();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let daemon = Daemon {
|
||||
address: STAGENET_REMOTE_NODE.into(),
|
||||
ssl: true,
|
||||
};
|
||||
let daemon = Daemon::try_from(STAGENET_REMOTE_NODE).unwrap();
|
||||
|
||||
{
|
||||
let wallet = WalletHandle::open_or_create(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ swap = { path = "../swap" }
|
|||
swap-env = { path = "../swap-env" }
|
||||
swap-feed = { path = "../swap-feed" }
|
||||
swap-serde = { path = "../swap-serde" }
|
||||
swap-machine = { path = "../swap-machine"}
|
||||
|
||||
# Async
|
||||
tokio = { workspace = true, features = ["process", "fs", "net", "parking_lot", "rt"] }
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ use swap::common::{self, get_logs, warn_if_outdated};
|
|||
use swap::database::{open_db, AccessMode};
|
||||
use swap::network::rendezvous::XmrBtcNamespace;
|
||||
use swap::network::swarm;
|
||||
use swap::protocol::alice::swap::is_complete;
|
||||
use swap::protocol::alice::{run, AliceState, TipConfig};
|
||||
use swap::protocol::{Database, State};
|
||||
use swap::seed::Seed;
|
||||
|
|
@ -43,6 +42,7 @@ use swap_env::config::{
|
|||
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
|
||||
};
|
||||
use swap_feed;
|
||||
use swap_machine::alice::is_complete;
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,6 @@ edition = "2021"
|
|||
bitcoin = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["macros", "server", "client-core", "http-client"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ clap = { version = "4", features = ["derive"] }
|
|||
jsonrpsee = { workspace = true, features = ["client-core", "http-client"] }
|
||||
monero = { workspace = true }
|
||||
rustyline = "17.0.0"
|
||||
serde_json = { workspace = true }
|
||||
shell-words = "1.1"
|
||||
swap-controller-api = { path = "../swap-controller-api" }
|
||||
tokio = { workspace = true }
|
||||
|
|
|
|||
44
swap-core/Cargo.toml
Normal file
44
swap-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[package]
|
||||
name = "swap-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
# Bitcoin stuff
|
||||
bdk_electrum = { workspace = true }
|
||||
bdk_wallet = { workspace = true, features = ["rusqlite"] }
|
||||
bitcoin = { workspace = true }
|
||||
bitcoin-wallet = { path = "../bitcoin-wallet" }
|
||||
curve25519-dalek = { workspace = true }
|
||||
ecdsa_fun = { workspace = true }
|
||||
electrum-pool = { path = "../electrum-pool" }
|
||||
monero = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
# Serialization
|
||||
rust_decimal = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
swap-serde = { path = "../swap-serde" }
|
||||
typeshare = { workspace = true }
|
||||
|
||||
# Tracing
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Errors
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# Randomness
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_cbor = { workspace = true }
|
||||
swap-env = { path = "../swap-env" }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
801
swap-core/src/bitcoin.rs
Normal file
801
swap-core/src/bitcoin.rs
Normal file
|
|
@ -0,0 +1,801 @@
|
|||
mod cancel;
|
||||
mod early_refund;
|
||||
mod lock;
|
||||
mod punish;
|
||||
mod redeem;
|
||||
mod refund;
|
||||
mod timelocks;
|
||||
|
||||
pub use crate::bitcoin::cancel::{CancelTimelock, PunishTimelock, TxCancel};
|
||||
pub use crate::bitcoin::early_refund::TxEarlyRefund;
|
||||
pub use crate::bitcoin::lock::TxLock;
|
||||
pub use crate::bitcoin::punish::TxPunish;
|
||||
pub use crate::bitcoin::redeem::TxRedeem;
|
||||
pub use crate::bitcoin::refund::TxRefund;
|
||||
pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks};
|
||||
pub use ::bitcoin::amount::Amount;
|
||||
pub use ::bitcoin::psbt::Psbt as PartiallySignedTransaction;
|
||||
pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid};
|
||||
pub use ecdsa_fun::Signature;
|
||||
pub use ecdsa_fun::adaptor::EncryptedSignature;
|
||||
pub use ecdsa_fun::fun::Scalar;
|
||||
|
||||
use ::bitcoin::hashes::Hash;
|
||||
use ::bitcoin::secp256k1::ecdsa;
|
||||
use ::bitcoin::sighash::SegwitV0Sighash as Sighash;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bdk_wallet::miniscript::descriptor::Wsh;
|
||||
use bdk_wallet::miniscript::{Descriptor, Segwitv0};
|
||||
use bitcoin_wallet::primitives::ScriptStatus;
|
||||
use ecdsa_fun::ECDSA;
|
||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||
use ecdsa_fun::fun::Point;
|
||||
use ecdsa_fun::nonce::Deterministic;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct SecretKey {
|
||||
inner: Scalar,
|
||||
public: Point,
|
||||
}
|
||||
|
||||
impl SecretKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public(&self) -> PublicKey {
|
||||
PublicKey(self.public)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
pub fn sign(&self, digest: Sighash) -> Signature {
|
||||
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
||||
|
||||
ecdsa.sign(&self.inner, &digest.to_byte_array())
|
||||
}
|
||||
|
||||
// TxRefund encsigning explanation:
|
||||
//
|
||||
// A and B, are the Bitcoin Public Keys which go on the joint output for
|
||||
// TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the
|
||||
// joint output for TxLock_Monero
|
||||
|
||||
// tx_refund: multisig(A, B), published by bob
|
||||
// bob can produce sig on B using b
|
||||
// alice sends over an encrypted signature on A encrypted with S_b
|
||||
// s_b is leaked to alice when bob publishes signed tx_refund allowing her to
|
||||
// recover s_b: recover(encsig, S_b, sig_tx_refund) = s_b
|
||||
// alice now has s_a and s_b and can refund monero
|
||||
|
||||
// self = a, Y = S_b, digest = tx_refund
|
||||
pub fn encsign(&self, Y: PublicKey, digest: Sighash) -> EncryptedSignature {
|
||||
let adaptor = Adaptor::<
|
||||
HashTranscript<Sha256, rand_chacha::ChaCha20Rng>,
|
||||
Deterministic<Sha256>,
|
||||
>::default();
|
||||
|
||||
adaptor.encrypted_sign(&self.inner, &Y.0, &digest.to_byte_array())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PublicKey(Point);
|
||||
|
||||
impl PublicKey {
|
||||
#[cfg(test)]
|
||||
pub fn random() -> Self {
|
||||
Self(Point::random(&mut rand::thread_rng()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for Point {
|
||||
fn from(from: PublicKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PublicKey> for bitcoin::PublicKey {
|
||||
type Error = bitcoin::key::FromSliceError;
|
||||
|
||||
fn try_from(pubkey: PublicKey) -> Result<Self, Self::Error> {
|
||||
let bytes = pubkey.0.to_bytes();
|
||||
bitcoin::PublicKey::from_slice(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<bitcoin::PublicKey> for PublicKey {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(pubkey: bitcoin::PublicKey) -> Result<Self, Self::Error> {
|
||||
let bytes = pubkey.to_bytes();
|
||||
let bytes_array: [u8; 33] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("Invalid public key length"))?;
|
||||
let point = Point::from_bytes(bytes_array)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid public key bytes"))?;
|
||||
Ok(PublicKey(point))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point> for PublicKey {
|
||||
fn from(p: Point) -> Self {
|
||||
Self(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for SecretKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretKey> for Scalar {
|
||||
fn from(sk: SecretKey) -> Self {
|
||||
sk.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for PublicKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
PublicKey(ecdsa.verification_key_for(&scalar))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_sig(
|
||||
verification_key: &PublicKey,
|
||||
transaction_sighash: &Sighash,
|
||||
sig: &Signature,
|
||||
) -> Result<()> {
|
||||
let ecdsa = ECDSA::verify_only();
|
||||
|
||||
if ecdsa.verify(
|
||||
&verification_key.0,
|
||||
&transaction_sighash.to_byte_array(),
|
||||
sig,
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("signature is invalid")]
|
||||
pub struct InvalidSignature;
|
||||
|
||||
pub fn verify_encsig(
|
||||
verification_key: PublicKey,
|
||||
encryption_key: PublicKey,
|
||||
digest: &Sighash,
|
||||
encsig: &EncryptedSignature,
|
||||
) -> Result<()> {
|
||||
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
||||
|
||||
if adaptor.verify_encrypted_signature(
|
||||
&verification_key.0,
|
||||
&encryption_key.0,
|
||||
&digest.to_byte_array(),
|
||||
encsig,
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidEncryptedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, thiserror::Error)]
|
||||
#[error("encrypted signature is invalid")]
|
||||
pub struct InvalidEncryptedSignature;
|
||||
|
||||
pub fn build_shared_output_descriptor(
|
||||
A: Point,
|
||||
B: Point,
|
||||
) -> Result<Descriptor<bitcoin::PublicKey>> {
|
||||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
|
||||
|
||||
let miniscript = MINISCRIPT_TEMPLATE
|
||||
.replace('A', &A.to_string())
|
||||
.replace('B', &B.to_string());
|
||||
|
||||
let miniscript =
|
||||
bdk_wallet::miniscript::Miniscript::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
||||
.expect("a valid miniscript");
|
||||
|
||||
Ok(Descriptor::Wsh(Wsh::new(miniscript)?))
|
||||
}
|
||||
|
||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
||||
|
||||
let s = adaptor
|
||||
.recover_decryption_key(&S.0, &sig, &encsig)
|
||||
.map(SecretKey::from)
|
||||
.context("Failed to recover secret from adaptor signature")?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn current_epoch(
|
||||
cancel_timelock: CancelTimelock,
|
||||
punish_timelock: PunishTimelock,
|
||||
tx_lock_status: ScriptStatus,
|
||||
tx_cancel_status: ScriptStatus,
|
||||
) -> ExpiredTimelocks {
|
||||
if tx_cancel_status.is_confirmed_with(punish_timelock) {
|
||||
return ExpiredTimelocks::Punish;
|
||||
}
|
||||
|
||||
if tx_lock_status.is_confirmed_with(cancel_timelock) {
|
||||
return ExpiredTimelocks::Cancel {
|
||||
blocks_left: tx_cancel_status.blocks_left_until(punish_timelock),
|
||||
};
|
||||
}
|
||||
|
||||
ExpiredTimelocks::None {
|
||||
blocks_left: tx_lock_status.blocks_left_until(cancel_timelock),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bitcoin_address {
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin::{
|
||||
Address,
|
||||
address::{NetworkChecked, NetworkUnchecked},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)]
|
||||
#[error(
|
||||
"Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}"
|
||||
)]
|
||||
pub struct BitcoinAddressNetworkMismatch {
|
||||
#[serde(with = "swap_serde::bitcoin::network")]
|
||||
expected: bitcoin::Network,
|
||||
#[serde(with = "swap_serde::bitcoin::network")]
|
||||
actual: bitcoin::Network,
|
||||
}
|
||||
|
||||
pub fn parse(addr_str: &str) -> Result<bitcoin::Address<NetworkUnchecked>> {
|
||||
let address = bitcoin::Address::from_str(addr_str)?;
|
||||
|
||||
if address.assume_checked_ref().address_type() != Some(bitcoin::AddressType::P2wpkh) {
|
||||
anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!")
|
||||
}
|
||||
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
/// Parse the address and validate the network.
|
||||
pub fn parse_and_validate_network(
|
||||
address: &str,
|
||||
expected_network: bitcoin::Network,
|
||||
) -> Result<bitcoin::Address> {
|
||||
let addres = bitcoin::Address::from_str(address)?;
|
||||
let addres = addres.require_network(expected_network).with_context(|| {
|
||||
format!("Bitcoin address network mismatch, expected `{expected_network:?}`")
|
||||
})?;
|
||||
Ok(addres)
|
||||
}
|
||||
|
||||
/// Parse the address and validate the network.
|
||||
pub fn parse_and_validate(address: &str, is_testnet: bool) -> Result<bitcoin::Address> {
|
||||
let expected_network = if is_testnet {
|
||||
bitcoin::Network::Testnet
|
||||
} else {
|
||||
bitcoin::Network::Bitcoin
|
||||
};
|
||||
parse_and_validate_network(address, expected_network)
|
||||
}
|
||||
|
||||
/// Validate the address network.
|
||||
pub fn validate(
|
||||
address: Address<NetworkUnchecked>,
|
||||
is_testnet: bool,
|
||||
) -> Result<Address<NetworkChecked>> {
|
||||
let expected_network = if is_testnet {
|
||||
bitcoin::Network::Testnet
|
||||
} else {
|
||||
bitcoin::Network::Bitcoin
|
||||
};
|
||||
validate_network(address, expected_network)
|
||||
}
|
||||
|
||||
/// Validate the address network.
|
||||
pub fn validate_network(
|
||||
address: Address<NetworkUnchecked>,
|
||||
expected_network: bitcoin::Network,
|
||||
) -> Result<Address<NetworkChecked>> {
|
||||
address
|
||||
.require_network(expected_network)
|
||||
.context("Bitcoin address network mismatch")
|
||||
}
|
||||
|
||||
/// Validate the address network even though the address is already checked.
|
||||
pub fn revalidate_network(
|
||||
address: Address,
|
||||
expected_network: bitcoin::Network,
|
||||
) -> Result<Address> {
|
||||
address
|
||||
.as_unchecked()
|
||||
.clone()
|
||||
.require_network(expected_network)
|
||||
.context("bitcoin address network mismatch")
|
||||
}
|
||||
|
||||
/// Validate the address network even though the address is already checked.
|
||||
pub fn revalidate(address: Address, is_testnet: bool) -> Result<Address> {
|
||||
revalidate_network(
|
||||
address,
|
||||
if is_testnet {
|
||||
bitcoin::Network::Testnet
|
||||
} else {
|
||||
bitcoin::Network::Bitcoin
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the ecdsa der signature bytes into a secp256kfun ecdsa signature type.
|
||||
pub fn extract_ecdsa_sig(sig: &[u8]) -> Result<Signature> {
|
||||
let data = &sig[..sig.len() - 1];
|
||||
let sig = ecdsa::Signature::from_der(data)?.serialize_compact();
|
||||
Signature::from_bytes(sig).ok_or(anyhow::anyhow!("invalid signature"))
|
||||
}
|
||||
|
||||
/// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23
|
||||
pub enum RpcErrorCode {
|
||||
/// Transaction or block was rejected by network rules. Error code -26.
|
||||
RpcVerifyRejected,
|
||||
/// Transaction or block was rejected by network rules. Error code -27.
|
||||
RpcVerifyAlreadyInChain,
|
||||
/// General error during transaction or block submission
|
||||
RpcVerifyError,
|
||||
/// Invalid address or key. Error code -5. Is throwns when a transaction is not found.
|
||||
/// See:
|
||||
/// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/mempool.cpp#L470-L472
|
||||
/// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/rawtransaction.cpp#L352-L368
|
||||
RpcInvalidAddressOrKey,
|
||||
}
|
||||
|
||||
impl From<RpcErrorCode> for i64 {
|
||||
fn from(code: RpcErrorCode) -> Self {
|
||||
match code {
|
||||
RpcErrorCode::RpcVerifyError => -25,
|
||||
RpcErrorCode::RpcVerifyRejected => -26,
|
||||
RpcErrorCode::RpcVerifyAlreadyInChain => -27,
|
||||
RpcErrorCode::RpcInvalidAddressOrKey => -5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_rpc_error_code(error: &anyhow::Error) -> anyhow::Result<i64> {
|
||||
// First try to extract an Electrum error from a MultiError if present
|
||||
if let Some(multi_error) = error.downcast_ref::<electrum_pool::MultiError>() {
|
||||
// Try to find the first Electrum error in the MultiError
|
||||
for single_error in multi_error.iter() {
|
||||
if let bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(
|
||||
string,
|
||||
)) = single_error
|
||||
{
|
||||
let json = serde_json::from_str(
|
||||
&string
|
||||
.replace("sendrawtransaction RPC error:", "")
|
||||
.replace("daemon error:", ""),
|
||||
)?;
|
||||
|
||||
let json_map = match json {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => continue, // Try next error if this one isn't a JSON object
|
||||
};
|
||||
|
||||
let error_code_value = match json_map.get("code") {
|
||||
Some(val) => val,
|
||||
None => continue, // Try next error if no error code field
|
||||
};
|
||||
|
||||
let error_code_number = match error_code_value {
|
||||
serde_json::Value::Number(num) => num,
|
||||
_ => continue, // Try next error if error code isn't a number
|
||||
};
|
||||
|
||||
if let Some(int) = error_code_number.as_i64() {
|
||||
return Ok(int);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we couldn't extract an RPC error code from any error in the MultiError
|
||||
bail!(
|
||||
"Error is of incorrect variant. We expected an Electrum error, but got: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Original logic for direct Electrum errors
|
||||
let string = match error.downcast_ref::<bdk_electrum::electrum_client::Error>() {
|
||||
Some(bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(string))) => {
|
||||
string
|
||||
}
|
||||
_ => bail!(
|
||||
"Error is of incorrect variant. We expected an Electrum error, but got: {}",
|
||||
error
|
||||
),
|
||||
};
|
||||
|
||||
let json = serde_json::from_str(
|
||||
&string
|
||||
.replace("sendrawtransaction RPC error:", "")
|
||||
.replace("daemon error:", ""),
|
||||
)?;
|
||||
|
||||
let json_map = match json {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => bail!("Json error is not json object "),
|
||||
};
|
||||
|
||||
let error_code_value = match json_map.get("code") {
|
||||
Some(val) => val,
|
||||
None => bail!("No error code field"),
|
||||
};
|
||||
|
||||
let error_code_number = match error_code_value {
|
||||
serde_json::Value::Number(num) => num,
|
||||
_ => bail!("Error code is not a number"),
|
||||
};
|
||||
|
||||
if let Some(int) = error_code_number.as_i64() {
|
||||
Ok(int)
|
||||
} else {
|
||||
bail!("Error code is not an unsigned integer")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("transaction does not spend anything")]
|
||||
pub struct NoInputs;
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("transaction has {0} inputs, expected 1")]
|
||||
pub struct TooManyInputs(usize);
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("empty witness stack")]
|
||||
pub struct EmptyWitnessStack;
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("input has {0} witnesses, expected 3")]
|
||||
pub struct NotThreeWitnesses(usize);
|
||||
|
||||
#[cfg(test)]
|
||||
pub use crate::bitcoin::wallet::TestWalletBuilder;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::monero::TransferProof;
|
||||
use crate::protocol::{alice, bob};
|
||||
use bitcoin::secp256k1;
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
use ecdsa_fun::fun::marker::{NonZero, Public};
|
||||
use monero::PrivateKey;
|
||||
use rand::rngs::OsRng;
|
||||
use std::matches;
|
||||
use swap_env::env::{GetConfig, Regtest};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() {
|
||||
let tx_lock_status = ScriptStatus::from_confirmations(4);
|
||||
let tx_cancel_status = ScriptStatus::Unseen;
|
||||
|
||||
let expired_timelock = current_epoch(
|
||||
CancelTimelock::new(5),
|
||||
PunishTimelock::new(5),
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
);
|
||||
|
||||
assert!(matches!(expired_timelock, ExpiredTimelocks::None { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_confirmations_ge_to_cancel_timelock_cancel_timelock_expired() {
|
||||
let tx_lock_status = ScriptStatus::from_confirmations(5);
|
||||
let tx_cancel_status = ScriptStatus::Unseen;
|
||||
|
||||
let expired_timelock = current_epoch(
|
||||
CancelTimelock::new(5),
|
||||
PunishTimelock::new(5),
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
);
|
||||
|
||||
assert!(matches!(expired_timelock, ExpiredTimelocks::Cancel { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_confirmations_ge_to_punish_timelock_punish_timelock_expired() {
|
||||
let tx_lock_status = ScriptStatus::from_confirmations(10);
|
||||
let tx_cancel_status = ScriptStatus::from_confirmations(5);
|
||||
|
||||
let expired_timelock = current_epoch(
|
||||
CancelTimelock::new(5),
|
||||
PunishTimelock::new(5),
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
);
|
||||
|
||||
assert_eq!(expired_timelock, ExpiredTimelocks::Punish)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn calculate_transaction_weights() {
|
||||
let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let spending_fee = Amount::from_sat(1_000);
|
||||
let btc_amount = Amount::from_sat(500_000);
|
||||
let xmr_amount = crate::monero::Amount::from_piconero(10000);
|
||||
|
||||
let tx_redeem_fee = alice_wallet
|
||||
.estimate_fee(TxRedeem::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
let tx_punish_fee = alice_wallet
|
||||
.estimate_fee(TxPunish::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
let tx_lock_fee = alice_wallet
|
||||
.estimate_fee(TxLock::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let redeem_address = alice_wallet.new_address().await.unwrap();
|
||||
let punish_address = alice_wallet.new_address().await.unwrap();
|
||||
|
||||
let config = Regtest::get_config();
|
||||
let alice_state0 = alice::State0::new(
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
tx_redeem_fee,
|
||||
tx_punish_fee,
|
||||
&mut OsRng,
|
||||
);
|
||||
|
||||
let bob_state0 = bob::State0::new(
|
||||
Uuid::new_v4(),
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config.bitcoin_cancel_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
bob_wallet.new_address().await.unwrap(),
|
||||
config.monero_finality_confirmations,
|
||||
spending_fee,
|
||||
spending_fee,
|
||||
tx_lock_fee,
|
||||
);
|
||||
|
||||
let message0 = bob_state0.next_message();
|
||||
|
||||
let (_, alice_state1) = alice_state0.receive(message0).unwrap();
|
||||
let alice_message1 = alice_state1.next_message();
|
||||
|
||||
let bob_state1 = bob_state0
|
||||
.receive(&bob_wallet, alice_message1)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_message2 = bob_state1.next_message();
|
||||
|
||||
let alice_state2 = alice_state1.receive(bob_message2).unwrap();
|
||||
let alice_message3 = alice_state2.next_message();
|
||||
|
||||
let bob_state2 = bob_state1.receive(alice_message3).unwrap();
|
||||
let bob_message4 = bob_state2.next_message();
|
||||
|
||||
let alice_state3 = alice_state2.receive(bob_message4).unwrap();
|
||||
|
||||
let (bob_state3, _tx_lock) = bob_state2.lock_btc().await.unwrap();
|
||||
let bob_state4 = bob_state3.xmr_locked(
|
||||
crate::monero::BlockHeight { height: 0 },
|
||||
// We use bogus values here, because they're irrelevant to this test
|
||||
TransferProof::new(
|
||||
crate::monero::TxHash("foo".into()),
|
||||
PrivateKey::from_scalar(Scalar::one()),
|
||||
),
|
||||
);
|
||||
let encrypted_signature = bob_state4.tx_redeem_encsig();
|
||||
let bob_state6 = bob_state4.cancel();
|
||||
|
||||
let cancel_transaction = alice_state3.signed_cancel_transaction().unwrap();
|
||||
let punish_transaction = alice_state3.signed_punish_transaction().unwrap();
|
||||
let redeem_transaction = alice_state3
|
||||
.signed_redeem_transaction(encrypted_signature)
|
||||
.unwrap();
|
||||
let refund_transaction = bob_state6.signed_refund_transaction().unwrap();
|
||||
|
||||
assert_weight(redeem_transaction, TxRedeem::weight().to_wu(), "TxRedeem");
|
||||
assert_weight(cancel_transaction, TxCancel::weight().to_wu(), "TxCancel");
|
||||
assert_weight(punish_transaction, TxPunish::weight().to_wu(), "TxPunish");
|
||||
assert_weight(refund_transaction, TxRefund::weight().to_wu(), "TxRefund");
|
||||
|
||||
// Test TxEarlyRefund transaction
|
||||
let early_refund_transaction = alice_state3
|
||||
.signed_early_refund_transaction()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_weight(
|
||||
early_refund_transaction,
|
||||
TxEarlyRefund::weight() as u64,
|
||||
"TxEarlyRefund",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tx_early_refund_can_be_constructed_and_signed() {
|
||||
let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let spending_fee = Amount::from_sat(1_000);
|
||||
let btc_amount = Amount::from_sat(500_000);
|
||||
let xmr_amount = crate::monero::Amount::from_piconero(10000);
|
||||
|
||||
let tx_redeem_fee = alice_wallet
|
||||
.estimate_fee(TxRedeem::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
let tx_punish_fee = alice_wallet
|
||||
.estimate_fee(TxPunish::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let refund_address = alice_wallet.new_address().await.unwrap();
|
||||
let punish_address = alice_wallet.new_address().await.unwrap();
|
||||
|
||||
let config = Regtest::get_config();
|
||||
let alice_state0 = alice::State0::new(
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config,
|
||||
refund_address.clone(),
|
||||
punish_address,
|
||||
tx_redeem_fee,
|
||||
tx_punish_fee,
|
||||
&mut OsRng,
|
||||
);
|
||||
|
||||
let bob_state0 = bob::State0::new(
|
||||
Uuid::new_v4(),
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config.bitcoin_cancel_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
bob_wallet.new_address().await.unwrap(),
|
||||
config.monero_finality_confirmations,
|
||||
spending_fee,
|
||||
spending_fee,
|
||||
spending_fee,
|
||||
);
|
||||
|
||||
// Complete the state machine up to State3
|
||||
let message0 = bob_state0.next_message();
|
||||
let (_, alice_state1) = alice_state0.receive(message0).unwrap();
|
||||
let alice_message1 = alice_state1.next_message();
|
||||
|
||||
let bob_state1 = bob_state0
|
||||
.receive(&bob_wallet, alice_message1)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_message2 = bob_state1.next_message();
|
||||
|
||||
let alice_state2 = alice_state1.receive(bob_message2).unwrap();
|
||||
let alice_message3 = alice_state2.next_message();
|
||||
|
||||
let bob_state2 = bob_state1.receive(alice_message3).unwrap();
|
||||
let bob_message4 = bob_state2.next_message();
|
||||
|
||||
let alice_state3 = alice_state2.receive(bob_message4).unwrap();
|
||||
|
||||
// Test TxEarlyRefund construction
|
||||
let tx_early_refund = alice_state3.tx_early_refund();
|
||||
|
||||
// Verify basic properties
|
||||
assert_eq!(tx_early_refund.txid(), tx_early_refund.txid()); // Should be deterministic
|
||||
assert!(tx_early_refund.digest() != Sighash::all_zeros()); // Should have valid digest
|
||||
|
||||
// Test that it can be signed and completed
|
||||
let early_refund_transaction = alice_state3
|
||||
.signed_early_refund_transaction()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Verify the transaction has expected structure
|
||||
assert_eq!(early_refund_transaction.input.len(), 1); // One input from lock tx
|
||||
assert_eq!(early_refund_transaction.output.len(), 1); // One output to refund address
|
||||
assert_eq!(
|
||||
early_refund_transaction.output[0].script_pubkey,
|
||||
refund_address.script_pubkey()
|
||||
);
|
||||
|
||||
// Verify the input is spending the lock transaction
|
||||
assert_eq!(
|
||||
early_refund_transaction.input[0].previous_output,
|
||||
alice_state3.tx_lock.as_outpoint()
|
||||
);
|
||||
|
||||
// Verify the amount is correct (lock amount minus fee)
|
||||
let expected_amount = alice_state3.tx_lock.lock_amount() - alice_state3.tx_refund_fee;
|
||||
assert_eq!(early_refund_transaction.output[0].value, expected_amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tx_early_refund_has_correct_weight() {
|
||||
// TxEarlyRefund should have the same weight as other similar transactions
|
||||
assert_eq!(TxEarlyRefund::weight(), 548);
|
||||
|
||||
// It should be the same as TxRedeem and TxRefund weights since they have similar structure
|
||||
assert_eq!(TxEarlyRefund::weight() as u64, TxRedeem::weight().to_wu());
|
||||
assert_eq!(TxEarlyRefund::weight() as u64, TxRefund::weight().to_wu());
|
||||
}
|
||||
|
||||
// Weights fluctuate because of the length of the signatures. Valid ecdsa
|
||||
// signatures can have 68, 69, 70, 71, or 72 bytes. Since most of our
|
||||
// transactions have 2 signatures the weight can be up to 8 bytes less than
|
||||
// the static weight (4 bytes per signature).
|
||||
fn assert_weight(transaction: Transaction, expected_weight: u64, tx_name: &str) {
|
||||
let is_weight = transaction.weight();
|
||||
|
||||
assert!(
|
||||
expected_weight - is_weight.to_wu() <= 8,
|
||||
"{} to have weight {}, but was {}. Transaction: {:#?}",
|
||||
tx_name,
|
||||
expected_weight,
|
||||
is_weight,
|
||||
transaction
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_point_hex() {
|
||||
// secp256kfun Point and secp256k1 PublicKey should have the same bytes and hex representation
|
||||
let secp = secp256k1::Secp256k1::default();
|
||||
let keypair = secp256k1::Keypair::new(&secp, &mut OsRng);
|
||||
|
||||
let pubkey = keypair.public_key();
|
||||
let point: Point<_, Public, NonZero> = Point::from_bytes(pubkey.serialize()).unwrap();
|
||||
|
||||
assert_eq!(pubkey.to_string(), point.to_string());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
use crate::bitcoin;
|
||||
use crate::bitcoin::wallet::Watchable;
|
||||
use crate::bitcoin::{
|
||||
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
|
||||
Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, build_shared_output_descriptor,
|
||||
};
|
||||
use ::bitcoin::Weight;
|
||||
use ::bitcoin::sighash::SighashCache;
|
||||
use ::bitcoin::transaction::Version;
|
||||
use ::bitcoin::Weight;
|
||||
use ::bitcoin::{
|
||||
locktime::absolute::LockTime as PackedLockTime, secp256k1, sighash::SegwitV0Sighash as Sighash,
|
||||
EcdsaSighashType, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Txid,
|
||||
locktime::absolute::LockTime as PackedLockTime, secp256k1, sighash::SegwitV0Sighash as Sighash,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use bdk_wallet::miniscript::Descriptor;
|
||||
use bitcoin_wallet::primitives::Watchable;
|
||||
use ecdsa_fun::Signature;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
use crate::bitcoin;
|
||||
use ::bitcoin::sighash::SighashCache;
|
||||
use ::bitcoin::{secp256k1, ScriptBuf};
|
||||
use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, EcdsaSighashType, Txid};
|
||||
use ::bitcoin::{EcdsaSighashType, Txid, sighash::SegwitV0Sighash as Sighash};
|
||||
use ::bitcoin::{ScriptBuf, secp256k1};
|
||||
use anyhow::{Context, Result};
|
||||
use bdk_wallet::miniscript::Descriptor;
|
||||
use bitcoin::{Address, Amount, Transaction};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::wallet::Watchable;
|
||||
use super::TxLock;
|
||||
use bitcoin_wallet::primitives::Watchable;
|
||||
|
||||
const TX_EARLY_REFUND_WEIGHT: usize = 548;
|
||||
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
use crate::bitcoin::wallet::Watchable;
|
||||
use crate::bitcoin::{
|
||||
build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet,
|
||||
};
|
||||
use crate::bitcoin::{Address, Amount, PublicKey, Transaction, build_shared_output_descriptor};
|
||||
use ::bitcoin::psbt::Psbt as PartiallySignedTransaction;
|
||||
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bdk_wallet::miniscript::Descriptor;
|
||||
use bdk_wallet::psbt::PsbtUtils;
|
||||
use bitcoin::{locktime::absolute::LockTime as PackedLockTime, ScriptBuf, Sequence};
|
||||
use bitcoin::{ScriptBuf, Sequence, locktime::absolute::LockTime as PackedLockTime};
|
||||
use bitcoin_wallet::primitives::Watchable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::wallet::EstimateFeeRate;
|
||||
|
||||
const SCRIPT_SIZE: usize = 34;
|
||||
const TX_LOCK_WEIGHT: usize = 485;
|
||||
|
||||
|
|
@ -23,10 +19,7 @@ pub struct TxLock {
|
|||
|
||||
impl TxLock {
|
||||
pub async fn new(
|
||||
wallet: &Wallet<
|
||||
bdk_wallet::rusqlite::Connection,
|
||||
impl EstimateFeeRate + Send + Sync + 'static,
|
||||
>,
|
||||
wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
amount: Amount,
|
||||
spending_fee: Amount,
|
||||
A: PublicKey,
|
||||
|
|
@ -202,8 +195,8 @@ impl Watchable for TxLock {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bitcoin::wallet::TestWalletBuilder;
|
||||
use crate::bitcoin::Amount;
|
||||
use crate::bitcoin::wallet::TestWalletBuilder;
|
||||
use ::bitcoin::psbt::Psbt as PartiallySignedTransaction;
|
||||
|
||||
// Basic setup function for tests
|
||||
|
|
@ -286,10 +279,7 @@ mod tests {
|
|||
async fn bob_make_psbt(
|
||||
A: PublicKey,
|
||||
B: PublicKey,
|
||||
wallet: &Wallet<
|
||||
bdk_wallet::rusqlite::Connection,
|
||||
impl EstimateFeeRate + Send + Sync + 'static,
|
||||
>,
|
||||
wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
amount: Amount,
|
||||
spending_fee: Amount,
|
||||
) -> PartiallySignedTransaction {
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::bitcoin::wallet::Watchable;
|
||||
use crate::bitcoin::{self, Address, Amount, PunishTimelock, Transaction, TxCancel, Txid};
|
||||
use ::bitcoin::sighash::SighashCache;
|
||||
use ::bitcoin::{secp256k1, sighash::SegwitV0Sighash as Sighash, EcdsaSighashType};
|
||||
use ::bitcoin::{EcdsaSighashType, secp256k1, sighash::SegwitV0Sighash as Sighash};
|
||||
use ::bitcoin::{ScriptBuf, Weight};
|
||||
use anyhow::{Context, Result};
|
||||
use bdk_wallet::miniscript::Descriptor;
|
||||
use bitcoin_wallet::primitives::Watchable;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
use crate::bitcoin::wallet::Watchable;
|
||||
use crate::bitcoin::{
|
||||
verify_encsig, verify_sig, Address, Amount, EmptyWitnessStack, EncryptedSignature, NoInputs,
|
||||
NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock,
|
||||
Address, Amount, EmptyWitnessStack, EncryptedSignature, NoInputs, NotThreeWitnesses, PublicKey,
|
||||
SecretKey, TooManyInputs, Transaction, TxLock, verify_encsig, verify_sig,
|
||||
};
|
||||
use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, Txid};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use ::bitcoin::{Txid, sighash::SegwitV0Sighash as Sighash};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bdk_wallet::miniscript::Descriptor;
|
||||
use bitcoin::sighash::SighashCache;
|
||||
use bitcoin::{secp256k1, ScriptBuf};
|
||||
use bitcoin::{EcdsaSighashType, Weight};
|
||||
use bitcoin::{ScriptBuf, secp256k1};
|
||||
use bitcoin_wallet::primitives::Watchable;
|
||||
use ecdsa_fun::Signature;
|
||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||
use ecdsa_fun::fun::Scalar;
|
||||
use ecdsa_fun::nonce::Deterministic;
|
||||
use ecdsa_fun::Signature;
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
use crate::bitcoin::wallet::Watchable;
|
||||
use crate::bitcoin;
|
||||
use crate::bitcoin::{
|
||||
verify_sig, Address, Amount, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey,
|
||||
TooManyInputs, Transaction, TxCancel,
|
||||
Address, Amount, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
|
||||
Transaction, TxCancel, verify_sig,
|
||||
};
|
||||
use crate::{bitcoin, monero};
|
||||
use ::bitcoin::sighash::SighashCache;
|
||||
use ::bitcoin::{secp256k1, ScriptBuf, Weight};
|
||||
use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, EcdsaSighashType, Txid};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use ::bitcoin::{EcdsaSighashType, Txid, sighash::SegwitV0Sighash as Sighash};
|
||||
use ::bitcoin::{ScriptBuf, Weight, secp256k1};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bdk_wallet::miniscript::Descriptor;
|
||||
use bitcoin_wallet::primitives::Watchable;
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
use ecdsa_fun::Signature;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -103,12 +104,10 @@ impl TxRefund {
|
|||
pub fn extract_monero_private_key(
|
||||
&self,
|
||||
published_refund_tx: Arc<bitcoin::Transaction>,
|
||||
s_a: monero::Scalar,
|
||||
s_a: Scalar,
|
||||
a: bitcoin::SecretKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
) -> Result<monero::PrivateKey> {
|
||||
let s_a = monero::PrivateKey { scalar: s_a };
|
||||
|
||||
) -> Result<Scalar> {
|
||||
let tx_refund_sig = self
|
||||
.extract_signature_by_key(published_refund_tx, a.public())
|
||||
.context("Failed to extract signature from Bitcoin refund tx")?;
|
||||
|
|
@ -117,7 +116,7 @@ impl TxRefund {
|
|||
let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
|
||||
.context("Failed to recover Monero secret key from Bitcoin signature")?;
|
||||
|
||||
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
|
||||
let s_b = crate::monero::primitives::private_key_from_secp256k1_scalar(s_b.into());
|
||||
|
||||
let spend_key = s_a + s_b;
|
||||
|
||||
2
swap-core/src/lib.rs
Normal file
2
swap-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod bitcoin;
|
||||
pub mod monero;
|
||||
5
swap-core/src/monero.rs
Normal file
5
swap-core/src/monero.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod ext;
|
||||
pub mod primitives;
|
||||
|
||||
pub use ext::*;
|
||||
pub use primitives::*;
|
||||
|
|
@ -5,7 +5,7 @@ pub trait ScalarExt {
|
|||
fn to_secpfun_scalar(&self) -> ecdsa_fun::fun::Scalar;
|
||||
}
|
||||
|
||||
impl ScalarExt for crate::monero::Scalar {
|
||||
impl ScalarExt for curve25519_dalek::scalar::Scalar {
|
||||
fn to_secpfun_scalar(&self) -> Scalar<Secret, NonZero> {
|
||||
let mut little_endian_bytes = self.to_bytes();
|
||||
|
||||
871
swap-core/src/monero/primitives.rs
Normal file
871
swap-core/src/monero/primitives.rs
Normal file
|
|
@ -0,0 +1,871 @@
|
|||
use crate::bitcoin;
|
||||
use anyhow::{Result, bail};
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
use monero::Address;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
use std::str::FromStr;
|
||||
use typeshare::typeshare;
|
||||
|
||||
use ::monero::network::Network;
|
||||
pub use ::monero::{PrivateKey, PublicKey};
|
||||
|
||||
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
||||
|
||||
/// A Monero block height.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct BlockHeight {
|
||||
pub height: u64,
|
||||
}
|
||||
|
||||
impl fmt::Display for BlockHeight {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.height)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> Scalar {
|
||||
let mut bytes = scalar.to_bytes();
|
||||
|
||||
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
|
||||
// ed25519 scalar is little endian
|
||||
bytes.reverse();
|
||||
|
||||
Scalar::from_bytes_mod_order(bytes)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PrivateViewKey(#[serde(with = "swap_serde::monero::private_key")] PrivateKey);
|
||||
|
||||
impl fmt::Display for PrivateViewKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to the Display implementation of PrivateKey
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateViewKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
let private_key = PrivateKey::from_scalar(scalar);
|
||||
|
||||
Self(private_key)
|
||||
}
|
||||
|
||||
pub fn public(&self) -> PublicViewKey {
|
||||
PublicViewKey(PublicKey::from_private_key(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for PrivateViewKey {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateViewKey> for PrivateKey {
|
||||
fn from(from: PrivateViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicViewKey> for PublicKey {
|
||||
fn from(from: PublicViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PublicViewKey(pub PublicKey);
|
||||
|
||||
/// Our own monero amount type, which we need because the monero crate
|
||||
/// doesn't implement Serialize and Deserialize.
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)]
|
||||
#[typeshare(serialized_as = "number")]
|
||||
pub struct Amount(u64);
|
||||
|
||||
// TX Fees on Monero can be found here:
|
||||
// - https://www.monero.how/monero-transaction-fees
|
||||
// - https://bitinfocharts.com/comparison/monero-transactionfees.html#1y
|
||||
//
|
||||
// In the last year the highest avg fee on any given day was around 0.00075 XMR
|
||||
// We use a multiplier of 4x to stay safe
|
||||
// 0.00075 XMR * 4 = 0.003 XMR (around $1 as of Jun. 4th 2025)
|
||||
// We DO NOT use this fee to construct any transactions. It is only to **estimate** how much
|
||||
// we need to reserve for the fee when determining our max giveable amount
|
||||
// We use a VERY conservative value here to stay on the safe side. We want to avoid not being able
|
||||
// to lock as much as we previously estimated.
|
||||
pub const CONSERVATIVE_MONERO_FEE: Amount = Amount::from_piconero(3_000_000_000);
|
||||
|
||||
impl Amount {
|
||||
pub const ZERO: Self = Self(0);
|
||||
pub const ONE_XMR: Self = Self(PICONERO_OFFSET);
|
||||
/// Create an [Amount] with piconero precision and the given number of
|
||||
/// piconeros.
|
||||
///
|
||||
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
|
||||
pub const fn from_piconero(amount: u64) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
|
||||
/// Return Monero Amount as Piconero.
|
||||
pub fn as_piconero(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return Monero Amount as XMR.
|
||||
pub fn as_xmr(&self) -> f64 {
|
||||
let amount_decimal = Decimal::from(self.0);
|
||||
let offset_decimal = Decimal::from(PICONERO_OFFSET);
|
||||
let result = amount_decimal / offset_decimal;
|
||||
|
||||
// Convert to f64 only at the end, after the division
|
||||
result
|
||||
.to_f64()
|
||||
.expect("Conversion from piconero to XMR should not overflow f64")
|
||||
}
|
||||
|
||||
/// Calculate the conservative max giveable of Monero we can spent given [`self`] is the balance
|
||||
/// of a Monero wallet
|
||||
/// This is going to be LESS than we can really spent because we assume a high fee
|
||||
pub fn max_conservative_giveable(&self) -> Self {
|
||||
let pico_minus_fee = self
|
||||
.as_piconero()
|
||||
.saturating_sub(CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
|
||||
Self::from_piconero(pico_minus_fee)
|
||||
}
|
||||
|
||||
/// Calculate the Monero balance needed to send the [`self`] Amount to another address
|
||||
/// E.g: Amount(1 XMR).min_conservative_balance_to_spend() with a fee of 0.1 XMR would be 1.1 XMR
|
||||
/// This is going to be MORE than we really need because we assume a high fee
|
||||
pub fn min_conservative_balance_to_spend(&self) -> Self {
|
||||
let pico_minus_fee = self
|
||||
.as_piconero()
|
||||
.saturating_add(CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
|
||||
Self::from_piconero(pico_minus_fee)
|
||||
}
|
||||
|
||||
/// Calculate the maximum amount of Bitcoin that can be bought at a given
|
||||
/// asking price for this amount of Monero including the median fee.
|
||||
pub fn max_bitcoin_for_price(&self, ask_price: bitcoin::Amount) -> Option<bitcoin::Amount> {
|
||||
let pico_minus_fee = self.max_conservative_giveable();
|
||||
|
||||
if pico_minus_fee.as_piconero() == 0 {
|
||||
return Some(bitcoin::Amount::ZERO);
|
||||
}
|
||||
|
||||
// safely convert the BTC/XMR rate to sat/pico
|
||||
let ask_sats = Decimal::from(ask_price.to_sat());
|
||||
let pico_per_xmr = Decimal::from(PICONERO_OFFSET);
|
||||
let ask_sats_per_pico = ask_sats / pico_per_xmr;
|
||||
|
||||
let pico = Decimal::from(pico_minus_fee.as_piconero());
|
||||
let max_sats = pico.checked_mul(ask_sats_per_pico)?;
|
||||
let satoshi = max_sats.to_u64()?;
|
||||
|
||||
Some(bitcoin::Amount::from_sat(satoshi))
|
||||
}
|
||||
|
||||
pub fn from_monero(amount: f64) -> Result<Self> {
|
||||
let decimal = Decimal::try_from(amount)?;
|
||||
Self::from_decimal(decimal)
|
||||
}
|
||||
|
||||
pub fn parse_monero(amount: &str) -> Result<Self> {
|
||||
let decimal = Decimal::from_str(amount)?;
|
||||
Self::from_decimal(decimal)
|
||||
}
|
||||
|
||||
pub fn as_piconero_decimal(&self) -> Decimal {
|
||||
Decimal::from(self.as_piconero())
|
||||
}
|
||||
|
||||
fn from_decimal(amount: Decimal) -> Result<Self> {
|
||||
let piconeros_dec =
|
||||
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
||||
let piconeros = piconeros_dec
|
||||
.to_u64()
|
||||
.ok_or_else(|| OverflowError(amount.to_string()))?;
|
||||
Ok(Amount(piconeros))
|
||||
}
|
||||
|
||||
/// Subtract but throw an error on underflow.
|
||||
pub fn checked_sub(self, rhs: Amount) -> Result<Self> {
|
||||
if self.0 < rhs.0 {
|
||||
bail!("checked sub would underflow");
|
||||
}
|
||||
|
||||
Ok(Amount::from_piconero(self.0 - rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// A Monero address with an associated percentage and human-readable label.
|
||||
///
|
||||
/// This structure represents a destination address for Monero transactions
|
||||
/// along with the percentage of funds it should receive and a descriptive label.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[typeshare]
|
||||
pub struct LabeledMoneroAddress {
|
||||
// If this is None, we will use an address of the internal Monero wallet
|
||||
#[typeshare(serialized_as = "string")]
|
||||
address: Option<monero::Address>,
|
||||
#[typeshare(serialized_as = "number")]
|
||||
percentage: Decimal,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl LabeledMoneroAddress {
|
||||
/// Creates a new labeled Monero address.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `address` - The Monero address
|
||||
/// * `percentage` - The percentage of funds (between 0.0 and 1.0)
|
||||
/// * `label` - A human-readable label for this address
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the percentage is not between 0.0 and 1.0 inclusive.
|
||||
fn new(
|
||||
address: impl Into<Option<monero::Address>>,
|
||||
percentage: Decimal,
|
||||
label: String,
|
||||
) -> Result<Self> {
|
||||
if percentage < Decimal::ZERO || percentage > Decimal::ONE {
|
||||
bail!(
|
||||
"Percentage must be between 0 and 1 inclusive, got: {}",
|
||||
percentage
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
address: address.into(),
|
||||
percentage,
|
||||
label,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_address(
|
||||
address: monero::Address,
|
||||
percentage: Decimal,
|
||||
label: String,
|
||||
) -> Result<Self> {
|
||||
Self::new(address, percentage, label)
|
||||
}
|
||||
|
||||
pub fn with_internal_address(percentage: Decimal, label: String) -> Result<Self> {
|
||||
Self::new(None, percentage, label)
|
||||
}
|
||||
|
||||
/// Returns the Monero address.
|
||||
pub fn address(&self) -> Option<monero::Address> {
|
||||
self.address.clone()
|
||||
}
|
||||
|
||||
/// Returns the percentage as a decimal.
|
||||
pub fn percentage(&self) -> Decimal {
|
||||
self.percentage
|
||||
}
|
||||
|
||||
/// Returns the human-readable label.
|
||||
pub fn label(&self) -> &str {
|
||||
&self.label
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of labeled Monero addresses that can receive funds in a transaction.
|
||||
///
|
||||
/// This structure manages multiple destination addresses with their associated
|
||||
/// percentages and labels. It's used for splitting Monero transactions across
|
||||
/// multiple recipients, such as for donations or multi-destination swaps.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[typeshare]
|
||||
pub struct MoneroAddressPool(Vec<LabeledMoneroAddress>);
|
||||
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
impl MoneroAddressPool {
|
||||
/// Creates a new address pool from a vector of labeled addresses.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `addresses` - Vector of labeled Monero addresses
|
||||
pub fn new(addresses: Vec<LabeledMoneroAddress>) -> Self {
|
||||
Self(addresses)
|
||||
}
|
||||
|
||||
/// Returns a vector of all Monero addresses in the pool.
|
||||
pub fn addresses(&self) -> Vec<Option<monero::Address>> {
|
||||
self.0.iter().map(|address| address.address()).collect()
|
||||
}
|
||||
|
||||
/// Returns a vector of all percentages as f64 values (0-1 range).
|
||||
pub fn percentages(&self) -> Vec<f64> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|address| {
|
||||
address
|
||||
.percentage()
|
||||
.to_f64()
|
||||
.expect("Decimal should convert to f64")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the labeled addresses.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &LabeledMoneroAddress> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Validates that all addresses in the pool are on the expected network.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `network` - The expected Monero network
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any address is on a different network than expected.
|
||||
pub fn assert_network(&self, network: Network) -> Result<()> {
|
||||
for address in self.0.iter() {
|
||||
if let Some(address) = address.address {
|
||||
if address.network != network {
|
||||
bail!(
|
||||
"Address pool contains addresses on the wrong network (address {} is on {:?}, expected {:?})",
|
||||
address,
|
||||
address.network,
|
||||
network
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assert that the sum of the percentages in the address pool is 1 (allowing for a small tolerance)
|
||||
pub fn assert_sum_to_one(&self) -> Result<()> {
|
||||
let sum = self
|
||||
.0
|
||||
.iter()
|
||||
.map(|address| address.percentage())
|
||||
.sum::<Decimal>();
|
||||
|
||||
const TOLERANCE: f64 = 1e-6;
|
||||
|
||||
if (sum - Decimal::ONE).abs()
|
||||
> Decimal::from_f64(TOLERANCE).expect("TOLERANCE constant should be a valid f64")
|
||||
{
|
||||
bail!("Address pool percentages do not sum to 1");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a vector of addresses with the empty addresses filled with the given primary address
|
||||
pub fn fill_empty_addresses(&self, primary_address: monero::Address) -> Vec<monero::Address> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|address| address.address().unwrap_or(primary_address))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<::monero::Address> for MoneroAddressPool {
|
||||
fn from(address: ::monero::Address) -> Self {
|
||||
Self(vec![
|
||||
LabeledMoneroAddress::new(address, Decimal::from(1), "user address".to_string())
|
||||
.expect("Percentage 1 is always valid"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// A request to watch for a transfer.
|
||||
pub struct WatchRequest {
|
||||
pub public_view_key: super::PublicViewKey,
|
||||
pub public_spend_key: monero::PublicKey,
|
||||
/// The proof of the transfer.
|
||||
pub transfer_proof: TransferProof,
|
||||
/// The expected amount of the transfer.
|
||||
pub expected_amount: monero::Amount,
|
||||
/// The number of confirmations required for the transfer to be considered confirmed.
|
||||
pub confirmation_target: u64,
|
||||
}
|
||||
|
||||
/// Transfer a specified amount of money to a specified address.
|
||||
pub struct TransferRequest {
|
||||
pub public_spend_key: monero::PublicKey,
|
||||
pub public_view_key: super::PublicViewKey,
|
||||
pub amount: monero::Amount,
|
||||
}
|
||||
|
||||
impl TransferRequest {
|
||||
pub fn address_and_amount(&self, network: Network) -> (Address, monero::Amount) {
|
||||
(
|
||||
Address::standard(network, self.public_spend_key, self.public_view_key.0),
|
||||
self.amount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Amount> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<u64> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for u64 {
|
||||
fn from(from: Amount) -> u64 {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<::monero::Amount> for Amount {
|
||||
fn from(from: ::monero::Amount) -> Self {
|
||||
Amount::from_piconero(from.as_pico())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for ::monero::Amount {
|
||||
fn from(from: Amount) -> Self {
|
||||
::monero::Amount::from_pico(from.as_piconero())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Amount {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut decimal = Decimal::from(self.0);
|
||||
decimal
|
||||
.set_scale(12)
|
||||
.expect("12 is smaller than max precision of 28");
|
||||
write!(f, "{} XMR", decimal)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TransferProof {
|
||||
pub tx_hash: TxHash,
|
||||
#[serde(with = "swap_serde::monero::private_key")]
|
||||
pub tx_key: PrivateKey,
|
||||
}
|
||||
|
||||
impl TransferProof {
|
||||
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
|
||||
Self { tx_hash, tx_key }
|
||||
}
|
||||
pub fn tx_hash(&self) -> TxHash {
|
||||
self.tx_hash.clone()
|
||||
}
|
||||
pub fn tx_key(&self) -> PrivateKey {
|
||||
self.tx_key
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add constructor/ change String to fixed length byte array
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TxHash(pub String);
|
||||
|
||||
impl From<TxHash> for String {
|
||||
fn from(from: TxHash) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TxHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TxHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("expected {expected}, got {actual}")]
|
||||
pub struct InsufficientFunds {
|
||||
pub expected: Amount,
|
||||
pub actual: Amount,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||
#[error("Overflow, cannot convert {0} to u64")]
|
||||
pub struct OverflowError(pub String);
|
||||
|
||||
pub mod monero_amount {
|
||||
use crate::monero::Amount;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_u64(x.as_piconero())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let picos = u64::deserialize(deserializer)?;
|
||||
let amount = Amount::from_piconero(picos);
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn display_monero_min() {
|
||||
let min_pics = 1;
|
||||
let amount = Amount::from_piconero(min_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("0.000000000001 XMR", monero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_monero_one() {
|
||||
let min_pics = 1000000000000;
|
||||
let amount = Amount::from_piconero(min_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("1.000000000000 XMR", monero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_monero_max() {
|
||||
let max_pics = 18_446_744_073_709_551_615;
|
||||
let amount = Amount::from_piconero(max_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("18446744.073709551615 XMR", monero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_min() {
|
||||
let monero_min = "0.000000000001";
|
||||
let amount = Amount::parse_monero(monero_min).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(1, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero() {
|
||||
let monero = "123";
|
||||
let amount = Amount::parse_monero(monero).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(123000000000000, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_max() {
|
||||
let monero = "18446744.073709551615";
|
||||
let amount = Amount::parse_monero(monero).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(18446744073709551615, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_overflows() {
|
||||
let overflow_pics = "18446744.073709551616";
|
||||
let error = Amount::parse_monero(overflow_pics).unwrap_err();
|
||||
assert_eq!(
|
||||
error.downcast_ref::<OverflowError>().unwrap(),
|
||||
&OverflowError(overflow_pics.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_bitcoin_to_trade() {
|
||||
// sanity check: if the asking price is 1 BTC / 1 XMR
|
||||
// and we have μ XMR + fee
|
||||
// then max BTC we can buy is μ
|
||||
let ask = bitcoin::Amount::from_btc(1.0).unwrap();
|
||||
|
||||
let xmr = Amount::parse_monero("1.0").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("0.5").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(0.5).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("2.5").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(2.5).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("420").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(420.0).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("0.00001").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(0.00001).unwrap());
|
||||
|
||||
// other ask prices
|
||||
|
||||
let ask = bitcoin::Amount::from_btc(0.5).unwrap();
|
||||
let xmr = Amount::parse_monero("2").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap());
|
||||
|
||||
let ask = bitcoin::Amount::from_btc(2.0).unwrap();
|
||||
let xmr = Amount::parse_monero("1").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(2.0).unwrap());
|
||||
|
||||
let ask = bitcoin::Amount::from_sat(382_900);
|
||||
let xmr = Amount::parse_monero("10").unwrap();
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_sat(3_827_851));
|
||||
|
||||
// example from https://github.com/comit-network/xmr-btc-swap/issues/1084
|
||||
// with rate from kraken at that time
|
||||
let ask = bitcoin::Amount::from_sat(685_800);
|
||||
let xmr = Amount::parse_monero("0.826286435921").unwrap();
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_sat(564_609));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_bitcoin_to_trade_overflow() {
|
||||
let xmr = Amount::from_monero(30.0).unwrap();
|
||||
let ask = bitcoin::Amount::from_sat(728_688);
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(bitcoin::Amount::from_sat(21_858_453), btc);
|
||||
|
||||
let xmr = Amount::from_piconero(u64::MAX);
|
||||
let ask = bitcoin::Amount::from_sat(u64::MAX);
|
||||
let btc = xmr.max_bitcoin_for_price(ask);
|
||||
|
||||
assert!(btc.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geting_max_bitcoin_to_trade_with_balance_smaller_than_locking_fee() {
|
||||
let ask = bitcoin::Amount::from_sat(382_900);
|
||||
let xmr = Amount::parse_monero("0.00001").unwrap();
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(bitcoin::Amount::ZERO, btc);
|
||||
}
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MoneroPrivateKey(
|
||||
#[serde(with = "swap_serde::monero::private_key")] ::monero::PrivateKey,
|
||||
);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MoneroAmount(#[serde(with = "swap_serde::monero::amount")] ::monero::Amount);
|
||||
|
||||
#[test]
|
||||
fn serde_monero_private_key_json() {
|
||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng)));
|
||||
let encoded = serde_json::to_vec(&key).unwrap();
|
||||
let decoded: MoneroPrivateKey = serde_json::from_slice(&encoded).unwrap();
|
||||
assert_eq!(key, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_monero_private_key_cbor() {
|
||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng)));
|
||||
let encoded = serde_cbor::to_vec(&key).unwrap();
|
||||
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(key, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_monero_amount() {
|
||||
let amount = MoneroAmount(::monero::Amount::from_pico(1000));
|
||||
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
||||
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(amount, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_basic() {
|
||||
// Test with balance larger than fee
|
||||
let balance = Amount::parse_monero("1.0").unwrap();
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(giveable.as_piconero(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_exact_fee() {
|
||||
// Test with balance exactly equal to fee
|
||||
let balance = CONSERVATIVE_MONERO_FEE;
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
assert_eq!(giveable, Amount::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_less_than_fee() {
|
||||
// Test with balance less than fee (should saturate to 0)
|
||||
let balance = Amount::from_piconero(CONSERVATIVE_MONERO_FEE.as_piconero() / 2);
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
assert_eq!(giveable, Amount::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_zero_balance() {
|
||||
// Test with zero balance
|
||||
let balance = Amount::ZERO;
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
assert_eq!(giveable, Amount::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_large_balance() {
|
||||
// Test with large balance
|
||||
let balance = Amount::parse_monero("100.0").unwrap();
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(giveable.as_piconero(), expected);
|
||||
|
||||
// Ensure the result makes sense
|
||||
assert!(giveable.as_piconero() > 0);
|
||||
assert!(giveable < balance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_basic() {
|
||||
// Test with 1 XMR amount to send
|
||||
let amount_to_send = Amount::parse_monero("1.0").unwrap();
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(min_balance.as_piconero(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_zero() {
|
||||
// Test with zero amount to send
|
||||
let amount_to_send = Amount::ZERO;
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
assert_eq!(min_balance, CONSERVATIVE_MONERO_FEE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_small_amount() {
|
||||
// Test with small amount
|
||||
let amount_to_send = Amount::from_piconero(1000);
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
let expected = 1000 + CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(min_balance.as_piconero(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_large_amount() {
|
||||
// Test with large amount
|
||||
let amount_to_send = Amount::parse_monero("50.0").unwrap();
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(min_balance.as_piconero(), expected);
|
||||
|
||||
// Ensure the result makes sense
|
||||
assert!(min_balance > amount_to_send);
|
||||
assert!(min_balance > CONSERVATIVE_MONERO_FEE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conservative_fee_functions_are_inverse() {
|
||||
// Test that the functions are somewhat inverse of each other
|
||||
let original_balance = Amount::parse_monero("5.0").unwrap();
|
||||
|
||||
// Get max giveable amount
|
||||
let max_giveable = original_balance.max_conservative_giveable();
|
||||
|
||||
// Calculate min balance needed to send that amount
|
||||
let min_balance_needed = max_giveable.min_conservative_balance_to_spend();
|
||||
|
||||
// The min balance needed should be equal to or slightly more than the original balance
|
||||
// (due to the conservative nature of the fee estimation)
|
||||
assert!(min_balance_needed >= original_balance);
|
||||
|
||||
// The difference should be at most the conservative fee
|
||||
let difference = min_balance_needed.as_piconero() - original_balance.as_piconero();
|
||||
assert!(difference <= CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conservative_fee_edge_cases() {
|
||||
// Test with maximum possible amount
|
||||
let max_amount = Amount::from_piconero(u64::MAX - CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
let giveable = max_amount.max_conservative_giveable();
|
||||
assert!(giveable.as_piconero() > 0);
|
||||
|
||||
// Test min balance calculation doesn't overflow
|
||||
let large_amount = Amount::from_piconero(u64::MAX / 2);
|
||||
let min_balance = large_amount.min_conservative_balance_to_spend();
|
||||
assert!(min_balance > large_amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labeled_monero_address_percentage_validation() {
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
let address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse::<monero::Address>().unwrap();
|
||||
|
||||
// Valid percentages should work (0-1 range)
|
||||
assert!(LabeledMoneroAddress::new(address, Decimal::ZERO, "test".to_string()).is_ok());
|
||||
assert!(LabeledMoneroAddress::new(address, Decimal::ONE, "test".to_string()).is_ok());
|
||||
assert!(LabeledMoneroAddress::new(address, Decimal::new(5, 1), "test".to_string()).is_ok()); // 0.5
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(9925, 4), "test".to_string()).is_ok()
|
||||
); // 0.9925
|
||||
|
||||
// Invalid percentages should fail
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(-1, 0), "test".to_string()).is_err()
|
||||
);
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(11, 1), "test".to_string()).is_err()
|
||||
); // 1.1
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(2, 0), "test".to_string()).is_err()
|
||||
); // 2.0
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::defaults::GetDefaults;
|
||||
use crate::env::{Mainnet, Testnet};
|
||||
use crate::prompt;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use config::ConfigError;
|
||||
use libp2p::core::Multiaddr;
|
||||
use rust_decimal::Decimal;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::defaults::{
|
||||
default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD,
|
||||
DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, default_rendezvous_points,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use console::Style;
|
||||
use dialoguer::Confirm;
|
||||
use dialoguer::{theme::ColorfulTheme, Input, Select};
|
||||
use dialoguer::{Input, Select, theme::ColorfulTheme};
|
||||
use libp2p::Multiaddr;
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
use url::Url;
|
||||
|
||||
/// Prompt user for data directory
|
||||
|
|
|
|||
|
|
@ -177,8 +177,9 @@ mod tests {
|
|||
.sell_quote(bitcoin::Amount::ONE_BTC)
|
||||
.unwrap();
|
||||
|
||||
let xmr_factor = xmr_no_spread.into().as_piconero_decimal()
|
||||
/ xmr_with_spread.into().as_piconero_decimal()
|
||||
let xmr_factor = Decimal::from_f64_retain(f64::from(xmr_no_spread.as_pico() as u32))
|
||||
.unwrap()
|
||||
/ Decimal::from_f64_retain(f64::from(xmr_with_spread.as_pico() as u32)).unwrap()
|
||||
- ONE;
|
||||
|
||||
assert!(xmr_with_spread < xmr_no_spread);
|
||||
|
|
|
|||
29
swap-machine/Cargo.toml
Normal file
29
swap-machine/Cargo.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "swap-machine"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
bitcoin-wallet = { path = "../bitcoin-wallet" }
|
||||
conquer-once = "0.4"
|
||||
curve25519-dalek = { workspace = true }
|
||||
ecdsa_fun = { workspace = true }
|
||||
libp2p = { workspace = true }
|
||||
monero = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
sigma_fun = { workspace = true, default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] }
|
||||
swap-core = { path = "../swap-core" }
|
||||
swap-env = { path = "../swap-env" }
|
||||
swap-serde = { path = "../swap-serde" }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
use crate::bitcoin::{
|
||||
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
||||
TxEarlyRefund, TxPunish, TxRedeem, TxRefund, Txid,
|
||||
};
|
||||
use crate::monero::wallet::{TransferRequest, WatchRequest};
|
||||
use crate::monero::BlockHeight;
|
||||
use crate::monero::TransferProof;
|
||||
use crate::monero_ext::ScalarExt;
|
||||
use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM};
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use crate::common::{CROSS_CURVE_PROOF_SYSTEM, Message0, Message1, Message2, Message3, Message4};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use swap_core::bitcoin::{
|
||||
CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, TxEarlyRefund,
|
||||
TxPunish, TxRedeem, TxRefund, Txid, current_epoch,
|
||||
};
|
||||
use swap_core::monero;
|
||||
use swap_core::monero::ScalarExt;
|
||||
use swap_core::monero::primitives::{BlockHeight, TransferProof, TransferRequest, WatchRequest};
|
||||
use swap_env::env::Config;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -49,7 +47,7 @@ pub enum AliceState {
|
|||
EncSigLearned {
|
||||
monero_wallet_restore_blockheight: BlockHeight,
|
||||
transfer_proof: TransferProof,
|
||||
encrypted_signature: Box<bitcoin::EncryptedSignature>,
|
||||
encrypted_signature: Box<swap_core::bitcoin::EncryptedSignature>,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcRedeemTransactionPublished {
|
||||
|
|
@ -87,6 +85,17 @@ pub enum AliceState {
|
|||
SafelyAborted,
|
||||
}
|
||||
|
||||
pub fn is_complete(state: &AliceState) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
AliceState::XmrRefunded
|
||||
| AliceState::BtcRedeemed
|
||||
| AliceState::BtcPunished { .. }
|
||||
| AliceState::SafelyAborted
|
||||
| AliceState::BtcEarlyRefunded(_)
|
||||
)
|
||||
}
|
||||
|
||||
impl fmt::Display for AliceState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
|
@ -118,13 +127,14 @@ impl fmt::Display for AliceState {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct State0 {
|
||||
a: bitcoin::SecretKey,
|
||||
a: swap_core::bitcoin::SecretKey,
|
||||
s_a: monero::Scalar,
|
||||
v_a: monero::PrivateViewKey,
|
||||
S_a_monero: monero::PublicKey,
|
||||
S_a_bitcoin: bitcoin::PublicKey,
|
||||
S_a_bitcoin: swap_core::bitcoin::PublicKey,
|
||||
dleq_proof_s_a: CrossCurveDLEQProof,
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
|
|
@ -151,7 +161,7 @@ impl State0 {
|
|||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let a = bitcoin::SecretKey::new_random(rng);
|
||||
let a = swap_core::bitcoin::SecretKey::new_random(rng);
|
||||
let v_a = monero::PrivateViewKey::new_random(rng);
|
||||
|
||||
let s_a = monero::Scalar::random(rng);
|
||||
|
|
@ -224,15 +234,16 @@ impl State0 {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct State1 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: monero::Scalar,
|
||||
a: swap_core::bitcoin::SecretKey,
|
||||
B: swap_core::bitcoin::PublicKey,
|
||||
s_a: swap_core::monero::Scalar,
|
||||
S_a_monero: monero::PublicKey,
|
||||
S_a_bitcoin: bitcoin::PublicKey,
|
||||
S_a_bitcoin: swap_core::bitcoin::PublicKey,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
S_b_bitcoin: swap_core::bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
v_a: monero::PrivateViewKey,
|
||||
dleq_proof_s_a: CrossCurveDLEQProof,
|
||||
|
|
@ -265,7 +276,8 @@ impl State1 {
|
|||
}
|
||||
|
||||
pub fn receive(self, msg: Message2) -> Result<State2> {
|
||||
let tx_lock = bitcoin::TxLock::from_psbt(msg.psbt, self.a.public(), self.B, self.btc)
|
||||
let tx_lock =
|
||||
swap_core::bitcoin::TxLock::from_psbt(msg.psbt, self.a.public(), self.B, self.btc)
|
||||
.context("Failed to re-construct TxLock from received PSBT")?;
|
||||
|
||||
Ok(State2 {
|
||||
|
|
@ -291,13 +303,14 @@ impl State1 {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct State2 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
a: swap_core::bitcoin::SecretKey,
|
||||
B: swap_core::bitcoin::PublicKey,
|
||||
s_a: monero::Scalar,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
S_b_bitcoin: swap_core::bitcoin::PublicKey,
|
||||
v: monero::PrivateViewKey,
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
|
|
@ -306,7 +319,7 @@ pub struct State2 {
|
|||
refund_address: bitcoin::Address,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
tx_lock: bitcoin::TxLock,
|
||||
tx_lock: swap_core::bitcoin::TxLock,
|
||||
tx_redeem_fee: bitcoin::Amount,
|
||||
tx_punish_fee: bitcoin::Amount,
|
||||
tx_refund_fee: bitcoin::Amount,
|
||||
|
|
@ -315,7 +328,7 @@ pub struct State2 {
|
|||
|
||||
impl State2 {
|
||||
pub fn next_message(&self) -> Message3 {
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
let tx_cancel = swap_core::bitcoin::TxCancel::new(
|
||||
&self.tx_lock,
|
||||
self.cancel_timelock,
|
||||
self.a.public(),
|
||||
|
|
@ -325,7 +338,7 @@ impl State2 {
|
|||
.expect("valid cancel tx");
|
||||
|
||||
let tx_refund =
|
||||
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
||||
swap_core::bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
||||
// Alice encsigns the refund transaction(bitcoin) digest with Bob's monero
|
||||
// pubkey(S_b). The refund transaction spends the output of
|
||||
// tx_lock_bitcoin to Bob's refund address.
|
||||
|
|
@ -342,7 +355,7 @@ impl State2 {
|
|||
|
||||
pub fn receive(self, msg: Message4) -> Result<State3> {
|
||||
// Create the TxCancel transaction ourself
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
let tx_cancel = swap_core::bitcoin::TxCancel::new(
|
||||
&self.tx_lock,
|
||||
self.cancel_timelock,
|
||||
self.a.public(),
|
||||
|
|
@ -351,11 +364,11 @@ impl State2 {
|
|||
)?;
|
||||
|
||||
// Check if the provided signature by Bob is valid for the transaction
|
||||
bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig)
|
||||
swap_core::bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig)
|
||||
.context("Failed to verify cancel transaction")?;
|
||||
|
||||
// Create the TxPunish transaction ourself
|
||||
let tx_punish = bitcoin::TxPunish::new(
|
||||
let tx_punish = swap_core::bitcoin::TxPunish::new(
|
||||
&tx_cancel,
|
||||
&self.punish_address,
|
||||
self.punish_timelock,
|
||||
|
|
@ -363,15 +376,22 @@ impl State2 {
|
|||
);
|
||||
|
||||
// Check if the provided signature by Bob is valid for the transaction
|
||||
bitcoin::verify_sig(&self.B, &tx_punish.digest(), &msg.tx_punish_sig)
|
||||
swap_core::bitcoin::verify_sig(&self.B, &tx_punish.digest(), &msg.tx_punish_sig)
|
||||
.context("Failed to verify punish transaction")?;
|
||||
|
||||
// Create the TxEarlyRefund transaction ourself
|
||||
let tx_early_refund =
|
||||
bitcoin::TxEarlyRefund::new(&self.tx_lock, &self.refund_address, self.tx_refund_fee);
|
||||
let tx_early_refund = swap_core::bitcoin::TxEarlyRefund::new(
|
||||
&self.tx_lock,
|
||||
&self.refund_address,
|
||||
self.tx_refund_fee,
|
||||
);
|
||||
|
||||
// Check if the provided signature by Bob is valid for the transaction
|
||||
bitcoin::verify_sig(&self.B, &tx_early_refund.digest(), &msg.tx_early_refund_sig)
|
||||
swap_core::bitcoin::verify_sig(
|
||||
&self.B,
|
||||
&tx_early_refund.digest(),
|
||||
&msg.tx_early_refund_sig,
|
||||
)
|
||||
.context("Failed to verify early refund transaction")?;
|
||||
|
||||
Ok(State3 {
|
||||
|
|
@ -400,13 +420,14 @@ impl State2 {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State3 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
a: swap_core::bitcoin::SecretKey,
|
||||
B: swap_core::bitcoin::PublicKey,
|
||||
pub s_a: monero::Scalar,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
S_b_bitcoin: swap_core::bitcoin::PublicKey,
|
||||
pub v: monero::PrivateViewKey,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
pub btc: bitcoin::Amount,
|
||||
|
|
@ -419,9 +440,9 @@ pub struct State3 {
|
|||
redeem_address: bitcoin::Address,
|
||||
#[serde(with = "swap_serde::bitcoin::address_serde")]
|
||||
punish_address: bitcoin::Address,
|
||||
pub tx_lock: bitcoin::TxLock,
|
||||
tx_punish_sig_bob: bitcoin::Signature,
|
||||
tx_cancel_sig_bob: bitcoin::Signature,
|
||||
pub tx_lock: swap_core::bitcoin::TxLock,
|
||||
tx_punish_sig_bob: swap_core::bitcoin::Signature,
|
||||
tx_cancel_sig_bob: swap_core::bitcoin::Signature,
|
||||
/// This field was added in this pull request:
|
||||
/// https://github.com/eigenwallet/core/pull/344
|
||||
///
|
||||
|
|
@ -432,7 +453,7 @@ pub struct State3 {
|
|||
/// to allow Alice to refund the Bitcoin early. If it is not present, Bob will have
|
||||
/// to wait for the timelock to expire.
|
||||
#[serde(default)]
|
||||
tx_early_refund_sig_bob: Option<bitcoin::Signature>,
|
||||
tx_early_refund_sig_bob: Option<swap_core::bitcoin::Signature>,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
tx_redeem_fee: bitcoin::Amount,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
|
|
@ -446,7 +467,7 @@ pub struct State3 {
|
|||
impl State3 {
|
||||
pub async fn expired_timelocks(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
let tx_cancel = self.tx_cancel();
|
||||
|
||||
|
|
@ -505,7 +526,11 @@ impl State3 {
|
|||
}
|
||||
|
||||
pub fn tx_refund(&self) -> TxRefund {
|
||||
bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address, self.tx_refund_fee)
|
||||
swap_core::bitcoin::TxRefund::new(
|
||||
&self.tx_cancel(),
|
||||
&self.refund_address,
|
||||
self.tx_refund_fee,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn tx_redeem(&self) -> TxRedeem {
|
||||
|
|
@ -513,24 +538,30 @@ impl State3 {
|
|||
}
|
||||
|
||||
pub fn tx_early_refund(&self) -> TxEarlyRefund {
|
||||
bitcoin::TxEarlyRefund::new(&self.tx_lock, &self.refund_address, self.tx_refund_fee)
|
||||
swap_core::bitcoin::TxEarlyRefund::new(
|
||||
&self.tx_lock,
|
||||
&self.refund_address,
|
||||
self.tx_refund_fee,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn extract_monero_private_key(
|
||||
&self,
|
||||
published_refund_tx: Arc<bitcoin::Transaction>,
|
||||
) -> Result<monero::PrivateKey> {
|
||||
Ok(monero::PrivateKey::from_scalar(
|
||||
self.tx_refund().extract_monero_private_key(
|
||||
published_refund_tx,
|
||||
self.s_a,
|
||||
self.a.clone(),
|
||||
self.S_b_bitcoin,
|
||||
)
|
||||
)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn check_for_tx_cancel(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Option<Arc<Transaction>>> {
|
||||
let tx_cancel = self.tx_cancel();
|
||||
let tx = bitcoin_wallet
|
||||
|
|
@ -543,7 +574,7 @@ impl State3 {
|
|||
|
||||
pub async fn fetch_tx_refund(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Option<Arc<Transaction>>> {
|
||||
let tx_refund = self.tx_refund();
|
||||
let tx = bitcoin_wallet
|
||||
|
|
@ -554,59 +585,19 @@ impl State3 {
|
|||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
|
||||
pub async fn submit_tx_cancel(
|
||||
&self,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Txid> {
|
||||
let transaction = self.signed_cancel_transaction()?;
|
||||
let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
|
||||
Ok(tx_id)
|
||||
}
|
||||
|
||||
pub async fn refund_xmr(
|
||||
pub async fn punish_btc(
|
||||
&self,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
swap_id: Uuid,
|
||||
spend_key: monero::PrivateKey,
|
||||
transfer_proof: TransferProof,
|
||||
) -> Result<()> {
|
||||
let view_key = self.v;
|
||||
|
||||
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
|
||||
// on the lock transaction.
|
||||
tracing::info!("Waiting for Monero lock transaction to be confirmed");
|
||||
let transfer_proof_2 = transfer_proof.clone();
|
||||
monero_wallet
|
||||
.wait_until_confirmed(
|
||||
self.lock_xmr_watch_request(transfer_proof_2, 10),
|
||||
Some(move |(confirmations, target_confirmations)| {
|
||||
tracing::debug!(
|
||||
%confirmations,
|
||||
%target_confirmations,
|
||||
"Monero lock transaction got a confirmation"
|
||||
);
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("Failed to wait for Monero lock transaction to be confirmed")?;
|
||||
|
||||
tracing::info!("Refunding Monero");
|
||||
|
||||
tracing::debug!(%swap_id, "Opening temporary Monero wallet from keys");
|
||||
let swap_wallet = monero_wallet
|
||||
.swap_wallet(swap_id, spend_key, view_key, transfer_proof.tx_hash())
|
||||
.await
|
||||
.context(format!("Failed to open/create swap wallet `{}`", swap_id))?;
|
||||
|
||||
tracing::debug!(%swap_id, "Sweeping Monero to redeem address");
|
||||
let main_address = monero_wallet.main_wallet().await.main_address().await;
|
||||
|
||||
swap_wallet
|
||||
.sweep(&main_address)
|
||||
.await
|
||||
.context("Failed to sweep Monero to redeem address")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn punish_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<Txid> {
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Txid> {
|
||||
let signed_tx_punish = self.signed_punish_transaction()?;
|
||||
|
||||
let (txid, subscription) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
|
||||
|
|
@ -617,9 +608,9 @@ impl State3 {
|
|||
|
||||
pub fn signed_redeem_transaction(
|
||||
&self,
|
||||
sig: bitcoin::EncryptedSignature,
|
||||
sig: swap_core::bitcoin::EncryptedSignature,
|
||||
) -> Result<bitcoin::Transaction> {
|
||||
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee)
|
||||
swap_core::bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee)
|
||||
.complete(sig, self.a.clone(), self.s_a.to_secpfun_scalar(), self.B)
|
||||
.context("Failed to complete Bitcoin redeem transaction")
|
||||
}
|
||||
|
|
@ -653,7 +644,7 @@ impl State3 {
|
|||
}
|
||||
|
||||
fn tx_punish(&self) -> TxPunish {
|
||||
bitcoin::TxPunish::new(
|
||||
swap_core::bitcoin::TxPunish::new(
|
||||
&self.tx_cancel(),
|
||||
&self.punish_address,
|
||||
self.punish_timelock,
|
||||
|
|
@ -663,9 +654,11 @@ impl State3 {
|
|||
|
||||
pub async fn watch_for_btc_tx_refund(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<monero::PrivateKey> {
|
||||
let tx_refund_status = bitcoin_wallet.subscribe_to(self.tx_refund()).await;
|
||||
let tx_refund_status = bitcoin_wallet
|
||||
.subscribe_to(Box::new(self.tx_refund()))
|
||||
.await;
|
||||
|
||||
tx_refund_status
|
||||
.wait_until_seen()
|
||||
|
|
@ -1,17 +1,9 @@
|
|||
use crate::bitcoin::wallet::{EstimateFeeRate, Subscription};
|
||||
use crate::bitcoin::{
|
||||
self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
||||
TxLock, Txid, Wallet,
|
||||
};
|
||||
use crate::monero::wallet::WatchRequest;
|
||||
use crate::monero::TransferProof;
|
||||
use crate::monero::{self, MoneroAddressPool, TxHash};
|
||||
use crate::monero_ext::ScalarExt;
|
||||
use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use crate::common::{CROSS_CURVE_PROOF_SYSTEM, Message0, Message1, Message2, Message3, Message4};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use bitcoin_wallet::primitives::Subscription;
|
||||
use ecdsa_fun::Signature;
|
||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||
use ecdsa_fun::nonce::Deterministic;
|
||||
use ecdsa_fun::Signature;
|
||||
use monero::BlockHeight;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -19,6 +11,13 @@ use sha2::Sha256;
|
|||
use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use swap_core::bitcoin::{
|
||||
self, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, TxLock, Txid,
|
||||
current_epoch,
|
||||
};
|
||||
use swap_core::monero::ScalarExt;
|
||||
use swap_core::monero::primitives::WatchRequest;
|
||||
use swap_core::monero::{self, TransferProof};
|
||||
use swap_serde::bitcoin::address_serde;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -98,7 +97,7 @@ impl BobState {
|
|||
/// Depending on the State, there are no locks to expire.
|
||||
pub async fn expired_timelocks(
|
||||
&self,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
bitcoin_wallet: Arc<dyn bitcoin_wallet::BitcoinWallet>,
|
||||
) -> Result<Option<ExpiredTimelocks>> {
|
||||
Ok(match self.clone() {
|
||||
BobState::Started { .. }
|
||||
|
|
@ -107,16 +106,16 @@ impl BobState {
|
|||
| BobState::SwapSetupCompleted(_) => None,
|
||||
BobState::BtcLocked { state3: state, .. }
|
||||
| BobState::XmrLockProofReceived { state, .. } => {
|
||||
Some(state.expired_timelock(&bitcoin_wallet).await?)
|
||||
Some(state.expired_timelock(bitcoin_wallet.as_ref()).await?)
|
||||
}
|
||||
BobState::XmrLocked(state) | BobState::EncSigSent(state) => {
|
||||
Some(state.expired_timelock(&bitcoin_wallet).await?)
|
||||
Some(state.expired_timelock(bitcoin_wallet.as_ref()).await?)
|
||||
}
|
||||
BobState::CancelTimelockExpired(state)
|
||||
| BobState::BtcCancelled(state)
|
||||
| BobState::BtcRefundPublished(state)
|
||||
| BobState::BtcEarlyRefundPublished(state) => {
|
||||
Some(state.expired_timelock(&bitcoin_wallet).await?)
|
||||
Some(state.expired_timelock(bitcoin_wallet.as_ref()).await?)
|
||||
}
|
||||
BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish),
|
||||
BobState::BtcRefunded(_)
|
||||
|
|
@ -127,6 +126,17 @@ impl BobState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(state: &BobState) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
BobState::BtcRefunded(..)
|
||||
| BobState::BtcEarlyRefunded { .. }
|
||||
| BobState::XmrRedeemed { .. }
|
||||
| BobState::SafelyAborted
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct State0 {
|
||||
swap_id: Uuid,
|
||||
|
|
@ -207,10 +217,7 @@ impl State0 {
|
|||
|
||||
pub async fn receive(
|
||||
self,
|
||||
wallet: &bitcoin::Wallet<
|
||||
bdk_wallet::rusqlite::Connection,
|
||||
impl EstimateFeeRate + Send + Sync + 'static,
|
||||
>,
|
||||
wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
msg: Message1,
|
||||
) -> Result<State1> {
|
||||
let valid = CROSS_CURVE_PROOF_SYSTEM.verify(
|
||||
|
|
@ -228,7 +235,7 @@ impl State0 {
|
|||
bail!("Alice's dleq proof doesn't verify")
|
||||
}
|
||||
|
||||
let tx_lock = bitcoin::TxLock::new(
|
||||
let tx_lock = swap_core::bitcoin::TxLock::new(
|
||||
wallet,
|
||||
self.btc,
|
||||
self.tx_lock_fee,
|
||||
|
|
@ -262,6 +269,7 @@ impl State0 {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug)]
|
||||
pub struct State1 {
|
||||
A: bitcoin::PublicKey,
|
||||
|
|
@ -336,6 +344,7 @@ impl State1 {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State2 {
|
||||
A: bitcoin::PublicKey,
|
||||
|
|
@ -427,6 +436,7 @@ impl State2 {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct State3 {
|
||||
A: bitcoin::PublicKey,
|
||||
|
|
@ -525,7 +535,7 @@ impl State3 {
|
|||
|
||||
pub async fn expired_timelock(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
let tx_cancel = TxCancel::new(
|
||||
&self.tx_lock,
|
||||
|
|
@ -552,7 +562,7 @@ impl State3 {
|
|||
|
||||
pub async fn check_for_tx_early_refund(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Option<Arc<Transaction>>> {
|
||||
let tx_early_refund = self.construct_tx_early_refund();
|
||||
let tx = bitcoin_wallet
|
||||
|
|
@ -564,6 +574,7 @@ impl State3 {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State4 {
|
||||
A: bitcoin::PublicKey,
|
||||
|
|
@ -594,7 +605,7 @@ pub struct State4 {
|
|||
impl State4 {
|
||||
pub async fn check_for_tx_redeem(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Option<State5>> {
|
||||
let tx_redeem =
|
||||
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee);
|
||||
|
|
@ -606,7 +617,9 @@ impl State4 {
|
|||
let tx_redeem_sig =
|
||||
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
|
||||
let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
|
||||
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
|
||||
let s_a = monero::PrivateKey::from_scalar(monero::private_key_from_secp256k1_scalar(
|
||||
s_a.into(),
|
||||
));
|
||||
|
||||
Ok(Some(State5 {
|
||||
s_a,
|
||||
|
|
@ -628,12 +641,15 @@ impl State4 {
|
|||
self.b.encsign(self.S_a_bitcoin, tx_redeem.digest())
|
||||
}
|
||||
|
||||
pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State5> {
|
||||
pub async fn watch_for_redeem_btc(
|
||||
&self,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<State5> {
|
||||
let tx_redeem =
|
||||
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee);
|
||||
|
||||
bitcoin_wallet
|
||||
.subscribe_to(tx_redeem.clone())
|
||||
.subscribe_to(Box::new(tx_redeem))
|
||||
.await
|
||||
.wait_until_seen()
|
||||
.await?;
|
||||
|
|
@ -647,7 +663,7 @@ impl State4 {
|
|||
|
||||
pub async fn expired_timelock(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
let tx_cancel = TxCancel::new(
|
||||
&self.tx_lock,
|
||||
|
|
@ -716,13 +732,13 @@ impl State5 {
|
|||
self.tx_lock.txid()
|
||||
}
|
||||
|
||||
pub fn lock_xmr_watch_request_for_sweep(&self) -> monero::wallet::WatchRequest {
|
||||
pub fn lock_xmr_watch_request_for_sweep(&self) -> swap_core::monero::primitives::WatchRequest {
|
||||
let S_b_monero =
|
||||
monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b));
|
||||
let S_a_monero = monero::PublicKey::from_private_key(&self.s_a);
|
||||
let S = S_a_monero + S_b_monero;
|
||||
|
||||
monero::wallet::WatchRequest {
|
||||
swap_core::monero::primitives::WatchRequest {
|
||||
public_spend_key: S,
|
||||
public_view_key: self.v.public(),
|
||||
transfer_proof: self.lock_transfer_proof.clone(),
|
||||
|
|
@ -732,50 +748,9 @@ impl State5 {
|
|||
expected_amount: self.xmr.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn redeem_xmr(
|
||||
&self,
|
||||
monero_wallet: &monero::Wallets,
|
||||
swap_id: Uuid,
|
||||
monero_receive_pool: MoneroAddressPool,
|
||||
) -> Result<Vec<TxHash>> {
|
||||
let (spend_key, view_key) = self.xmr_keys();
|
||||
|
||||
tracing::info!(%swap_id, "Redeeming Monero from extracted keys");
|
||||
|
||||
tracing::debug!(%swap_id, "Opening temporary Monero wallet");
|
||||
|
||||
let wallet = monero_wallet
|
||||
.swap_wallet(
|
||||
swap_id,
|
||||
spend_key,
|
||||
view_key,
|
||||
self.lock_transfer_proof.tx_hash(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to open Monero wallet")?;
|
||||
|
||||
tracing::debug!(%swap_id, receive_address=?monero_receive_pool, "Sweeping Monero to receive address");
|
||||
|
||||
let main_address = monero_wallet.main_wallet().await.main_address().await;
|
||||
|
||||
let tx_hashes = wallet
|
||||
.sweep_multi_destination(
|
||||
&monero_receive_pool.fill_empty_addresses(main_address),
|
||||
&monero_receive_pool.percentages(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to redeem Monero")?
|
||||
.into_iter()
|
||||
.map(|tx_receipt| TxHash(tx_receipt.txid))
|
||||
.collect();
|
||||
|
||||
tracing::info!(%swap_id, txids=?tx_hashes, "Monero sweep completed");
|
||||
|
||||
Ok(tx_hashes)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct State6 {
|
||||
A: bitcoin::PublicKey,
|
||||
|
|
@ -800,7 +775,7 @@ pub struct State6 {
|
|||
impl State6 {
|
||||
pub async fn expired_timelock(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
let tx_cancel = TxCancel::new(
|
||||
&self.tx_lock,
|
||||
|
|
@ -833,7 +808,7 @@ impl State6 {
|
|||
|
||||
pub async fn check_for_tx_cancel(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Option<Arc<Transaction>>> {
|
||||
let tx_cancel = self.construct_tx_cancel()?;
|
||||
|
||||
|
|
@ -847,7 +822,7 @@ impl State6 {
|
|||
|
||||
pub async fn submit_tx_cancel(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<(Txid, Subscription)> {
|
||||
let transaction = self
|
||||
.construct_tx_cancel()?
|
||||
|
|
@ -861,7 +836,7 @@ impl State6 {
|
|||
|
||||
pub async fn publish_refund_btc(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<bitcoin::Txid> {
|
||||
let signed_tx_refund = self.signed_refund_transaction()?;
|
||||
let signed_tx_refund_txid = signed_tx_refund.compute_txid();
|
||||
|
|
@ -922,7 +897,7 @@ impl State6 {
|
|||
|
||||
pub async fn check_for_tx_early_refund(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet,
|
||||
) -> Result<Option<Arc<Transaction>>> {
|
||||
let tx_early_refund = self.construct_tx_early_refund();
|
||||
|
||||
169
swap-machine/src/common/mod.rs
Normal file
169
swap-machine/src/common/mod.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use crate::alice::AliceState;
|
||||
use crate::alice::is_complete as alice_is_complete;
|
||||
use crate::bob::BobState;
|
||||
use crate::bob::is_complete as bob_is_complete;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use conquer_once::Lazy;
|
||||
use libp2p::{Multiaddr, PeerId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use sigma_fun::HashTranscript;
|
||||
use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof};
|
||||
use std::convert::TryInto;
|
||||
use swap_core::bitcoin;
|
||||
use swap_core::monero::{self, MoneroAddressPool};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub static CROSS_CURVE_PROOF_SYSTEM: Lazy<
|
||||
CrossCurveDLEQ<HashTranscript<Sha256, rand_chacha::ChaCha20Rng>>,
|
||||
> = Lazy::new(|| {
|
||||
CrossCurveDLEQ::<HashTranscript<Sha256, rand_chacha::ChaCha20Rng>>::new(
|
||||
(*ecdsa_fun::fun::G).normalize(),
|
||||
curve25519_dalek::constants::ED25519_BASEPOINT_POINT,
|
||||
)
|
||||
});
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message0 {
|
||||
pub swap_id: Uuid,
|
||||
pub B: bitcoin::PublicKey,
|
||||
pub S_b_monero: monero::PublicKey,
|
||||
pub S_b_bitcoin: bitcoin::PublicKey,
|
||||
pub dleq_proof_s_b: CrossCurveDLEQProof,
|
||||
pub v_b: monero::PrivateViewKey,
|
||||
#[serde(with = "swap_serde::bitcoin::address_serde")]
|
||||
pub refund_address: bitcoin::Address,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
pub tx_refund_fee: bitcoin::Amount,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
pub tx_cancel_fee: bitcoin::Amount,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message1 {
|
||||
pub A: bitcoin::PublicKey,
|
||||
pub S_a_monero: monero::PublicKey,
|
||||
pub S_a_bitcoin: bitcoin::PublicKey,
|
||||
pub dleq_proof_s_a: CrossCurveDLEQProof,
|
||||
pub v_a: monero::PrivateViewKey,
|
||||
#[serde(with = "swap_serde::bitcoin::address_serde")]
|
||||
pub redeem_address: bitcoin::Address,
|
||||
#[serde(with = "swap_serde::bitcoin::address_serde")]
|
||||
pub punish_address: bitcoin::Address,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
pub tx_redeem_fee: bitcoin::Amount,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
pub tx_punish_fee: bitcoin::Amount,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message2 {
|
||||
pub psbt: bitcoin::PartiallySignedTransaction,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message3 {
|
||||
pub tx_cancel_sig: bitcoin::Signature,
|
||||
pub tx_refund_encsig: bitcoin::EncryptedSignature,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message4 {
|
||||
pub tx_punish_sig: bitcoin::Signature,
|
||||
pub tx_cancel_sig: bitcoin::Signature,
|
||||
pub tx_early_refund_sig: bitcoin::Signature,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum State {
|
||||
Alice(AliceState),
|
||||
Bob(BobState),
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn swap_finished(&self) -> bool {
|
||||
match self {
|
||||
State::Alice(state) => alice_is_complete(state),
|
||||
State::Bob(state) => bob_is_complete(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AliceState> for State {
|
||||
fn from(alice: AliceState) -> Self {
|
||||
Self::Alice(alice)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BobState> for State {
|
||||
fn from(bob: BobState) -> Self {
|
||||
Self::Bob(bob)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[error("Not in the role of Alice")]
|
||||
pub struct NotAlice;
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[error("Not in the role of Bob")]
|
||||
pub struct NotBob;
|
||||
|
||||
impl TryInto<BobState> for State {
|
||||
type Error = NotBob;
|
||||
|
||||
fn try_into(self) -> std::result::Result<BobState, Self::Error> {
|
||||
match self {
|
||||
State::Alice(_) => Err(NotBob),
|
||||
State::Bob(state) => Ok(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<AliceState> for State {
|
||||
type Error = NotAlice;
|
||||
|
||||
fn try_into(self) -> std::result::Result<AliceState, Self::Error> {
|
||||
match self {
|
||||
State::Alice(state) => Ok(state),
|
||||
State::Bob(_) => Err(NotAlice),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Database {
|
||||
async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()>;
|
||||
async fn get_peer_id(&self, swap_id: Uuid) -> Result<PeerId>;
|
||||
async fn insert_monero_address_pool(
|
||||
&self,
|
||||
swap_id: Uuid,
|
||||
address: MoneroAddressPool,
|
||||
) -> Result<()>;
|
||||
async fn get_monero_address_pool(&self, swap_id: Uuid) -> Result<MoneroAddressPool>;
|
||||
async fn get_monero_addresses(&self) -> Result<Vec<::monero::Address>>;
|
||||
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>;
|
||||
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>>;
|
||||
async fn get_all_peer_addresses(&self) -> Result<Vec<(PeerId, Vec<Multiaddr>)>>;
|
||||
async fn get_swap_start_date(&self, swap_id: Uuid) -> Result<String>;
|
||||
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>;
|
||||
async fn get_state(&self, swap_id: Uuid) -> Result<State>;
|
||||
async fn get_states(&self, swap_id: Uuid) -> Result<Vec<State>>;
|
||||
async fn all(&self) -> Result<Vec<(Uuid, State)>>;
|
||||
async fn insert_buffered_transfer_proof(
|
||||
&self,
|
||||
swap_id: Uuid,
|
||||
proof: monero::TransferProof,
|
||||
) -> Result<()>;
|
||||
async fn get_buffered_transfer_proof(
|
||||
&self,
|
||||
swap_id: Uuid,
|
||||
) -> Result<Option<monero::TransferProof>>;
|
||||
}
|
||||
3
swap-machine/src/lib.rs
Normal file
3
swap-machine/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod alice;
|
||||
pub mod bob;
|
||||
pub mod common;
|
||||
|
|
@ -8,7 +8,6 @@ name = "orchestrator"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
chrono = "0.4.41"
|
||||
compose_spec = "0.3.0"
|
||||
|
|
@ -18,6 +17,7 @@ serde_yaml = "0.9.34"
|
|||
swap-env = { path = "../swap-env" }
|
||||
toml = { workspace = true }
|
||||
url = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ mod images;
|
|||
mod prompt;
|
||||
|
||||
use crate::compose::{
|
||||
IntoSpec, OrchestratorDirectories, OrchestratorImage, OrchestratorImages, OrchestratorInput,
|
||||
OrchestratorNetworks, ASB_DATA_DIR, DOCKER_COMPOSE_FILE,
|
||||
ASB_DATA_DIR, DOCKER_COMPOSE_FILE, IntoSpec, OrchestratorDirectories, OrchestratorImage,
|
||||
OrchestratorImages, OrchestratorInput, OrchestratorNetworks,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use swap_env::config::{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use dialoguer::{theme::ColorfulTheme, Select};
|
||||
use dialoguer::{Select, theme::ColorfulTheme};
|
||||
use swap_env::prompt as config_prompt;
|
||||
use url::Url;
|
||||
|
||||
|
|
|
|||
12
swap-proptest/Cargo.toml
Normal file
12
swap-proptest/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "swap-proptest"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { workspace = true }
|
||||
ecdsa_fun = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
pub mod urls {
|
||||
use serde::de::Unexpected;
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
use serde::{Deserialize, Deserializer, de};
|
||||
use serde_json::Value;
|
||||
use url::Url;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
pub mod multiaddresses {
|
||||
use libp2p::Multiaddr;
|
||||
use serde::de::Unexpected;
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
use serde::{Deserialize, Deserializer, de};
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Multiaddr>, D::Error>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ pub enum network {
|
|||
|
||||
pub mod private_key {
|
||||
use hex;
|
||||
use monero::consensus::{Decodable, Encodable};
|
||||
use monero::PrivateKey;
|
||||
use monero::consensus::{Decodable, Encodable};
|
||||
use serde::de::Visitor;
|
||||
use serde::ser::Error;
|
||||
use serde::{de, Deserializer, Serializer};
|
||||
use serde::{Deserializer, Serializer, de};
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ pub mod amount {
|
|||
}
|
||||
|
||||
pub mod address {
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -22,23 +22,24 @@ bdk_wallet = { workspace = true, features = ["rusqlite", "test-utils"] }
|
|||
anyhow = { workspace = true }
|
||||
arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service"] }
|
||||
async-compression = { version = "0.3", features = ["bzip2", "tokio"] }
|
||||
async-trait = "0.1"
|
||||
async-trait = { workspace = true }
|
||||
asynchronous-codec = "0.7.0"
|
||||
atty = "0.2"
|
||||
backoff = { workspace = true }
|
||||
base64 = "0.22"
|
||||
big-bytes = "1"
|
||||
bitcoin = { workspace = true }
|
||||
bitcoin-wallet = { path = "../bitcoin-wallet" }
|
||||
bmrng = "0.5.2"
|
||||
comfy-table = "7.1"
|
||||
config = { version = "0.14", default-features = false, features = ["toml"] }
|
||||
conquer-once = "0.4"
|
||||
curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" }
|
||||
curve25519-dalek = { workspace = true }
|
||||
data-encoding = "2.6"
|
||||
derive_builder = "0.20.2"
|
||||
dfx-swiss-sdk = { workspace = true, optional = true }
|
||||
dialoguer = "0.11"
|
||||
ecdsa_fun = { version = "0.10", default-features = false, features = ["libsecp_compat", "serde", "adaptor"] }
|
||||
ecdsa_fun = { workspace = true, features = ["libsecp_compat", "serde", "adaptor"] }
|
||||
ed25519-dalek = "1"
|
||||
electrum-pool = { path = "../electrum-pool" }
|
||||
fns = "0.0.7"
|
||||
|
|
@ -57,7 +58,7 @@ once_cell = { workspace = true }
|
|||
pem = "3.0"
|
||||
proptest = "1"
|
||||
rand = { workspace = true }
|
||||
rand_chacha = "0.3"
|
||||
rand_chacha = { workspace = true }
|
||||
regex = "1.10"
|
||||
reqwest = { workspace = true, features = ["http2", "rustls-tls-native-roots", "stream", "socks"] }
|
||||
rust_decimal = { version = "1", features = ["serde-float"] }
|
||||
|
|
@ -68,15 +69,17 @@ serde = { workspace = true }
|
|||
serde_cbor = "0.11"
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { version = "1", features = ["macros"] }
|
||||
sha2 = "0.10"
|
||||
sigma_fun = { version = "0.7", default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] }
|
||||
sha2 = { workspace = true }
|
||||
sigma_fun = { workspace = true, default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] }
|
||||
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||
structopt = "0.3"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
swap-controller-api = { path = "../swap-controller-api" }
|
||||
swap-core = { path = "../swap-core" }
|
||||
swap-env = { path = "../swap-env" }
|
||||
swap-feed = { path = "../swap-feed" }
|
||||
swap-fs = { path = "../swap-fs" }
|
||||
swap-machine = { path = "../swap-machine" }
|
||||
swap-serde = { path = "../swap-serde" }
|
||||
tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false }
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use self::quote::{
|
|||
make_quote, unlocked_monero_balance_with_timeout, QuoteCacheKey, QUOTE_CACHE_TTL,
|
||||
};
|
||||
use crate::asb::{Behaviour, OutEvent};
|
||||
use crate::monero;
|
||||
use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason;
|
||||
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
|
||||
use crate::network::quote::BidQuote;
|
||||
|
|
@ -10,8 +11,8 @@ use crate::network::transfer_proof;
|
|||
use crate::protocol::alice::swap::has_already_processed_enc_sig;
|
||||
use crate::protocol::alice::{AliceState, State3, Swap, TipConfig};
|
||||
use crate::protocol::{Database, State};
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use futures::future;
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
|
@ -26,6 +27,7 @@ use std::fmt::Debug;
|
|||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use swap_core::bitcoin;
|
||||
use swap_env::env;
|
||||
use swap_feed::LatestRate;
|
||||
use tokio::fs::{write, File};
|
||||
|
|
@ -41,7 +43,7 @@ where
|
|||
{
|
||||
swarm: libp2p::Swarm<Behaviour<LR>>,
|
||||
env_config: env::Config,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
latest_rate: LR,
|
||||
|
|
@ -132,7 +134,7 @@ where
|
|||
pub fn new(
|
||||
swarm: Swarm<Behaviour<LR>>,
|
||||
env_config: env::Config,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
latest_rate: LR,
|
||||
|
|
@ -245,7 +247,7 @@ where
|
|||
}
|
||||
};
|
||||
|
||||
let wallet_snapshot = match WalletSnapshot::capture(&self.bitcoin_wallet, &self.monero_wallet, &self.external_redeem_address, btc).await {
|
||||
let wallet_snapshot = match WalletSnapshot::capture(self.bitcoin_wallet.clone(), &self.monero_wallet, &self.external_redeem_address, btc).await {
|
||||
Ok(wallet_snapshot) => wallet_snapshot,
|
||||
Err(error) => {
|
||||
tracing::error!("Swap request will be ignored because we were unable to create wallet snapshot for swap: {:#}", error);
|
||||
|
|
@ -1188,7 +1190,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Ok(balance) },
|
||||
|| async { Ok(reserved_items) },
|
||||
None,
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::bitcoin::{self, Txid};
|
||||
use crate::protocol::alice::AliceState;
|
||||
use crate::protocol::Database;
|
||||
use anyhow::{bail, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use swap_core::bitcoin::Txid;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
@ -14,7 +15,7 @@ pub enum Error {
|
|||
|
||||
pub async fn punish(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
db: Arc<dyn Database>,
|
||||
) -> Result<(Txid, AliceState)> {
|
||||
let state = db.get_state(swap_id).await?.try_into()?;
|
||||
|
|
@ -47,7 +48,7 @@ pub async fn punish(
|
|||
|
||||
tracing::info!(%swap_id, "Trying to manually punish swap");
|
||||
|
||||
let txid = state3.punish_btc(&bitcoin_wallet).await?;
|
||||
let txid = state3.punish_btc(bitcoin_wallet.as_ref()).await?;
|
||||
|
||||
let state = AliceState::BtcPunished {
|
||||
state3: state3.clone(),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::bitcoin::{Txid, Wallet};
|
||||
use crate::bitcoin::Wallet;
|
||||
use crate::protocol::alice::AliceState;
|
||||
use crate::protocol::Database;
|
||||
use anyhow::{bail, Result};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use swap_core::bitcoin::Txid;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub enum Finality {
|
||||
|
|
@ -60,7 +61,9 @@ pub async fn redeem(
|
|||
Ok((txid, state))
|
||||
}
|
||||
AliceState::BtcRedeemTransactionPublished { state3, .. } => {
|
||||
let subscription = bitcoin_wallet.subscribe_to(state3.tx_redeem()).await;
|
||||
let subscription = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_redeem()))
|
||||
.await;
|
||||
|
||||
if let Finality::Await = finality {
|
||||
subscription.wait_until_final().await?;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::bitcoin::{self};
|
||||
use crate::common::retry;
|
||||
use crate::monero;
|
||||
use crate::protocol::alice::swap::XmrRefundable;
|
||||
use crate::protocol::alice::AliceState;
|
||||
use crate::protocol::Database;
|
||||
use anyhow::{bail, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use libp2p::PeerId;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -27,7 +28,7 @@ pub enum Error {
|
|||
|
||||
pub async fn refund(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
db: Arc<dyn Database>,
|
||||
) -> Result<AliceState> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use crate::asb::event_loop::EventLoopService;
|
||||
use crate::monero;
|
||||
use crate::protocol::Database;
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use jsonrpsee::server::{ServerBuilder, ServerHandle};
|
||||
use jsonrpsee::types::error::ErrorCode;
|
||||
use jsonrpsee::types::ErrorObjectOwned;
|
||||
|
|
@ -20,7 +21,7 @@ impl RpcServer {
|
|||
pub async fn start(
|
||||
host: String,
|
||||
port: u16,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
event_loop_service: EventLoopService,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
|
|
@ -54,7 +55,7 @@ impl RpcServer {
|
|||
}
|
||||
|
||||
pub struct RpcImpl {
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
event_loop_service: EventLoopService,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
|
|
|
|||
|
|
@ -1,788 +1,3 @@
|
|||
pub mod wallet;
|
||||
|
||||
mod cancel;
|
||||
mod early_refund;
|
||||
mod lock;
|
||||
mod punish;
|
||||
mod redeem;
|
||||
mod refund;
|
||||
mod timelocks;
|
||||
|
||||
pub use crate::bitcoin::cancel::{CancelTimelock, PunishTimelock, TxCancel};
|
||||
pub use crate::bitcoin::early_refund::TxEarlyRefund;
|
||||
pub use crate::bitcoin::lock::TxLock;
|
||||
pub use crate::bitcoin::punish::TxPunish;
|
||||
pub use crate::bitcoin::redeem::TxRedeem;
|
||||
pub use crate::bitcoin::refund::TxRefund;
|
||||
pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks};
|
||||
pub use ::bitcoin::amount::Amount;
|
||||
pub use ::bitcoin::psbt::Psbt as PartiallySignedTransaction;
|
||||
pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid};
|
||||
pub use ecdsa_fun::adaptor::EncryptedSignature;
|
||||
pub use ecdsa_fun::fun::Scalar;
|
||||
pub use ecdsa_fun::Signature;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use wallet::TestWalletBuilder;
|
||||
|
||||
use crate::bitcoin::wallet::ScriptStatus;
|
||||
use ::bitcoin::hashes::Hash;
|
||||
use ::bitcoin::secp256k1::ecdsa;
|
||||
use ::bitcoin::sighash::SegwitV0Sighash as Sighash;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bdk_wallet::miniscript::descriptor::Wsh;
|
||||
use bdk_wallet::miniscript::{Descriptor, Segwitv0};
|
||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||
use ecdsa_fun::fun::Point;
|
||||
use ecdsa_fun::nonce::Deterministic;
|
||||
use ecdsa_fun::ECDSA;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct SecretKey {
|
||||
inner: Scalar,
|
||||
public: Point,
|
||||
}
|
||||
|
||||
impl SecretKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public(&self) -> PublicKey {
|
||||
PublicKey(self.public)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
pub fn sign(&self, digest: Sighash) -> Signature {
|
||||
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
||||
|
||||
ecdsa.sign(&self.inner, &digest.to_byte_array())
|
||||
}
|
||||
|
||||
// TxRefund encsigning explanation:
|
||||
//
|
||||
// A and B, are the Bitcoin Public Keys which go on the joint output for
|
||||
// TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the
|
||||
// joint output for TxLock_Monero
|
||||
|
||||
// tx_refund: multisig(A, B), published by bob
|
||||
// bob can produce sig on B using b
|
||||
// alice sends over an encrypted signature on A encrypted with S_b
|
||||
// s_b is leaked to alice when bob publishes signed tx_refund allowing her to
|
||||
// recover s_b: recover(encsig, S_b, sig_tx_refund) = s_b
|
||||
// alice now has s_a and s_b and can refund monero
|
||||
|
||||
// self = a, Y = S_b, digest = tx_refund
|
||||
pub fn encsign(&self, Y: PublicKey, digest: Sighash) -> EncryptedSignature {
|
||||
let adaptor = Adaptor::<
|
||||
HashTranscript<Sha256, rand_chacha::ChaCha20Rng>,
|
||||
Deterministic<Sha256>,
|
||||
>::default();
|
||||
|
||||
adaptor.encrypted_sign(&self.inner, &Y.0, &digest.to_byte_array())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PublicKey(Point);
|
||||
|
||||
impl PublicKey {
|
||||
#[cfg(test)]
|
||||
pub fn random() -> Self {
|
||||
Self(Point::random(&mut rand::thread_rng()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for Point {
|
||||
fn from(from: PublicKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PublicKey> for bitcoin::PublicKey {
|
||||
type Error = bitcoin::key::FromSliceError;
|
||||
|
||||
fn try_from(pubkey: PublicKey) -> Result<Self, Self::Error> {
|
||||
let bytes = pubkey.0.to_bytes();
|
||||
bitcoin::PublicKey::from_slice(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point> for PublicKey {
|
||||
fn from(p: Point) -> Self {
|
||||
Self(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for SecretKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretKey> for Scalar {
|
||||
fn from(sk: SecretKey) -> Self {
|
||||
sk.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for PublicKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
PublicKey(ecdsa.verification_key_for(&scalar))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_sig(
|
||||
verification_key: &PublicKey,
|
||||
transaction_sighash: &Sighash,
|
||||
sig: &Signature,
|
||||
) -> Result<()> {
|
||||
let ecdsa = ECDSA::verify_only();
|
||||
|
||||
if ecdsa.verify(
|
||||
&verification_key.0,
|
||||
&transaction_sighash.to_byte_array(),
|
||||
sig,
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("signature is invalid")]
|
||||
pub struct InvalidSignature;
|
||||
|
||||
pub fn verify_encsig(
|
||||
verification_key: PublicKey,
|
||||
encryption_key: PublicKey,
|
||||
digest: &Sighash,
|
||||
encsig: &EncryptedSignature,
|
||||
) -> Result<()> {
|
||||
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
||||
|
||||
if adaptor.verify_encrypted_signature(
|
||||
&verification_key.0,
|
||||
&encryption_key.0,
|
||||
&digest.to_byte_array(),
|
||||
encsig,
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidEncryptedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, thiserror::Error)]
|
||||
#[error("encrypted signature is invalid")]
|
||||
pub struct InvalidEncryptedSignature;
|
||||
|
||||
pub fn build_shared_output_descriptor(
|
||||
A: Point,
|
||||
B: Point,
|
||||
) -> Result<Descriptor<bitcoin::PublicKey>> {
|
||||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
|
||||
|
||||
let miniscript = MINISCRIPT_TEMPLATE
|
||||
.replace('A', &A.to_string())
|
||||
.replace('B', &B.to_string());
|
||||
|
||||
let miniscript =
|
||||
bdk_wallet::miniscript::Miniscript::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
||||
.expect("a valid miniscript");
|
||||
|
||||
Ok(Descriptor::Wsh(Wsh::new(miniscript)?))
|
||||
}
|
||||
|
||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
||||
|
||||
let s = adaptor
|
||||
.recover_decryption_key(&S.0, &sig, &encsig)
|
||||
.map(SecretKey::from)
|
||||
.context("Failed to recover secret from adaptor signature")?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn current_epoch(
|
||||
cancel_timelock: CancelTimelock,
|
||||
punish_timelock: PunishTimelock,
|
||||
tx_lock_status: ScriptStatus,
|
||||
tx_cancel_status: ScriptStatus,
|
||||
) -> ExpiredTimelocks {
|
||||
if tx_cancel_status.is_confirmed_with(punish_timelock) {
|
||||
return ExpiredTimelocks::Punish;
|
||||
}
|
||||
|
||||
if tx_lock_status.is_confirmed_with(cancel_timelock) {
|
||||
return ExpiredTimelocks::Cancel {
|
||||
blocks_left: tx_cancel_status.blocks_left_until(punish_timelock),
|
||||
};
|
||||
}
|
||||
|
||||
ExpiredTimelocks::None {
|
||||
blocks_left: tx_lock_status.blocks_left_until(cancel_timelock),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bitcoin_address {
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin::{
|
||||
address::{NetworkChecked, NetworkUnchecked},
|
||||
Address,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)]
|
||||
#[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
|
||||
pub struct BitcoinAddressNetworkMismatch {
|
||||
#[serde(with = "swap_serde::bitcoin::network")]
|
||||
expected: bitcoin::Network,
|
||||
#[serde(with = "swap_serde::bitcoin::network")]
|
||||
actual: bitcoin::Network,
|
||||
}
|
||||
|
||||
pub fn parse(addr_str: &str) -> Result<bitcoin::Address<NetworkUnchecked>> {
|
||||
let address = bitcoin::Address::from_str(addr_str)?;
|
||||
|
||||
if address.assume_checked_ref().address_type() != Some(bitcoin::AddressType::P2wpkh) {
|
||||
anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!")
|
||||
}
|
||||
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
/// Parse the address and validate the network.
|
||||
pub fn parse_and_validate_network(
|
||||
address: &str,
|
||||
expected_network: bitcoin::Network,
|
||||
) -> Result<bitcoin::Address> {
|
||||
let addres = bitcoin::Address::from_str(address)?;
|
||||
let addres = addres.require_network(expected_network).with_context(|| {
|
||||
format!("Bitcoin address network mismatch, expected `{expected_network:?}`")
|
||||
})?;
|
||||
Ok(addres)
|
||||
}
|
||||
|
||||
/// Parse the address and validate the network.
|
||||
pub fn parse_and_validate(address: &str, is_testnet: bool) -> Result<bitcoin::Address> {
|
||||
let expected_network = if is_testnet {
|
||||
bitcoin::Network::Testnet
|
||||
} else {
|
||||
bitcoin::Network::Bitcoin
|
||||
};
|
||||
parse_and_validate_network(address, expected_network)
|
||||
}
|
||||
|
||||
/// Validate the address network.
|
||||
pub fn validate(
|
||||
address: Address<NetworkUnchecked>,
|
||||
is_testnet: bool,
|
||||
) -> Result<Address<NetworkChecked>> {
|
||||
let expected_network = if is_testnet {
|
||||
bitcoin::Network::Testnet
|
||||
} else {
|
||||
bitcoin::Network::Bitcoin
|
||||
};
|
||||
validate_network(address, expected_network)
|
||||
}
|
||||
|
||||
/// Validate the address network.
|
||||
pub fn validate_network(
|
||||
address: Address<NetworkUnchecked>,
|
||||
expected_network: bitcoin::Network,
|
||||
) -> Result<Address<NetworkChecked>> {
|
||||
address
|
||||
.require_network(expected_network)
|
||||
.context("Bitcoin address network mismatch")
|
||||
}
|
||||
|
||||
/// Validate the address network even though the address is already checked.
|
||||
pub fn revalidate_network(
|
||||
address: Address,
|
||||
expected_network: bitcoin::Network,
|
||||
) -> Result<Address> {
|
||||
address
|
||||
.as_unchecked()
|
||||
.clone()
|
||||
.require_network(expected_network)
|
||||
.context("bitcoin address network mismatch")
|
||||
}
|
||||
|
||||
/// Validate the address network even though the address is already checked.
|
||||
pub fn revalidate(address: Address, is_testnet: bool) -> Result<Address> {
|
||||
revalidate_network(
|
||||
address,
|
||||
if is_testnet {
|
||||
bitcoin::Network::Testnet
|
||||
} else {
|
||||
bitcoin::Network::Bitcoin
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the ecdsa der signature bytes into a secp256kfun ecdsa signature type.
|
||||
pub fn extract_ecdsa_sig(sig: &[u8]) -> Result<Signature> {
|
||||
let data = &sig[..sig.len() - 1];
|
||||
let sig = ecdsa::Signature::from_der(data)?.serialize_compact();
|
||||
Signature::from_bytes(sig).ok_or(anyhow::anyhow!("invalid signature"))
|
||||
}
|
||||
|
||||
/// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23
|
||||
pub enum RpcErrorCode {
|
||||
/// Transaction or block was rejected by network rules. Error code -26.
|
||||
RpcVerifyRejected,
|
||||
/// Transaction or block was rejected by network rules. Error code -27.
|
||||
RpcVerifyAlreadyInChain,
|
||||
/// General error during transaction or block submission
|
||||
RpcVerifyError,
|
||||
/// Invalid address or key. Error code -5. Is throwns when a transaction is not found.
|
||||
/// See:
|
||||
/// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/mempool.cpp#L470-L472
|
||||
/// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/rawtransaction.cpp#L352-L368
|
||||
RpcInvalidAddressOrKey,
|
||||
}
|
||||
|
||||
impl From<RpcErrorCode> for i64 {
|
||||
fn from(code: RpcErrorCode) -> Self {
|
||||
match code {
|
||||
RpcErrorCode::RpcVerifyError => -25,
|
||||
RpcErrorCode::RpcVerifyRejected => -26,
|
||||
RpcErrorCode::RpcVerifyAlreadyInChain => -27,
|
||||
RpcErrorCode::RpcInvalidAddressOrKey => -5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_rpc_error_code(error: &anyhow::Error) -> anyhow::Result<i64> {
|
||||
// First try to extract an Electrum error from a MultiError if present
|
||||
if let Some(multi_error) = error.downcast_ref::<electrum_pool::MultiError>() {
|
||||
// Try to find the first Electrum error in the MultiError
|
||||
for single_error in multi_error.iter() {
|
||||
if let bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(
|
||||
string,
|
||||
)) = single_error
|
||||
{
|
||||
let json = serde_json::from_str(
|
||||
&string
|
||||
.replace("sendrawtransaction RPC error:", "")
|
||||
.replace("daemon error:", ""),
|
||||
)?;
|
||||
|
||||
let json_map = match json {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => continue, // Try next error if this one isn't a JSON object
|
||||
};
|
||||
|
||||
let error_code_value = match json_map.get("code") {
|
||||
Some(val) => val,
|
||||
None => continue, // Try next error if no error code field
|
||||
};
|
||||
|
||||
let error_code_number = match error_code_value {
|
||||
serde_json::Value::Number(num) => num,
|
||||
_ => continue, // Try next error if error code isn't a number
|
||||
};
|
||||
|
||||
if let Some(int) = error_code_number.as_i64() {
|
||||
return Ok(int);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we couldn't extract an RPC error code from any error in the MultiError
|
||||
bail!(
|
||||
"Error is of incorrect variant. We expected an Electrum error, but got: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Original logic for direct Electrum errors
|
||||
let string = match error.downcast_ref::<bdk_electrum::electrum_client::Error>() {
|
||||
Some(bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(string))) => {
|
||||
string
|
||||
}
|
||||
_ => bail!(
|
||||
"Error is of incorrect variant. We expected an Electrum error, but got: {}",
|
||||
error
|
||||
),
|
||||
};
|
||||
|
||||
let json = serde_json::from_str(
|
||||
&string
|
||||
.replace("sendrawtransaction RPC error:", "")
|
||||
.replace("daemon error:", ""),
|
||||
)?;
|
||||
|
||||
let json_map = match json {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => bail!("Json error is not json object "),
|
||||
};
|
||||
|
||||
let error_code_value = match json_map.get("code") {
|
||||
Some(val) => val,
|
||||
None => bail!("No error code field"),
|
||||
};
|
||||
|
||||
let error_code_number = match error_code_value {
|
||||
serde_json::Value::Number(num) => num,
|
||||
_ => bail!("Error code is not a number"),
|
||||
};
|
||||
|
||||
if let Some(int) = error_code_number.as_i64() {
|
||||
Ok(int)
|
||||
} else {
|
||||
bail!("Error code is not an unsigned integer")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("transaction does not spend anything")]
|
||||
pub struct NoInputs;
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("transaction has {0} inputs, expected 1")]
|
||||
pub struct TooManyInputs(usize);
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("empty witness stack")]
|
||||
pub struct EmptyWitnessStack;
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("input has {0} witnesses, expected 3")]
|
||||
pub struct NotThreeWitnesses(usize);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::monero::TransferProof;
|
||||
use crate::protocol::{alice, bob};
|
||||
use bitcoin::secp256k1;
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
use ecdsa_fun::fun::marker::{NonZero, Public};
|
||||
use monero::PrivateKey;
|
||||
use rand::rngs::OsRng;
|
||||
use std::matches;
|
||||
use swap_env::env::{GetConfig, Regtest};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() {
|
||||
let tx_lock_status = ScriptStatus::from_confirmations(4);
|
||||
let tx_cancel_status = ScriptStatus::Unseen;
|
||||
|
||||
let expired_timelock = current_epoch(
|
||||
CancelTimelock::new(5),
|
||||
PunishTimelock::new(5),
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
);
|
||||
|
||||
assert!(matches!(expired_timelock, ExpiredTimelocks::None { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_confirmations_ge_to_cancel_timelock_cancel_timelock_expired() {
|
||||
let tx_lock_status = ScriptStatus::from_confirmations(5);
|
||||
let tx_cancel_status = ScriptStatus::Unseen;
|
||||
|
||||
let expired_timelock = current_epoch(
|
||||
CancelTimelock::new(5),
|
||||
PunishTimelock::new(5),
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
);
|
||||
|
||||
assert!(matches!(expired_timelock, ExpiredTimelocks::Cancel { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_confirmations_ge_to_punish_timelock_punish_timelock_expired() {
|
||||
let tx_lock_status = ScriptStatus::from_confirmations(10);
|
||||
let tx_cancel_status = ScriptStatus::from_confirmations(5);
|
||||
|
||||
let expired_timelock = current_epoch(
|
||||
CancelTimelock::new(5),
|
||||
PunishTimelock::new(5),
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
);
|
||||
|
||||
assert_eq!(expired_timelock, ExpiredTimelocks::Punish)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn calculate_transaction_weights() {
|
||||
let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let spending_fee = Amount::from_sat(1_000);
|
||||
let btc_amount = Amount::from_sat(500_000);
|
||||
let xmr_amount = crate::monero::Amount::from_piconero(10000);
|
||||
|
||||
let tx_redeem_fee = alice_wallet
|
||||
.estimate_fee(TxRedeem::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
let tx_punish_fee = alice_wallet
|
||||
.estimate_fee(TxPunish::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
let tx_lock_fee = alice_wallet
|
||||
.estimate_fee(TxLock::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let redeem_address = alice_wallet.new_address().await.unwrap();
|
||||
let punish_address = alice_wallet.new_address().await.unwrap();
|
||||
|
||||
let config = Regtest::get_config();
|
||||
let alice_state0 = alice::State0::new(
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
tx_redeem_fee,
|
||||
tx_punish_fee,
|
||||
&mut OsRng,
|
||||
);
|
||||
|
||||
let bob_state0 = bob::State0::new(
|
||||
Uuid::new_v4(),
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config.bitcoin_cancel_timelock.into(),
|
||||
config.bitcoin_punish_timelock.into(),
|
||||
bob_wallet.new_address().await.unwrap(),
|
||||
config.monero_finality_confirmations,
|
||||
spending_fee,
|
||||
spending_fee,
|
||||
tx_lock_fee,
|
||||
);
|
||||
|
||||
let message0 = bob_state0.next_message();
|
||||
|
||||
let (_, alice_state1) = alice_state0.receive(message0).unwrap();
|
||||
let alice_message1 = alice_state1.next_message();
|
||||
|
||||
let bob_state1 = bob_state0
|
||||
.receive(&bob_wallet, alice_message1)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_message2 = bob_state1.next_message();
|
||||
|
||||
let alice_state2 = alice_state1.receive(bob_message2).unwrap();
|
||||
let alice_message3 = alice_state2.next_message();
|
||||
|
||||
let bob_state2 = bob_state1.receive(alice_message3).unwrap();
|
||||
let bob_message4 = bob_state2.next_message();
|
||||
|
||||
let alice_state3 = alice_state2.receive(bob_message4).unwrap();
|
||||
|
||||
let (bob_state3, _tx_lock) = bob_state2.lock_btc().await.unwrap();
|
||||
let bob_state4 = bob_state3.xmr_locked(
|
||||
crate::monero::BlockHeight { height: 0 },
|
||||
// We use bogus values here, because they're irrelevant to this test
|
||||
TransferProof::new(
|
||||
crate::monero::TxHash("foo".into()),
|
||||
PrivateKey::from_scalar(Scalar::one()),
|
||||
),
|
||||
);
|
||||
let encrypted_signature = bob_state4.tx_redeem_encsig();
|
||||
let bob_state6 = bob_state4.cancel();
|
||||
|
||||
let cancel_transaction = alice_state3.signed_cancel_transaction().unwrap();
|
||||
let punish_transaction = alice_state3.signed_punish_transaction().unwrap();
|
||||
let redeem_transaction = alice_state3
|
||||
.signed_redeem_transaction(encrypted_signature)
|
||||
.unwrap();
|
||||
let refund_transaction = bob_state6.signed_refund_transaction().unwrap();
|
||||
|
||||
assert_weight(redeem_transaction, TxRedeem::weight().to_wu(), "TxRedeem");
|
||||
assert_weight(cancel_transaction, TxCancel::weight().to_wu(), "TxCancel");
|
||||
assert_weight(punish_transaction, TxPunish::weight().to_wu(), "TxPunish");
|
||||
assert_weight(refund_transaction, TxRefund::weight().to_wu(), "TxRefund");
|
||||
|
||||
// Test TxEarlyRefund transaction
|
||||
let early_refund_transaction = alice_state3
|
||||
.signed_early_refund_transaction()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_weight(
|
||||
early_refund_transaction,
|
||||
TxEarlyRefund::weight() as u64,
|
||||
"TxEarlyRefund",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tx_early_refund_can_be_constructed_and_signed() {
|
||||
let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat())
|
||||
.build()
|
||||
.await;
|
||||
let spending_fee = Amount::from_sat(1_000);
|
||||
let btc_amount = Amount::from_sat(500_000);
|
||||
let xmr_amount = crate::monero::Amount::from_piconero(10000);
|
||||
|
||||
let tx_redeem_fee = alice_wallet
|
||||
.estimate_fee(TxRedeem::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
let tx_punish_fee = alice_wallet
|
||||
.estimate_fee(TxPunish::weight(), Some(btc_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let refund_address = alice_wallet.new_address().await.unwrap();
|
||||
let punish_address = alice_wallet.new_address().await.unwrap();
|
||||
|
||||
let config = Regtest::get_config();
|
||||
let alice_state0 = alice::State0::new(
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config,
|
||||
refund_address.clone(),
|
||||
punish_address,
|
||||
tx_redeem_fee,
|
||||
tx_punish_fee,
|
||||
&mut OsRng,
|
||||
);
|
||||
|
||||
let bob_state0 = bob::State0::new(
|
||||
Uuid::new_v4(),
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config.bitcoin_cancel_timelock.into(),
|
||||
config.bitcoin_punish_timelock.into(),
|
||||
bob_wallet.new_address().await.unwrap(),
|
||||
config.monero_finality_confirmations,
|
||||
spending_fee,
|
||||
spending_fee,
|
||||
spending_fee,
|
||||
);
|
||||
|
||||
// Complete the state machine up to State3
|
||||
let message0 = bob_state0.next_message();
|
||||
let (_, alice_state1) = alice_state0.receive(message0).unwrap();
|
||||
let alice_message1 = alice_state1.next_message();
|
||||
|
||||
let bob_state1 = bob_state0
|
||||
.receive(&bob_wallet, alice_message1)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_message2 = bob_state1.next_message();
|
||||
|
||||
let alice_state2 = alice_state1.receive(bob_message2).unwrap();
|
||||
let alice_message3 = alice_state2.next_message();
|
||||
|
||||
let bob_state2 = bob_state1.receive(alice_message3).unwrap();
|
||||
let bob_message4 = bob_state2.next_message();
|
||||
|
||||
let alice_state3 = alice_state2.receive(bob_message4).unwrap();
|
||||
|
||||
// Test TxEarlyRefund construction
|
||||
let tx_early_refund = alice_state3.tx_early_refund();
|
||||
|
||||
// Verify basic properties
|
||||
assert_eq!(tx_early_refund.txid(), tx_early_refund.txid()); // Should be deterministic
|
||||
assert!(tx_early_refund.digest() != Sighash::all_zeros()); // Should have valid digest
|
||||
|
||||
// Test that it can be signed and completed
|
||||
let early_refund_transaction = alice_state3
|
||||
.signed_early_refund_transaction()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Verify the transaction has expected structure
|
||||
assert_eq!(early_refund_transaction.input.len(), 1); // One input from lock tx
|
||||
assert_eq!(early_refund_transaction.output.len(), 1); // One output to refund address
|
||||
assert_eq!(
|
||||
early_refund_transaction.output[0].script_pubkey,
|
||||
refund_address.script_pubkey()
|
||||
);
|
||||
|
||||
// Verify the input is spending the lock transaction
|
||||
assert_eq!(
|
||||
early_refund_transaction.input[0].previous_output,
|
||||
alice_state3.tx_lock.as_outpoint()
|
||||
);
|
||||
|
||||
// Verify the amount is correct (lock amount minus fee)
|
||||
let expected_amount = alice_state3.tx_lock.lock_amount() - alice_state3.tx_refund_fee;
|
||||
assert_eq!(early_refund_transaction.output[0].value, expected_amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tx_early_refund_has_correct_weight() {
|
||||
// TxEarlyRefund should have the same weight as other similar transactions
|
||||
assert_eq!(TxEarlyRefund::weight(), 548);
|
||||
|
||||
// It should be the same as TxRedeem and TxRefund weights since they have similar structure
|
||||
assert_eq!(TxEarlyRefund::weight() as u64, TxRedeem::weight().to_wu());
|
||||
assert_eq!(TxEarlyRefund::weight() as u64, TxRefund::weight().to_wu());
|
||||
}
|
||||
|
||||
// Weights fluctuate because of the length of the signatures. Valid ecdsa
|
||||
// signatures can have 68, 69, 70, 71, or 72 bytes. Since most of our
|
||||
// transactions have 2 signatures the weight can be up to 8 bytes less than
|
||||
// the static weight (4 bytes per signature).
|
||||
fn assert_weight(transaction: Transaction, expected_weight: u64, tx_name: &str) {
|
||||
let is_weight = transaction.weight();
|
||||
|
||||
assert!(
|
||||
expected_weight - is_weight.to_wu() <= 8,
|
||||
"{} to have weight {}, but was {}. Transaction: {:#?}",
|
||||
tx_name,
|
||||
expected_weight,
|
||||
is_weight,
|
||||
transaction
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_point_hex() {
|
||||
// secp256kfun Point and secp256k1 PublicKey should have the same bytes and hex representation
|
||||
let secp = secp256k1::Secp256k1::default();
|
||||
let keypair = secp256k1::Keypair::new(&secp, &mut OsRng);
|
||||
|
||||
let pubkey = keypair.public_key();
|
||||
let point: Point<_, Public, NonZero> = Point::from_bytes(pubkey.serialize()).unwrap();
|
||||
|
||||
assert_eq!(pubkey.to_string(), point.to_string());
|
||||
}
|
||||
}
|
||||
pub use swap_core::bitcoin::*;
|
||||
pub use wallet::*;
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ use bdk_wallet::{Balance, PersistedWallet};
|
|||
use bitcoin::bip32::Xpriv;
|
||||
use bitcoin::{psbt::Psbt as PartiallySignedTransaction, Txid};
|
||||
use bitcoin::{ScriptBuf, Weight};
|
||||
use derive_builder::Builder;
|
||||
use electrum_pool::ElectrumBalancer;
|
||||
use moka;
|
||||
use rust_decimal::prelude::*;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -34,17 +36,13 @@ use std::sync::Arc;
|
|||
use std::sync::Mutex as SyncMutex;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use swap_core::bitcoin::bitcoin_address::revalidate_network;
|
||||
use swap_core::bitcoin::BlockHeight;
|
||||
use sync_ext::{CumulativeProgressHandle, InnerSyncCallback, SyncCallbackExt};
|
||||
use tokio::sync::watch;
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tracing::{debug_span, Instrument};
|
||||
|
||||
use super::bitcoin_address::revalidate_network;
|
||||
use super::BlockHeight;
|
||||
use derive_builder::Builder;
|
||||
use electrum_pool::ElectrumBalancer;
|
||||
use moka;
|
||||
|
||||
/// We allow transaction fees of up to 20% of the transferred amount to ensure
|
||||
/// that lock transactions can always be published, even when fees are high.
|
||||
const MAX_RELATIVE_TX_FEE: Decimal = dec!(0.20);
|
||||
|
|
@ -239,63 +237,9 @@ pub enum PersisterConfig {
|
|||
InMemorySqlite,
|
||||
}
|
||||
|
||||
/// A subscription to the status of a given transaction
|
||||
/// that can be used to wait for the transaction to be confirmed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Subscription {
|
||||
/// A receiver used to await updates to the status of the transaction.
|
||||
receiver: watch::Receiver<ScriptStatus>,
|
||||
/// The number of confirmations we require for a transaction to be considered final.
|
||||
finality_confirmations: u32,
|
||||
/// The transaction ID we are subscribing to.
|
||||
txid: Txid,
|
||||
}
|
||||
|
||||
/// The possible statuses of a script.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ScriptStatus {
|
||||
Unseen,
|
||||
InMempool,
|
||||
Confirmed(Confirmed),
|
||||
Retrying,
|
||||
}
|
||||
|
||||
/// The status of a confirmed transaction.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Confirmed {
|
||||
/// The depth of this transaction within the blockchain.
|
||||
///
|
||||
/// Zero if the transaction is included in the latest block.
|
||||
depth: u32,
|
||||
}
|
||||
|
||||
/// Defines a watchable transaction.
|
||||
///
|
||||
/// For a transaction to be watchable, we need to know two things: Its
|
||||
/// transaction ID and the specific output script that is going to change.
|
||||
/// A transaction can obviously have multiple outputs but our protocol purposes,
|
||||
/// we are usually interested in a specific one.
|
||||
pub trait Watchable {
|
||||
/// The transaction ID.
|
||||
fn id(&self) -> Txid;
|
||||
/// The script of the output we are interested in.
|
||||
fn script(&self) -> ScriptBuf;
|
||||
/// Convenience method to get both the script and the txid.
|
||||
fn script_and_txid(&self) -> (ScriptBuf, Txid) {
|
||||
(self.script(), self.id())
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that can estimate fee rates and minimum relay fees.
|
||||
pub trait EstimateFeeRate {
|
||||
/// Estimate the fee rate for a given target block.
|
||||
fn estimate_feerate(
|
||||
&self,
|
||||
target_block: u32,
|
||||
) -> impl std::future::Future<Output = Result<FeeRate>> + Send;
|
||||
/// Get the minimum relay fee.
|
||||
fn min_relay_fee(&self) -> impl std::future::Future<Output = Result<FeeRate>> + Send;
|
||||
}
|
||||
pub use bitcoin_wallet::primitives::{
|
||||
Confirmed, EstimateFeeRate, ScriptStatus, Subscription, Watchable,
|
||||
};
|
||||
|
||||
/// A caching wrapper around EstimateFeeRate implementations.
|
||||
///
|
||||
|
|
@ -725,7 +669,10 @@ impl Wallet {
|
|||
|
||||
// to watch for confirmations, watching a single output is enough
|
||||
let subscription = self
|
||||
.subscribe_to((txid, transaction.output[0].script_pubkey.clone()))
|
||||
.subscribe_to(Box::new((
|
||||
txid,
|
||||
transaction.output[0].script_pubkey.clone(),
|
||||
)))
|
||||
.await;
|
||||
|
||||
let client = self.electrum_client.lock().await;
|
||||
|
|
@ -808,10 +755,7 @@ impl Wallet {
|
|||
Ok(last_tx.tx_node.txid)
|
||||
}
|
||||
|
||||
pub async fn status_of_script<T>(&self, tx: &T) -> Result<ScriptStatus>
|
||||
where
|
||||
T: Watchable,
|
||||
{
|
||||
pub async fn status_of_script(&self, tx: &dyn Watchable) -> Result<ScriptStatus> {
|
||||
self.electrum_client
|
||||
.lock()
|
||||
.await
|
||||
|
|
@ -819,7 +763,7 @@ impl Wallet {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn subscribe_to(&self, tx: impl Watchable + Send + Sync + 'static) -> Subscription {
|
||||
pub async fn subscribe_to(&self, tx: Box<dyn Watchable>) -> Subscription {
|
||||
let txid = tx.id();
|
||||
let script = tx.script();
|
||||
|
||||
|
|
@ -1209,7 +1153,6 @@ where
|
|||
electrum_rate_sat_vb = electrum_rate.to_sat_per_vb_ceil(),
|
||||
mempool_space_rate_sat_vb = mempool_space_rate.to_sat_per_vb_ceil(),
|
||||
"Successfully fetched fee rates from both Electrum and mempool.space. We will use the higher one"
|
||||
|
||||
);
|
||||
Ok(std::cmp::max(electrum_rate, mempool_space_rate))
|
||||
}
|
||||
|
|
@ -1372,7 +1315,8 @@ where
|
|||
change_override: Option<Address>,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
// Check address and change address for network equality.
|
||||
let address = revalidate_network(address, self.network)?;
|
||||
let address =
|
||||
swap_core::bitcoin::bitcoin_address::revalidate_network(address, self.network)?;
|
||||
|
||||
change_override
|
||||
.as_ref()
|
||||
|
|
@ -1431,11 +1375,14 @@ where
|
|||
change_override: Option<Address>,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
// Check address and change address for network equality.
|
||||
let address = revalidate_network(address, self.network)?;
|
||||
let address =
|
||||
swap_core::bitcoin::bitcoin_address::revalidate_network(address, self.network)?;
|
||||
|
||||
change_override
|
||||
.as_ref()
|
||||
.map(|a| revalidate_network(a.clone(), self.network))
|
||||
.map(|a| {
|
||||
swap_core::bitcoin::bitcoin_address::revalidate_network(a.clone(), self.network)
|
||||
})
|
||||
.transpose()
|
||||
.context("Change address is not on the correct network")?;
|
||||
|
||||
|
|
@ -1687,7 +1634,7 @@ impl Client {
|
|||
/// As opposed to [`update_state`] this function does not
|
||||
/// check the time since the last update before refreshing
|
||||
/// It therefore also does not take a [`force`] parameter
|
||||
pub async fn update_state_single(&mut self, script: &impl Watchable) -> Result<()> {
|
||||
pub async fn update_state_single(&mut self, script: &dyn Watchable) -> Result<()> {
|
||||
self.update_script_history(script).await?;
|
||||
self.update_block_height().await?;
|
||||
|
||||
|
|
@ -1781,7 +1728,7 @@ impl Client {
|
|||
}
|
||||
|
||||
/// Update the script history of a single script.
|
||||
pub async fn update_script_history(&mut self, script: &impl Watchable) -> Result<()> {
|
||||
pub async fn update_script_history(&mut self, script: &dyn Watchable) -> Result<()> {
|
||||
let (script_buf, _) = script.script_and_txid();
|
||||
let script_clone = script_buf.clone();
|
||||
|
||||
|
|
@ -1858,7 +1805,7 @@ impl Client {
|
|||
/// Get the status of a script.
|
||||
pub async fn status_of_script(
|
||||
&mut self,
|
||||
script: &impl Watchable,
|
||||
script: &dyn Watchable,
|
||||
force: bool,
|
||||
) -> Result<ScriptStatus> {
|
||||
let (script_buf, txid) = script.script_and_txid();
|
||||
|
|
@ -2106,6 +2053,122 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SyncRequestBuilderFactory {
|
||||
chain_tip: bdk_wallet::chain::CheckPoint,
|
||||
spks: Vec<((KeychainKind, u32), ScriptBuf)>,
|
||||
}
|
||||
|
||||
impl SyncRequestBuilderFactory {
|
||||
fn build(self) -> SyncRequestBuilder<(KeychainKind, u32)> {
|
||||
SyncRequest::builder()
|
||||
.chain_tip(self.chain_tip)
|
||||
.spks_with_indexes(self.spks)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl bitcoin_wallet::BitcoinWallet for Wallet {
|
||||
async fn balance(&self) -> Result<Amount> {
|
||||
Wallet::balance(self).await
|
||||
}
|
||||
|
||||
async fn balance_info(&self) -> Result<Balance> {
|
||||
Wallet::balance_info(self).await
|
||||
}
|
||||
|
||||
async fn new_address(&self) -> Result<Address> {
|
||||
Wallet::new_address(self).await
|
||||
}
|
||||
|
||||
async fn send_to_address(
|
||||
&self,
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
spending_fee: Amount,
|
||||
change_override: Option<Address>,
|
||||
) -> Result<bitcoin::psbt::Psbt> {
|
||||
Wallet::send_to_address(self, address, amount, spending_fee, change_override).await
|
||||
}
|
||||
|
||||
async fn send_to_address_dynamic_fee(
|
||||
&self,
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
change_override: Option<Address>,
|
||||
) -> Result<bitcoin::psbt::Psbt> {
|
||||
Wallet::send_to_address_dynamic_fee(self, address, amount, change_override).await
|
||||
}
|
||||
|
||||
async fn sweep_balance_to_address_dynamic_fee(
|
||||
&self,
|
||||
address: Address,
|
||||
) -> Result<bitcoin::psbt::Psbt> {
|
||||
Wallet::sweep_balance_to_address_dynamic_fee(self, address).await
|
||||
}
|
||||
|
||||
async fn sign_and_finalize(&self, psbt: bitcoin::psbt::Psbt) -> Result<bitcoin::Transaction> {
|
||||
Wallet::sign_and_finalize(self, psbt).await
|
||||
}
|
||||
|
||||
async fn broadcast(
|
||||
&self,
|
||||
transaction: bitcoin::Transaction,
|
||||
kind: &str,
|
||||
) -> Result<(Txid, bitcoin_wallet::Subscription)> {
|
||||
Wallet::broadcast(self, transaction, kind).await
|
||||
}
|
||||
|
||||
async fn sync(&self) -> Result<()> {
|
||||
Wallet::sync(self).await
|
||||
}
|
||||
|
||||
async fn subscribe_to(
|
||||
&self,
|
||||
tx: Box<dyn bitcoin_wallet::Watchable>,
|
||||
) -> bitcoin_wallet::Subscription {
|
||||
Wallet::subscribe_to(self, tx).await
|
||||
}
|
||||
|
||||
async fn status_of_script(
|
||||
&self,
|
||||
tx: &dyn bitcoin_wallet::Watchable,
|
||||
) -> Result<bitcoin_wallet::primitives::ScriptStatus> {
|
||||
Wallet::status_of_script(self, tx).await
|
||||
}
|
||||
|
||||
async fn get_raw_transaction(
|
||||
&self,
|
||||
txid: Txid,
|
||||
) -> Result<Option<std::sync::Arc<bitcoin::Transaction>>> {
|
||||
Wallet::get_raw_transaction(self, txid).await
|
||||
}
|
||||
|
||||
async fn max_giveable(&self, locking_script_size: usize) -> Result<(Amount, Amount)> {
|
||||
Wallet::max_giveable(self, locking_script_size).await
|
||||
}
|
||||
|
||||
async fn estimate_fee(
|
||||
&self,
|
||||
weight: Weight,
|
||||
transfer_amount: Option<Amount>,
|
||||
) -> Result<Amount> {
|
||||
Wallet::estimate_fee(self, weight, transfer_amount).await
|
||||
}
|
||||
|
||||
fn network(&self) -> Network {
|
||||
self.network
|
||||
}
|
||||
|
||||
fn finality_confirmations(&self) -> u32 {
|
||||
self.finality_confirmations
|
||||
}
|
||||
|
||||
async fn wallet_export(&self, role: &str) -> Result<FullyNodedExport> {
|
||||
Wallet::wallet_export(self, role).await
|
||||
}
|
||||
}
|
||||
|
||||
impl EstimateFeeRate for Client {
|
||||
async fn estimate_feerate(&self, target_block: u32) -> Result<FeeRate> {
|
||||
// Now that the Electrum client methods are async, we can parallelize the calls
|
||||
|
|
@ -2342,61 +2405,6 @@ fn trace_status_change(txid: Txid, old: Option<ScriptStatus>, new: ScriptStatus)
|
|||
new
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub async fn wait_until_final(&self) -> Result<()> {
|
||||
let conf_target = self.finality_confirmations;
|
||||
let txid = self.txid;
|
||||
|
||||
tracing::info!(%txid, required_confirmation=%conf_target, "Waiting for Bitcoin transaction finality");
|
||||
|
||||
let mut seen_confirmations = 0;
|
||||
|
||||
self.wait_until(|status| match status {
|
||||
ScriptStatus::Confirmed(inner) => {
|
||||
let confirmations = inner.confirmations();
|
||||
|
||||
if confirmations > seen_confirmations {
|
||||
tracing::info!(%txid,
|
||||
seen_confirmations = %confirmations,
|
||||
needed_confirmations = %conf_target,
|
||||
"Waiting for Bitcoin transaction finality");
|
||||
seen_confirmations = confirmations;
|
||||
}
|
||||
|
||||
inner.meets_target(conf_target)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_until_seen(&self) -> Result<()> {
|
||||
self.wait_until(ScriptStatus::has_been_seen).await
|
||||
}
|
||||
|
||||
pub async fn wait_until_confirmed_with<T>(&self, target: T) -> Result<()>
|
||||
where
|
||||
T: Into<u32>,
|
||||
T: Copy,
|
||||
{
|
||||
self.wait_until(|status| status.is_confirmed_with(target))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_until(&self, mut predicate: impl FnMut(&ScriptStatus) -> bool) -> Result<()> {
|
||||
let mut receiver = self.receiver.clone();
|
||||
|
||||
while !predicate(&receiver.borrow()) {
|
||||
receiver
|
||||
.changed()
|
||||
.await
|
||||
.context("Failed while waiting for next status update")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimate the absolute fee for a transaction.
|
||||
///
|
||||
/// This function takes the following parameters:
|
||||
|
|
@ -2617,111 +2625,6 @@ mod mempool_client {
|
|||
}
|
||||
}
|
||||
|
||||
impl Watchable for (Txid, ScriptBuf) {
|
||||
fn id(&self) -> Txid {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn script(&self) -> ScriptBuf {
|
||||
self.1.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptStatus {
|
||||
pub fn from_confirmations(confirmations: u32) -> Self {
|
||||
match confirmations {
|
||||
0 => Self::InMempool,
|
||||
confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Confirmed {
|
||||
pub fn new(depth: u32) -> Self {
|
||||
Self { depth }
|
||||
}
|
||||
|
||||
/// Compute the depth of a transaction based on its inclusion height and the
|
||||
/// latest known block.
|
||||
///
|
||||
/// Our information about the latest block might be outdated. To avoid an
|
||||
/// overflow, we make sure the depth is 0 in case the inclusion height
|
||||
/// exceeds our latest known block,
|
||||
pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self {
|
||||
let depth = latest_block.saturating_sub(inclusion_height);
|
||||
|
||||
Self { depth }
|
||||
}
|
||||
|
||||
pub fn confirmations(&self) -> u32 {
|
||||
self.depth + 1
|
||||
}
|
||||
|
||||
pub fn meets_target<T>(&self, target: T) -> bool
|
||||
where
|
||||
T: Into<u32>,
|
||||
{
|
||||
self.confirmations() >= target.into()
|
||||
}
|
||||
|
||||
pub fn blocks_left_until<T>(&self, target: T) -> u32
|
||||
where
|
||||
T: Into<u32> + Copy,
|
||||
{
|
||||
if self.meets_target(target) {
|
||||
0
|
||||
} else {
|
||||
target.into() - self.confirmations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptStatus {
|
||||
/// Check if the script has any confirmations.
|
||||
pub fn is_confirmed(&self) -> bool {
|
||||
matches!(self, ScriptStatus::Confirmed(_))
|
||||
}
|
||||
|
||||
/// Check if the script has met the given confirmation target.
|
||||
pub fn is_confirmed_with<T>(&self, target: T) -> bool
|
||||
where
|
||||
T: Into<u32>,
|
||||
{
|
||||
match self {
|
||||
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the number of blocks left until the target is met.
|
||||
pub fn blocks_left_until<T>(&self, target: T) -> u32
|
||||
where
|
||||
T: Into<u32> + Copy,
|
||||
{
|
||||
match self {
|
||||
ScriptStatus::Confirmed(inner) => inner.blocks_left_until(target),
|
||||
_ => target.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_been_seen(&self) -> bool {
|
||||
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ScriptStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ScriptStatus::Unseen => write!(f, "unseen"),
|
||||
ScriptStatus::InMempool => write!(f, "in mempool"),
|
||||
ScriptStatus::Retrying => write!(f, "retrying"),
|
||||
ScriptStatus::Confirmed(inner) => {
|
||||
write!(f, "confirmed with {} blocks", inner.confirmations())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod pre_1_0_0_bdk {
|
||||
//! This module contains some code for creating a bdk wallet from before the update.
|
||||
//! We need to keep this around to be able to migrate the wallet.
|
||||
|
|
@ -2989,11 +2892,114 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::bitcoin::{PublicKey, TxLock};
|
||||
use crate::tracing_ext::capture_logs;
|
||||
use async_trait::async_trait;
|
||||
use bdk::bitcoin::psbt::Psbt;
|
||||
use bitcoin::address::NetworkUnchecked;
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use proptest::prelude::*;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
// Implement BitcoinWallet trait for a stub wallet and panic when the function is not implemented
|
||||
#[async_trait]
|
||||
impl BitcoinWallet for Wallet<Connection, StaticFeeRate> {
|
||||
async fn balance(&self) -> Result<Amount> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn balance_info(&self) -> Result<Balance> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn new_address(&self) -> Result<Address> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn send_to_address(
|
||||
&self,
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
spending_fee: Amount,
|
||||
change_override: Option<Address>,
|
||||
) -> Result<Psbt> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn send_to_address_dynamic_fee(
|
||||
&self,
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
change_override: Option<Address>,
|
||||
) -> Result<bitcoin::psbt::Psbt> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn sweep_balance_to_address_dynamic_fee(
|
||||
&self,
|
||||
address: Address,
|
||||
) -> Result<bitcoin::psbt::Psbt> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn sign_and_finalize(
|
||||
&self,
|
||||
psbt: bitcoin::psbt::Psbt,
|
||||
) -> Result<bitcoin::Transaction> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn broadcast(
|
||||
&self,
|
||||
transaction: bitcoin::Transaction,
|
||||
kind: &str,
|
||||
) -> Result<(Txid, Subscription)> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn sync(&self) -> Result<()> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn subscribe_to(&self, tx: Box<dyn Watchable>) -> Subscription {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn status_of_script(&self, tx: &dyn Watchable) -> Result<ScriptStatus> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn get_raw_transaction(
|
||||
&self,
|
||||
txid: Txid,
|
||||
) -> Result<Option<std::sync::Arc<bitcoin::Transaction>>> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn max_giveable(&self, locking_script_size: usize) -> Result<(Amount, Amount)> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn estimate_fee(
|
||||
&self,
|
||||
weight: Weight,
|
||||
transfer_amount: Option<Amount>,
|
||||
) -> Result<Amount> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
fn network(&self) -> Network {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
fn finality_confirmations(&self) -> u32 {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
|
||||
async fn wallet_export(&self, role: &str) -> Result<FullyNodedExport> {
|
||||
unimplemented!("stub method called erroniosly")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn given_depth_0_should_meet_confirmation_target_one() {
|
||||
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
||||
|
|
@ -3654,17 +3660,3 @@ TRACE swap::bitcoin::wallet: Bitcoin transaction status changed txid=00000000000
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SyncRequestBuilderFactory {
|
||||
chain_tip: bdk_wallet::chain::CheckPoint,
|
||||
spks: Vec<((KeychainKind, u32), ScriptBuf)>,
|
||||
}
|
||||
|
||||
impl SyncRequestBuilderFactory {
|
||||
fn build(self) -> SyncRequestBuilder<(KeychainKind, u32)> {
|
||||
SyncRequest::builder()
|
||||
.chain_tip(self.chain_tip)
|
||||
.spks_with_indexes(self.spks)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::tauri_bindings::TauriHandle;
|
||||
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock};
|
||||
use crate::bitcoin::wallet;
|
||||
use crate::cli::api::tauri_bindings::{
|
||||
ApprovalRequestType, MoneroNodeConfig, SelectMakerDetails, SendMoneroDetails, TauriEmitter,
|
||||
TauriSwapProgressEvent,
|
||||
|
|
@ -14,9 +14,9 @@ use crate::monero::MoneroAddressPool;
|
|||
use crate::network::quote::BidQuote;
|
||||
use crate::network::rendezvous::XmrBtcNamespace;
|
||||
use crate::network::swarm;
|
||||
use crate::protocol::bob::{BobState, Swap};
|
||||
use crate::protocol::{bob, Database, State};
|
||||
use crate::{bitcoin, cli, monero};
|
||||
use crate::protocol::bob::{self, BobState, Swap};
|
||||
use crate::protocol::{Database, State};
|
||||
use crate::{cli, monero};
|
||||
use ::bitcoin::address::NetworkUnchecked;
|
||||
use ::bitcoin::Txid;
|
||||
use ::monero::Network;
|
||||
|
|
@ -36,6 +36,8 @@ use std::path::PathBuf;
|
|||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use swap_core::bitcoin;
|
||||
use swap_core::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock};
|
||||
use thiserror::Error;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use super::request::BalanceResponse;
|
||||
use crate::bitcoin;
|
||||
use crate::cli::api::request::{
|
||||
GetMoneroBalanceResponse, GetMoneroHistoryResponse, GetMoneroSyncProgressResponse,
|
||||
};
|
||||
use crate::cli::list_sellers::QuoteWithAddress;
|
||||
use crate::monero::MoneroAddressPool;
|
||||
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
||||
use crate::{monero, network::quote::BidQuote};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use bitcoin::Txid;
|
||||
|
|
@ -17,6 +16,8 @@ use std::sync::Arc;
|
|||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use strum::Display;
|
||||
use swap_core::bitcoin;
|
||||
use swap_core::bitcoin::ExpiredTimelocks;
|
||||
use tokio::sync::oneshot;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::bitcoin;
|
||||
use crate::monero::{Scalar, TransferProof};
|
||||
use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason;
|
||||
use crate::network::quote::BidQuote;
|
||||
|
|
@ -9,6 +8,7 @@ use crate::network::{
|
|||
};
|
||||
use crate::protocol::bob::State2;
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use libp2p::request_response::{
|
||||
InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel,
|
||||
};
|
||||
|
|
@ -104,7 +104,7 @@ impl Behaviour {
|
|||
pub fn new(
|
||||
alice: PeerId,
|
||||
env_config: env::Config,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
identify_params: (identity::Keypair, XmrBtcNamespace),
|
||||
) -> Self {
|
||||
let agentVersion = format!("cli/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use crate::bitcoin::{ExpiredTimelocks, Wallet};
|
||||
use crate::bitcoin::Wallet;
|
||||
use crate::monero::BlockHeight;
|
||||
use crate::protocol::bob::BobState;
|
||||
use crate::protocol::Database;
|
||||
use anyhow::{bail, Result};
|
||||
use bitcoin::Txid;
|
||||
use std::sync::Arc;
|
||||
use swap_core::bitcoin::ExpiredTimelocks;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn cancel_and_refund(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::bitcoin::{bitcoin_address, Amount};
|
||||
use crate::cli::api::request::{
|
||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs,
|
||||
GetHistoryArgs, ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs,
|
||||
|
|
@ -12,6 +11,7 @@ use std::ffi::OsString;
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use structopt::{clap, StructOpt};
|
||||
use swap_core::bitcoin::{bitcoin_address, Amount};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::bitcoin::EncryptedSignature;
|
||||
use crate::cli::behaviour::{Behaviour, OutEvent};
|
||||
use crate::monero;
|
||||
use crate::network::cooperative_xmr_redeem_after_punish::{self, Request, Response};
|
||||
|
|
@ -18,6 +17,7 @@ use libp2p::{PeerId, Swarm};
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use swap_core::bitcoin::EncryptedSignature;
|
||||
use uuid::Uuid;
|
||||
|
||||
static REQUEST_RESPONSE_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use super::api::tauri_bindings::{BackgroundRefundProgress, TauriBackgroundProgress, TauriEmitter};
|
||||
use super::api::SwapLock;
|
||||
use super::cancel_and_refund;
|
||||
use crate::bitcoin::{ExpiredTimelocks, Wallet};
|
||||
use crate::bitcoin::Wallet;
|
||||
use crate::cli::api::tauri_bindings::TauriHandle;
|
||||
use crate::protocol::bob::BobState;
|
||||
use crate::protocol::{Database, State};
|
||||
|
|
@ -9,6 +9,7 @@ use anyhow::{Context, Result};
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use swap_core::bitcoin::ExpiredTimelocks;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A long running task which watches for changes to timelocks and balance
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::bitcoin::EncryptedSignature;
|
||||
use crate::monero;
|
||||
use crate::monero::BlockHeight;
|
||||
use crate::monero::TransferProof;
|
||||
|
|
@ -6,6 +5,7 @@ use crate::protocol::alice;
|
|||
use crate::protocol::alice::AliceState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use swap_core::bitcoin::EncryptedSignature;
|
||||
|
||||
// Large enum variant is fine because this is only used for database
|
||||
// and is dropped once written in DB.
|
||||
|
|
|
|||
|
|
@ -23,11 +23,7 @@ pub mod common;
|
|||
pub mod database;
|
||||
pub mod libp2p_ext;
|
||||
pub mod monero;
|
||||
mod monero_ext;
|
||||
pub mod network;
|
||||
pub mod protocol;
|
||||
pub mod seed;
|
||||
pub mod tracing_ext;
|
||||
|
||||
#[cfg(test)]
|
||||
mod proptest;
|
||||
|
|
|
|||
|
|
@ -4,842 +4,5 @@ pub mod wallet_rpc;
|
|||
pub use ::monero::network::Network;
|
||||
pub use ::monero::{Address, PrivateKey, PublicKey};
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use wallet::{Daemon, Wallet, Wallets, WatchRequest};
|
||||
|
||||
use crate::bitcoin;
|
||||
use anyhow::{bail, Result};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rust_decimal::prelude::*;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
use std::str::FromStr;
|
||||
use typeshare::typeshare;
|
||||
|
||||
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
||||
|
||||
/// A Monero block height.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct BlockHeight {
|
||||
pub height: u64,
|
||||
}
|
||||
|
||||
impl fmt::Display for BlockHeight {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.height)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey {
|
||||
let mut bytes = scalar.to_bytes();
|
||||
|
||||
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
|
||||
// ed25519 scalar is little endian
|
||||
bytes.reverse();
|
||||
|
||||
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PrivateViewKey(#[serde(with = "swap_serde::monero::private_key")] PrivateKey);
|
||||
|
||||
impl fmt::Display for PrivateViewKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to the Display implementation of PrivateKey
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateViewKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
let private_key = PrivateKey::from_scalar(scalar);
|
||||
|
||||
Self(private_key)
|
||||
}
|
||||
|
||||
pub fn public(&self) -> PublicViewKey {
|
||||
PublicViewKey(PublicKey::from_private_key(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for PrivateViewKey {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateViewKey> for PrivateKey {
|
||||
fn from(from: PrivateViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicViewKey> for PublicKey {
|
||||
fn from(from: PublicViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PublicViewKey(PublicKey);
|
||||
|
||||
/// Our own monero amount type, which we need because the monero crate
|
||||
/// doesn't implement Serialize and Deserialize.
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)]
|
||||
#[typeshare(serialized_as = "number")]
|
||||
pub struct Amount(u64);
|
||||
|
||||
// TX Fees on Monero can be found here:
|
||||
// - https://www.monero.how/monero-transaction-fees
|
||||
// - https://bitinfocharts.com/comparison/monero-transactionfees.html#1y
|
||||
//
|
||||
// In the last year the highest avg fee on any given day was around 0.00075 XMR
|
||||
// We use a multiplier of 4x to stay safe
|
||||
// 0.00075 XMR * 4 = 0.003 XMR (around $1 as of Jun. 4th 2025)
|
||||
// We DO NOT use this fee to construct any transactions. It is only to **estimate** how much
|
||||
// we need to reserve for the fee when determining our max giveable amount
|
||||
// We use a VERY conservative value here to stay on the safe side. We want to avoid not being able
|
||||
// to lock as much as we previously estimated.
|
||||
pub const CONSERVATIVE_MONERO_FEE: Amount = Amount::from_piconero(3_000_000_000);
|
||||
|
||||
impl Amount {
|
||||
pub const ZERO: Self = Self(0);
|
||||
pub const ONE_XMR: Self = Self(PICONERO_OFFSET);
|
||||
/// Create an [Amount] with piconero precision and the given number of
|
||||
/// piconeros.
|
||||
///
|
||||
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
|
||||
pub const fn from_piconero(amount: u64) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
|
||||
/// Return Monero Amount as Piconero.
|
||||
pub fn as_piconero(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return Monero Amount as XMR.
|
||||
pub fn as_xmr(&self) -> f64 {
|
||||
let amount_decimal = Decimal::from(self.0);
|
||||
let offset_decimal = Decimal::from(PICONERO_OFFSET);
|
||||
let result = amount_decimal / offset_decimal;
|
||||
|
||||
// Convert to f64 only at the end, after the division
|
||||
result
|
||||
.to_f64()
|
||||
.expect("Conversion from piconero to XMR should not overflow f64")
|
||||
}
|
||||
|
||||
/// Calculate the conservative max giveable of Monero we can spent given [`self`] is the balance
|
||||
/// of a Monero wallet
|
||||
/// This is going to be LESS than we can really spent because we assume a high fee
|
||||
pub fn max_conservative_giveable(&self) -> Self {
|
||||
let pico_minus_fee = self
|
||||
.as_piconero()
|
||||
.saturating_sub(CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
|
||||
Self::from_piconero(pico_minus_fee)
|
||||
}
|
||||
|
||||
/// Calculate the Monero balance needed to send the [`self`] Amount to another address
|
||||
/// E.g: Amount(1 XMR).min_conservative_balance_to_spend() with a fee of 0.1 XMR would be 1.1 XMR
|
||||
/// This is going to be MORE than we really need because we assume a high fee
|
||||
pub fn min_conservative_balance_to_spend(&self) -> Self {
|
||||
let pico_minus_fee = self
|
||||
.as_piconero()
|
||||
.saturating_add(CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
|
||||
Self::from_piconero(pico_minus_fee)
|
||||
}
|
||||
|
||||
/// Calculate the maximum amount of Bitcoin that can be bought at a given
|
||||
/// asking price for this amount of Monero including the median fee.
|
||||
pub fn max_bitcoin_for_price(&self, ask_price: bitcoin::Amount) -> Option<bitcoin::Amount> {
|
||||
let pico_minus_fee = self.max_conservative_giveable();
|
||||
|
||||
if pico_minus_fee.as_piconero() == 0 {
|
||||
return Some(bitcoin::Amount::ZERO);
|
||||
}
|
||||
|
||||
// safely convert the BTC/XMR rate to sat/pico
|
||||
let ask_sats = Decimal::from(ask_price.to_sat());
|
||||
let pico_per_xmr = Decimal::from(PICONERO_OFFSET);
|
||||
let ask_sats_per_pico = ask_sats / pico_per_xmr;
|
||||
|
||||
let pico = Decimal::from(pico_minus_fee.as_piconero());
|
||||
let max_sats = pico.checked_mul(ask_sats_per_pico)?;
|
||||
let satoshi = max_sats.to_u64()?;
|
||||
|
||||
Some(bitcoin::Amount::from_sat(satoshi))
|
||||
}
|
||||
|
||||
pub fn from_monero(amount: f64) -> Result<Self> {
|
||||
let decimal = Decimal::try_from(amount)?;
|
||||
Self::from_decimal(decimal)
|
||||
}
|
||||
|
||||
pub fn parse_monero(amount: &str) -> Result<Self> {
|
||||
let decimal = Decimal::from_str(amount)?;
|
||||
Self::from_decimal(decimal)
|
||||
}
|
||||
|
||||
pub fn as_piconero_decimal(&self) -> Decimal {
|
||||
Decimal::from(self.as_piconero())
|
||||
}
|
||||
|
||||
fn from_decimal(amount: Decimal) -> Result<Self> {
|
||||
let piconeros_dec =
|
||||
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
||||
let piconeros = piconeros_dec
|
||||
.to_u64()
|
||||
.ok_or_else(|| OverflowError(amount.to_string()))?;
|
||||
Ok(Amount(piconeros))
|
||||
}
|
||||
|
||||
/// Subtract but throw an error on underflow.
|
||||
pub fn checked_sub(self, rhs: Amount) -> Result<Self> {
|
||||
if self.0 < rhs.0 {
|
||||
bail!("checked sub would underflow");
|
||||
}
|
||||
|
||||
Ok(Amount::from_piconero(self.0 - rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// A Monero address with an associated percentage and human-readable label.
|
||||
///
|
||||
/// This structure represents a destination address for Monero transactions
|
||||
/// along with the percentage of funds it should receive and a descriptive label.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[typeshare]
|
||||
pub struct LabeledMoneroAddress {
|
||||
// If this is None, we will use an address of the internal Monero wallet
|
||||
#[typeshare(serialized_as = "string")]
|
||||
address: Option<monero::Address>,
|
||||
#[typeshare(serialized_as = "number")]
|
||||
percentage: Decimal,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl LabeledMoneroAddress {
|
||||
/// Creates a new labeled Monero address.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `address` - The Monero address
|
||||
/// * `percentage` - The percentage of funds (between 0.0 and 1.0)
|
||||
/// * `label` - A human-readable label for this address
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the percentage is not between 0.0 and 1.0 inclusive.
|
||||
fn new(
|
||||
address: impl Into<Option<monero::Address>>,
|
||||
percentage: Decimal,
|
||||
label: String,
|
||||
) -> Result<Self> {
|
||||
if percentage < Decimal::ZERO || percentage > Decimal::ONE {
|
||||
bail!(
|
||||
"Percentage must be between 0 and 1 inclusive, got: {}",
|
||||
percentage
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
address: address.into(),
|
||||
percentage,
|
||||
label,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_address(
|
||||
address: monero::Address,
|
||||
percentage: Decimal,
|
||||
label: String,
|
||||
) -> Result<Self> {
|
||||
Self::new(address, percentage, label)
|
||||
}
|
||||
|
||||
pub fn with_internal_address(percentage: Decimal, label: String) -> Result<Self> {
|
||||
Self::new(None, percentage, label)
|
||||
}
|
||||
|
||||
/// Returns the Monero address.
|
||||
pub fn address(&self) -> Option<monero::Address> {
|
||||
self.address.clone()
|
||||
}
|
||||
|
||||
/// Returns the percentage as a decimal.
|
||||
pub fn percentage(&self) -> Decimal {
|
||||
self.percentage
|
||||
}
|
||||
|
||||
/// Returns the human-readable label.
|
||||
pub fn label(&self) -> &str {
|
||||
&self.label
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of labeled Monero addresses that can receive funds in a transaction.
|
||||
///
|
||||
/// This structure manages multiple destination addresses with their associated
|
||||
/// percentages and labels. It's used for splitting Monero transactions across
|
||||
/// multiple recipients, such as for donations or multi-destination swaps.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[typeshare]
|
||||
pub struct MoneroAddressPool(Vec<LabeledMoneroAddress>);
|
||||
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
impl MoneroAddressPool {
|
||||
/// Creates a new address pool from a vector of labeled addresses.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `addresses` - Vector of labeled Monero addresses
|
||||
pub fn new(addresses: Vec<LabeledMoneroAddress>) -> Self {
|
||||
Self(addresses)
|
||||
}
|
||||
|
||||
/// Returns a vector of all Monero addresses in the pool.
|
||||
pub fn addresses(&self) -> Vec<Option<monero::Address>> {
|
||||
self.0.iter().map(|address| address.address()).collect()
|
||||
}
|
||||
|
||||
/// Returns a vector of all percentages as f64 values (0-1 range).
|
||||
pub fn percentages(&self) -> Vec<f64> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|address| {
|
||||
address
|
||||
.percentage()
|
||||
.to_f64()
|
||||
.expect("Decimal should convert to f64")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the labeled addresses.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &LabeledMoneroAddress> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Validates that all addresses in the pool are on the expected network.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `network` - The expected Monero network
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any address is on a different network than expected.
|
||||
pub fn assert_network(&self, network: Network) -> Result<()> {
|
||||
for address in self.0.iter() {
|
||||
if let Some(address) = address.address {
|
||||
if address.network != network {
|
||||
bail!("Address pool contains addresses on the wrong network (address {} is on {:?}, expected {:?})", address, address.network, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assert that the sum of the percentages in the address pool is 1 (allowing for a small tolerance)
|
||||
pub fn assert_sum_to_one(&self) -> Result<()> {
|
||||
let sum = self
|
||||
.0
|
||||
.iter()
|
||||
.map(|address| address.percentage())
|
||||
.sum::<Decimal>();
|
||||
|
||||
const TOLERANCE: f64 = 1e-6;
|
||||
|
||||
if (sum - Decimal::ONE).abs()
|
||||
> Decimal::from_f64(TOLERANCE).expect("TOLERANCE constant should be a valid f64")
|
||||
{
|
||||
bail!("Address pool percentages do not sum to 1");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a vector of addresses with the empty addresses filled with the given primary address
|
||||
pub fn fill_empty_addresses(&self, primary_address: monero::Address) -> Vec<monero::Address> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|address| address.address().unwrap_or(primary_address))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<::monero::Address> for MoneroAddressPool {
|
||||
fn from(address: ::monero::Address) -> Self {
|
||||
Self(vec![LabeledMoneroAddress::new(
|
||||
address,
|
||||
Decimal::from(1),
|
||||
"user address".to_string(),
|
||||
)
|
||||
.expect("Percentage 1 is always valid")])
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Amount> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<u64> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for u64 {
|
||||
fn from(from: Amount) -> u64 {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<::monero::Amount> for Amount {
|
||||
fn from(from: ::monero::Amount) -> Self {
|
||||
Amount::from_piconero(from.as_pico())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for ::monero::Amount {
|
||||
fn from(from: Amount) -> Self {
|
||||
::monero::Amount::from_pico(from.as_piconero())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Amount {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut decimal = Decimal::from(self.0);
|
||||
decimal
|
||||
.set_scale(12)
|
||||
.expect("12 is smaller than max precision of 28");
|
||||
write!(f, "{} XMR", decimal)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TransferProof {
|
||||
tx_hash: TxHash,
|
||||
#[serde(with = "swap_serde::monero::private_key")]
|
||||
tx_key: PrivateKey,
|
||||
}
|
||||
|
||||
impl TransferProof {
|
||||
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
|
||||
Self { tx_hash, tx_key }
|
||||
}
|
||||
pub fn tx_hash(&self) -> TxHash {
|
||||
self.tx_hash.clone()
|
||||
}
|
||||
pub fn tx_key(&self) -> PrivateKey {
|
||||
self.tx_key
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add constructor/ change String to fixed length byte array
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TxHash(pub String);
|
||||
|
||||
impl From<TxHash> for String {
|
||||
fn from(from: TxHash) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TxHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TxHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("expected {expected}, got {actual}")]
|
||||
pub struct InsufficientFunds {
|
||||
pub expected: Amount,
|
||||
pub actual: Amount,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||
#[error("Overflow, cannot convert {0} to u64")]
|
||||
pub struct OverflowError(pub String);
|
||||
|
||||
pub mod monero_amount {
|
||||
use crate::monero::Amount;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_u64(x.as_piconero())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let picos = u64::deserialize(deserializer)?;
|
||||
let amount = Amount::from_piconero(picos);
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn display_monero_min() {
|
||||
let min_pics = 1;
|
||||
let amount = Amount::from_piconero(min_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("0.000000000001 XMR", monero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_monero_one() {
|
||||
let min_pics = 1000000000000;
|
||||
let amount = Amount::from_piconero(min_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("1.000000000000 XMR", monero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_monero_max() {
|
||||
let max_pics = 18_446_744_073_709_551_615;
|
||||
let amount = Amount::from_piconero(max_pics);
|
||||
let monero = amount.to_string();
|
||||
assert_eq!("18446744.073709551615 XMR", monero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_min() {
|
||||
let monero_min = "0.000000000001";
|
||||
let amount = Amount::parse_monero(monero_min).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(1, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero() {
|
||||
let monero = "123";
|
||||
let amount = Amount::parse_monero(monero).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(123000000000000, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_max() {
|
||||
let monero = "18446744.073709551615";
|
||||
let amount = Amount::parse_monero(monero).unwrap();
|
||||
let pics = amount.0;
|
||||
assert_eq!(18446744073709551615, pics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_monero_overflows() {
|
||||
let overflow_pics = "18446744.073709551616";
|
||||
let error = Amount::parse_monero(overflow_pics).unwrap_err();
|
||||
assert_eq!(
|
||||
error.downcast_ref::<OverflowError>().unwrap(),
|
||||
&OverflowError(overflow_pics.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_bitcoin_to_trade() {
|
||||
// sanity check: if the asking price is 1 BTC / 1 XMR
|
||||
// and we have μ XMR + fee
|
||||
// then max BTC we can buy is μ
|
||||
let ask = bitcoin::Amount::from_btc(1.0).unwrap();
|
||||
|
||||
let xmr = Amount::parse_monero("1.0").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("0.5").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(0.5).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("2.5").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(2.5).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("420").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(420.0).unwrap());
|
||||
|
||||
let xmr = Amount::parse_monero("0.00001").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(0.00001).unwrap());
|
||||
|
||||
// other ask prices
|
||||
|
||||
let ask = bitcoin::Amount::from_btc(0.5).unwrap();
|
||||
let xmr = Amount::parse_monero("2").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap());
|
||||
|
||||
let ask = bitcoin::Amount::from_btc(2.0).unwrap();
|
||||
let xmr = Amount::parse_monero("1").unwrap() + CONSERVATIVE_MONERO_FEE;
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_btc(2.0).unwrap());
|
||||
|
||||
let ask = bitcoin::Amount::from_sat(382_900);
|
||||
let xmr = Amount::parse_monero("10").unwrap();
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_sat(3_827_851));
|
||||
|
||||
// example from https://github.com/comit-network/xmr-btc-swap/issues/1084
|
||||
// with rate from kraken at that time
|
||||
let ask = bitcoin::Amount::from_sat(685_800);
|
||||
let xmr = Amount::parse_monero("0.826286435921").unwrap();
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(btc, bitcoin::Amount::from_sat(564_609));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_bitcoin_to_trade_overflow() {
|
||||
let xmr = Amount::from_monero(30.0).unwrap();
|
||||
let ask = bitcoin::Amount::from_sat(728_688);
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(bitcoin::Amount::from_sat(21_858_453), btc);
|
||||
|
||||
let xmr = Amount::from_piconero(u64::MAX);
|
||||
let ask = bitcoin::Amount::from_sat(u64::MAX);
|
||||
let btc = xmr.max_bitcoin_for_price(ask);
|
||||
|
||||
assert!(btc.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geting_max_bitcoin_to_trade_with_balance_smaller_than_locking_fee() {
|
||||
let ask = bitcoin::Amount::from_sat(382_900);
|
||||
let xmr = Amount::parse_monero("0.00001").unwrap();
|
||||
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
||||
|
||||
assert_eq!(bitcoin::Amount::ZERO, btc);
|
||||
}
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
||||
|
||||
#[test]
|
||||
fn serde_monero_private_key_json() {
|
||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
||||
crate::monero::Scalar::random(&mut OsRng),
|
||||
));
|
||||
let encoded = serde_json::to_vec(&key).unwrap();
|
||||
let decoded: MoneroPrivateKey = serde_json::from_slice(&encoded).unwrap();
|
||||
assert_eq!(key, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_monero_private_key_cbor() {
|
||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
||||
crate::monero::Scalar::random(&mut OsRng),
|
||||
));
|
||||
let encoded = serde_cbor::to_vec(&key).unwrap();
|
||||
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(key, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_monero_amount() {
|
||||
let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000));
|
||||
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
||||
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(amount, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_basic() {
|
||||
// Test with balance larger than fee
|
||||
let balance = Amount::parse_monero("1.0").unwrap();
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(giveable.as_piconero(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_exact_fee() {
|
||||
// Test with balance exactly equal to fee
|
||||
let balance = CONSERVATIVE_MONERO_FEE;
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
assert_eq!(giveable, Amount::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_less_than_fee() {
|
||||
// Test with balance less than fee (should saturate to 0)
|
||||
let balance = Amount::from_piconero(CONSERVATIVE_MONERO_FEE.as_piconero() / 2);
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
assert_eq!(giveable, Amount::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_zero_balance() {
|
||||
// Test with zero balance
|
||||
let balance = Amount::ZERO;
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
assert_eq!(giveable, Amount::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_conservative_giveable_large_balance() {
|
||||
// Test with large balance
|
||||
let balance = Amount::parse_monero("100.0").unwrap();
|
||||
let giveable = balance.max_conservative_giveable();
|
||||
let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(giveable.as_piconero(), expected);
|
||||
|
||||
// Ensure the result makes sense
|
||||
assert!(giveable.as_piconero() > 0);
|
||||
assert!(giveable < balance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_basic() {
|
||||
// Test with 1 XMR amount to send
|
||||
let amount_to_send = Amount::parse_monero("1.0").unwrap();
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(min_balance.as_piconero(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_zero() {
|
||||
// Test with zero amount to send
|
||||
let amount_to_send = Amount::ZERO;
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
assert_eq!(min_balance, CONSERVATIVE_MONERO_FEE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_small_amount() {
|
||||
// Test with small amount
|
||||
let amount_to_send = Amount::from_piconero(1000);
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
let expected = 1000 + CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(min_balance.as_piconero(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_conservative_balance_to_spend_large_amount() {
|
||||
// Test with large amount
|
||||
let amount_to_send = Amount::parse_monero("50.0").unwrap();
|
||||
let min_balance = amount_to_send.min_conservative_balance_to_spend();
|
||||
let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero();
|
||||
assert_eq!(min_balance.as_piconero(), expected);
|
||||
|
||||
// Ensure the result makes sense
|
||||
assert!(min_balance > amount_to_send);
|
||||
assert!(min_balance > CONSERVATIVE_MONERO_FEE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conservative_fee_functions_are_inverse() {
|
||||
// Test that the functions are somewhat inverse of each other
|
||||
let original_balance = Amount::parse_monero("5.0").unwrap();
|
||||
|
||||
// Get max giveable amount
|
||||
let max_giveable = original_balance.max_conservative_giveable();
|
||||
|
||||
// Calculate min balance needed to send that amount
|
||||
let min_balance_needed = max_giveable.min_conservative_balance_to_spend();
|
||||
|
||||
// The min balance needed should be equal to or slightly more than the original balance
|
||||
// (due to the conservative nature of the fee estimation)
|
||||
assert!(min_balance_needed >= original_balance);
|
||||
|
||||
// The difference should be at most the conservative fee
|
||||
let difference = min_balance_needed.as_piconero() - original_balance.as_piconero();
|
||||
assert!(difference <= CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conservative_fee_edge_cases() {
|
||||
// Test with maximum possible amount
|
||||
let max_amount = Amount::from_piconero(u64::MAX - CONSERVATIVE_MONERO_FEE.as_piconero());
|
||||
let giveable = max_amount.max_conservative_giveable();
|
||||
assert!(giveable.as_piconero() > 0);
|
||||
|
||||
// Test min balance calculation doesn't overflow
|
||||
let large_amount = Amount::from_piconero(u64::MAX / 2);
|
||||
let min_balance = large_amount.min_conservative_balance_to_spend();
|
||||
assert!(min_balance > large_amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labeled_monero_address_percentage_validation() {
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
let address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse::<monero::Address>().unwrap();
|
||||
|
||||
// Valid percentages should work (0-1 range)
|
||||
assert!(LabeledMoneroAddress::new(address, Decimal::ZERO, "test".to_string()).is_ok());
|
||||
assert!(LabeledMoneroAddress::new(address, Decimal::ONE, "test".to_string()).is_ok());
|
||||
assert!(LabeledMoneroAddress::new(address, Decimal::new(5, 1), "test".to_string()).is_ok()); // 0.5
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(9925, 4), "test".to_string()).is_ok()
|
||||
); // 0.9925
|
||||
|
||||
// Invalid percentages should fail
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(-1, 0), "test".to_string()).is_err()
|
||||
);
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(11, 1), "test".to_string()).is_err()
|
||||
); // 1.1
|
||||
assert!(
|
||||
LabeledMoneroAddress::new(address, Decimal::new(2, 0), "test".to_string()).is_err()
|
||||
); // 2.0
|
||||
}
|
||||
}
|
||||
pub use swap_core::monero::primitives::*;
|
||||
pub use wallet::{Daemon, Wallet, Wallets};
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
//! - wait for transactions to be confirmed
|
||||
//! - send money from one wallet to another.
|
||||
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
pub use monero_sys::{Daemon, WalletHandle as Wallet, WalletHandleListener};
|
||||
|
||||
use crate::common::throttle::{throttle, Throttle};
|
||||
use anyhow::{Context, Result};
|
||||
use monero::{Address, Network};
|
||||
use monero_simple_request_rpc::SimpleRequestRpc;
|
||||
use monero_sys::WalletEventListener;
|
||||
pub use monero_sys::{Daemon, WalletHandle as Wallet, WalletHandleListener};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ use crate::cli::api::{
|
|||
tauri_bindings::{MoneroWalletUpdate, TauriEmitter, TauriEvent, TauriHandle},
|
||||
};
|
||||
|
||||
use super::{BlockHeight, TransferProof, TxHash};
|
||||
use super::{BlockHeight, TxHash, WatchRequest};
|
||||
|
||||
/// Entrance point to the Monero blockchain.
|
||||
/// You can use this struct to open specific wallets and monitor the blockchain.
|
||||
|
|
@ -45,25 +45,6 @@ pub struct Wallets {
|
|||
wallet_database: Option<Arc<monero_sys::Database>>,
|
||||
}
|
||||
|
||||
/// A request to watch for a transfer.
|
||||
pub struct WatchRequest {
|
||||
pub public_view_key: super::PublicViewKey,
|
||||
pub public_spend_key: monero::PublicKey,
|
||||
/// The proof of the transfer.
|
||||
pub transfer_proof: TransferProof,
|
||||
/// The expected amount of the transfer.
|
||||
pub expected_amount: monero::Amount,
|
||||
/// The number of confirmations required for the transfer to be considered confirmed.
|
||||
pub confirmation_target: u64,
|
||||
}
|
||||
|
||||
/// Transfer a specified amount of money to a specified address.
|
||||
pub struct TransferRequest {
|
||||
pub public_spend_key: monero::PublicKey,
|
||||
pub public_view_key: super::PublicViewKey,
|
||||
pub amount: monero::Amount,
|
||||
}
|
||||
|
||||
struct TauriWalletListener {
|
||||
// one throttle wrapper per expensive update
|
||||
balance_throttle: Throttle<()>,
|
||||
|
|
@ -513,15 +494,6 @@ impl Wallets {
|
|||
}
|
||||
}
|
||||
|
||||
impl TransferRequest {
|
||||
pub fn address_and_amount(&self, network: Network) -> (Address, monero::Amount) {
|
||||
(
|
||||
Address::standard(network, self.public_spend_key, self.public_view_key.0),
|
||||
self.amount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pass this to [`Wallet::wait_until_confirmed`] or [`Wallet::wait_until_synced`]
|
||||
/// to not receive any confirmation callbacks.
|
||||
pub fn no_listener<T>() -> Option<impl Fn(T) + Send + 'static> {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,8 @@
|
|||
use ::monero::Network;
|
||||
use anyhow::{bail, Context, Error, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use anyhow::{Context, Error, Result};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::time::Duration;
|
||||
|
||||
// See: https://www.moneroworld.com/#nodes, https://monero.fail
|
||||
// We don't need any testnet nodes because we don't support testnet at all
|
||||
static MONERO_DAEMONS: Lazy<[MoneroDaemon; 12]> = Lazy::new(|| {
|
||||
[
|
||||
MoneroDaemon::new("http://xmr-node.cakewallet.com:18081", Network::Mainnet),
|
||||
MoneroDaemon::new("http://nodex.monerujo.io:18081", Network::Mainnet),
|
||||
MoneroDaemon::new("http://nodes.hashvault.pro:18081", Network::Mainnet),
|
||||
MoneroDaemon::new("http://p2pmd.xmrvsbeast.com:18081", Network::Mainnet),
|
||||
MoneroDaemon::new("http://node.monerodevs.org:18089", Network::Mainnet),
|
||||
MoneroDaemon::new("http://xmr-node-uk.cakewallet.com:18081", Network::Mainnet),
|
||||
MoneroDaemon::new("http://xmr.litepay.ch:18081", Network::Mainnet),
|
||||
MoneroDaemon::new("http://stagenet.xmr-tw.org:38081", Network::Stagenet),
|
||||
MoneroDaemon::new("http://node.monerodevs.org:38089", Network::Stagenet),
|
||||
MoneroDaemon::new("http://singapore.node.xmr.pm:38081", Network::Stagenet),
|
||||
MoneroDaemon::new("http://xmr-lux.boldsuck.org:38081", Network::Stagenet),
|
||||
MoneroDaemon::new("http://stagenet.community.rino.io:38081", Network::Stagenet),
|
||||
]
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MoneroDaemon {
|
||||
|
|
@ -92,40 +71,6 @@ struct MoneroDaemonGetInfoResponse {
|
|||
testnet: bool,
|
||||
}
|
||||
|
||||
/// Chooses an available Monero daemon based on the specified network.
|
||||
async fn choose_monero_daemon(network: Network) -> Result<MoneroDaemon, Error> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.https_only(false)
|
||||
.build()?;
|
||||
|
||||
// We only want to check for daemons that match the specified network
|
||||
let network_matching_daemons = MONERO_DAEMONS
|
||||
.iter()
|
||||
.filter(|daemon| daemon.network == network);
|
||||
|
||||
for daemon in network_matching_daemons {
|
||||
match daemon.is_available(&client).await {
|
||||
Ok(true) => {
|
||||
tracing::debug!(%daemon, "Found available Monero daemon");
|
||||
return Ok(daemon.clone());
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!(?err, %daemon, "Failed to connect to Monero daemon");
|
||||
continue;
|
||||
}
|
||||
Ok(false) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
bail!("No Monero daemon could be found. Please specify one manually or try again later.")
|
||||
}
|
||||
|
||||
/// Public wrapper around [`choose_monero_daemon`].
|
||||
pub async fn choose_monero_node(network: Network) -> Result<MoneroDaemon, Error> {
|
||||
choose_monero_daemon(network).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ impl AsRef<str> for EncryptedSignatureProtocol {
|
|||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub swap_id: Uuid,
|
||||
pub tx_redeem_encsig: crate::bitcoin::EncryptedSignature,
|
||||
pub tx_redeem_encsig: swap_core::bitcoin::EncryptedSignature,
|
||||
}
|
||||
|
||||
pub fn alice() -> Behaviour {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::{asb, bitcoin, cli};
|
||||
use crate::{asb, cli};
|
||||
use libp2p::request_response::{self, ProtocolSupport};
|
||||
use libp2p::{PeerId, StreamProtocol};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use swap_core::bitcoin;
|
||||
use typeshare::typeshare;
|
||||
|
||||
const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0";
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ use crate::network::swap_setup::{
|
|||
};
|
||||
use crate::protocol::alice::{State0, State3};
|
||||
use crate::protocol::{Message0, Message2, Message4};
|
||||
use crate::{asb, bitcoin, monero};
|
||||
use crate::{asb, monero};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use futures::future::{BoxFuture, OptionFuture};
|
||||
use futures::AsyncWriteExt;
|
||||
use futures::FutureExt;
|
||||
|
|
@ -17,8 +18,10 @@ use libp2p::swarm::{ConnectionHandlerEvent, NetworkBehaviour, SubstreamProtocol,
|
|||
use libp2p::{Multiaddr, PeerId};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::time::{Duration, Instant};
|
||||
use swap_core::bitcoin;
|
||||
use swap_env::env;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ pub struct WalletSnapshot {
|
|||
|
||||
impl WalletSnapshot {
|
||||
pub async fn capture(
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: &monero::Wallets,
|
||||
external_redeem_address: &Option<bitcoin::Address>,
|
||||
transfer_amount: bitcoin::Amount,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use crate::network::swap_setup::{protocol, BlockchainNetwork, SpotPriceError, SpotPriceResponse};
|
||||
use crate::protocol::bob::{State0, State2};
|
||||
use crate::protocol::{Message1, Message3};
|
||||
use crate::{bitcoin, cli, monero};
|
||||
use crate::{cli, monero};
|
||||
use anyhow::{Context, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use futures::future::{BoxFuture, OptionFuture};
|
||||
use futures::AsyncWriteExt;
|
||||
use futures::FutureExt;
|
||||
|
|
@ -16,6 +17,7 @@ use std::collections::VecDeque;
|
|||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
use swap_core::bitcoin;
|
||||
use swap_env::env;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -24,13 +26,13 @@ use super::{read_cbor_message, write_cbor_message, SpotPriceRequest};
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct Behaviour {
|
||||
env_config: env::Config,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
new_swaps: VecDeque<(PeerId, NewSwap)>,
|
||||
completed_swaps: VecDeque<(PeerId, Completed)>,
|
||||
}
|
||||
|
||||
impl Behaviour {
|
||||
pub fn new(env_config: env::Config, bitcoin_wallet: Arc<bitcoin::Wallet>) -> Self {
|
||||
pub fn new(env_config: env::Config, bitcoin_wallet: Arc<dyn BitcoinWallet>) -> Self {
|
||||
Self {
|
||||
env_config,
|
||||
bitcoin_wallet,
|
||||
|
|
@ -116,12 +118,12 @@ pub struct Handler {
|
|||
env_config: env::Config,
|
||||
timeout: Duration,
|
||||
new_swaps: VecDeque<NewSwap>,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
keep_alive: bool,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
fn new(env_config: env::Config, bitcoin_wallet: Arc<bitcoin::Wallet>) -> Self {
|
||||
fn new(env_config: env::Config, bitcoin_wallet: Arc<dyn BitcoinWallet>) -> Self {
|
||||
Self {
|
||||
env_config,
|
||||
outbound_stream: OptionFuture::from(None),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::asb::{LatestRate, RendezvousNode};
|
|||
use crate::libp2p_ext::MultiAddrExt;
|
||||
use crate::network::rendezvous::XmrBtcNamespace;
|
||||
use crate::seed::Seed;
|
||||
use crate::{asb, bitcoin, cli};
|
||||
use crate::{asb, cli};
|
||||
use anyhow::Result;
|
||||
use arti_client::TorClient;
|
||||
use libp2p::swarm::NetworkBehaviour;
|
||||
|
|
@ -11,6 +11,7 @@ use libp2p::{identity, Multiaddr, Swarm};
|
|||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use swap_core::bitcoin;
|
||||
use swap_env::env;
|
||||
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,167 +1,4 @@
|
|||
use crate::monero::MoneroAddressPool;
|
||||
use crate::protocol::alice::swap::is_complete as alice_is_complete;
|
||||
use crate::protocol::alice::AliceState;
|
||||
use crate::protocol::bob::swap::is_complete as bob_is_complete;
|
||||
use crate::protocol::bob::BobState;
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use conquer_once::Lazy;
|
||||
use libp2p::{Multiaddr, PeerId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof};
|
||||
use sigma_fun::HashTranscript;
|
||||
use std::convert::TryInto;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod alice;
|
||||
pub mod bob;
|
||||
|
||||
pub static CROSS_CURVE_PROOF_SYSTEM: Lazy<
|
||||
CrossCurveDLEQ<HashTranscript<Sha256, rand_chacha::ChaCha20Rng>>,
|
||||
> = Lazy::new(|| {
|
||||
CrossCurveDLEQ::<HashTranscript<Sha256, rand_chacha::ChaCha20Rng>>::new(
|
||||
(*ecdsa_fun::fun::G).normalize(),
|
||||
curve25519_dalek::constants::ED25519_BASEPOINT_POINT,
|
||||
)
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message0 {
|
||||
swap_id: Uuid,
|
||||
B: bitcoin::PublicKey,
|
||||
S_b_monero: monero::PublicKey,
|
||||
S_b_bitcoin: bitcoin::PublicKey,
|
||||
dleq_proof_s_b: CrossCurveDLEQProof,
|
||||
v_b: monero::PrivateViewKey,
|
||||
#[serde(with = "swap_serde::bitcoin::address_serde")]
|
||||
refund_address: bitcoin::Address,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
tx_refund_fee: bitcoin::Amount,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
tx_cancel_fee: bitcoin::Amount,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message1 {
|
||||
A: bitcoin::PublicKey,
|
||||
S_a_monero: monero::PublicKey,
|
||||
S_a_bitcoin: bitcoin::PublicKey,
|
||||
dleq_proof_s_a: CrossCurveDLEQProof,
|
||||
v_a: monero::PrivateViewKey,
|
||||
#[serde(with = "swap_serde::bitcoin::address_serde")]
|
||||
redeem_address: bitcoin::Address,
|
||||
#[serde(with = "swap_serde::bitcoin::address_serde")]
|
||||
punish_address: bitcoin::Address,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
tx_redeem_fee: bitcoin::Amount,
|
||||
#[serde(with = "::bitcoin::amount::serde::as_sat")]
|
||||
tx_punish_fee: bitcoin::Amount,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message2 {
|
||||
psbt: bitcoin::PartiallySignedTransaction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message3 {
|
||||
tx_cancel_sig: bitcoin::Signature,
|
||||
tx_refund_encsig: bitcoin::EncryptedSignature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message4 {
|
||||
tx_punish_sig: bitcoin::Signature,
|
||||
tx_cancel_sig: bitcoin::Signature,
|
||||
tx_early_refund_sig: bitcoin::Signature,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum State {
|
||||
Alice(AliceState),
|
||||
Bob(BobState),
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn swap_finished(&self) -> bool {
|
||||
match self {
|
||||
State::Alice(state) => alice_is_complete(state),
|
||||
State::Bob(state) => bob_is_complete(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AliceState> for State {
|
||||
fn from(alice: AliceState) -> Self {
|
||||
Self::Alice(alice)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BobState> for State {
|
||||
fn from(bob: BobState) -> Self {
|
||||
Self::Bob(bob)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[error("Not in the role of Alice")]
|
||||
pub struct NotAlice;
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[error("Not in the role of Bob")]
|
||||
pub struct NotBob;
|
||||
|
||||
impl TryInto<BobState> for State {
|
||||
type Error = NotBob;
|
||||
|
||||
fn try_into(self) -> std::result::Result<BobState, Self::Error> {
|
||||
match self {
|
||||
State::Alice(_) => Err(NotBob),
|
||||
State::Bob(state) => Ok(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<AliceState> for State {
|
||||
type Error = NotAlice;
|
||||
|
||||
fn try_into(self) -> std::result::Result<AliceState, Self::Error> {
|
||||
match self {
|
||||
State::Alice(state) => Ok(state),
|
||||
State::Bob(_) => Err(NotAlice),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Database {
|
||||
async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()>;
|
||||
async fn get_peer_id(&self, swap_id: Uuid) -> Result<PeerId>;
|
||||
async fn insert_monero_address_pool(
|
||||
&self,
|
||||
swap_id: Uuid,
|
||||
address: MoneroAddressPool,
|
||||
) -> Result<()>;
|
||||
async fn get_monero_address_pool(&self, swap_id: Uuid) -> Result<MoneroAddressPool>;
|
||||
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>>;
|
||||
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>;
|
||||
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>>;
|
||||
async fn get_all_peer_addresses(&self) -> Result<Vec<(PeerId, Vec<Multiaddr>)>>;
|
||||
async fn get_swap_start_date(&self, swap_id: Uuid) -> Result<String>;
|
||||
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>;
|
||||
async fn get_state(&self, swap_id: Uuid) -> Result<State>;
|
||||
async fn get_states(&self, swap_id: Uuid) -> Result<Vec<State>>;
|
||||
async fn all(&self) -> Result<Vec<(Uuid, State)>>;
|
||||
async fn insert_buffered_transfer_proof(
|
||||
&self,
|
||||
swap_id: Uuid,
|
||||
proof: monero::TransferProof,
|
||||
) -> Result<()>;
|
||||
async fn get_buffered_transfer_proof(
|
||||
&self,
|
||||
swap_id: Uuid,
|
||||
) -> Result<Option<monero::TransferProof>>;
|
||||
}
|
||||
pub use swap_machine::common::*;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
//! Run an XMR/BTC swap in the role of Alice.
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
pub use crate::protocol::alice::swap::*;
|
||||
use crate::protocol::Database;
|
||||
use crate::{asb, bitcoin, monero};
|
||||
use crate::{asb, monero};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use rust_decimal::Decimal;
|
||||
use std::sync::Arc;
|
||||
use swap_env::env::Config;
|
||||
pub use swap_machine::alice::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use self::state::*;
|
||||
pub use self::swap::{run, run_until};
|
||||
|
||||
pub mod state;
|
||||
pub mod swap;
|
||||
|
||||
pub struct Swap {
|
||||
pub state: AliceState,
|
||||
pub event_loop_handle: asb::EventLoopHandle,
|
||||
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
pub bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
pub monero_wallet: Arc<monero::Wallets>,
|
||||
pub env_config: Config,
|
||||
pub developer_tip: TipConfig,
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::asb::{EventLoopHandle, LatestRate};
|
||||
use crate::bitcoin::ExpiredTimelocks;
|
||||
use crate::common::retry;
|
||||
use crate::monero;
|
||||
use crate::monero::TransferProof;
|
||||
use crate::protocol::alice::{AliceState, Swap, TipConfig};
|
||||
use crate::{bitcoin, monero};
|
||||
use ::bitcoin::consensus::encode::serialize_hex;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use rust_decimal::Decimal;
|
||||
use swap_core::bitcoin::ExpiredTimelocks;
|
||||
use swap_env::env::Config;
|
||||
use swap_machine::alice::State3;
|
||||
use tokio::select;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -36,12 +38,12 @@ where
|
|||
{
|
||||
let mut current_state = swap.state;
|
||||
|
||||
while !is_complete(¤t_state) && !exit_early(¤t_state) {
|
||||
while !swap_machine::alice::is_complete(¤t_state) && !exit_early(¤t_state) {
|
||||
current_state = next_state(
|
||||
swap.swap_id,
|
||||
current_state,
|
||||
&mut swap.event_loop_handle,
|
||||
swap.bitcoin_wallet.as_ref(),
|
||||
swap.bitcoin_wallet.clone(),
|
||||
swap.monero_wallet.clone(),
|
||||
&swap.env_config,
|
||||
swap.developer_tip.clone(),
|
||||
|
|
@ -61,7 +63,7 @@ async fn next_state<LR>(
|
|||
swap_id: Uuid,
|
||||
state: AliceState,
|
||||
event_loop_handle: &mut EventLoopHandle,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
env_config: &Config,
|
||||
developer_tip: TipConfig,
|
||||
|
|
@ -78,7 +80,9 @@ where
|
|||
|
||||
Ok(match state {
|
||||
AliceState::Started { state3 } => {
|
||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
||||
let tx_lock_status = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_lock.clone()))
|
||||
.await;
|
||||
|
||||
match timeout(
|
||||
env_config.bitcoin_lock_mempool_timeout,
|
||||
|
|
@ -100,7 +104,9 @@ where
|
|||
}
|
||||
}
|
||||
AliceState::BtcLockTransactionSeen { state3 } => {
|
||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
||||
let tx_lock_status = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_lock.clone()))
|
||||
.await;
|
||||
|
||||
match timeout(
|
||||
env_config.bitcoin_lock_confirmed_timeout,
|
||||
|
|
@ -141,7 +147,7 @@ where
|
|||
// because there is no way for the swap to succeed.
|
||||
if !matches!(
|
||||
state3
|
||||
.expired_timelocks(bitcoin_wallet)
|
||||
.expired_timelocks(&*bitcoin_wallet)
|
||||
.await
|
||||
.context("Failed to check for expired timelocks before locking Monero")
|
||||
.map_err(backoff::Error::transient)?,
|
||||
|
|
@ -238,7 +244,9 @@ where
|
|||
let tx_early_refund_txid = tx_early_refund.compute_txid();
|
||||
|
||||
// Bob might cancel the swap and refund for himself. We won't need to early refund anymore.
|
||||
let tx_cancel_status = bitcoin_wallet.subscribe_to(state3.tx_cancel()).await;
|
||||
let tx_cancel_status = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_cancel()))
|
||||
.await;
|
||||
|
||||
let backoff = backoff::ExponentialBackoffBuilder::new()
|
||||
// We give up after 6 hours
|
||||
|
|
@ -304,7 +312,7 @@ where
|
|||
monero_wallet_restore_blockheight,
|
||||
transfer_proof,
|
||||
state3,
|
||||
} => match state3.expired_timelocks(bitcoin_wallet).await? {
|
||||
} => match state3.expired_timelocks(&*bitcoin_wallet).await? {
|
||||
ExpiredTimelocks::None { .. } => {
|
||||
tracing::info!("Locked Monero, waiting for confirmations");
|
||||
monero_wallet
|
||||
|
|
@ -343,7 +351,9 @@ where
|
|||
transfer_proof,
|
||||
state3,
|
||||
} => {
|
||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
||||
let tx_lock_status = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_lock.clone()))
|
||||
.await;
|
||||
|
||||
tokio::select! {
|
||||
result = event_loop_handle.send_transfer_proof(transfer_proof.clone()) => {
|
||||
|
|
@ -375,8 +385,9 @@ where
|
|||
transfer_proof,
|
||||
state3,
|
||||
} => {
|
||||
let tx_lock_status_subscription =
|
||||
bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
||||
let tx_lock_status_subscription = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_lock.clone()))
|
||||
.await;
|
||||
|
||||
select! {
|
||||
biased; // make sure the cancel timelock expiry future is polled first
|
||||
|
|
@ -430,7 +441,9 @@ where
|
|||
"Waiting for cancellation timelock to expire",
|
||||
);
|
||||
|
||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
||||
let tx_lock_status = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_lock.clone()))
|
||||
.await;
|
||||
|
||||
tx_lock_status
|
||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
||||
|
|
@ -454,7 +467,7 @@ where
|
|||
match backoff::future::retry_notify(backoff.clone(), || async {
|
||||
// If the cancel timelock is expired, there is no need to try to publish the redeem transaction anymore
|
||||
if !matches!(
|
||||
state3.expired_timelocks(bitcoin_wallet).await?,
|
||||
state3.expired_timelocks(&*bitcoin_wallet).await?,
|
||||
ExpiredTimelocks::None { .. }
|
||||
) {
|
||||
return Ok(None);
|
||||
|
|
@ -502,7 +515,9 @@ where
|
|||
}
|
||||
}
|
||||
AliceState::BtcRedeemTransactionPublished { state3, .. } => {
|
||||
let subscription = bitcoin_wallet.subscribe_to(state3.tx_redeem()).await;
|
||||
let subscription = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_redeem()))
|
||||
.await;
|
||||
|
||||
match subscription.wait_until_final().await {
|
||||
Ok(_) => AliceState::BtcRedeemed,
|
||||
|
|
@ -516,13 +531,17 @@ where
|
|||
transfer_proof,
|
||||
state3,
|
||||
} => {
|
||||
if state3.check_for_tx_cancel(bitcoin_wallet).await?.is_none() {
|
||||
if state3
|
||||
.check_for_tx_cancel(&*bitcoin_wallet)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
// If Bob hasn't yet broadcasted the cancel transaction, Alice has to publish it
|
||||
// to be able to eventually punish. Since the punish timelock is
|
||||
// relative to the publication of the cancel transaction we have to ensure it
|
||||
// gets published once the cancel timelock expires.
|
||||
|
||||
if let Err(e) = state3.submit_tx_cancel(bitcoin_wallet).await {
|
||||
if let Err(e) = state3.submit_tx_cancel(&*bitcoin_wallet).await {
|
||||
// TODO: Actually ensure the transaction is published
|
||||
// What about a wrapper function ensure_tx_published that repeats the tx submission until
|
||||
// our subscription sees it in the mempool?
|
||||
|
|
@ -545,10 +564,12 @@ where
|
|||
transfer_proof,
|
||||
state3,
|
||||
} => {
|
||||
let tx_cancel_status = bitcoin_wallet.subscribe_to(state3.tx_cancel()).await;
|
||||
let tx_cancel_status = bitcoin_wallet
|
||||
.subscribe_to(Box::new(state3.tx_cancel()))
|
||||
.await;
|
||||
|
||||
select! {
|
||||
spend_key = state3.watch_for_btc_tx_refund(bitcoin_wallet) => {
|
||||
spend_key = state3.watch_for_btc_tx_refund(&*bitcoin_wallet) => {
|
||||
let spend_key = spend_key?;
|
||||
|
||||
AliceState::BtcRefunded {
|
||||
|
|
@ -603,7 +624,7 @@ where
|
|||
} => {
|
||||
// TODO: We should retry indefinitely here until we find the refund transaction
|
||||
// TODO: If we crash while we are waiting for the punish_tx to be confirmed (punish_btc waits until confirmation), we will remain in this state forever because we will attempt to re-publish the punish transaction
|
||||
let punish = state3.punish_btc(bitcoin_wallet).await;
|
||||
let punish = state3.punish_btc(&*bitcoin_wallet).await;
|
||||
|
||||
match punish {
|
||||
Ok(_) => AliceState::BtcPunished {
|
||||
|
|
@ -653,15 +674,84 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
pub fn is_complete(state: &AliceState) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
AliceState::XmrRefunded
|
||||
| AliceState::BtcRedeemed
|
||||
| AliceState::BtcPunished { .. }
|
||||
| AliceState::SafelyAborted
|
||||
| AliceState::BtcEarlyRefunded(_)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait XmrRefundable {
|
||||
async fn refund_xmr(
|
||||
&self,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
swap_id: Uuid,
|
||||
spend_key: monero::PrivateKey,
|
||||
transfer_proof: TransferProof,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
impl XmrRefundable for State3 {
|
||||
async fn refund_xmr(
|
||||
&self,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
swap_id: Uuid,
|
||||
spend_key: monero::PrivateKey,
|
||||
transfer_proof: TransferProof,
|
||||
) -> Result<()> {
|
||||
let view_key = self.v;
|
||||
|
||||
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
|
||||
// on the lock transaction.
|
||||
tracing::info!("Waiting for Monero lock transaction to be confirmed");
|
||||
let transfer_proof_2 = transfer_proof.clone();
|
||||
monero_wallet
|
||||
.wait_until_confirmed(
|
||||
self.lock_xmr_watch_request(transfer_proof_2, 10),
|
||||
Some(move |(confirmations, target_confirmations)| {
|
||||
tracing::debug!(
|
||||
%confirmations,
|
||||
%target_confirmations,
|
||||
"Monero lock transaction got a confirmation"
|
||||
);
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("Failed to wait for Monero lock transaction to be confirmed")?;
|
||||
|
||||
tracing::info!("Refunding Monero");
|
||||
|
||||
tracing::debug!(%swap_id, "Opening temporary Monero wallet from keys");
|
||||
let swap_wallet = monero_wallet
|
||||
.swap_wallet(swap_id, spend_key, view_key, transfer_proof.tx_hash())
|
||||
.await
|
||||
.context(format!("Failed to open/create swap wallet `{}`", swap_id))?;
|
||||
|
||||
// Update blockheight to ensure that the wallet knows the funds are unlocked
|
||||
tracing::debug!(%swap_id, "Updating temporary Monero wallet's blockheight");
|
||||
let _ = swap_wallet
|
||||
.blockchain_height()
|
||||
.await
|
||||
.context("Couldn't get Monero blockheight")?;
|
||||
|
||||
tracing::debug!(%swap_id, "Sweeping Monero to redeem address");
|
||||
let main_address = monero_wallet.main_wallet().await.main_address().await;
|
||||
|
||||
swap_wallet
|
||||
.sweep(&main_address)
|
||||
.await
|
||||
.context("Failed to sweep Monero to redeem address")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl XmrRefundable for Box<State3> {
|
||||
async fn refund_xmr(
|
||||
&self,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
swap_id: Uuid,
|
||||
spend_key: monero::PrivateKey,
|
||||
transfer_proof: TransferProof,
|
||||
) -> Result<()> {
|
||||
(**self)
|
||||
.refund_xmr(monero_wallet, swap_id, spend_key, transfer_proof)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Build transfer destinations for the Monero lock transaction, optionally including a developer tip.
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use bitcoin_wallet::BitcoinWallet;
|
||||
use std::convert::TryInto;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::cli::api::tauri_bindings::TauriHandle;
|
||||
use crate::monero::MoneroAddressPool;
|
||||
use crate::protocol::Database;
|
||||
use crate::{bitcoin, cli, monero};
|
||||
use crate::{cli, monero};
|
||||
|
||||
use swap_core::bitcoin;
|
||||
use swap_env::env;
|
||||
|
||||
pub use self::state::*;
|
||||
pub use self::swap::{run, run_until};
|
||||
use std::convert::TryInto;
|
||||
pub use crate::protocol::bob::swap::*;
|
||||
pub use swap_machine::bob::*;
|
||||
|
||||
pub mod state;
|
||||
pub mod swap;
|
||||
|
||||
pub struct Swap {
|
||||
pub state: BobState,
|
||||
pub event_loop_handle: cli::EventLoopHandle,
|
||||
pub db: Arc<dyn Database + Send + Sync>,
|
||||
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
pub bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
pub monero_wallet: Arc<monero::Wallets>,
|
||||
pub env_config: env::Config,
|
||||
pub id: Uuid,
|
||||
|
|
@ -33,7 +35,7 @@ impl Swap {
|
|||
pub fn new(
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
id: Uuid,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
env_config: env::Config,
|
||||
event_loop_handle: cli::EventLoopHandle,
|
||||
|
|
@ -63,7 +65,7 @@ impl Swap {
|
|||
pub async fn from_db(
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
id: Uuid,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
env_config: env::Config,
|
||||
event_loop_handle: cli::EventLoopHandle,
|
||||
|
|
|
|||
|
|
@ -1,34 +1,25 @@
|
|||
use crate::bitcoin::wallet::ScriptStatus;
|
||||
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
||||
use crate::cli::api::tauri_bindings::LockBitcoinDetails;
|
||||
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent};
|
||||
use crate::cli::EventLoopHandle;
|
||||
use crate::common::retry;
|
||||
use crate::monero;
|
||||
use crate::monero::MoneroAddressPool;
|
||||
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
|
||||
use crate::network::swap_setup::bob::NewSwap;
|
||||
use crate::protocol::bob::state::*;
|
||||
use crate::protocol::bob::*;
|
||||
use crate::protocol::{bob, Database};
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{bail, Context as AnyContext, Result};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use swap_core::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
||||
use swap_core::monero::TxHash;
|
||||
use swap_env::env;
|
||||
use swap_machine::bob::State5;
|
||||
use tokio::select;
|
||||
use uuid::Uuid;
|
||||
|
||||
const PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS: u64 = 60 * 3;
|
||||
|
||||
pub fn is_complete(state: &BobState) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
BobState::BtcRefunded(..)
|
||||
| BobState::BtcEarlyRefunded { .. }
|
||||
| BobState::XmrRedeemed { .. }
|
||||
| BobState::SafelyAborted
|
||||
)
|
||||
}
|
||||
|
||||
/// Identifies states that have already processed the transfer proof.
|
||||
/// This is used to be able to acknowledge the transfer proof multiple times (if it was already processed).
|
||||
/// This is necessary because sometimes our acknowledgement might not reach Alice.
|
||||
|
|
@ -73,7 +64,7 @@ pub async fn run_until(
|
|||
current_state.clone(),
|
||||
&mut swap.event_loop_handle,
|
||||
swap.db.clone(),
|
||||
swap.bitcoin_wallet.as_ref(),
|
||||
swap.bitcoin_wallet.clone(),
|
||||
swap.monero_wallet.clone(),
|
||||
swap.monero_receive_pool.clone(),
|
||||
swap.event_emitter.clone(),
|
||||
|
|
@ -101,7 +92,7 @@ async fn next_state(
|
|||
state: BobState,
|
||||
event_loop_handle: &mut EventLoopHandle,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
bitcoin_wallet: Arc<dyn BitcoinWallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
monero_receive_pool: MoneroAddressPool,
|
||||
event_emitter: Option<TauriHandle>,
|
||||
|
|
@ -267,16 +258,19 @@ async fn next_state(
|
|||
},
|
||||
);
|
||||
|
||||
let (tx_early_refund_status, tx_lock_status) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(state3.construct_tx_early_refund()),
|
||||
bitcoin_wallet.subscribe_to(state3.tx_lock.clone())
|
||||
let (tx_early_refund_status, tx_lock_status): (
|
||||
bitcoin_wallet::Subscription,
|
||||
bitcoin_wallet::Subscription,
|
||||
) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(Box::new(state3.construct_tx_early_refund())),
|
||||
bitcoin_wallet.subscribe_to(Box::new(state3.tx_lock.clone()))
|
||||
);
|
||||
|
||||
// Check explicitly whether the cancel timelock has expired
|
||||
// (Most likely redundant but cannot hurt)
|
||||
// We only warn if this fails
|
||||
if let Ok(true) = state3
|
||||
.expired_timelock(bitcoin_wallet)
|
||||
.expired_timelock(&*bitcoin_wallet)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!(?err, "Failed to check for cancel timelock expiration");
|
||||
|
|
@ -291,7 +285,7 @@ async fn next_state(
|
|||
// (Most likely redundant because we already do this below but cannot hurt)
|
||||
// We only warn if this fail here
|
||||
if let Ok(Some(_)) = state3
|
||||
.check_for_tx_early_refund(bitcoin_wallet)
|
||||
.check_for_tx_early_refund(&*bitcoin_wallet)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!(?err, "Failed to check for early refund transaction");
|
||||
|
|
@ -324,7 +318,7 @@ async fn next_state(
|
|||
let cancel_timelock_expires = tx_lock_status.wait_until(|status| {
|
||||
// Emit a tauri event on new confirmations
|
||||
match status {
|
||||
ScriptStatus::Confirmed(confirmed) => {
|
||||
bitcoin_wallet::primitives::ScriptStatus::Confirmed(confirmed) => {
|
||||
event_emitter.emit_swap_progress_event(
|
||||
swap_id,
|
||||
TauriSwapProgressEvent::BtcLockTxInMempool {
|
||||
|
|
@ -333,7 +327,7 @@ async fn next_state(
|
|||
},
|
||||
);
|
||||
}
|
||||
ScriptStatus::InMempool => {
|
||||
bitcoin_wallet::primitives::ScriptStatus::InMempool => {
|
||||
event_emitter.emit_swap_progress_event(
|
||||
swap_id,
|
||||
TauriSwapProgressEvent::BtcLockTxInMempool {
|
||||
|
|
@ -342,7 +336,8 @@ async fn next_state(
|
|||
},
|
||||
);
|
||||
}
|
||||
ScriptStatus::Unseen | ScriptStatus::Retrying => {
|
||||
bitcoin_wallet::primitives::ScriptStatus::Unseen
|
||||
| bitcoin_wallet::primitives::ScriptStatus::Retrying => {
|
||||
event_emitter.emit_swap_progress_event(
|
||||
swap_id,
|
||||
TauriSwapProgressEvent::BtcLockTxInMempool {
|
||||
|
|
@ -402,7 +397,7 @@ async fn next_state(
|
|||
// Check if the cancel timelock has expired
|
||||
// If it has, we have to cancel the swap
|
||||
if state
|
||||
.expired_timelock(bitcoin_wallet)
|
||||
.expired_timelock(&*bitcoin_wallet)
|
||||
.await?
|
||||
.cancel_timelock_expired()
|
||||
{
|
||||
|
|
@ -413,9 +408,12 @@ async fn next_state(
|
|||
|
||||
let tx_early_refund = state.construct_tx_early_refund();
|
||||
|
||||
let (tx_lock_status, tx_early_refund_status) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(state.tx_lock.clone()),
|
||||
bitcoin_wallet.subscribe_to(tx_early_refund.clone())
|
||||
let (tx_lock_status, tx_early_refund_status): (
|
||||
bitcoin_wallet::Subscription,
|
||||
bitcoin_wallet::Subscription,
|
||||
) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())),
|
||||
bitcoin_wallet.subscribe_to(Box::new(tx_early_refund.clone()))
|
||||
);
|
||||
|
||||
// Clone these so that we can move them into the listener closure
|
||||
|
|
@ -485,22 +483,25 @@ async fn next_state(
|
|||
// In case we send the encrypted signature to Alice, but she doesn't give us a confirmation
|
||||
// We need to check if she still published the Bitcoin redeem transaction
|
||||
// Otherwise we risk staying stuck in "XmrLocked"
|
||||
if let Some(state5) = state.check_for_tx_redeem(bitcoin_wallet).await? {
|
||||
if let Some(state5) = state.check_for_tx_redeem(&*bitcoin_wallet).await? {
|
||||
return Ok(BobState::BtcRedeemed(state5));
|
||||
}
|
||||
|
||||
// Check whether we can cancel the swap and do so if possible.
|
||||
if state
|
||||
.expired_timelock(bitcoin_wallet)
|
||||
.expired_timelock(&*bitcoin_wallet)
|
||||
.await?
|
||||
.cancel_timelock_expired()
|
||||
{
|
||||
return Ok(BobState::CancelTimelockExpired(state.cancel()));
|
||||
}
|
||||
|
||||
let (tx_lock_status, tx_early_refund_status) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(state.tx_lock.clone()),
|
||||
bitcoin_wallet.subscribe_to(state.construct_tx_early_refund())
|
||||
let (tx_lock_status, tx_early_refund_status): (
|
||||
bitcoin_wallet::Subscription,
|
||||
bitcoin_wallet::Subscription,
|
||||
) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())),
|
||||
bitcoin_wallet.subscribe_to(Box::new(state.construct_tx_early_refund()))
|
||||
);
|
||||
|
||||
// Alice has locked her Monero
|
||||
|
|
@ -538,27 +539,30 @@ async fn next_state(
|
|||
// We need to make sure that Alice did not publish the redeem transaction while we were offline
|
||||
// Even if the cancel timelock expired, if Alice published the redeem transaction while we were away we cannot miss it
|
||||
// If we do we cannot refund and will never be able to leave the "CancelTimelockExpired" state
|
||||
if let Some(state5) = state.check_for_tx_redeem(bitcoin_wallet).await? {
|
||||
if let Some(state5) = state.check_for_tx_redeem(&*bitcoin_wallet).await? {
|
||||
return Ok(BobState::BtcRedeemed(state5));
|
||||
}
|
||||
|
||||
if state
|
||||
.expired_timelock(bitcoin_wallet)
|
||||
.expired_timelock(&*bitcoin_wallet)
|
||||
.await?
|
||||
.cancel_timelock_expired()
|
||||
{
|
||||
return Ok(BobState::CancelTimelockExpired(state.cancel()));
|
||||
}
|
||||
|
||||
let (tx_lock_status, tx_early_refund_status) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(state.tx_lock.clone()),
|
||||
bitcoin_wallet.subscribe_to(state.construct_tx_early_refund())
|
||||
let (tx_lock_status, tx_early_refund_status): (
|
||||
bitcoin_wallet::Subscription,
|
||||
bitcoin_wallet::Subscription,
|
||||
) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())),
|
||||
bitcoin_wallet.subscribe_to(Box::new(state.construct_tx_early_refund()))
|
||||
);
|
||||
|
||||
select! {
|
||||
// Wait for Alice to redeem the Bitcoin
|
||||
// We can then extract the key and redeem our Monero
|
||||
state5 = state.watch_for_redeem_btc(bitcoin_wallet) => {
|
||||
state5 = state.watch_for_redeem_btc(&*bitcoin_wallet) => {
|
||||
BobState::BtcRedeemed(state5?)
|
||||
},
|
||||
// Wait for the cancel timelock to expire
|
||||
|
|
@ -613,6 +617,7 @@ async fn next_state(
|
|||
"Redeeming Monero",
|
||||
|| async {
|
||||
state
|
||||
.clone()
|
||||
.redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone())
|
||||
.await
|
||||
.map_err(backoff::Error::transient)
|
||||
|
|
@ -639,16 +644,20 @@ async fn next_state(
|
|||
event_emitter
|
||||
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::CancelTimelockExpired);
|
||||
|
||||
if state6.check_for_tx_cancel(bitcoin_wallet).await?.is_none() {
|
||||
if state6
|
||||
.check_for_tx_cancel(&*bitcoin_wallet)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
tracing::debug!("Couldn't find tx_cancel yet, publishing ourselves");
|
||||
|
||||
if let Err(tx_cancel_err) = state6.submit_tx_cancel(bitcoin_wallet).await {
|
||||
if let Err(tx_cancel_err) = state6.submit_tx_cancel(&*bitcoin_wallet).await {
|
||||
tracing::warn!(err = %tx_cancel_err, "Failed to publish tx_cancel even though it is not present in the chain. Did Alice already refund us our Bitcoin early?");
|
||||
|
||||
// If tx_cancel is not present in the chain and we fail to publish it. There's only one logical conclusion:
|
||||
// The tx_lock UTXO has been spent by the tx_early_refund transaction
|
||||
// Therefore we check for the early refund transaction
|
||||
match state6.check_for_tx_early_refund(bitcoin_wallet).await? {
|
||||
match state6.check_for_tx_early_refund(&*bitcoin_wallet).await? {
|
||||
Some(_) => {
|
||||
return Ok(BobState::BtcEarlyRefundPublished(state6));
|
||||
}
|
||||
|
|
@ -670,14 +679,14 @@ async fn next_state(
|
|||
);
|
||||
|
||||
// Bob has cancelled the swap
|
||||
match state.expired_timelock(bitcoin_wallet).await? {
|
||||
match state.expired_timelock(&*bitcoin_wallet).await? {
|
||||
ExpiredTimelocks::None { .. } => {
|
||||
bail!(
|
||||
"Internal error: canceled state reached before cancel timelock was expired"
|
||||
);
|
||||
}
|
||||
ExpiredTimelocks::Cancel { .. } => {
|
||||
let btc_refund_txid = state.publish_refund_btc(bitcoin_wallet).await?;
|
||||
let btc_refund_txid = state.publish_refund_btc(&*bitcoin_wallet).await?;
|
||||
|
||||
tracing::info!(%btc_refund_txid, "Refunded our Bitcoin");
|
||||
|
||||
|
|
@ -702,9 +711,12 @@ async fn next_state(
|
|||
let tx_refund = state.construct_tx_refund()?;
|
||||
let tx_early_refund = state.construct_tx_early_refund();
|
||||
|
||||
let (tx_refund_status, tx_early_refund_status) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(tx_refund.clone()),
|
||||
bitcoin_wallet.subscribe_to(tx_early_refund.clone()),
|
||||
let (tx_refund_status, tx_early_refund_status): (
|
||||
bitcoin_wallet::Subscription,
|
||||
bitcoin_wallet::Subscription,
|
||||
) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(Box::new(tx_refund.clone())),
|
||||
bitcoin_wallet.subscribe_to(Box::new(tx_early_refund.clone())),
|
||||
);
|
||||
|
||||
// Either of these two refund transactions could have been published
|
||||
|
|
@ -753,9 +765,12 @@ async fn next_state(
|
|||
);
|
||||
|
||||
// Wait for confirmations
|
||||
let (tx_lock_status, tx_early_refund_status) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(state.tx_lock.clone()),
|
||||
bitcoin_wallet.subscribe_to(tx_early_refund_tx.clone()),
|
||||
let (tx_lock_status, tx_early_refund_status): (
|
||||
bitcoin_wallet::Subscription,
|
||||
bitcoin_wallet::Subscription,
|
||||
) = tokio::join!(
|
||||
bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())),
|
||||
bitcoin_wallet.subscribe_to(Box::new(tx_early_refund_tx.clone())),
|
||||
);
|
||||
|
||||
select! {
|
||||
|
|
@ -852,6 +867,7 @@ async fn next_state(
|
|||
"Redeeming Monero",
|
||||
|| async {
|
||||
state5
|
||||
.clone()
|
||||
.redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone())
|
||||
.await
|
||||
.map_err(backoff::Error::transient)
|
||||
|
|
@ -941,3 +957,63 @@ async fn next_state(
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
trait XmrRedeemable {
|
||||
async fn redeem_xmr(
|
||||
self,
|
||||
monero_wallet: &monero::Wallets,
|
||||
swap_id: Uuid,
|
||||
monero_receive_pool: MoneroAddressPool,
|
||||
) -> Result<Vec<TxHash>>;
|
||||
}
|
||||
|
||||
impl XmrRedeemable for State5 {
|
||||
async fn redeem_xmr(
|
||||
self: State5,
|
||||
monero_wallet: &monero::Wallets,
|
||||
swap_id: Uuid,
|
||||
monero_receive_pool: MoneroAddressPool,
|
||||
) -> Result<Vec<TxHash>> {
|
||||
let (spend_key, view_key) = self.xmr_keys();
|
||||
|
||||
tracing::info!(%swap_id, "Redeeming Monero from extracted keys");
|
||||
|
||||
tracing::debug!(%swap_id, "Opening temporary Monero wallet");
|
||||
|
||||
let wallet = monero_wallet
|
||||
.swap_wallet(
|
||||
swap_id,
|
||||
spend_key,
|
||||
view_key,
|
||||
self.lock_transfer_proof.tx_hash(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to open Monero wallet")?;
|
||||
|
||||
// Update blockheight to ensure that the wallet knows the funds are unlocked
|
||||
tracing::debug!(%swap_id, "Updating temporary Monero wallet's blockheight");
|
||||
let _ = wallet
|
||||
.blockchain_height()
|
||||
.await
|
||||
.context("Couldn't get Monero blockheight")?;
|
||||
|
||||
tracing::debug!(%swap_id, receive_address=?monero_receive_pool, "Sweeping Monero to receive address");
|
||||
|
||||
let main_address = monero_wallet.main_wallet().await.main_address().await;
|
||||
|
||||
let tx_hashes = wallet
|
||||
.sweep_multi_destination(
|
||||
&monero_receive_pool.fill_empty_addresses(main_address),
|
||||
&monero_receive_pool.percentages(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to redeem Monero")?
|
||||
.into_iter()
|
||||
.map(|tx_receipt| TxHash(tx_receipt.txid))
|
||||
.collect();
|
||||
|
||||
tracing::info!(%swap_id, txids=?tx_hashes, "Monero sweep completed");
|
||||
|
||||
Ok(tx_hashes)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() {
|
|||
if let BobState::BtcLocked { state3, .. } = bob_swap.state.clone() {
|
||||
bob_swap
|
||||
.bitcoin_wallet
|
||||
.subscribe_to(state3.tx_lock)
|
||||
.subscribe_to(Box::new(state3.tx_lock))
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async fn given_alice_and_bob_manually_cancel_and_refund_after_funds_locked_both_
|
|||
if let BobState::BtcLocked { state3, .. } = bob_swap.state.clone() {
|
||||
bob_swap
|
||||
.bitcoin_wallet
|
||||
.subscribe_to(state3.tx_lock)
|
||||
.subscribe_to(Box::new(state3.tx_lock))
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ async fn alice_manually_punishes_after_bob_dead() {
|
|||
// Ensure cancel timelock is expired
|
||||
if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state {
|
||||
alice_bitcoin_wallet
|
||||
.subscribe_to(state3.tx_lock)
|
||||
.subscribe_to(Box::new(state3.tx_lock))
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
||||
.await?;
|
||||
|
|
@ -54,7 +54,7 @@ async fn alice_manually_punishes_after_bob_dead() {
|
|||
// Ensure punish timelock is expired
|
||||
if let AliceState::BtcCancelled { state3, .. } = alice_state {
|
||||
alice_bitcoin_wallet
|
||||
.subscribe_to(state3.tx_cancel())
|
||||
.subscribe_to(Box::new(state3.tx_cancel()))
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.punish_timelock)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ async fn alice_manually_punishes_after_bob_dead_and_bob_cancels() {
|
|||
// Ensure cancel timelock is expired
|
||||
if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state {
|
||||
alice_bitcoin_wallet
|
||||
.subscribe_to(state3.tx_lock)
|
||||
.subscribe_to(Box::new(state3.tx_lock))
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
||||
.await?;
|
||||
|
|
@ -54,7 +54,7 @@ async fn alice_manually_punishes_after_bob_dead_and_bob_cancels() {
|
|||
// Ensure punish timelock is expired
|
||||
if let AliceState::BtcCancelled { state3, .. } = alice_state {
|
||||
alice_bitcoin_wallet
|
||||
.subscribe_to(state3.tx_cancel())
|
||||
.subscribe_to(Box::new(state3.tx_cancel()))
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.punish_timelock)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ async fn alice_punishes_after_restart_if_bob_dead() {
|
|||
// cancel transaction is not published at this point)
|
||||
if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state {
|
||||
alice_bitcoin_wallet
|
||||
.subscribe_to(state3.tx_lock)
|
||||
.subscribe_to(Box::new(state3.tx_lock))
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
VERSION=1.0.0-rc.19
|
||||
|
||||
mkdir bdk
|
||||
stat ./target/debug/swap || exit 1
|
||||
cp ./target/debug/swap bdk/swap-current
|
||||
pushd bdk
|
||||
|
||||
echo "download swap $VERSION"
|
||||
curl -L "https://github.com/eigenwallet/core/releases/download/${VERSION}/swap_${VERSION}_Linux_x86_64.tar" | tar xv
|
||||
|
||||
echo "create testnet wallet with $VERSION"
|
||||
./swap --testnet --data-base-dir . --debug balance || exit 1
|
||||
echo "check testnet wallet with this version"
|
||||
./swap-current --testnet --data-base-dir . --debug balance || exit 1
|
||||
|
||||
echo "create mainnet wallet with $VERSION"
|
||||
./swap --version || exit 1
|
||||
./swap --data-base-dir . --debug balance || exit 1
|
||||
echo "check mainnet wallet with this version"
|
||||
./swap-current --version || exit 1
|
||||
./swap-current --data-base-dir . --debug balance || exit 1
|
||||
|
||||
exit 0
|
||||
|
|
@ -26,7 +26,7 @@ async fn given_bob_restarts_while_alice_redeems_btc() {
|
|||
if let BobState::EncSigSent(state4) = bob_swap.state.clone() {
|
||||
bob_swap
|
||||
.bitcoin_wallet
|
||||
.subscribe_to(state4.tx_lock)
|
||||
.subscribe_to(Box::new(state4.tx_lock))
|
||||
.await
|
||||
.wait_until_confirmed_with(state4.cancel_timelock)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// MIT License
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::{self, /* SystemTime, UNIX_EPOCH, */ Duration};
|
||||
|
||||
pub fn throttle<F, T>(closure: F, delay: Duration) -> Throttle<T>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue