780: Sqlite database r=rishflab a=rishflab

Closes #427, #762, #770

Co-authored-by: rishflab <rishflab@hotmail.com>
pull/804/head^2
bors[bot] 3 years ago committed by GitHub
commit 9ea73a8e66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -104,7 +104,6 @@ jobs:
alice_manually_punishes_after_bob_dead,
alice_refunds_after_restart_bob_refunded,
ensure_same_swap_id,
concurrent_bobs_after_xmr_lock_proof_sent,
concurrent_bobs_before_xmr_lock_proof_sent,
alice_manually_redeems_after_enc_sig_learned
]

@ -14,10 +14,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
The force flag was used to ignore blockheight and protocol state checks.
Users can still restart a swap with these checks using the `resume` subcommand.
- Changed log level of the "Advancing state", "Establishing Connection through Tor proxy" and "Connection through Tor established" log message from tracing to debug in the CLI.
- ASB and CLI can migrate their data to sqlite to store swaps and related data.
This makes it easier to build applications on top of xmr-btc-swap by enabling developers to read swap information directly from the database.
This resolved an issue where users where unable to run concurrent processes, for example, users could not print the swap history if another ASB or CLI process was running.
The sqlite database filed is named `sqlite` and is found in the data directory.
You can print the data directory using the `config` subcommand.
The schema can be found here [here](swap/migrations/20210903050345_create_swaps_table.sql).
#### Database migration guide
##### Delete old data
The simplest way to migrate is to accept the loss of data and delete the old database.
1. Find the location of the old database using the `config` subcommand.
2. Delete the database
3. Run xmr-btc-swap
xmr-btc swap will create a new sqlite database and use that from now on.
##### Preserve old data
It is possible to migrate critical data from the old db to the sqlite but there are many pitfalls.
1. Run xmr-btc-swap as you would normally
xmr-btc-swap will try and automatically migrate your existing data to the new database.
If the existing database contains swaps for very early releases, the migration will fail due to an incompatible schema.
2. Print out the swap history using the `history` subcommand.
3. Print out the swap history stored in the old database by also passing the `--sled` flag.
eg. `swap-cli --sled history`
4. Compare the old and new history to see if you are happy with migration.
5. If you are unhappy with the new history you can continue to use the old database by passing the `--sled flag`
### Added
- Added a `disable-timestamp` flag to the ASB that disables timestamps from logs.
- A `config` subcommand that prints the current configuration including the data directory location.
This feature should alleviate difficulties users were having when finding where xmr-btc-swap was storing data.
## [0.8.3] - 2021-09-03

248
Cargo.lock generated

@ -49,6 +49,17 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]]
name = "ahash"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
dependencies = [
"getrandom 0.2.2",
"once_cell",
"version_check 0.9.3",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
@ -128,6 +139,15 @@ dependencies = [
"pin-project-lite 0.2.6",
]
[[package]]
name = "atoi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic"
version = "0.5.0"
@ -610,7 +630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1b9d958c2b1368a663f05538fc1b5975adce1e19f435acceae987aceeeb369"
dependencies = [
"lazy_static",
"nom",
"nom 5.1.2",
"serde",
"toml",
]
@ -691,6 +711,21 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
[[package]]
name = "crc"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10c2722795460108a7872e1cd933a85d6ec38abc4baecad51028f702da28889f"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403"
[[package]]
name = "crc32fast"
version = "1.2.1"
@ -723,6 +758,16 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.5"
@ -937,6 +982,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "ecdsa_fun"
version = "0.6.2-alpha.0"
@ -977,6 +1028,9 @@ name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
dependencies = [
"serde",
]
[[package]]
name = "electrum-client"
@ -1175,6 +1229,17 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "futures-intrusive"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e"
dependencies = [
"futures-core",
"lock_api 0.4.5",
"parking_lot 0.11.2",
]
[[package]]
name = "futures-io"
version = "0.3.17"
@ -1362,7 +1427,25 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
dependencies = [
"ahash",
"ahash 0.4.7",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash 0.7.4",
]
[[package]]
name = "hashlink"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
"hashbrown 0.11.2",
]
[[package]]
@ -1614,7 +1697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg 1.0.1",
"hashbrown",
"hashbrown 0.9.1",
]
[[package]]
@ -2088,6 +2171,17 @@ dependencies = [
"libsecp256k1-core",
]
[[package]]
name = "libsqlite3-sys"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.2"
@ -2148,7 +2242,7 @@ version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f374d42cdfc1d7dbf3d3dec28afab2eb97ffbf43a3234d795b5986dbf4b90ba"
dependencies = [
"hashbrown",
"hashbrown 0.9.1",
]
[[package]]
@ -2231,6 +2325,12 @@ dependencies = [
"syn",
]
[[package]]
name = "minimal-lexical"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d"
[[package]]
name = "miniscript"
version = "5.1.0"
@ -2429,6 +2529,17 @@ dependencies = [
"version_check 0.9.3",
]
[[package]]
name = "nom"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1"
dependencies = [
"memchr",
"minimal-lexical",
"version_check 0.9.3",
]
[[package]]
name = "ntapi"
version = "0.3.6"
@ -3536,6 +3647,7 @@ version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
@ -3804,12 +3916,122 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "sqlformat"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4"
dependencies = [
"itertools 0.10.1",
"nom 7.0.0",
"unicode_categories",
]
[[package]]
name = "sqlx"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4b94ab0f8c21ee4899b93b06451ef5d965f1a355982ee73684338228498440"
dependencies = [
"sqlx-core",
"sqlx-macros",
]
[[package]]
name = "sqlx-core"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec28b91a01e1fe286d6ba66f68289a2286df023fc97444e1fd86c2fd6d5dc026"
dependencies = [
"ahash 0.7.4",
"atoi",
"bitflags",
"byteorder",
"bytes 1.0.1",
"crc",
"crossbeam-channel",
"crossbeam-queue",
"crossbeam-utils",
"either",
"futures-channel",
"futures-core",
"futures-intrusive",
"futures-util",
"hashlink",
"hex 0.4.3",
"itoa",
"libc",
"libsqlite3-sys",
"log 0.4.14",
"memchr",
"once_cell",
"parking_lot 0.11.2",
"percent-encoding 2.1.0",
"rustls 0.19.0",
"serde",
"sha2",
"smallvec",
"sqlformat",
"sqlx-rt",
"stringprep",
"thiserror",
"tokio-stream",
"url 2.2.2",
"webpki",
"webpki-roots 0.21.0",
"whoami",
]
[[package]]
name = "sqlx-macros"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dc33c35d54774eed73d54568d47a6ac099aed8af5e1556a017c131be88217d5"
dependencies = [
"dotenv",
"either",
"futures",
"heck",
"hex 0.4.3",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-rt",
"syn",
"url 2.2.2",
]
[[package]]
name = "sqlx-rt"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14302b678d9c76b28f2e60115211e25e0aabc938269991745a169753dc00e35c"
dependencies = [
"once_cell",
"tokio",
"tokio-rustls",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stringprep"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "strsim"
version = "0.8.0"
@ -3906,6 +4128,7 @@ dependencies = [
"ed25519-dalek",
"futures",
"get-port",
"hex 0.4.3",
"hyper 0.14.12",
"itertools 0.10.1",
"libp2p",
@ -3930,6 +4153,7 @@ dependencies = [
"sigma_fun",
"sled",
"spectral",
"sqlx",
"structopt",
"strum",
"tempfile",
@ -4508,6 +4732,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "universal-hash"
version = "0.4.0"
@ -4790,6 +5020,16 @@ dependencies = [
"thiserror",
]
[[package]]
name = "whoami"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7741161a40200a867c96dfa5574544efa4178cf4c8f770b62dd1cc0362d7ae1"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "widestring"
version = "0.4.3"

@ -17,7 +17,6 @@ status = [
"docker_tests (alice_manually_punishes_after_bob_dead)",
"docker_tests (alice_refunds_after_restart_bob_refunded)",
"docker_tests (ensure_same_swap_id)",
"docker_tests (concurrent_bobs_after_xmr_lock_proof_sent)",
"docker_tests (concurrent_bobs_before_xmr_lock_proof_sent)",
"docker_tests (alice_manually_redeems_after_enc_sig_learned)"
]

@ -29,6 +29,7 @@ directories-next = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "libsecp_compat", "serde" ] }
ed25519-dalek = "1"
futures = { version = "0.3", default-features = false }
hex = "0.4"
itertools = "0.10"
libp2p = { git = "https://github.com/comit-network/rust-libp2p", branch = "rendezvous", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous" ] }
miniscript = { version = "5", features = [ "serde" ] }
@ -49,6 +50,7 @@ serde_with = { version = "1", features = [ "macros" ] }
sha2 = "0.9"
sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde" ] }
sled = "0.34"
sqlx = { version = "0.5", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] }
structopt = "0.3"
strum = { version = "0.21", features = [ "derive" ] }
thiserror = "1"

@ -0,0 +1,25 @@
CREATE TABLE if NOT EXISTS swap_states
(
id INTEGER PRIMARY KEY autoincrement NOT NULL,
swap_id TEXT NOT NULL,
entered_at TEXT NOT NULL,
state TEXT NOT NULL
);
CREATE TABLE if NOT EXISTS monero_addresses
(
swap_id TEXT PRIMARY KEY NOT NULL,
address TEXT NOT NULL
);
CREATE TABLE if NOT EXISTS peers
(
swap_id TEXT PRIMARY KEY NOT NULL,
peer_id TEXT NOT NULL
);
CREATE TABLE if NOT EXISTS peer_addresses
(
peer_id TEXT NOT NULL,
address TEXT NOT NULL
);

@ -0,0 +1,6 @@
# crated temporary DB
# run the migration scripts to create the tables
# prepare the sqlx-data.json rust mappings
DATABASE_URL=sqlite:tempdb cargo sqlx database create
DATABASE_URL=sqlite:tempdb cargo sqlx migrate run
DATABASE_URL=sqlite:./swap/tempdb cargo sqlx prepare -- --bin swap

@ -0,0 +1,139 @@
{
"db": "SQLite",
"081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6": {
"query": "\n SELECT peer_id\n FROM peers\n WHERE swap_id = ?\n ",
"describe": {
"columns": [
{
"name": "peer_id",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
}
},
"0ab84c094964968e96a3f2bf590d9ae92227d057386921e0e57165b887de3c75": {
"query": "\n insert into peer_addresses (\n peer_id,\n address\n ) values (?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
}
},
"1ec38c85e7679b2eb42b3df75d9098772ce44fdb8db3012d3c2410d828b74157": {
"query": "\n SELECT swap_id, state\n FROM (\n SELECT max(id), swap_id, state\n FROM swap_states\n GROUP BY swap_id\n )\n ",
"describe": {
"columns": [
{
"name": "swap_id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "state",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
}
},
"2a356078a41b321234adf2aa385b501749f907f7c422945a8bdda2b6274f5225": {
"query": "\n insert into peers (\n swap_id,\n peer_id\n ) values (?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
}
},
"50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0": {
"query": "\n insert into monero_addresses (\n swap_id,\n address\n ) values (?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
}
},
"88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf": {
"query": "\n SELECT state\n FROM swap_states\n WHERE swap_id = ?\n ORDER BY id desc\n LIMIT 1;\n\n ",
"describe": {
"columns": [
{
"name": "state",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
}
},
"a0eb85d04ee3842c52291dad4d225941d1141af735922fcbc665868997fce304": {
"query": "\n SELECT address\n FROM peer_addresses\n WHERE peer_id = ?\n ",
"describe": {
"columns": [
{
"name": "address",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
}
},
"b703032b4ddc627a1124817477e7a8e5014bdc694c36a14053ef3bb2fc0c69b0": {
"query": "\n insert into swap_states (\n swap_id,\n entered_at,\n state\n ) values (?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
}
},
"ce270dd4a4b9615695a79864240c5401e2122077365e5e5a19408c068c7f9454": {
"query": "\n SELECT address\n FROM monero_addresses\n WHERE swap_id = ?\n ",
"describe": {
"columns": [
{
"name": "address",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
}
}
}

@ -21,6 +21,7 @@ where
let json = args.json;
let disable_timestamp = args.disable_timestamp;
let testnet = args.testnet;
let sled = args.sled;
let config = args.config;
let command: RawCommand = args.cmd;
@ -29,6 +30,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::Start { resume_only },
@ -37,6 +39,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::History,
@ -45,6 +48,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::WithdrawBtc {
@ -56,10 +60,20 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::Balance,
},
RawCommand::Config => Arguments {
testnet,
json,
sled,
disable_timestamp,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::Config,
},
RawCommand::ManualRecovery(ManualRecovery::Redeem {
redeem_params: RecoverCommandParams { swap_id },
do_not_await_finality,
@ -67,6 +81,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::Redeem {
@ -81,6 +96,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::Cancel { swap_id },
@ -91,6 +107,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::Refund { swap_id },
@ -101,6 +118,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::Punish { swap_id },
@ -109,6 +127,7 @@ where
testnet,
json,
disable_timestamp,
sled,
config_path: config_path(config, testnet)?,
env_config: env_config(testnet),
cmd: Command::SafelyAbort { swap_id },
@ -168,6 +187,7 @@ pub struct BitcoinAddressNetworkMismatch {
pub struct Arguments {
pub testnet: bool,
pub json: bool,
pub sled: bool,
pub disable_timestamp: bool,
pub config_path: PathBuf,
pub env_config: env::Config,
@ -180,6 +200,7 @@ pub enum Command {
resume_only: bool,
},
History,
Config,
WithdrawBtc {
amount: Option<Amount>,
address: Address,
@ -228,6 +249,13 @@ pub struct RawArguments {
)]
pub disable_timestamp: bool,
#[structopt(
short,
long = "sled",
help = "Forces the asb to use the deprecated sled db if it is available"
)]
pub sled: bool,
#[structopt(
long = "config",
help = "Provide a custom path to the configuration file. The configuration file must be a toml file.",
@ -252,6 +280,8 @@ pub enum RawCommand {
},
#[structopt(about = "Prints swap-id and the state of each swap ever made.")]
History,
#[structopt(about = "Prints the current config")]
Config,
#[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")]
WithdrawBtc {
#[structopt(
@ -344,6 +374,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -362,6 +393,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -380,6 +412,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -402,6 +435,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -429,6 +463,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -455,6 +490,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -481,6 +517,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -507,6 +544,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
@ -527,6 +565,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -545,6 +584,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -563,6 +603,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -587,6 +628,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -614,6 +656,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -641,6 +684,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -668,6 +712,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -695,6 +740,7 @@ mod tests {
let expected_args = Arguments {
testnet: true,
json: false,
sled: false,
disable_timestamp: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
@ -715,6 +761,7 @@ mod tests {
let expected_args = Arguments {
testnet: false,
json: false,
sled: false,
disable_timestamp: true,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,

@ -1,9 +1,9 @@
use crate::asb::{Behaviour, OutEvent, Rate};
use crate::database::Database;
use crate::network::quote::BidQuote;
use crate::network::swap_setup::alice::WalletSnapshot;
use crate::network::transfer_proof;
use crate::protocol::alice::{AliceState, State3, Swap};
use crate::protocol::{Database, State};
use crate::{bitcoin, env, kraken, monero};
use anyhow::{Context, Result};
use futures::future;
@ -14,7 +14,7 @@ use libp2p::swarm::SwarmEvent;
use libp2p::{PeerId, Swarm};
use rust_decimal::Decimal;
use std::collections::HashMap;
use std::convert::Infallible;
use std::convert::{Infallible, TryInto};
use std::fmt::Debug;
use std::sync::Arc;
use tokio::sync::mpsc;
@ -39,7 +39,7 @@ where
env_config: env::Config,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
db: Arc<dyn Database + Send + Sync>,
latest_rate: LR,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
@ -71,7 +71,7 @@ where
env_config: env::Config,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
db: Arc<dyn Database + Send + Sync>,
latest_rate: LR,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
@ -108,16 +108,21 @@ where
self.inflight_encrypted_signatures
.push(future::pending().boxed());
let unfinished_swaps = match self.db.unfinished_alice() {
Ok(unfinished_swaps) => unfinished_swaps,
Err(_) => {
tracing::error!("Failed to load unfinished swaps");
let swaps = match self.db.all().await {
Ok(swaps) => swaps,
Err(e) => {
tracing::error!("Failed to load swaps from database: {}", e);
return;
}
};
let unfinished_swaps = swaps
.into_iter()
.filter(|(_swap_id, state)| !state.swap_finished())
.collect::<Vec<(Uuid, State)>>();
for (swap_id, state) in unfinished_swaps {
let peer_id = match self.db.get_peer_id(swap_id) {
let peer_id = match self.db.get_peer_id(swap_id).await {
Ok(peer_id) => peer_id,
Err(_) => {
tracing::warn!(%swap_id, "Resuming swap skipped because no peer-id found for swap in database");
@ -133,7 +138,7 @@ where
monero_wallet: self.monero_wallet.clone(),
env_config: self.env_config,
db: self.db.clone(),
state: state.into(),
state: state.try_into().expect("Alice state loaded from db"),
swap_id,
};
@ -197,7 +202,7 @@ where
}
SwarmEvent::Behaviour(OutEvent::EncryptedSignatureReceived{ msg, channel, peer }) => {
let swap_id = msg.swap_id;
let swap_peer = self.db.get_peer_id(swap_id);
let swap_peer = self.db.get_peer_id(swap_id).await;
// Ensure that an incoming encrypted signature is sent by the peer-id associated with the swap
let swap_peer = match swap_peer {

@ -1,16 +1,17 @@
use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Txid, Wallet};
use crate::database::{Database, Swap};
use crate::protocol::alice::AliceState;
use crate::protocol::Database;
use anyhow::{bail, Result};
use std::convert::TryInto;
use std::sync::Arc;
use uuid::Uuid;
pub async fn cancel(
swap_id: Uuid,
bitcoin_wallet: Arc<Wallet>,
db: Arc<Database>,
db: Arc<dyn Database>,
) -> Result<(Txid, AliceState)> {
let state = db.get_state(swap_id)?.try_into_alice()?.into();
let state = db.get_state(swap_id).await?.try_into()?;
let (monero_wallet_restore_blockheight, transfer_proof, state3) = match state {
@ -58,8 +59,7 @@ pub async fn cancel(
transfer_proof,
state3,
};
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok((txid, state))

@ -1,7 +1,8 @@
use crate::bitcoin::{self, Txid};
use crate::database::{Database, Swap};
use crate::protocol::alice::AliceState;
use crate::protocol::Database;
use anyhow::{bail, Result};
use std::convert::TryInto;
use std::sync::Arc;
use uuid::Uuid;
@ -14,9 +15,9 @@ pub enum Error {
pub async fn punish(
swap_id: Uuid,
bitcoin_wallet: Arc<bitcoin::Wallet>,
db: Arc<Database>,
db: Arc<dyn Database>,
) -> Result<(Txid, AliceState)> {
let state = db.get_state(swap_id)?.try_into_alice()?.into();
let state = db.get_state(swap_id).await?.try_into()?;
let state3 = match state {
// Punish potentially possible (no knowledge of cancel transaction)
@ -46,8 +47,7 @@ pub async fn punish(
let txid = state3.punish_btc(&bitcoin_wallet).await?;
let state = AliceState::BtcPunished;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok((txid, state))

@ -1,7 +1,8 @@
use crate::bitcoin::{Txid, Wallet};
use crate::database::{Database, Swap};
use crate::protocol::alice::AliceState;
use crate::protocol::Database;
use anyhow::{bail, Result};
use std::convert::TryInto;
use std::sync::Arc;
use uuid::Uuid;
@ -23,10 +24,10 @@ impl Finality {
pub async fn redeem(
swap_id: Uuid,
bitcoin_wallet: Arc<Wallet>,
db: Arc<Database>,
db: Arc<dyn Database>,
finality: Finality,
) -> Result<(Txid, AliceState)> {
let state = db.get_state(swap_id)?.try_into_alice()?.into();
let state = db.get_state(swap_id).await?.try_into()?;
match state {
AliceState::EncSigLearned {
@ -42,17 +43,14 @@ pub async fn redeem(
subscription.wait_until_seen().await?;
let state = AliceState::BtcRedeemTransactionPublished { state3 };
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
.await?;
db.insert_latest_state(swap_id, state.into()).await?;
if let Finality::Await = finality {
subscription.wait_until_final().await?;
}
let state = AliceState::BtcRedeemed;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok((txid, state))
@ -64,8 +62,7 @@ pub async fn redeem(
}
let state = AliceState::BtcRedeemed;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
db.insert_latest_state(swap_id, state.clone().into())
.await?;
let txid = state3.tx_redeem().txid();

@ -1,9 +1,10 @@
use crate::bitcoin::{self};
use crate::database::{Database, Swap};
use crate::monero;
use crate::protocol::alice::AliceState;
use crate::protocol::Database;
use anyhow::{bail, Result};
use libp2p::PeerId;
use std::convert::TryInto;
use std::sync::Arc;
use uuid::Uuid;
@ -26,9 +27,9 @@ pub async fn refund(
swap_id: Uuid,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
db: Arc<dyn Database>,
) -> Result<AliceState> {
let state = db.get_state(swap_id)?.try_into_alice()?.into();
let state = db.get_state(swap_id).await?.try_into()?;
let (monero_wallet_restore_blockheight, transfer_proof, state3) = match state {
// In case no XMR has been locked, move to Safely Aborted
@ -66,7 +67,7 @@ pub async fn refund(
tracing::debug!(%swap_id, "Bitcoin refund transaction found, extracting key to refund Monero");
state3.extract_monero_private_key(published_refund_tx)?
} else {
let bob_peer_id = db.get_peer_id(swap_id)?;
let bob_peer_id = db.get_peer_id(swap_id).await?;
bail!(Error::RefundTransactionNotPublishedYet(bob_peer_id),);
};
@ -81,8 +82,7 @@ pub async fn refund(
.await?;
let state = AliceState::XmrRefunded;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok(state)

@ -1,11 +1,12 @@
use crate::database::{Database, Swap};
use crate::protocol::alice::AliceState;
use crate::protocol::Database;
use anyhow::{bail, Result};
use std::convert::TryInto;
use std::sync::Arc;
use uuid::Uuid;
pub async fn safely_abort(swap_id: Uuid, db: Arc<Database>) -> Result<AliceState> {
let state = db.get_state(swap_id)?.try_into_alice()?.into();
pub async fn safely_abort(swap_id: Uuid, db: Arc<dyn Database>) -> Result<AliceState> {
let state = db.get_state(swap_id).await?.try_into()?;
match state {
AliceState::Started { .. }
@ -13,8 +14,7 @@ pub async fn safely_abort(swap_id: Uuid, db: Arc<Database>) -> Result<AliceState
| AliceState::BtcLocked { .. } => {
let state = AliceState::SafelyAborted;
let db_state = (&state).into();
db.insert_latest_state(swap_id, Swap::Alice(db_state))
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok(state)

@ -18,6 +18,7 @@ use libp2p::core::multiaddr::Protocol;
use libp2p::core::Multiaddr;
use libp2p::swarm::AddressScore;
use libp2p::Swarm;
use std::convert::TryInto;
use std::env;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
@ -28,11 +29,11 @@ use swap::asb::config::{
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
};
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
use swap::database::Database;
use swap::database::open_db;
use swap::monero::Amount;
use swap::network::rendezvous::XmrBtcNamespace;
use swap::network::swarm;
use swap::protocol::alice::run;
use swap::protocol::alice::{run, AliceState};
use swap::seed::Seed;
use swap::tor::AuthenticatedClient;
use swap::{asb, bitcoin, kraken, monero, tor};
@ -46,6 +47,7 @@ async fn main() -> Result<()> {
testnet,
json,
disable_timestamp,
sled,
config_path,
env_config,
cmd,
@ -91,9 +93,9 @@ async fn main() -> Result<()> {
}
let db_path = config.data.dir.join("database");
let sled_path = config.data.dir.join(db_path);
let db = Database::open(config.data.dir.join(db_path).as_path())
.context("Could not open database")?;
let db = open_db(sled_path, config.data.dir.join("sqlite"), sled).await?;
let seed =
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");
@ -177,7 +179,7 @@ async fn main() -> Result<()> {
env_config,
Arc::new(bitcoin_wallet),
Arc::new(monero_wallet),
Arc::new(db),
db,
kraken_rate.clone(),
config.maker.min_buy_btc,
config.maker.max_buy_btc,
@ -208,12 +210,17 @@ async fn main() -> Result<()> {
table.set_header(vec!["SWAP ID", "STATE"]);
for (swap_id, state) in db.all_alice()? {
for (swap_id, state) in db.all().await? {
let state: AliceState = state.try_into()?;
table.add_row(vec![swap_id.to_string(), state.to_string()]);
}
println!("{}", table);
}
Command::Config => {
let config_json = serde_json::to_string_pretty(&config)?;
println!("{}", config_json);
}
Command::WithdrawBtc { amount, address } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
@ -248,7 +255,7 @@ async fn main() -> Result<()> {
Command::Cancel { swap_id } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let (txid, _) = cancel(swap_id, Arc::new(bitcoin_wallet), Arc::new(db)).await?;
let (txid, _) = cancel(swap_id, Arc::new(bitcoin_wallet), db).await?;
tracing::info!("Cancel transaction successfully published with id {}", txid);
}
@ -260,7 +267,7 @@ async fn main() -> Result<()> {
swap_id,
Arc::new(bitcoin_wallet),
Arc::new(monero_wallet),
Arc::new(db),
db,
)
.await?;
@ -269,12 +276,12 @@ async fn main() -> Result<()> {
Command::Punish { swap_id } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let (txid, _) = punish(swap_id, Arc::new(bitcoin_wallet), Arc::new(db)).await?;
let (txid, _) = punish(swap_id, Arc::new(bitcoin_wallet), db).await?;
tracing::info!("Punish transaction successfully published with id {}", txid);
}
Command::SafelyAbort { swap_id } => {
safely_abort(swap_id, Arc::new(db)).await?;
safely_abort(swap_id, db).await?;
tracing::info!("Swap safely aborted");
}
@ -287,7 +294,7 @@ async fn main() -> Result<()> {
let (txid, _) = redeem(
swap_id,
Arc::new(bitcoin_wallet),
Arc::new(db),
db,
Finality::from_bool(do_not_await_finality),
)
.await?;

@ -17,6 +17,7 @@ use comfy_table::Table;
use qrcode::render::unicode;
use qrcode::QrCode;
use std::cmp::min;
use std::convert::TryInto;
use std::env;
use std::future::Future;
use std::path::PathBuf;
@ -25,13 +26,13 @@ use std::time::Duration;
use swap::bitcoin::TxLock;
use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command, ParseResult};
use swap::cli::{list_sellers, EventLoop, SellerStatus};
use swap::database::Database;
use swap::database::open_db;
use swap::env::Config;
use swap::libp2p_ext::MultiAddrExt;
use swap::network::quote::BidQuote;
use swap::network::swarm;
use swap::protocol::bob;
use swap::protocol::bob::Swap;
use swap::protocol::bob::{BobState, Swap};
use swap::seed::Seed;
use swap::{bitcoin, cli, monero};
use url::Url;
@ -44,6 +45,7 @@ async fn main() -> Result<()> {
data_dir,
debug,
json,
sled,
cmd,
} = match parse_args_and_apply_defaults(env::args_os())? {
ParseResult::Arguments(args) => args,
@ -66,8 +68,7 @@ async fn main() -> Result<()> {
let swap_id = Uuid::new_v4();
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let db = open_db(data_dir.join("database"), data_dir.join("sqlite"), sled).await?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
@ -82,7 +83,6 @@ async fn main() -> Result<()> {
let (monero_wallet, _process) =
init_monero_wallet(data_dir, monero_daemon_address, env_config).await?;
let bitcoin_wallet = Arc::new(bitcoin_wallet);
let seller_peer_id = seller
.extract_peer_id()
.context("Seller address must contain peer ID")?;
@ -139,20 +139,49 @@ async fn main() -> Result<()> {
}
}
Command::History => {
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let db = open_db(data_dir.join("database"), data_dir.join("sqlite"), sled).await?;
let mut table = Table::new();
table.set_header(vec!["SWAP ID", "STATE"]);
for (swap_id, state) in db.all_bob()? {
for (swap_id, state) in db.all().await? {
let state: BobState = state.try_into()?;
table.add_row(vec![swap_id.to_string(), state.to_string()]);
}
println!("{}", table);
}
Command::Config => {
println!("Data directory: {}", data_dir.display());
println!(
"Log files locations: {}",
format!("{}/wallet", data_dir.display())
);
println!(
"Sled folder location: {}",
format!("{}/database", data_dir.display())
);
println!(
"Sqlite file location: {}",
format!("{}/sqlite", data_dir.display())
);
println!(
"Seed file location: {}",
format!("{}/seed.pem", data_dir.display())
);
println!(
"Monero-wallet-rpc location: {}",
format!("{}/monero", data_dir.display())
);
println!(
"Internal bitcoin wallet location: {}",
format!("{}/wallet", data_dir.display())
);
println!(
"Internal bitcoin wallet location: {}",
format!("{}/wallet", data_dir.display())
);
}
Command::WithdrawBtc {
bitcoin_electrum_rpc_url,
bitcoin_target_block,
@ -215,8 +244,7 @@ async fn main() -> Result<()> {
tor_socks5_port,
} => {
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let db = open_db(data_dir.join("database"), data_dir.join("sqlite"), sled).await?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
@ -232,8 +260,8 @@ async fn main() -> Result<()> {
init_monero_wallet(data_dir, monero_daemon_address, env_config).await?;
let bitcoin_wallet = Arc::new(bitcoin_wallet);
let seller_peer_id = db.get_peer_id(swap_id)?;
let seller_addresses = db.get_addresses(seller_peer_id)?;
let seller_peer_id = db.get_peer_id(swap_id).await?;
let seller_addresses = db.get_addresses(seller_peer_id).await?;
let behaviour = cli::Behaviour::new(seller_peer_id, env_config, bitcoin_wallet.clone());
let mut swarm =
@ -251,7 +279,7 @@ async fn main() -> Result<()> {
EventLoop::new(swap_id, swarm, seller_peer_id, env_config)?;
let handle = tokio::spawn(event_loop.run());
let monero_receive_address = db.get_monero_address(swap_id)?;
let monero_receive_address = db.get_monero_address(swap_id).await?;
let swap = Swap::from_db(
db,
swap_id,
@ -260,7 +288,8 @@ async fn main() -> Result<()> {
env_config,
event_loop_handle,
monero_receive_address,
)?;
)
.await?;
tokio::select! {
event_loop_result = handle => {
@ -277,8 +306,7 @@ async fn main() -> Result<()> {
bitcoin_target_block,
} => {
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let db = open_db(data_dir.join("database"), data_dir.join("sqlite"), sled).await?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
@ -300,8 +328,7 @@ async fn main() -> Result<()> {
bitcoin_target_block,
} => {
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let db = open_db(data_dir.join("database"), data_dir.join("sqlite"), sled).await?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;

@ -1,16 +1,17 @@
use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Txid, Wallet};
use crate::database::{Database, Swap};
use crate::protocol::bob::BobState;
use crate::protocol::Database;
use anyhow::{bail, Result};
use std::convert::TryInto;
use std::sync::Arc;
use uuid::Uuid;
pub async fn cancel(
swap_id: Uuid,
bitcoin_wallet: Arc<Wallet>,
db: Database,
db: Arc<dyn Database>,
) -> Result<(Txid, BobState)> {
let state = db.get_state(swap_id)?.try_into_bob()?.into();
let state = db.get_state(swap_id).await?.try_into()?;
let state6 = match state {
BobState::BtcLocked(state3) => state3.cancel(),
@ -48,8 +49,8 @@ pub async fn cancel(
};
let state = BobState::BtcCancelled(state6);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok((txid, state))
}

@ -33,6 +33,7 @@ pub struct Arguments {
pub env_config: env::Config,
pub debug: bool,
pub json: bool,
pub sled: bool,
pub data_dir: PathBuf,
pub cmd: Command,
}
@ -66,6 +67,7 @@ where
let debug = args.debug;
let json = args.json;
let sled = args.sled;
let is_testnet = args.testnet;
let data = args.data;
@ -90,6 +92,7 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::BuyXmr {
seller,
@ -106,9 +109,18 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::History,
},
RawCommand::Config => Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Config,
},
RawCommand::Balance { bitcoin } => {
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
bitcoin.apply_defaults(is_testnet)?;
@ -117,6 +129,7 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Balance {
bitcoin_electrum_rpc_url,
@ -136,6 +149,7 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::WithdrawBtc {
bitcoin_electrum_rpc_url,
@ -159,6 +173,7 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Resume {
swap_id,
@ -180,6 +195,7 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Cancel {
swap_id,
@ -199,6 +215,7 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Refund {
swap_id,
@ -214,6 +231,7 @@ where
env_config: env_config_from(is_testnet),
debug,
json,
sled,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::ListSellers {
rendezvous_point,
@ -238,6 +256,7 @@ pub enum Command {
tor_socks5_port: u16,
},
History,
Config,
WithdrawBtc {
bitcoin_electrum_rpc_url: Url,
bitcoin_target_block: usize,
@ -304,6 +323,13 @@ struct RawArguments {
)]
json: bool,
#[structopt(
short,
long = "sled",
help = "Forces the swap-cli to use the deprecated sled db if it is available"
)]
sled: bool,
#[structopt(subcommand)]
cmd: RawCommand,
}
@ -338,6 +364,8 @@ enum RawCommand {
},
/// Show a list of past, ongoing and completed swaps
History,
#[structopt(about = "Prints the current config")]
Config,
#[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")]
WithdrawBtc {
#[structopt(flatten)]
@ -1105,6 +1133,7 @@ mod tests {
env_config: env::Testnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(TESTNET),
cmd: Command::BuyXmr {
seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(),
@ -1125,6 +1154,7 @@ mod tests {
env_config: env::Mainnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(MAINNET),
cmd: Command::BuyXmr {
seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(),
@ -1144,6 +1174,7 @@ mod tests {
env_config: env::Testnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(TESTNET),
cmd: Command::Resume {
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
@ -1161,6 +1192,7 @@ mod tests {
env_config: env::Mainnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(MAINNET),
cmd: Command::Resume {
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
@ -1177,6 +1209,7 @@ mod tests {
env_config: env::Testnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(TESTNET),
cmd: Command::Cancel {
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
@ -1192,6 +1225,7 @@ mod tests {
env_config: env::Mainnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(MAINNET),
cmd: Command::Cancel {
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
@ -1206,6 +1240,7 @@ mod tests {
env_config: env::Testnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(TESTNET),
cmd: Command::Refund {
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
@ -1221,6 +1256,7 @@ mod tests {
env_config: env::Mainnet::get_config(),
debug: false,
json: false,
sled: false,
data_dir: data_dir_path_cli().join(MAINNET),
cmd: Command::Refund {
swap_id: Uuid::from_str(SWAP_ID).unwrap(),

@ -1,12 +1,17 @@
use crate::bitcoin::Wallet;
use crate::database::{Database, Swap};
use crate::protocol::bob::BobState;
use crate::protocol::Database;
use anyhow::{bail, Result};
use std::convert::TryInto;
use std::sync::Arc;
use uuid::Uuid;
pub async fn refund(swap_id: Uuid, bitcoin_wallet: Arc<Wallet>, db: Database) -> Result<BobState> {
let state = db.get_state(swap_id)?.try_into_bob()?.into();
pub async fn refund(
swap_id: Uuid,
bitcoin_wallet: Arc<Wallet>,
db: Arc<dyn Database>,
) -> Result<BobState> {
let state = db.get_state(swap_id).await?.try_into()?;
let state6 = match state {
BobState::BtcLocked(state3) => state3.cancel(),
@ -31,9 +36,8 @@ pub async fn refund(swap_id: Uuid, bitcoin_wallet: Arc<Wallet>, db: Database) ->
state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?;
let state = BobState::BtcRefunded(state6);
let db_state = state.clone().into();
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
db.insert_latest_state(swap_id, state.clone().into())
.await?;
Ok(state)
}

@ -1,18 +1,20 @@
pub use self::sled::SledDatabase;
pub use alice::Alice;
pub use bob::Bob;
pub use sqlite::SqliteDatabase;
use anyhow::{anyhow, bail, Context, Result};
use itertools::Itertools;
use libp2p::{Multiaddr, PeerId};
use serde::de::DeserializeOwned;
use crate::fs::ensure_directory_exists;
use crate::protocol::{Database, State};
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::path::Path;
use std::str::FromStr;
use uuid::Uuid;
use std::sync::Arc;
mod alice;
mod bob;
mod sled;
mod sqlite;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Swap {
@ -20,6 +22,24 @@ pub enum Swap {
Bob(Bob),
}
impl From<State> for Swap {
fn from(state: State) -> Self {
match state {
State::Alice(state) => Swap::Alice(state.into()),
State::Bob(state) => Swap::Bob(state.into()),
}
}
}
impl From<Swap> for State {
fn from(value: Swap) -> Self {
match value {
Swap::Alice(alice) => State::Alice(alice.into()),
Swap::Bob(bob) => State::Bob(bob.into()),
}
}
}
impl From<Alice> for Swap {
fn from(from: Alice) -> Self {
Swap::Alice(from)
@ -65,413 +85,74 @@ impl Swap {
}
}
pub struct Database {
swaps: sled::Tree,
peers: sled::Tree,
addresses: sled::Tree,
monero_addresses: sled::Tree,
}
impl Database {
pub fn open(path: &Path) -> Result<Self> {
tracing::debug!("Opening database at {}", path.display());
let db =
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
let swaps = db.open_tree("swaps")?;
let peers = db.open_tree("peers")?;
let addresses = db.open_tree("addresses")?;
let monero_addresses = db.open_tree("monero_addresses")?;
Ok(Database {
swaps,
peers,
addresses,
monero_addresses,
})
}
pub async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()> {
let peer_id_str = peer_id.to_string();
let key = serialize(&swap_id)?;
let value = serialize(&peer_id_str).context("Could not serialize peer-id")?;
self.peers.insert(key, value)?;
self.peers
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_peer_id(&self, swap_id: Uuid) -> Result<PeerId> {
let key = serialize(&swap_id)?;
let encoded = self
.peers
.get(&key)?
.ok_or_else(|| anyhow!("No peer-id found for swap id {} in database", swap_id))?;
let peer_id: String = deserialize(&encoded).context("Could not deserialize peer-id")?;
Ok(PeerId::from_str(peer_id.as_str())?)
}
pub async fn insert_monero_address(
&self,
swap_id: Uuid,
address: monero::Address,
) -> Result<()> {
let key = swap_id.as_bytes();
let value = serialize(&address)?;
self.monero_addresses.insert(key, value)?;
self.monero_addresses
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_monero_address(&self, swap_id: Uuid) -> Result<monero::Address> {
let encoded = self
.monero_addresses
.get(swap_id.as_bytes())?
.ok_or_else(|| {
anyhow!(
"No Monero address found for swap id {} in database",
swap_id
)
})?;
let monero_address = deserialize(&encoded)?;
Ok(monero_address)
}
pub async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()> {
let key = peer_id.to_bytes();
let existing_addresses = self.addresses.get(&key)?;
let new_addresses = {
let existing_addresses = existing_addresses.clone();
Some(match existing_addresses {
Some(encoded) => {
let mut addresses = deserialize::<Vec<Multiaddr>>(&encoded)?;
addresses.push(address);
serialize(&addresses)?
pub async fn open_db(
sled_path: impl AsRef<Path>,
sqlite_path: impl AsRef<Path>,
force_sled: bool,
) -> Result<Arc<dyn Database + Send + Sync>> {
// if sled exists and sqlite doesnt exist try and migrate
// if sled and sqlite exists and the sled flag is set, use sled
// if sled and sqlite exists, use sqlite
match (
sled_path.as_ref().exists(),
sqlite_path.as_ref().exists(),
force_sled,
) {
(true, false, false) => {
tracing::info!("Attempting to migrate old data to the new sqlite database...");
let sled_db = SledDatabase::open(sled_path.as_ref()).await?;
ensure_directory_exists(sqlite_path.as_ref())?;
tokio::fs::File::create(&sqlite_path).await?;
let sqlite = SqliteDatabase::open(sqlite_path).await?;
let swap_states = sled_db.all().await?;
for (swap_id, state) in swap_states.iter() {
sqlite.insert_latest_state(*swap_id, state.clone()).await?;
}
let monero_addresses = sled_db.get_all_monero_addresses();
for (swap_id, monero_address) in monero_addresses.flatten() {
sqlite
.insert_monero_address(swap_id, monero_address)
.await?;
}
let peer_addresses = sled_db.get_all_addresses();
for (peer_id, addresses) in peer_addresses.flatten() {
for address in addresses {
sqlite.insert_address(peer_id, address).await?;
}
None => serialize(&[address])?,
})
};
self.addresses
.compare_and_swap(key, existing_addresses, new_addresses)??;
self.addresses
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>> {
let key = peer_id.to_bytes();
let addresses = match self.addresses.get(&key)? {
Some(encoded) => deserialize(&encoded).context("Failed to deserialize addresses")?,
None => vec![],
};
Ok(addresses)
}
pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> {
let key = serialize(&swap_id)?;
let new_value = serialize(&state).context("Could not serialize new state value")?;
let old_value = self.swaps.get(&key)?;
self.swaps
.compare_and_swap(key, old_value, Some(new_value))
.context("Could not write in the DB")?
.context("Stored swap somehow changed, aborting saving")?;
self.swaps
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_state(&self, swap_id: Uuid) -> Result<Swap> {
let key = serialize(&swap_id)?;
let encoded = self
.swaps
.get(&key)?
.ok_or_else(|| anyhow!("Swap with id {} not found in database", swap_id))?;
let state = deserialize(&encoded).context("Could not deserialize state")?;
Ok(state)
}
pub fn all_alice(&self) -> Result<Vec<(Uuid, Alice)>> {
self.all_alice_iter().collect()
}
fn all_alice_iter(&self) -> impl Iterator<Item = Result<(Uuid, Alice)>> {
self.all_swaps_iter().map(|item| {
let (swap_id, swap) = item?;
Ok((swap_id, swap.try_into_alice()?))
})
}
pub fn all_bob(&self) -> Result<Vec<(Uuid, Bob)>> {
self.all_bob_iter().collect()
}
fn all_bob_iter(&self) -> impl Iterator<Item = Result<(Uuid, Bob)>> {
self.all_swaps_iter().map(|item| {
let (swap_id, swap) = item?;
Ok((swap_id, swap.try_into_bob()?))
})
}
fn all_swaps_iter(&self) -> impl Iterator<Item = Result<(Uuid, Swap)>> {
self.swaps.iter().map(|item| {
let (key, value) = item.context("Failed to retrieve swap from DB")?;
let swap_id = deserialize::<Uuid>(&key)?;
let swap = deserialize::<Swap>(&value).context("Failed to deserialize swap")?;
}
Ok((swap_id, swap))
})
}
pub fn unfinished_alice(&self) -> Result<Vec<(Uuid, Alice)>> {
self.all_alice_iter()
.filter_ok(|(_swap_id, alice)| !matches!(alice, Alice::Done(_)))
.collect()
}
}
pub fn serialize<T>(t: &T) -> Result<Vec<u8>>
where
T: Serialize,
{
Ok(serde_cbor::to_vec(t)?)
}
pub fn deserialize<T>(v: &[u8]) -> Result<T>
where
T: DeserializeOwned,
{
Ok(serde_cbor::from_slice(&v)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::alice::{Alice, AliceEndState};
use crate::database::bob::{Bob, BobEndState};
#[tokio::test]
async fn can_write_and_read_to_multiple_keys() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let state_1 = Swap::Alice(Alice::Done(AliceEndState::BtcRedeemed));
let swap_id_1 = Uuid::new_v4();
db.insert_latest_state(swap_id_1, state_1.clone())
.await
.expect("Failed to save second state");
let state_2 = Swap::Bob(Bob::Done(BobEndState::SafelyAborted));
let swap_id_2 = Uuid::new_v4();
db.insert_latest_state(swap_id_2, state_2.clone())
.await
.expect("Failed to save first state");
let recovered_1 = db
.get_state(swap_id_1)
.expect("Failed to recover first state");
let recovered_2 = db
.get_state(swap_id_2)
.expect("Failed to recover second state");
assert_eq!(recovered_1, state_1);
assert_eq!(recovered_2, state_2);
}
#[tokio::test]
async fn can_write_twice_to_one_key() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let state = Swap::Alice(Alice::Done(AliceEndState::SafelyAborted));
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state.clone())
.await
.expect("Failed to save state the first time");
let recovered = db
.get_state(swap_id)
.expect("Failed to recover state the first time");
// We insert and recover twice to ensure database implementation allows the
// caller to write to an existing key
db.insert_latest_state(swap_id, recovered)
.await
.expect("Failed to save state the second time");
let recovered = db
.get_state(swap_id)
.expect("Failed to recover state the second time");
assert_eq!(recovered, state);
}
#[tokio::test]
async fn all_swaps_as_alice() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let alice_state = Alice::Done(AliceEndState::BtcPunished);
let alice_swap = Swap::Alice(alice_state.clone());
let alice_swap_id = Uuid::new_v4();
db.insert_latest_state(alice_swap_id, alice_swap)
.await
.expect("Failed to save alice state 1");
let alice_swaps = db.all_alice().unwrap();
assert_eq!(alice_swaps.len(), 1);
assert!(alice_swaps.contains(&(alice_swap_id, alice_state)));
let bob_state = Bob::Done(BobEndState::SafelyAborted);
let bob_swap = Swap::Bob(bob_state);
let bob_swap_id = Uuid::new_v4();
db.insert_latest_state(bob_swap_id, bob_swap)
.await
.expect("Failed to save bob state 1");
let err = db.all_alice().unwrap_err();
assert_eq!(err.downcast_ref::<NotAlice>().unwrap(), &NotAlice);
}
#[tokio::test]
async fn all_swaps_as_bob() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let bob_state = Bob::Done(BobEndState::SafelyAborted);
let bob_swap = Swap::Bob(bob_state.clone());
let bob_swap_id = Uuid::new_v4();
db.insert_latest_state(bob_swap_id, bob_swap)
.await
.expect("Failed to save bob state 1");
let peers = sled_db.get_all_peers();
for (swap_id, peer_id) in peers.flatten() {
sqlite.insert_peer_id(swap_id, peer_id).await?;
}
let bob_swaps = db.all_bob().unwrap();
assert_eq!(bob_swaps.len(), 1);
assert!(bob_swaps.contains(&(bob_swap_id, bob_state)));
tracing::info!("Sucessfully migrated data to sqlite! Using sqlite.");
let alice_state = Alice::Done(AliceEndState::BtcPunished);
let alice_swap = Swap::Alice(alice_state);
let alice_swap_id = Uuid::new_v4();
db.insert_latest_state(alice_swap_id, alice_swap)
.await
.expect("Failed to save alice state 1");
let err = db.all_bob().unwrap_err();
assert_eq!(err.downcast_ref::<NotBob>().unwrap(), &NotBob);
}
#[tokio::test]
async fn can_save_swap_state_and_peer_id_with_same_swap_id() -> Result<()> {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let alice_id = Uuid::new_v4();
let alice_state = Alice::Done(AliceEndState::BtcPunished);
let alice_swap = Swap::Alice(alice_state);
let peer_id = PeerId::random();
db.insert_latest_state(alice_id, alice_swap.clone()).await?;
db.insert_peer_id(alice_id, peer_id).await?;
let loaded_swap = db.get_state(alice_id)?;
let loaded_peer_id = db.get_peer_id(alice_id)?;
assert_eq!(alice_swap, loaded_swap);
assert_eq!(peer_id, loaded_peer_id);
Ok(())
}
#[tokio::test]
async fn test_reopen_db() -> Result<()> {
let db_dir = tempfile::tempdir().unwrap();
let alice_id = Uuid::new_v4();
let alice_state = Alice::Done(AliceEndState::BtcPunished);
let alice_swap = Swap::Alice(alice_state);
let peer_id = PeerId::random();
{
let db = Database::open(db_dir.path()).unwrap();
db.insert_latest_state(alice_id, alice_swap.clone()).await?;
db.insert_peer_id(alice_id, peer_id).await?;
Ok(Arc::new(sqlite))
}
let db = Database::open(db_dir.path()).unwrap();
let loaded_swap = db.get_state(alice_id)?;
let loaded_peer_id = db.get_peer_id(alice_id)?;
assert_eq!(alice_swap, loaded_swap);
assert_eq!(peer_id, loaded_peer_id);
Ok(())
}
#[tokio::test]
async fn save_and_load_addresses() -> Result<()> {
let db_dir = tempfile::tempdir()?;
let peer_id = PeerId::random();
let home1 = "/ip4/127.0.0.1/tcp/1".parse::<Multiaddr>()?;
let home2 = "/ip4/127.0.0.1/tcp/2".parse::<Multiaddr>()?;
{
let db = Database::open(db_dir.path())?;
db.insert_address(peer_id, home1.clone()).await?;
db.insert_address(peer_id, home2.clone()).await?;
(_, false, false) => {
tracing::debug!("Creating and using new sqlite database.");
ensure_directory_exists(sqlite_path.as_ref())?;
tokio::fs::File::create(&sqlite_path).await?;
let sqlite = SqliteDatabase::open(sqlite_path).await?;
Ok(Arc::new(sqlite))
}
(_, true, false) => {
tracing::debug!("Using existing sqlite database.");
let sqlite = SqliteDatabase::open(sqlite_path).await?;
Ok(Arc::new(sqlite))
}
(false, _, true) => {
bail!("Sled database does not exist at specified location")
}
(true, _, true) => {
tracing::debug!("Sled flag set. Using sled database.");
let sled = SledDatabase::open(sled_path.as_ref()).await?;
Ok(Arc::new(sled))
}
let addresses = Database::open(db_dir.path())?.get_addresses(peer_id)?;
assert_eq!(addresses, vec![home1, home2]);
Ok(())
}
#[tokio::test]
async fn save_and_load_monero_address() -> Result<()> {
let db_dir = tempfile::tempdir()?;
let swap_id = Uuid::new_v4();
Database::open(db_dir.path())?.insert_monero_address(swap_id, "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?).await?;
let loaded_monero_address = Database::open(db_dir.path())?.get_monero_address(swap_id)?;
assert_eq!(loaded_monero_address.to_string(), "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a");
Ok(())
}
}

@ -78,8 +78,8 @@ pub enum AliceEndState {
BtcPunished,
}
impl From<&AliceState> for Alice {
fn from(alice_state: &AliceState) -> Self {
impl From<AliceState> for Alice {
fn from(alice_state: AliceState) -> Self {
match alice_state {
AliceState::Started { state3 } => Alice::Started {
state3: state3.as_ref().clone(),
@ -95,8 +95,8 @@ impl From<&AliceState> for Alice {
transfer_proof,
state3,
} => Alice::XmrLockTransactionSent {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
monero_wallet_restore_blockheight,
transfer_proof,
state3: state3.as_ref().clone(),
},
AliceState::XmrLocked {
@ -104,8 +104,8 @@ impl From<&AliceState> for Alice {
transfer_proof,
state3,
} => Alice::XmrLocked {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
monero_wallet_restore_blockheight,
transfer_proof,
state3: state3.as_ref().clone(),
},
AliceState::XmrLockTransferProofSent {
@ -113,8 +113,8 @@ impl From<&AliceState> for Alice {
transfer_proof,
state3,
} => Alice::XmrLockTransferProofSent {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
monero_wallet_restore_blockheight,
transfer_proof,
state3: state3.as_ref().clone(),
},
AliceState::EncSigLearned {
@ -123,10 +123,10 @@ impl From<&AliceState> for Alice {
state3,
encrypted_signature,
} => Alice::EncSigLearned {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
monero_wallet_restore_blockheight,
transfer_proof,
state3: state3.as_ref().clone(),
encrypted_signature: *encrypted_signature.clone(),
encrypted_signature: encrypted_signature.as_ref().clone(),
},
AliceState::BtcRedeemTransactionPublished { state3 } => {
Alice::BtcRedeemTransactionPublished {
@ -139,8 +139,8 @@ impl From<&AliceState> for Alice {
transfer_proof,
state3,
} => Alice::BtcCancelled {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
monero_wallet_restore_blockheight,
transfer_proof,
state3: state3.as_ref().clone(),
},
AliceState::BtcRefunded {
@ -149,9 +149,9 @@ impl From<&AliceState> for Alice {
spend_key,
state3,
} => Alice::BtcRefunded {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
spend_key: *spend_key,
monero_wallet_restore_blockheight,
transfer_proof,
spend_key,
state3: state3.as_ref().clone(),
},
AliceState::BtcPunishable {
@ -159,8 +159,8 @@ impl From<&AliceState> for Alice {
transfer_proof,
state3,
} => Alice::BtcPunishable {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
monero_wallet_restore_blockheight,
transfer_proof,
state3: state3.as_ref().clone(),
},
AliceState::XmrRefunded => Alice::Done(AliceEndState::XmrRefunded),
@ -169,8 +169,8 @@ impl From<&AliceState> for Alice {
transfer_proof,
state3,
} => Alice::CancelTimelockExpired {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
monero_wallet_restore_blockheight,
transfer_proof,
state3: state3.as_ref().clone(),
},
AliceState::BtcPunished => Alice::Done(AliceEndState::BtcPunished),

@ -0,0 +1,400 @@
use crate::database::Swap;
use crate::protocol::{Database, State};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use libp2p::{Multiaddr, PeerId};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::path::Path;
use std::str::FromStr;
use uuid::Uuid;
pub use crate::database::alice::Alice;
pub use crate::database::bob::Bob;
pub struct SledDatabase {
swaps: sled::Tree,
peers: sled::Tree,
addresses: sled::Tree,
monero_addresses: sled::Tree,
}
#[async_trait]
impl Database for SledDatabase {
async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()> {
let peer_id_str = peer_id.to_string();
let key = serialize(&swap_id)?;
let value = serialize(&peer_id_str).context("Could not serialize peer-id")?;
self.peers.insert(key, value)?;
self.peers
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
async fn get_peer_id(&self, swap_id: Uuid) -> Result<PeerId> {
let key = serialize(&swap_id)?;
let encoded = self
.peers
.get(&key)?
.ok_or_else(|| anyhow!("No peer-id found for swap id {} in database", swap_id))?;
let peer_id: String = deserialize(&encoded).context("Could not deserialize peer-id")?;
Ok(PeerId::from_str(peer_id.as_str())?)
}
async fn insert_monero_address(&self, swap_id: Uuid, address: monero::Address) -> Result<()> {
let key = swap_id.as_bytes();
let value = serialize(&address)?;
self.monero_addresses.insert(key, value)?;
self.monero_addresses
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
async fn get_monero_address(&self, swap_id: Uuid) -> Result<monero::Address> {
let encoded = self
.monero_addresses
.get(swap_id.as_bytes())?
.ok_or_else(|| {
anyhow!(
"No Monero address found for swap id {} in database",
swap_id
)
})?;
let monero_address = deserialize(&encoded)?;
Ok(monero_address)
}
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()> {
let key = peer_id.to_bytes();
let existing_addresses = self.addresses.get(&key)?;
let new_addresses = {
let existing_addresses = existing_addresses.clone();
Some(match existing_addresses {
Some(encoded) => {
let mut addresses = deserialize::<Vec<Multiaddr>>(&encoded)?;
addresses.push(address);
serialize(&addresses)?
}
None => serialize(&[address])?,
})
};
self.addresses
.compare_and_swap(key, existing_addresses, new_addresses)??;
self.addresses
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>> {
let key = peer_id.to_bytes();
let addresses = match self.addresses.get(&key)? {
Some(encoded) => deserialize(&encoded).context("Failed to deserialize addresses")?,
None => vec![],
};
Ok(addresses)
}
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()> {
let key = serialize(&swap_id)?;
let swap = Swap::from(state);
let new_value = serialize(&swap).context("Could not serialize new state value")?;
let old_value = self.swaps.get(&key)?;
self.swaps
.compare_and_swap(key, old_value, Some(new_value))
.context("Could not write in the DB")?
.context("Stored swap somehow changed, aborting saving")?;
self.swaps
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
async fn get_state(&self, swap_id: Uuid) -> Result<State> {
let key = serialize(&swap_id)?;
let encoded = self
.swaps
.get(&key)?
.ok_or_else(|| anyhow!("Swap with id {} not found in database", swap_id))?;
let swap = deserialize::<Swap>(&encoded).context("Could not deserialize state")?;
let state = State::from(swap);
Ok(state)
}
async fn all(&self) -> Result<Vec<(Uuid, State)>> {
self.all_iter().collect()
}
}
impl SledDatabase {
pub async fn open(path: &Path) -> Result<Self> {
tracing::debug!("Opening database at {}", path.display());
let db =
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
let swaps = db.open_tree("swaps")?;
let peers = db.open_tree("peers")?;
let addresses = db.open_tree("addresses")?;
let monero_addresses = db.open_tree("monero_addresses")?;
Ok(SledDatabase {
swaps,
peers,
addresses,
monero_addresses,
})
}
pub fn get_all_peers(&self) -> impl Iterator<Item = Result<(Uuid, PeerId)>> {
self.peers.iter().map(|item| {
let (key, value) = item.context("Failed to retrieve peer id from DB")?;
let swap_id = deserialize::<Uuid>(&key)?;
let peer_id_bytes =
deserialize::<Vec<u8>>(&value).context("Failed to deserialize swap")?;
let peer_id = PeerId::from_bytes(&peer_id_bytes)?;
Ok((swap_id, peer_id))
})
}
pub fn get_all_addresses(&self) -> impl Iterator<Item = Result<(PeerId, Vec<Multiaddr>)>> {
self.addresses.iter().map(|item| {
let (key, value) = item.context("Failed to retrieve peer address from DB")?;
let peer_id_bytes = deserialize::<Vec<u8>>(&key)?;
let addr =
deserialize::<Vec<Multiaddr>>(&value).context("Failed to deserialize swap")?;
let peer_id = PeerId::from_bytes(&peer_id_bytes)?;
Ok((peer_id, addr))
})
}
pub fn get_all_monero_addresses(
&self,
) -> impl Iterator<Item = Result<(Uuid, monero::Address)>> {
self.monero_addresses.iter().map(|item| {
let (key, value) = item.context("Failed to retrieve monero address from DB")?;
let swap_id = deserialize::<Uuid>(&key)?;
let addr =
deserialize::<monero::Address>(&value).context("Failed to deserialize swap")?;
Ok((swap_id, addr))
})
}
fn all_iter(&self) -> impl Iterator<Item = Result<(Uuid, State)>> {
self.swaps.iter().map(|item| {
let (key, value) = item.context("Failed to retrieve swap from DB")?;
let swap_id = deserialize::<Uuid>(&key)?;
let swap = deserialize::<Swap>(&value).context("Failed to deserialize swap")?;
let state = State::from(swap);
Ok((swap_id, state))
})
}
}
pub fn serialize<T>(t: &T) -> Result<Vec<u8>>
where
T: Serialize,
{
Ok(serde_cbor::to_vec(t)?)
}
pub fn deserialize<T>(v: &[u8]) -> Result<T>
where
T: DeserializeOwned,
{
Ok(serde_cbor::from_slice(&v)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::alice::AliceState;
#[tokio::test]
async fn can_write_and_read_to_multiple_keys() {
let db_dir = tempfile::tempdir().unwrap();
let db = SledDatabase::open(db_dir.path()).await.unwrap();
let state_1 = State::from(AliceState::BtcRedeemed);
let swap_id_1 = Uuid::new_v4();
db.insert_latest_state(swap_id_1, state_1.clone())
.await
.expect("Failed to save second state");
let state_2 = State::from(AliceState::BtcPunished);
let swap_id_2 = Uuid::new_v4();
db.insert_latest_state(swap_id_2, state_2.clone())
.await
.expect("Failed to save first state");
let recovered_1 = db
.get_state(swap_id_1)
.await
.expect("Failed to recover first state");
let recovered_2 = db
.get_state(swap_id_2)
.await
.expect("Failed to recover second state");
assert_eq!(recovered_1, state_1);
assert_eq!(recovered_2, state_2);
}
#[tokio::test]
async fn can_write_twice_to_one_key() {
let db_dir = tempfile::tempdir().unwrap();
let db = SledDatabase::open(db_dir.path()).await.unwrap();
let state = State::from(AliceState::SafelyAborted);
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state.clone())
.await
.expect("Failed to save state the first time");
let recovered = db
.get_state(swap_id)
.await
.expect("Failed to recover state the first time");
// We insert and recover twice to ensure database implementation allows the
// caller to write to an existing key
db.insert_latest_state(swap_id, recovered)
.await
.expect("Failed to save state the second time");
let recovered = db
.get_state(swap_id)
.await
.expect("Failed to recover state the second time");
assert_eq!(recovered, state);
}
#[tokio::test]
async fn can_save_swap_state_and_peer_id_with_same_swap_id() -> Result<()> {
let db_dir = tempfile::tempdir().unwrap();
let db = SledDatabase::open(db_dir.path()).await.unwrap();
let alice_id = Uuid::new_v4();
let alice_state = State::from(AliceState::BtcPunished);
let peer_id = PeerId::random();
db.insert_latest_state(alice_id, alice_state.clone())
.await?;
db.insert_peer_id(alice_id, peer_id).await?;
let loaded_swap = db.get_state(alice_id).await?;
let loaded_peer_id = db.get_peer_id(alice_id).await?;
assert_eq!(alice_state, loaded_swap);
assert_eq!(peer_id, loaded_peer_id);
Ok(())
}
#[tokio::test]
async fn test_reopen_db() -> Result<()> {
let db_dir = tempfile::tempdir().unwrap();
let alice_id = Uuid::new_v4();
let alice_state = State::from(AliceState::BtcPunished);
let peer_id = PeerId::random();
{
let db = SledDatabase::open(db_dir.path()).await.unwrap();
db.insert_latest_state(alice_id, alice_state.clone())
.await?;
db.insert_peer_id(alice_id, peer_id).await?;
}
let db = SledDatabase::open(db_dir.path()).await.unwrap();
let loaded_swap = db.get_state(alice_id).await?;
let loaded_peer_id = db.get_peer_id(alice_id).await?;
assert_eq!(alice_state, loaded_swap);
assert_eq!(peer_id, loaded_peer_id);
Ok(())
}
#[tokio::test]
async fn save_and_load_addresses() -> Result<()> {
let db_dir = tempfile::tempdir()?;
let peer_id = PeerId::random();
let home1 = "/ip4/127.0.0.1/tcp/1".parse::<Multiaddr>()?;
let home2 = "/ip4/127.0.0.1/tcp/2".parse::<Multiaddr>()?;
{
let db = SledDatabase::open(db_dir.path()).await?;
db.insert_address(peer_id, home1.clone()).await?;
db.insert_address(peer_id, home2.clone()).await?;
}
let addresses = SledDatabase::open(db_dir.path())
.await?
.get_addresses(peer_id)
.await?;
assert_eq!(addresses, vec![home1, home2]);
Ok(())
}
#[tokio::test]
async fn save_and_load_monero_address() -> Result<()> {
let db_dir = tempfile::tempdir()?;
let swap_id = Uuid::new_v4();
SledDatabase::open(db_dir.path()).await?.insert_monero_address(swap_id, "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?).await?;
let loaded_monero_address = SledDatabase::open(db_dir.path())
.await?
.get_monero_address(swap_id)
.await?;
assert_eq!(loaded_monero_address.to_string(), "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a");
Ok(())
}
}

@ -0,0 +1,383 @@
use crate::database::Swap;
use crate::monero::Address;
use crate::protocol::{Database, State};
use anyhow::{Context, Result};
use async_trait::async_trait;
use libp2p::{Multiaddr, PeerId};
use sqlx::sqlite::Sqlite;
use sqlx::{Pool, SqlitePool};
use std::path::Path;
use std::str::FromStr;
use time::OffsetDateTime;
use uuid::Uuid;
pub struct SqliteDatabase {
pool: Pool<Sqlite>,
}
impl SqliteDatabase {
pub async fn open(path: impl AsRef<Path>) -> Result<Self>
where
Self: std::marker::Sized,
{
let path_str = format!("sqlite:{}", path.as_ref().display());
let pool = SqlitePool::connect(&path_str).await?;
let mut sqlite = Self { pool };
sqlite.run_migrations().await?;
Ok(sqlite)
}
async fn run_migrations(&mut self) -> anyhow::Result<()> {
sqlx::migrate!("./migrations").run(&self.pool).await?;
Ok(())
}
}
#[async_trait]
impl Database for SqliteDatabase {
async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()> {
let mut conn = self.pool.acquire().await?;
let swap_id = swap_id.to_string();
let peer_id = peer_id.to_string();
sqlx::query!(
r#"
insert into peers (
swap_id,
peer_id
) values (?, ?);
"#,
swap_id,
peer_id
)
.execute(&mut conn)
.await?;
Ok(())
}
async fn get_peer_id(&self, swap_id: Uuid) -> Result<PeerId> {
let mut conn = self.pool.acquire().await?;
let swap_id = swap_id.to_string();
let row = sqlx::query!(
r#"
SELECT peer_id
FROM peers
WHERE swap_id = ?
"#,
swap_id
)
.fetch_one(&mut conn)
.await?;
let peer_id = PeerId::from_str(&row.peer_id)?;
Ok(peer_id)
}
async fn insert_monero_address(&self, swap_id: Uuid, address: Address) -> Result<()> {
let mut conn = self.pool.acquire().await?;
let swap_id = swap_id.to_string();
let address = address.to_string();
sqlx::query!(
r#"
insert into monero_addresses (
swap_id,
address
) values (?, ?);
"#,
swap_id,
address
)
.execute(&mut conn)
.await?;
Ok(())
}
async fn get_monero_address(&self, swap_id: Uuid) -> Result<Address> {
let mut conn = self.pool.acquire().await?;
let swap_id = swap_id.to_string();
let row = sqlx::query!(
r#"
SELECT address
FROM monero_addresses
WHERE swap_id = ?
"#,
swap_id
)
.fetch_one(&mut conn)
.await?;
let address = row.address.parse()?;
Ok(address)
}
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()> {
let mut conn = self.pool.acquire().await?;
let peer_id = peer_id.to_string();
let address = address.to_string();
sqlx::query!(
r#"
insert into peer_addresses (
peer_id,
address
) values (?, ?);
"#,
peer_id,
address
)
.execute(&mut conn)
.await?;
Ok(())
}
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>> {
let mut conn = self.pool.acquire().await?;
let peer_id = peer_id.to_string();
let rows = sqlx::query!(
r#"
SELECT address
FROM peer_addresses
WHERE peer_id = ?
"#,
peer_id,
)
.fetch_all(&mut conn)
.await?;
let addresses = rows
.iter()
.map(|row| {
let multiaddr = Multiaddr::from_str(&row.address)?;
Ok(multiaddr)
})
.collect::<Result<Vec<Multiaddr>>>();
addresses
}
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()> {
let mut conn = self.pool.acquire().await?;
let entered_at = OffsetDateTime::now_utc();
let swap_id = swap_id.to_string();
let swap = serde_json::to_string(&Swap::from(state))?;
let entered_at = entered_at.to_string();
sqlx::query!(
r#"
insert into swap_states (
swap_id,
entered_at,
state
) values (?, ?, ?);
"#,
swap_id,
entered_at,
swap
)
.execute(&mut conn)
.await?;
Ok(())
}
async fn get_state(&self, swap_id: Uuid) -> Result<State> {
let mut conn = self.pool.acquire().await?;
let swap_id = swap_id.to_string();
let row = sqlx::query!(
r#"
SELECT state
FROM swap_states
WHERE swap_id = ?
ORDER BY id desc
LIMIT 1;
"#,
swap_id
)
.fetch_all(&mut conn)
.await?;
let row = row
.first()
.context(format!("No state in database for swap: {}", swap_id))?;
let swap: Swap = serde_json::from_str(&row.state)?;
Ok(swap.into())
}
async fn all(&self) -> Result<Vec<(Uuid, State)>> {
let mut conn = self.pool.acquire().await?;
let rows = sqlx::query!(
r#"
SELECT swap_id, state
FROM (
SELECT max(id), swap_id, state
FROM swap_states
GROUP BY swap_id
)
"#
)
.fetch_all(&mut conn)
.await?;
let result = rows
.iter()
.map(|row| {
let swap_id = Uuid::from_str(&row.swap_id)?;
let state = match serde_json::from_str::<Swap>(&row.state) {
Ok(a) => Ok(State::from(a)),
Err(e) => Err(e),
}?;
Ok((swap_id, state))
})
.collect::<Result<Vec<(Uuid, State)>>>();
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::alice::AliceState;
use crate::protocol::bob::BobState;
use std::fs::File;
use tempfile::tempdir;
#[tokio::test]
async fn test_insert_and_load_state() {
let db = setup_test_db().await.unwrap();
let state_1 = State::Alice(AliceState::BtcRedeemed);
let swap_id_1 = Uuid::new_v4();
db.insert_latest_state(swap_id_1, state_1).await.unwrap();
let state_1 = State::Alice(AliceState::BtcRedeemed);
db.insert_latest_state(swap_id_1, state_1.clone())
.await
.unwrap();
let state_1_loaded = db.get_state(swap_id_1).await.unwrap();
assert_eq!(state_1, state_1_loaded);
}
#[tokio::test]
async fn test_retrieve_all_latest_states() {
let db = setup_test_db().await.unwrap();
let state_1 = State::Alice(AliceState::BtcRedeemed);
let state_2 = State::Alice(AliceState::BtcPunished);
let state_3 = State::Alice(AliceState::SafelyAborted);
let state_4 = State::Bob(BobState::SafelyAborted);
let swap_id_1 = Uuid::new_v4();
let swap_id_2 = Uuid::new_v4();
db.insert_latest_state(swap_id_1, state_1.clone())
.await
.unwrap();
db.insert_latest_state(swap_id_1, state_2.clone())
.await
.unwrap();
db.insert_latest_state(swap_id_1, state_3.clone())
.await
.unwrap();
db.insert_latest_state(swap_id_2, state_4.clone())
.await
.unwrap();
let latest_loaded = db.all().await.unwrap();
assert_eq!(latest_loaded.len(), 2);
assert!(latest_loaded.contains(&(swap_id_1, state_3)));
assert!(latest_loaded.contains(&(swap_id_2, state_4)));
assert!(!latest_loaded.contains(&(swap_id_1, state_1)));
assert!(!latest_loaded.contains(&(swap_id_1, state_2)));
}
#[tokio::test]
async fn test_insert_load_monero_address() -> Result<()> {
let db = setup_test_db().await?;
let swap_id = Uuid::new_v4();
let monero_address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?;
db.insert_monero_address(swap_id, monero_address).await?;
let loaded_monero_address = db.get_monero_address(swap_id).await?;
assert_eq!(monero_address, loaded_monero_address);
Ok(())
}
#[tokio::test]
async fn test_insert_and_load_multiaddr() -> Result<()> {
let db = setup_test_db().await?;
let peer_id = PeerId::random();
let multiaddr1 = "/ip4/127.0.0.1".parse::<Multiaddr>()?;
let multiaddr2 = "/ip4/127.0.0.2".parse::<Multiaddr>()?;
db.insert_address(peer_id, multiaddr1.clone()).await?;
db.insert_address(peer_id, multiaddr2.clone()).await?;
let loaded_multiaddr = db.get_addresses(peer_id).await?;
assert!(loaded_multiaddr.contains(&multiaddr1));
assert!(loaded_multiaddr.contains(&multiaddr2));
assert_eq!(loaded_multiaddr.len(), 2);
Ok(())
}
#[tokio::test]
async fn test_insert_and_load_peer_id() -> Result<()> {
let db = setup_test_db().await?;
let peer_id = PeerId::random();
let multiaddr1 = "/ip4/127.0.0.1".parse::<Multiaddr>()?;
let multiaddr2 = "/ip4/127.0.0.2".parse::<Multiaddr>()?;
db.insert_address(peer_id, multiaddr1.clone()).await?;
db.insert_address(peer_id, multiaddr2.clone()).await?;
let loaded_multiaddr = db.get_addresses(peer_id).await?;
assert!(loaded_multiaddr.contains(&multiaddr1));
assert!(loaded_multiaddr.contains(&multiaddr2));
assert_eq!(loaded_multiaddr.len(), 2);
Ok(())
}
async fn setup_test_db() -> Result<SqliteDatabase> {
let temp_db = tempdir().unwrap().into_path().join("tempdb");
// file has to exist in order to connect with sqlite
File::create(temp_db.clone()).unwrap();
let db = SqliteDatabase::open(temp_db).await?;
Ok(db)
}
}

@ -1,10 +1,11 @@
use crate::asb;
use crate::bitcoin::{CancelTimelock, PunishTimelock};
use serde::Serialize;
use std::cmp::max;
use std::time::Duration;
use time::ext::NumericalStdDuration;
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Serialize)]
pub struct Config {
pub bitcoin_lock_mempool_timeout: Duration,
pub bitcoin_lock_confirmed_timeout: Duration,
@ -15,6 +16,7 @@ pub struct Config {
pub bitcoin_network: bitcoin::Network,
pub monero_avg_block_time: Duration,
pub monero_finality_confirmations: u64,
#[serde(with = "monero_network")]
pub monero_network: monero::Network,
}
@ -123,6 +125,23 @@ pub fn new(is_testnet: bool, asb_config: &asb::config::Config) -> Config {
}
}
mod monero_network {
use crate::monero::Network;
use serde::Serializer;
pub fn serialize<S>(x: &monero::Network, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let str = match x {
Network::Mainnet => "mainnet",
Network::Stagenet => "stagenet",
Network::Testnet => "testnet",
};
s.serialize_str(&str)
}
}
#[cfg(test)]
mod tests {
use super::*;

@ -234,6 +234,15 @@ pub mod monero_private_key {
let mut s = s;
PrivateKey::consensus_decode(&mut s).map_err(|err| E::custom(format!("{:?}", err)))
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let bytes = hex::decode(s).map_err(|err| E::custom(format!("{:?}", err)))?;
PrivateKey::consensus_decode(&mut bytes.as_slice())
.map_err(|err| E::custom(format!("{:?}", err)))
}
}
pub fn serialize<S>(x: &PrivateKey, s: S) -> Result<S::Ok, S::Error>
@ -243,7 +252,11 @@ pub mod monero_private_key {
let mut bytes = Cursor::new(vec![]);
x.consensus_encode(&mut bytes)
.map_err(|err| S::Error::custom(format!("{:?}", err)))?;
s.serialize_bytes(bytes.into_inner().as_ref())
if s.is_human_readable() {
s.serialize_str(&hex::encode(bytes.into_inner()))
} else {
s.serialize_bytes(bytes.into_inner().as_ref())
}
}
pub fn deserialize<'de, D>(
@ -252,7 +265,13 @@ pub mod monero_private_key {
where
D: Deserializer<'de>,
{
let key = deserializer.deserialize_bytes(BytesVisitor)?;
let key = {
if deserializer.is_human_readable() {
deserializer.deserialize_string(BytesVisitor)?
} else {
deserializer.deserialize_bytes(BytesVisitor)?
}
};
Ok(key)
}
}
@ -351,7 +370,17 @@ mod tests {
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
#[test]
fn serde_monero_private_key() {
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),
));

@ -1,10 +1,18 @@
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 ecdsa_fun::fun::marker::Mark;
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;
@ -65,3 +73,74 @@ pub struct Message4 {
tx_punish_sig: bitcoin::Signature,
tx_cancel_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)]
#[error("Not in the role of Alice")]
pub struct NotAlice;
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
#[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(&self, swap_id: Uuid, address: monero::Address) -> Result<()>;
async fn get_monero_address(&self, swap_id: Uuid) -> Result<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 insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>;
async fn get_state(&self, swap_id: Uuid) -> Result<State>;
async fn all(&self) -> Result<Vec<(Uuid, State)>>;
}

@ -1,7 +1,7 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use crate::database::Database;
use crate::env::Config;
use crate::protocol::Database;
use crate::{asb, bitcoin, monero};
use std::sync::Arc;
use uuid::Uuid;
@ -19,5 +19,5 @@ pub struct Swap {
pub monero_wallet: Arc<monero::Wallet>,
pub env_config: Config,
pub swap_id: Uuid,
pub db: Arc<Database>,
pub db: Arc<dyn Database + Send + Sync>,
}

@ -16,7 +16,7 @@ use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
use std::fmt;
use uuid::Uuid;
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub enum AliceState {
Started {
state3: Box<State3>,

@ -4,7 +4,7 @@ use crate::asb::{EventLoopHandle, LatestRate};
use crate::bitcoin::ExpiredTimelocks;
use crate::env::Config;
use crate::protocol::alice::{AliceState, Swap};
use crate::{bitcoin, database, monero};
use crate::{bitcoin, monero};
use anyhow::{bail, Context, Result};
use tokio::select;
use tokio::time::timeout;
@ -40,9 +40,8 @@ where
)
.await?;
let db_state = (&current_state).into();
swap.db
.insert_latest_state(swap.swap_id, database::Swap::Alice(db_state))
.insert_latest_state(swap.swap_id, current_state.clone().into())
.await?;
}
@ -398,7 +397,7 @@ where
})
}
fn is_complete(state: &AliceState) -> bool {
pub(crate) fn is_complete(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrRefunded

@ -3,11 +3,12 @@ use std::sync::Arc;
use anyhow::Result;
use uuid::Uuid;
use crate::database::Database;
use crate::protocol::Database;
use crate::{bitcoin, cli, env, monero};
pub use self::state::*;
pub use self::swap::{run, run_until};
use std::convert::TryInto;
pub mod state;
pub mod swap;
@ -15,7 +16,7 @@ pub mod swap;
pub struct Swap {
pub state: BobState,
pub event_loop_handle: cli::EventLoopHandle,
pub db: Database,
pub db: Arc<dyn Database + Send + Sync>,
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
pub env_config: env::Config,
@ -26,7 +27,7 @@ pub struct Swap {
impl Swap {
#[allow(clippy::too_many_arguments)]
pub fn new(
db: Database,
db: Arc<dyn Database + Send + Sync>,
id: Uuid,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
@ -52,8 +53,8 @@ impl Swap {
}
#[allow(clippy::too_many_arguments)]
pub fn from_db(
db: Database,
pub async fn from_db(
db: Arc<dyn Database + Send + Sync>,
id: Uuid,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
@ -61,7 +62,7 @@ impl Swap {
event_loop_handle: cli::EventLoopHandle,
monero_receive_address: monero::Address,
) -> Result<Self> {
let state = db.get_state(id)?.try_into_bob()?.into();
let state = db.get_state(id).await?.try_into()?;
Ok(Self {
state,

@ -21,7 +21,7 @@ use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum BobState {
Started {
btc_amount: bitcoin::Amount,

@ -1,6 +1,5 @@
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
use crate::cli::EventLoopHandle;
use crate::database::Swap;
use crate::network::swap_setup::bob::NewSwap;
use crate::protocol::bob;
use crate::protocol::bob::state::*;
@ -33,7 +32,7 @@ pub async fn run_until(
while !is_target_state(&current_state) {
current_state = next_state(
swap.id,
current_state,
current_state.clone(),
&mut swap.event_loop_handle,
swap.bitcoin_wallet.as_ref(),
swap.monero_wallet.as_ref(),
@ -41,9 +40,8 @@ pub async fn run_until(
)
.await?;
let db_state = current_state.clone().into();
swap.db
.insert_latest_state(swap.id, Swap::Bob(db_state))
.insert_latest_state(swap.id, current_state.clone().into())
.await?;
}

@ -16,15 +16,16 @@ use std::sync::Arc;
use std::time::Duration;
use swap::asb::FixedRate;
use swap::bitcoin::{CancelTimelock, PunishTimelock, TxCancel, TxPunish, TxRedeem, TxRefund};
use swap::database::Database;
use swap::database::SqliteDatabase;
use swap::env::{Config, GetConfig};
use swap::fs::ensure_directory_exists;
use swap::network::swarm;
use swap::protocol::alice::{AliceState, Swap};
use swap::protocol::bob::BobState;
use swap::protocol::{alice, bob};
use swap::seed::Seed;
use swap::{asb, bitcoin, cli, env, monero};
use tempfile::tempdir;
use tempfile::{tempdir, NamedTempFile};
use testcontainers::clients::Cli;
use testcontainers::{Container, Docker, RunArgs};
use tokio::sync::mpsc;
@ -82,7 +83,7 @@ where
.parse()
.expect("failed to parse Alice's address");
let alice_db_path = tempdir().unwrap().into_path();
let alice_db_path = NamedTempFile::new().unwrap().path().to_path_buf();
let (alice_handle, alice_swap_handle) = start_alice(
&alice_seed,
alice_db_path.clone(),
@ -110,7 +111,7 @@ where
let bob_params = BobParams {
seed: Seed::random().unwrap(),
db_path: tempdir().unwrap().path().to_path_buf(),
db_path: NamedTempFile::new().unwrap().path().to_path_buf(),
bitcoin_wallet: bob_bitcoin_wallet.clone(),
monero_wallet: bob_monero_wallet.clone(),
alice_address: alice_listen_address.clone(),
@ -222,7 +223,13 @@ async fn start_alice(
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
) -> (AliceApplicationHandle, Receiver<alice::Swap>) {
let db = Arc::new(Database::open(db_path.as_path()).unwrap());
if let Some(parent_dir) = db_path.parent() {
ensure_directory_exists(parent_dir).unwrap();
}
if !&db_path.exists() {
tokio::fs::File::create(&db_path).await.unwrap();
}
let db = Arc::new(SqliteDatabase::open(db_path.as_path()).await.unwrap());
let min_buy = bitcoin::Amount::from_sat(u64::MIN);
let max_buy = bitcoin::Amount::from_sat(u64::MAX);
@ -402,7 +409,14 @@ struct BobParams {
impl BobParams {
pub async fn new_swap_from_db(&self, swap_id: Uuid) -> Result<(bob::Swap, cli::EventLoop)> {
let (event_loop, handle) = self.new_eventloop(swap_id).await?;
let db = Database::open(&self.db_path)?;
if let Some(parent_dir) = self.db_path.parent() {
ensure_directory_exists(parent_dir)?;
}
if !self.db_path.exists() {
tokio::fs::File::create(&self.db_path).await?;
}
let db = Arc::new(SqliteDatabase::open(&self.db_path).await?);
let swap = bob::Swap::from_db(
db,
@ -412,7 +426,8 @@ impl BobParams {
self.env_config,
handle,
self.monero_wallet.get_main_address(),
)?;
)
.await?;
Ok((swap, event_loop))
}
@ -424,7 +439,14 @@ impl BobParams {
let swap_id = Uuid::new_v4();
let (event_loop, handle) = self.new_eventloop(swap_id).await?;
let db = Database::open(&self.db_path)?;
if let Some(parent_dir) = self.db_path.parent() {
ensure_directory_exists(parent_dir)?;
}
if !self.db_path.exists() {
tokio::fs::File::create(&self.db_path).await?;
}
let db = Arc::new(SqliteDatabase::open(&self.db_path).await?);
let swap = bob::Swap::new(
db,

Loading…
Cancel
Save