mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-26 18:56:23 -05:00
feat(gui): Voluntary donations (#418)
* poc: monero receive pool with multiple redeem addresses for bob with given ratios * fix: use new monero_receive_pool arg for buy_xmr * update sweep/sweep_multi to return TxReceipt instead of String containing txid * fix test (generate 1 block before checking balance after transfer) * add move distribute function to rust, add property tests * use rust distribute * update sqlx cache/tempdb * sqlx fix * feat: update ui to display the monero address pool * fix: remove unused functions, set dispatcher for tracing in wallet threads, use new subtract_fee wallet2 functionality * Add patch system * add wallet2_api_allow_subtract_from_fee patch * apply git patches * split monero-sys patches into chunks * refactor * .sqlx needs to be commited, revert unbound issue * display pool on XmrRedeemInMempoolPage.tsx page, commit .sqlx folder * fmt * refactor * assert MoneroAddressPool is on correct network, differntiate between stagenet and mainnet donaiton address * looks ok * re-add retry logic, database errors, ... * add test * add tests * fmt comments, changelog --------- Co-authored-by: Binarybaron <binarybaron@protonmail.com>
This commit is contained in:
parent
cd4aa5201a
commit
11b891f530
48 changed files with 2091 additions and 380 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -78,4 +78,4 @@
|
||||||
"rust-analyzer.cargo.extraEnv": {
|
"rust-analyzer.cargo.extraEnv": {
|
||||||
"CARGO_TARGET_DIR": "target-check"
|
"CARGO_TARGET_DIR": "target-check"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- GUI: Users can donate a small percentage of their swap to the projects donation address. Donations will be used to fund development. This is completely optional and **disabled** by default. Monero is used exclusively for donations, ensuring full anonymity for users. Donations are only ever send for successful swaps (not refunded ones). We clearly and transparently state where how much Monero is going before the user approves a swap.
|
||||||
|
|
||||||
## [2.3.0-beta.2] - 2025-06-24
|
## [2.3.0-beta.2] - 2025-06-24
|
||||||
|
|
||||||
- ASB + GUI + CLI: We now cache fee estimates for the Bitcoin wallet for up to 2 minutes. This improves the speed of fee estimation and reduces the number of requests to the Electrum servers.
|
- ASB + GUI + CLI: We now cache fee estimates for the Bitcoin wallet for up to 2 minutes. This improves the speed of fee estimation and reduces the number of requests to the Electrum servers.
|
||||||
|
|
|
||||||
246
Cargo.lock
generated
246
Cargo.lock
generated
|
|
@ -400,7 +400,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"synstructure 0.13.2",
|
"synstructure 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -423,7 +423,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -565,7 +565,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -605,7 +605,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -622,7 +622,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -811,7 +811,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -900,9 +900,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.6.0"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
|
|
@ -1204,7 +1204,7 @@ checksum = "e0b121a9fe0df916e362fb3271088d071159cdf11db0e4182d02152850756eff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1296,7 +1296,7 @@ dependencies = [
|
||||||
"proc-macro-crate 3.3.0",
|
"proc-macro-crate 3.3.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1685,7 +1685,7 @@ dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2084,7 +2084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2094,7 +2094,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2143,7 +2143,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2185,7 +2185,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"scratch",
|
"scratch",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2198,7 +2198,7 @@ dependencies = [
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2216,7 +2216,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2288,7 +2288,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim 0.11.1",
|
"strsim 0.11.1",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2321,7 +2321,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core 0.20.11",
|
"darling_core 0.20.11",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2347,7 +2347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
|
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2430,7 +2430,7 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"sha3",
|
"sha3",
|
||||||
"strum 0.27.1",
|
"strum 0.27.1",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"void",
|
"void",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2442,7 +2442,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2463,7 +2463,7 @@ dependencies = [
|
||||||
"darling 0.20.11",
|
"darling 0.20.11",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2494,7 +2494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_builder_core",
|
"derive_builder_core",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2517,7 +2517,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2547,7 +2547,7 @@ dependencies = [
|
||||||
"convert_case 0.6.0",
|
"convert_case 0.6.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2560,7 +2560,7 @@ dependencies = [
|
||||||
"convert_case 0.7.1",
|
"convert_case 0.7.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2577,6 +2577,15 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diffy"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
|
||||||
|
dependencies = [
|
||||||
|
"nu-ansi-term 0.50.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -2706,7 +2715,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2729,7 +2738,7 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2994,7 +3003,7 @@ dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3007,7 +3016,7 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3028,7 +3037,17 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3258,7 +3277,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3446,7 +3465,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3800,7 +3819,7 @@ dependencies = [
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3908,7 +3927,7 @@ dependencies = [
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4815,9 +4834,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jpeg-decoder"
|
name = "jpeg-decoder"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
|
checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
|
|
@ -5462,7 +5481,7 @@ dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5985,8 +6004,11 @@ dependencies = [
|
||||||
"cmake",
|
"cmake",
|
||||||
"cxx",
|
"cxx",
|
||||||
"cxx-build",
|
"cxx-build",
|
||||||
|
"diffy",
|
||||||
"futures",
|
"futures",
|
||||||
"monero",
|
"monero",
|
||||||
|
"quickcheck",
|
||||||
|
"quickcheck_macros",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"testcontainers",
|
"testcontainers",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -6280,6 +6302,15 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
|
@ -6355,23 +6386,24 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum"
|
name = "num_enum"
|
||||||
version = "0.7.3"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
|
checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num_enum_derive",
|
"num_enum_derive",
|
||||||
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum_derive"
|
name = "num_enum_derive"
|
||||||
version = "0.7.3"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 3.3.0",
|
"proc-macro-crate 3.3.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6698,7 +6730,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6978,7 +7010,7 @@ dependencies = [
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7105,7 +7137,7 @@ dependencies = [
|
||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7152,7 +7184,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7437,7 +7469,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7551,6 +7583,28 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quickcheck"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
|
||||||
|
dependencies = [
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quickcheck_macros"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.8"
|
version = "0.11.8"
|
||||||
|
|
@ -7851,7 +7905,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8137,7 +8191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6268b74858287e1a062271b988a0c534bf85bbeb567fe09331bf40ed78113d5"
|
checksum = "f6268b74858287e1a062271b988a0c534bf85bbeb567fe09331bf40ed78113d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8429,7 +8483,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8733,7 +8787,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8744,7 +8798,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8786,7 +8840,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8860,7 +8914,7 @@ dependencies = [
|
||||||
"darling 0.20.11",
|
"darling 0.20.11",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8885,7 +8939,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9325,7 +9379,7 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-macros-core",
|
"sqlx-macros-core",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9348,7 +9402,7 @@ dependencies = [
|
||||||
"sqlx-mysql",
|
"sqlx-mysql",
|
||||||
"sqlx-postgres",
|
"sqlx-postgres",
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
@ -9618,7 +9672,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9631,7 +9685,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9760,9 +9814,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.103"
|
version = "2.0.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
|
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -9798,7 +9852,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9902,7 +9956,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -10019,7 +10073,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"time 0.3.41",
|
"time 0.3.41",
|
||||||
|
|
@ -10037,7 +10091,7 @@ dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"tauri-codegen",
|
"tauri-codegen",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
]
|
]
|
||||||
|
|
@ -10242,7 +10296,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
"zip 4.1.0",
|
"zip 4.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -10435,7 +10489,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -10446,7 +10500,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -10574,7 +10628,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -11844,7 +11898,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -11885,7 +11939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers",
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term 0.46.0",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -11918,7 +11972,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568"
|
checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12021,7 +12075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f"
|
checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12221,7 +12275,7 @@ dependencies = [
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zip 4.1.0",
|
"zip 4.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12353,7 +12407,7 @@ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12474,7 +12528,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -12509,7 +12563,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
@ -12760,7 +12814,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12938,7 +12992,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12949,7 +13003,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12960,7 +13014,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12971,7 +13025,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -12992,9 +13046,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
version = "0.5.2"
|
version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
|
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
"windows-result 0.3.4",
|
"windows-result 0.3.4",
|
||||||
|
|
@ -13643,7 +13697,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"synstructure 0.13.2",
|
"synstructure 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -13690,7 +13744,7 @@ dependencies = [
|
||||||
"proc-macro-crate 3.3.0",
|
"proc-macro-crate 3.3.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"zbus_names",
|
"zbus_names",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
|
|
@ -13725,7 +13779,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -13745,7 +13799,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"synstructure 0.13.2",
|
"synstructure 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -13766,7 +13820,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -13799,7 +13853,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -13818,9 +13872,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "4.1.0"
|
version = "4.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0"
|
checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"arbitrary",
|
"arbitrary",
|
||||||
|
|
@ -13912,7 +13966,7 @@ dependencies = [
|
||||||
"proc-macro-crate 3.3.0",
|
"proc-macro-crate 3.3.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -13926,6 +13980,6 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
"syn 2.0.103",
|
"syn 2.0.104",
|
||||||
"winnow 0.7.11",
|
"winnow 0.7.11",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -280,20 +280,12 @@ impl<'c> Monero {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Funds a specific wallet address with XMR
|
pub async fn generate_block(&self) -> Result<()> {
|
||||||
///
|
let miner_wallet = self.wallet("miner")?;
|
||||||
/// This function is useful when you want to fund an address that isn't managed by
|
let miner_address = miner_wallet.address().await?.to_string();
|
||||||
/// a wallet in the testcontainer setup, like an external wallet address.
|
self.monerod()
|
||||||
pub async fn fund_address(&self, address: &str, amount: u64) -> Result<()> {
|
.generate_blocks(15, miner_address.clone())
|
||||||
let monerod = &self.monerod;
|
.await?;
|
||||||
|
|
||||||
// Make sure miner has funds by generating blocks
|
|
||||||
monerod.generate_blocks(120, address.to_string()).await?;
|
|
||||||
|
|
||||||
// Mine more blocks to confirm the transaction
|
|
||||||
monerod.generate_blocks(10, address.to_string()).await?;
|
|
||||||
|
|
||||||
tracing::info!("Successfully funded address with {} piconero", amount);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,6 +476,11 @@ impl MoneroWallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn transfer(&self, address: &Address, amount_pico: u64) -> Result<TxReceipt> {
|
pub async fn transfer(&self, address: &Address, amount_pico: u64) -> Result<TxReceipt> {
|
||||||
|
tracing::info!(
|
||||||
|
"`{}` transferring {}",
|
||||||
|
self.name,
|
||||||
|
Amount::from_pico(amount_pico),
|
||||||
|
);
|
||||||
let amount = Amount::from_pico(amount_pico);
|
let amount = Amount::from_pico(amount_pico);
|
||||||
self.wallet
|
self.wallet
|
||||||
.transfer(address, amount)
|
.transfer(address, amount)
|
||||||
|
|
@ -491,6 +488,31 @@ impl MoneroWallet {
|
||||||
.context("Failed to perform transfer")
|
.context("Failed to perform transfer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn sweep(&self, address: &Address) -> Result<TxReceipt> {
|
||||||
|
tracing::info!("`{}` sweeping", self.name);
|
||||||
|
|
||||||
|
self.wallet
|
||||||
|
.sweep(address)
|
||||||
|
.await
|
||||||
|
.context("Failed to perform sweep")?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.context("No transaction receipts returned from sweep")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sweep_multi(&self, addresses: &[Address], ratios: &[f64]) -> Result<TxReceipt> {
|
||||||
|
tracing::info!("`{}` sweeping multi ({:?})", self.name, ratios);
|
||||||
|
self.balance().await?;
|
||||||
|
|
||||||
|
self.wallet
|
||||||
|
.sweep_multi(addresses, ratios)
|
||||||
|
.await
|
||||||
|
.context("Failed to perform sweep")?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.context("No transaction receipts returned from sweep")
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn blockchain_height(&self) -> Result<u64> {
|
pub async fn blockchain_height(&self) -> Result<u64> {
|
||||||
self.wallet.blockchain_height().await
|
self.wallet.blockchain_height().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use monero::Amount;
|
||||||
use monero_harness::{Monero, MoneroWalletRpc};
|
use monero_harness::{Monero, MoneroWalletRpc};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
|
|
@ -14,18 +15,27 @@ async fn fund_transfer_and_check_tx_key() {
|
||||||
|
|
||||||
let fund_alice: u64 = 1_000_000_000_000;
|
let fund_alice: u64 = 1_000_000_000_000;
|
||||||
let fund_bob = 0;
|
let fund_bob = 0;
|
||||||
|
let fund_candice = 0;
|
||||||
let send_to_bob = 5_000_000_000;
|
let send_to_bob = 5_000_000_000;
|
||||||
|
|
||||||
let tc = Cli::default();
|
let tc = Cli::default();
|
||||||
let (monero, _monerod_container, _wallet_containers) =
|
let (monero, _monerod_container, _wallet_containers) =
|
||||||
Monero::new(&tc, vec!["alice", "bob"]).await.unwrap();
|
Monero::new(&tc, vec!["alice", "bob", "candice"])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let alice_wallet = monero.wallet("alice").unwrap();
|
let alice_wallet = monero.wallet("alice").unwrap();
|
||||||
let bob_wallet = monero.wallet("bob").unwrap();
|
let bob_wallet = monero.wallet("bob").unwrap();
|
||||||
|
let candice_wallet = monero.wallet("candice").unwrap();
|
||||||
|
|
||||||
monero.init_miner().await.unwrap();
|
monero.init_miner().await.unwrap();
|
||||||
monero.init_wallet("alice", vec![fund_alice]).await.unwrap();
|
monero.init_wallet("alice", vec![fund_alice]).await.unwrap();
|
||||||
monero.init_wallet("bob", vec![fund_bob]).await.unwrap();
|
monero.init_wallet("bob", vec![fund_bob]).await.unwrap();
|
||||||
|
monero
|
||||||
|
.init_wallet("candice", vec![fund_candice])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
monero.start_miner().await.unwrap();
|
monero.start_miner().await.unwrap();
|
||||||
|
let miner_address = monero.wallet("miner").unwrap().address().await.unwrap();
|
||||||
|
|
||||||
tracing::info!("Waiting for Alice to catch up");
|
tracing::info!("Waiting for Alice to catch up");
|
||||||
|
|
||||||
|
|
@ -44,12 +54,62 @@ async fn fund_transfer_and_check_tx_key() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
monero.generate_block().await.unwrap();
|
||||||
|
|
||||||
tracing::info!("Waiting for Bob to catch up");
|
tracing::info!("Waiting for Bob to catch up");
|
||||||
|
|
||||||
wait_for_wallet_to_catch_up(bob_wallet, send_to_bob).await;
|
wait_for_wallet_to_catch_up(bob_wallet, send_to_bob).await;
|
||||||
|
|
||||||
|
tracing::info!("Bob caught up");
|
||||||
|
|
||||||
let got_bob_balance = bob_wallet.balance().await.unwrap();
|
let got_bob_balance = bob_wallet.balance().await.unwrap();
|
||||||
assert_eq!(send_to_bob, got_bob_balance, "Funds not transferred to Bob");
|
assert_eq!(send_to_bob, got_bob_balance, "Funds not transferred to Bob");
|
||||||
|
|
||||||
|
bob_wallet
|
||||||
|
.sweep(&alice_wallet.address().await.unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
monero.generate_block().await.unwrap();
|
||||||
|
|
||||||
|
wait_for_wallet_to_catch_up(bob_wallet, 0).await;
|
||||||
|
|
||||||
|
assert_eq!(0, bob_wallet.balance().await.unwrap(), "Bob not swept");
|
||||||
|
|
||||||
|
alice_wallet
|
||||||
|
.sweep_multi(
|
||||||
|
&[
|
||||||
|
bob_wallet.address().await.unwrap(),
|
||||||
|
candice_wallet.address().await.unwrap(),
|
||||||
|
],
|
||||||
|
&[99.9, 0.1],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
monero.generate_block().await.unwrap();
|
||||||
|
|
||||||
|
wait_for_wallet_to_catch_up(alice_wallet, 0).await;
|
||||||
|
|
||||||
|
assert_eq!(0, alice_wallet.balance().await.unwrap(), "Alice not swept");
|
||||||
|
|
||||||
|
bob_wallet.refresh().await.unwrap();
|
||||||
|
candice_wallet.refresh().await.unwrap();
|
||||||
|
|
||||||
|
let bob_balance = bob_wallet.balance().await.unwrap();
|
||||||
|
let candice_balance = candice_wallet.balance().await.unwrap();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
bob_balance = bob_balance,
|
||||||
|
candice_balance = candice_balance,
|
||||||
|
"Bob and Candice balances"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(0 < bob_balance, "Bob not funded");
|
||||||
|
assert!(0 < candice_balance, "Candice not funded");
|
||||||
|
|
||||||
|
assert!(0 < bob_balance, "Bob not funded");
|
||||||
|
assert!(0 < candice_balance, "Candice not funded");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_wallet_to_catch_up(wallet: &MoneroWalletRpc, expected_balance: u64) {
|
async fn wait_for_wallet_to_catch_up(wallet: &MoneroWalletRpc, expected_balance: u64) {
|
||||||
|
|
@ -65,4 +125,10 @@ async fn wait_for_wallet_to_catch_up(wallet: &MoneroWalletRpc, expected_balance:
|
||||||
wallet.refresh().await.unwrap();
|
wallet.refresh().await.unwrap();
|
||||||
sleep(Duration::from_secs(2)).await;
|
sleep(Duration::from_secs(2)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::warn!(
|
||||||
|
"Wallet {} not caught up to expected balance of {}",
|
||||||
|
wallet.name(),
|
||||||
|
Amount::from_pico(expected_balance)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
backoff = "0.4.0"
|
backoff = { version = "0.4.0", features = ["futures", "tokio"] }
|
||||||
cxx = "1.0.137"
|
cxx = "1.0.137"
|
||||||
monero = { version = "0.12", features = ["serde_support"] }
|
monero = { version = "0.12", features = ["serde_support"] }
|
||||||
tokio = { version = "1.44.2", features = ["sync", "time", "rt"] }
|
tokio = { version = "1.44.2", features = ["sync", "time", "rt"] }
|
||||||
|
|
@ -14,10 +14,13 @@ tracing = "0.1.41"
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cmake = "0.1.54"
|
cmake = "0.1.54"
|
||||||
cxx-build = "1.0.137"
|
cxx-build = "1.0.137"
|
||||||
|
diffy = "0.4.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
|
quickcheck = "1.0"
|
||||||
|
quickcheck_macros = "1.0"
|
||||||
tempfile = "3.19.1"
|
tempfile = "3.19.1"
|
||||||
testcontainers = "0.15"
|
testcontainers = "0.15"
|
||||||
tokio = { version = "1.44.2", features = ["full"] }
|
tokio = { version = "1.44.2", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,52 @@
|
||||||
use cmake::Config;
|
use cmake::Config;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Represents a patch to be applied to the Monero codebase
|
||||||
|
struct EmbeddedPatch {
|
||||||
|
name: &'static str,
|
||||||
|
description: &'static str,
|
||||||
|
patch_unified: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to create embedded patches with compile-time file inclusion
|
||||||
|
macro_rules! embedded_patch {
|
||||||
|
($name:literal, $description:literal, $patch_file:literal) => {
|
||||||
|
EmbeddedPatch {
|
||||||
|
name: $name,
|
||||||
|
description: $description,
|
||||||
|
patch_unified: include_str!($patch_file),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Embedded patches applied at compile time
|
||||||
|
const EMBEDDED_PATCHES: &[EmbeddedPatch] = &[embedded_patch!(
|
||||||
|
"wallet2_api_allow_subtract_from_fee",
|
||||||
|
"Adds subtract_fee_from_outputs parameter to wallet2_api transaction creation methods",
|
||||||
|
"patches/wallet2_api_allow_subtract_from_fee.patch"
|
||||||
|
)];
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let is_github_actions: bool = std::env::var("GITHUB_ACTIONS").is_ok();
|
let is_github_actions: bool = std::env::var("GITHUB_ACTIONS").is_ok();
|
||||||
let is_docker_build: bool = std::env::var("DOCKER_BUILD").is_ok();
|
let is_docker_build: bool = std::env::var("DOCKER_BUILD").is_ok();
|
||||||
|
|
||||||
// Only rerun this when the bridge.rs or static_bridge.h file changes.
|
// Eerun this when the bridge.rs or static_bridge.h file changes.
|
||||||
println!("cargo:rerun-if-changed=src/bridge.rs");
|
println!("cargo:rerun-if-changed=src/bridge.rs");
|
||||||
println!("cargo:rerun-if-changed=src/bridge.h");
|
println!("cargo:rerun-if-changed=src/bridge.h");
|
||||||
|
|
||||||
|
// Rerun if this build script changes (since it contains embedded patches)
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
|
||||||
|
// Rerun if the patches directory or any patch files change
|
||||||
|
println!("cargo:rerun-if-changed=patches");
|
||||||
|
|
||||||
|
// Apply embedded patches before building
|
||||||
|
apply_embedded_patches().expect("Failed to apply embedded patches");
|
||||||
|
|
||||||
// Build with the monero library all dependencies required
|
// Build with the monero library all dependencies required
|
||||||
let mut config = Config::new("monero");
|
let mut config = Config::new("monero");
|
||||||
|
|
||||||
let output_directory = config
|
let output_directory = config
|
||||||
.build_target("wallet_api")
|
.build_target("wallet_api")
|
||||||
// Builds currently fail in Release mode
|
// Builds currently fail in Release mode
|
||||||
|
|
@ -38,6 +75,7 @@ fn main() {
|
||||||
.define("GTEST_HAS_ABSL", "OFF")
|
.define("GTEST_HAS_ABSL", "OFF")
|
||||||
// Use lightweight crypto library
|
// Use lightweight crypto library
|
||||||
.define("MONERO_WALLET_CRYPTO_LIBRARY", "cn")
|
.define("MONERO_WALLET_CRYPTO_LIBRARY", "cn")
|
||||||
|
.build_arg("-Wno-dev") // Disable warnings we can't fix anyway
|
||||||
.build_arg(match (is_github_actions, is_docker_build) {
|
.build_arg(match (is_github_actions, is_docker_build) {
|
||||||
(true, _) => "-j1",
|
(true, _) => "-j1",
|
||||||
(_, true) => "-j1",
|
(_, true) => "-j1",
|
||||||
|
|
@ -266,3 +304,130 @@ fn main() {
|
||||||
|
|
||||||
build.compile("monero-sys");
|
build.compile("monero-sys");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split a multi-file patch into individual file patches
|
||||||
|
fn split_patch_by_files(
|
||||||
|
patch_content: &str,
|
||||||
|
) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
|
||||||
|
let mut file_patches = Vec::new();
|
||||||
|
let lines: Vec<&str> = patch_content.lines().collect();
|
||||||
|
|
||||||
|
let mut current_file_patch = String::new();
|
||||||
|
let mut current_file_path: Option<String> = None;
|
||||||
|
let mut in_file_section = false;
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
if line.starts_with("diff --git ") {
|
||||||
|
// Save previous file patch if we have one
|
||||||
|
if let Some(file_path) = current_file_path.take() {
|
||||||
|
if !current_file_patch.trim().is_empty() {
|
||||||
|
file_patches.push((file_path, current_file_patch.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new file patch
|
||||||
|
current_file_patch.clear();
|
||||||
|
current_file_patch.push_str(line);
|
||||||
|
current_file_patch.push('\n');
|
||||||
|
|
||||||
|
// Extract file path from diff line (e.g., "diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp")
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() >= 4 {
|
||||||
|
let file_path = parts[2].strip_prefix("a/").unwrap_or(parts[2]);
|
||||||
|
current_file_path = Some(file_path.to_string());
|
||||||
|
}
|
||||||
|
in_file_section = true;
|
||||||
|
} else if in_file_section {
|
||||||
|
current_file_patch.push_str(line);
|
||||||
|
current_file_patch.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't forget the last file
|
||||||
|
if let Some(file_path) = current_file_path {
|
||||||
|
if !current_file_patch.trim().is_empty() {
|
||||||
|
file_patches.push((file_path, current_file_patch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file_patches)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_embedded_patches() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let monero_dir = Path::new("monero");
|
||||||
|
|
||||||
|
if !monero_dir.exists() {
|
||||||
|
return Err("Monero directory not found. Please ensure the monero submodule is initialized and present.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
for embedded in EMBEDDED_PATCHES {
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Processing embedded patch: {} ({})",
|
||||||
|
embedded.name, embedded.description
|
||||||
|
);
|
||||||
|
|
||||||
|
// Split the patch into individual file patches
|
||||||
|
let file_patches = split_patch_by_files(embedded.patch_unified)
|
||||||
|
.map_err(|e| format!("Failed to split patch {}: {}", embedded.name, e))?;
|
||||||
|
|
||||||
|
if file_patches.is_empty() {
|
||||||
|
return Err(format!("No file patches found in patch {}", embedded.name).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Found {} file(s) in patch {}",
|
||||||
|
file_patches.len(),
|
||||||
|
embedded.name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply each file patch individually
|
||||||
|
for (file_path, patch_content) in file_patches {
|
||||||
|
println!("cargo:warning=Applying patch to file: {}", file_path);
|
||||||
|
|
||||||
|
// Parse the individual file patch
|
||||||
|
let patch = diffy::Patch::from_str(&patch_content)
|
||||||
|
.map_err(|e| format!("Failed to parse patch for {}: {}", file_path, e))?;
|
||||||
|
|
||||||
|
let target_path = monero_dir.join(&file_path);
|
||||||
|
|
||||||
|
if !target_path.exists() {
|
||||||
|
return Err(format!("Target file {} not found!", file_path).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = fs::read_to_string(&target_path)
|
||||||
|
.map_err(|e| format!("Failed to read {}: {}", file_path, e))?;
|
||||||
|
|
||||||
|
let patched = match diffy::apply(¤t, &patch) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
// Try reversing the patch – if that succeeds the file already contains the changes
|
||||||
|
if diffy::apply(¤t, &patch.reverse()).is_ok() {
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Patch for {} already applied – skipping",
|
||||||
|
file_path
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to apply patch to {}: hunk mismatch (not already applied)",
|
||||||
|
file_path
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::write(&target_path, patched)
|
||||||
|
.map_err(|e| format!("Failed to write {}: {}", file_path, e))?;
|
||||||
|
|
||||||
|
println!("cargo:warning=Successfully applied patch to: {}", file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Successfully applied all file patches for: {} ({})",
|
||||||
|
embedded.name, embedded.description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
63
monero-sys/patches/wallet2_api_allow_subtract_from_fee.patch
Normal file
63
monero-sys/patches/wallet2_api_allow_subtract_from_fee.patch
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Applies the new functionality from: https://github.com/monero-project/monero/pull/8861
|
||||||
|
# to the wallet2_api
|
||||||
|
# The pull request only added it for wallet2 (which is different from wallet2_api)
|
||||||
|
|
||||||
|
diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
|
||||||
|
index 96393eaaa..f2f8e7fbb 100644
|
||||||
|
--- a/src/wallet/api/wallet.cpp
|
||||||
|
+++ b/src/wallet/api/wallet.cpp
|
||||||
|
@@ -1777,7 +1777,7 @@ PendingTransaction* WalletImpl::restoreMultisigTransaction(const string& signDat
|
||||||
|
// - unconfirmed_transfer_details;
|
||||||
|
// - confirmed_transfer_details)
|
||||||
|
|
||||||
|
-PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<string> &dst_addr, const string &payment_id, optional<std::vector<uint64_t>> amount, uint32_t mixin_count, PendingTransaction::Priority priority, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices)
|
||||||
|
+PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<string> &dst_addr, const string &payment_id, optional<std::vector<uint64_t>> amount, uint32_t mixin_count, PendingTransaction::Priority priority, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, std::set<uint32_t> subtract_fee_from_outputs)
|
||||||
|
|
||||||
|
{
|
||||||
|
clearStatus();
|
||||||
|
@@ -1862,7 +1862,7 @@ PendingTransaction *WalletImpl::createTransactionMultDest(const std::vector<stri
|
||||||
|
if (amount) {
|
||||||
|
transaction->m_pending_tx = m_wallet->create_transactions_2(dsts, fake_outs_count,
|
||||||
|
adjusted_priority,
|
||||||
|
- extra, subaddr_account, subaddr_indices);
|
||||||
|
+ extra, subaddr_account, subaddr_indices, subtract_fee_from_outputs);
|
||||||
|
} else {
|
||||||
|
transaction->m_pending_tx = m_wallet->create_transactions_all(0, info.address, info.is_subaddress, 1, fake_outs_count,
|
||||||
|
adjusted_priority,
|
||||||
|
@@ -1949,7 +1949,7 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const
|
||||||
|
PendingTransaction::Priority priority, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices)
|
||||||
|
|
||||||
|
{
|
||||||
|
- return createTransactionMultDest(std::vector<string> {dst_addr}, payment_id, amount ? (std::vector<uint64_t> {*amount}) : (optional<std::vector<uint64_t>>()), mixin_count, priority, subaddr_account, subaddr_indices);
|
||||||
|
+ return createTransactionMultDest(std::vector<string> {dst_addr}, payment_id, amount ? (std::vector<uint64_t> {*amount}) : (optional<std::vector<uint64_t>>()), mixin_count, priority, subaddr_account, subaddr_indices, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingTransaction *WalletImpl::createSweepUnmixableTransaction()
|
||||||
|
diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
|
||||||
|
index b7f77a186..f81f13ac2 100644
|
||||||
|
--- a/src/wallet/api/wallet.h
|
||||||
|
+++ b/src/wallet/api/wallet.h
|
||||||
|
@@ -157,7 +157,8 @@ public:
|
||||||
|
optional<std::vector<uint64_t>> amount, uint32_t mixin_count,
|
||||||
|
PendingTransaction::Priority priority = PendingTransaction::Priority_Low,
|
||||||
|
uint32_t subaddr_account = 0,
|
||||||
|
- std::set<uint32_t> subaddr_indices = {}) override;
|
||||||
|
+ std::set<uint32_t> subaddr_indices = {},
|
||||||
|
+ std::set<uint32_t> subtract_fee_from_outputs = {}) override;
|
||||||
|
PendingTransaction * createTransaction(const std::string &dst_addr, const std::string &payment_id,
|
||||||
|
optional<uint64_t> amount, uint32_t mixin_count,
|
||||||
|
PendingTransaction::Priority priority = PendingTransaction::Priority_Low,
|
||||||
|
diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
|
||||||
|
index ca807ac87..f5f0a8f39 100644
|
||||||
|
--- a/src/wallet/api/wallet2_api.h
|
||||||
|
+++ b/src/wallet/api/wallet2_api.h
|
||||||
|
@@ -936,7 +936,8 @@ struct Wallet
|
||||||
|
optional<std::vector<uint64_t>> amount, uint32_t mixin_count,
|
||||||
|
PendingTransaction::Priority = PendingTransaction::Priority_Low,
|
||||||
|
uint32_t subaddr_account = 0,
|
||||||
|
- std::set<uint32_t> subaddr_indices = {}) = 0;
|
||||||
|
+ std::set<uint32_t> subaddr_indices = {},
|
||||||
|
+ std::set<uint32_t> subtract_fee_from_outputs = {}) = 0;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief createTransaction creates transaction. if dst_addr is an integrated address, payment_id is ignored
|
||||||
|
|
@ -122,6 +122,9 @@ namespace Monero
|
||||||
return wallet.createTransaction(dest_address, "", Monero::optional<uint64_t>(amount), 0, PendingTransaction::Priority_Default);
|
return wallet.createTransaction(dest_address, "", Monero::optional<uint64_t>(amount), 0, PendingTransaction::Priority_Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a transaction that spends all the unlocked balance to a single destination.
|
||||||
|
*/
|
||||||
inline PendingTransaction *createSweepTransaction(
|
inline PendingTransaction *createSweepTransaction(
|
||||||
Wallet &wallet,
|
Wallet &wallet,
|
||||||
const std::string &dest_address)
|
const std::string &dest_address)
|
||||||
|
|
@ -129,6 +132,52 @@ namespace Monero
|
||||||
return wallet.createTransaction(dest_address, "", Monero::optional<uint64_t>(), 0, PendingTransaction::Priority_Default);
|
return wallet.createTransaction(dest_address, "", Monero::optional<uint64_t>(), 0, PendingTransaction::Priority_Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a transaction that spends the unlocked balance to multiple destinations with given ratios.
|
||||||
|
* Ratiosn must sum to 1.
|
||||||
|
*/
|
||||||
|
inline PendingTransaction *createTransactionMultiDest(
|
||||||
|
Wallet &wallet,
|
||||||
|
const std::vector<std::string> &dest_addresses,
|
||||||
|
const std::vector<uint64_t> &amounts)
|
||||||
|
{
|
||||||
|
size_t n = dest_addresses.size();
|
||||||
|
|
||||||
|
// Check if we have any destinations at all
|
||||||
|
if (n == 0)
|
||||||
|
{
|
||||||
|
// wallet.setStatusError("Number of destinations must be greater than 0");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the number of destinations and sweep ratios match
|
||||||
|
if (amounts.size() != n)
|
||||||
|
{
|
||||||
|
// wallet.setStatusError("Number of destinations and sweep ratios must match");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the actual multi‐dest transaction
|
||||||
|
// No change left -> wallet drops it
|
||||||
|
// N outputs, fee should be the same as the one estimated above
|
||||||
|
|
||||||
|
// Find the highest output and choose it for subtract_fee_indices
|
||||||
|
std::set<uint32_t> subtract_fee_indices;
|
||||||
|
auto max_it = std::max_element(amounts.begin(), amounts.end());
|
||||||
|
size_t max_index = std::distance(amounts.begin(), max_it);
|
||||||
|
subtract_fee_indices.insert(static_cast<uint32_t>(max_index));
|
||||||
|
|
||||||
|
return wallet.createTransactionMultDest(
|
||||||
|
dest_addresses,
|
||||||
|
"", // No Payment ID
|
||||||
|
Monero::optional<std::vector<uint64_t>>(amounts),
|
||||||
|
0, // No mixin count
|
||||||
|
PendingTransaction::Priority_Default,
|
||||||
|
0, // subaddr_account
|
||||||
|
{}, // subaddr_indices
|
||||||
|
subtract_fee_indices); // Subtract fee from all outputs
|
||||||
|
}
|
||||||
|
|
||||||
inline bool setWalletDaemon(Wallet &wallet, const std::string &daemon_address)
|
inline bool setWalletDaemon(Wallet &wallet, const std::string &daemon_address)
|
||||||
{
|
{
|
||||||
return wallet.setDaemon(daemon_address);
|
return wallet.setDaemon(daemon_address);
|
||||||
|
|
@ -169,6 +218,13 @@ namespace Monero
|
||||||
{
|
{
|
||||||
return std::make_unique<std::string>(wallet.filename());
|
return std::make_unique<std::string>(wallet.filename());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void vector_string_push_back(
|
||||||
|
std::vector<std::string> &v,
|
||||||
|
const std::string &s)
|
||||||
|
{
|
||||||
|
v.push_back(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "easylogging++.h"
|
#include "easylogging++.h"
|
||||||
|
|
@ -226,7 +282,7 @@ namespace monero_rust_log
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the rust function to forward the log message.
|
// Call the rust function to forward the log message.
|
||||||
monero_rust_log::forward_cpp_log(
|
forward_cpp_log(
|
||||||
span_name.c_str(),
|
span_name.c_str(),
|
||||||
level,
|
level,
|
||||||
m->file().length() > 0 ? m->file() : "",
|
m->file().length() > 0 ? m->file() : "",
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,15 @@ pub mod ffi {
|
||||||
dest_address: &CxxString,
|
dest_address: &CxxString,
|
||||||
) -> Result<*mut PendingTransaction>;
|
) -> Result<*mut PendingTransaction>;
|
||||||
|
|
||||||
|
/// Create a multi-sweep transaction.
|
||||||
|
fn createTransactionMultiDest(
|
||||||
|
wallet: Pin<&mut Wallet>,
|
||||||
|
dest_addresses: &CxxVector<CxxString>,
|
||||||
|
amounts: &CxxVector<u64>,
|
||||||
|
) -> *mut PendingTransaction;
|
||||||
|
|
||||||
|
fn vector_string_push_back(v: Pin<&mut CxxVector<CxxString>>, s: &CxxString);
|
||||||
|
|
||||||
/// Get the status of a pending transaction.
|
/// Get the status of a pending transaction.
|
||||||
fn status(self: &PendingTransaction) -> Result<i32>;
|
fn status(self: &PendingTransaction) -> Result<i32>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use backoff::{future, retry_notify};
|
use backoff::{future::retry_notify, retry_notify as blocking_retry_notify};
|
||||||
use cxx::let_cxx_string;
|
use cxx::{let_cxx_string, CxxString, CxxVector, UniquePtr};
|
||||||
|
use monero::Amount;
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||||
oneshot,
|
oneshot,
|
||||||
|
|
@ -111,6 +112,7 @@ pub struct TxStatus {
|
||||||
pub struct TxReceipt {
|
pub struct TxReceipt {
|
||||||
pub txid: String,
|
pub txid: String,
|
||||||
pub tx_key: String,
|
pub tx_key: String,
|
||||||
|
/// The blockchain height at the time of publication.
|
||||||
pub height: u64,
|
pub height: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,9 +144,15 @@ impl WalletHandle {
|
||||||
|
|
||||||
let thread_name = format!("wallet-{}", wallet_name);
|
let thread_name = format!("wallet-{}", wallet_name);
|
||||||
|
|
||||||
|
// Capture current dispatcher before spawning
|
||||||
|
let current_dispatcher = tracing::dispatcher::get_default(|d| d.clone());
|
||||||
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name(thread_name)
|
.name(thread_name)
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
|
// Set the dispatcher for this thread
|
||||||
|
let _guard = tracing::dispatcher::set_default(¤t_dispatcher);
|
||||||
|
|
||||||
let mut manager = WalletManager::new(daemon.clone(), &wallet_name)
|
let mut manager = WalletManager::new(daemon.clone(), &wallet_name)
|
||||||
.expect("wallet manager to be created");
|
.expect("wallet manager to be created");
|
||||||
let wallet = manager
|
let wallet = manager
|
||||||
|
|
@ -188,11 +196,17 @@ impl WalletHandle {
|
||||||
|
|
||||||
let thread_name = format!("wallet-{}", wallet_name);
|
let thread_name = format!("wallet-{}", wallet_name);
|
||||||
|
|
||||||
|
// Capture current dispatcher before spawning
|
||||||
|
let current_dispatcher = tracing::dispatcher::get_default(|d| d.clone());
|
||||||
|
|
||||||
// Spawn the wallet thread – all interactions with the wallet must
|
// Spawn the wallet thread – all interactions with the wallet must
|
||||||
// happen on the same OS thread.
|
// happen on the same OS thread.
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name(thread_name)
|
.name(thread_name)
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
|
// Set the dispatcher for this thread
|
||||||
|
let _guard = tracing::dispatcher::set_default(¤t_dispatcher);
|
||||||
|
|
||||||
// Create the wallet manager in this thread first.
|
// Create the wallet manager in this thread first.
|
||||||
let mut manager = WalletManager::new(daemon.clone(), &wallet_name)
|
let mut manager = WalletManager::new(daemon.clone(), &wallet_name)
|
||||||
.expect("wallet manager to be created");
|
.expect("wallet manager to be created");
|
||||||
|
|
@ -266,9 +280,15 @@ impl WalletHandle {
|
||||||
|
|
||||||
let thread_name = format!("wallet-{}", wallet_name);
|
let thread_name = format!("wallet-{}", wallet_name);
|
||||||
|
|
||||||
|
// Capture current dispatcher before spawning
|
||||||
|
let current_dispatcher = tracing::dispatcher::get_default(|d| d.clone());
|
||||||
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name(thread_name)
|
.name(thread_name)
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
|
// Set the dispatcher for this thread
|
||||||
|
let _guard = tracing::dispatcher::set_default(¤t_dispatcher);
|
||||||
|
|
||||||
let wallet_name = path
|
let wallet_name = path
|
||||||
.split('/')
|
.split('/')
|
||||||
.last()
|
.last()
|
||||||
|
|
@ -379,7 +399,7 @@ impl WalletHandle {
|
||||||
) -> anyhow::Result<TxReceipt> {
|
) -> anyhow::Result<TxReceipt> {
|
||||||
let address = *address;
|
let address = *address;
|
||||||
|
|
||||||
future::retry_notify(backoff(None, None), || async {
|
retry_notify(backoff(None, None), || async {
|
||||||
self.call(move |wallet| wallet.transfer(&address, amount))
|
self.call(move |wallet| wallet.transfer(&address, amount))
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::transient)
|
.map_err(backoff::Error::transient)
|
||||||
|
|
@ -391,10 +411,10 @@ impl WalletHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sweep all funds to an address.
|
/// Sweep all funds to an address.
|
||||||
pub async fn sweep(&self, address: &monero::Address) -> anyhow::Result<Vec<String>> {
|
pub async fn sweep(&self, address: &monero::Address) -> anyhow::Result<Vec<TxReceipt>> {
|
||||||
let address = *address;
|
let address = *address;
|
||||||
|
|
||||||
future::retry_notify(backoff(None, None), || async {
|
retry_notify(backoff(None, None), || async {
|
||||||
self.call(move |wallet| wallet.sweep(&address))
|
self.call(move |wallet| wallet.sweep(&address))
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::transient)
|
.map_err(backoff::Error::transient)
|
||||||
|
|
@ -415,6 +435,21 @@ impl WalletHandle {
|
||||||
self.call(move |wallet| wallet.creation_height()).await
|
self.call(move |wallet| wallet.creation_height()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sweep all funds to a set of addresses.
|
||||||
|
pub async fn sweep_multi(
|
||||||
|
&self,
|
||||||
|
addresses: &[monero::Address],
|
||||||
|
percentages: &[f64],
|
||||||
|
) -> anyhow::Result<Vec<TxReceipt>> {
|
||||||
|
let addresses = addresses.to_vec();
|
||||||
|
let percentages = percentages.to_vec();
|
||||||
|
|
||||||
|
tracing::debug!(addresses=?addresses, percentages=?percentages, "Sweeping multi");
|
||||||
|
|
||||||
|
self.call(move |wallet| wallet.sweep_multi(&addresses, &percentages))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the unlocked balance of the wallet.
|
/// Get the unlocked balance of the wallet.
|
||||||
pub async fn unlocked_balance(&self) -> monero::Amount {
|
pub async fn unlocked_balance(&self) -> monero::Amount {
|
||||||
self.call(move |wallet| wallet.unlocked_balance()).await
|
self.call(move |wallet| wallet.unlocked_balance()).await
|
||||||
|
|
@ -999,7 +1034,7 @@ impl FfiWallet {
|
||||||
|
|
||||||
tracing::debug!(address=%wallet.main_address(), "Initializing wallet");
|
tracing::debug!(address=%wallet.main_address(), "Initializing wallet");
|
||||||
|
|
||||||
retry_notify(
|
blocking_retry_notify(
|
||||||
backoff(None, None),
|
backoff(None, None),
|
||||||
|| {
|
|| {
|
||||||
wallet
|
wallet
|
||||||
|
|
@ -1418,7 +1453,7 @@ impl FfiWallet {
|
||||||
|
|
||||||
/// Sweep all funds from the wallet to a specified address.
|
/// Sweep all funds from the wallet to a specified address.
|
||||||
/// Returns a list of transaction ids of the created transactions.
|
/// Returns a list of transaction ids of the created transactions.
|
||||||
fn sweep(&mut self, address: &monero::Address) -> anyhow::Result<Vec<String>> {
|
fn sweep(&mut self, address: &monero::Address) -> anyhow::Result<Vec<TxReceipt>> {
|
||||||
tracing::info!("Sweeping funds to {}, refreshing wallet first", address);
|
tracing::info!("Sweeping funds to {}, refreshing wallet first", address);
|
||||||
|
|
||||||
self.refresh_blocking()?;
|
self.refresh_blocking()?;
|
||||||
|
|
@ -1447,7 +1482,190 @@ impl FfiWallet {
|
||||||
// Dispose of the transaction to avoid leaking memory.
|
// Dispose of the transaction to avoid leaking memory.
|
||||||
self.dispose_transaction(pending_tx);
|
self.dispose_transaction(pending_tx);
|
||||||
|
|
||||||
result.map(|_| txids)
|
// Check for errors only after cleaning up the memory.
|
||||||
|
result.context("Failed to publish transaction")?;
|
||||||
|
|
||||||
|
// Get the receipts for the transactions.
|
||||||
|
let mut receipts = Vec::new();
|
||||||
|
|
||||||
|
for txid in txids {
|
||||||
|
let_cxx_string!(txid_cxx = &txid);
|
||||||
|
|
||||||
|
let tx_key = ffi::walletGetTxKey(&self.inner, &txid_cxx)
|
||||||
|
.context("Failed to get tx key from wallet: FFI call failed with exception")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let height = self.blockchain_height();
|
||||||
|
|
||||||
|
receipts.push(TxReceipt {
|
||||||
|
txid: txid.clone(),
|
||||||
|
tx_key,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(receipts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sweep all funds to a set of addresses with a set of ratios.
|
||||||
|
fn sweep_multi(
|
||||||
|
&mut self,
|
||||||
|
addresses: &[monero::Address],
|
||||||
|
ratios: &[f64],
|
||||||
|
) -> anyhow::Result<Vec<TxReceipt>> {
|
||||||
|
tracing::warn!("STARTED MULTI SWEEP");
|
||||||
|
|
||||||
|
if addresses.len() == 0 {
|
||||||
|
bail!("No addresses to sweep to");
|
||||||
|
}
|
||||||
|
|
||||||
|
if addresses.len() != ratios.len() {
|
||||||
|
bail!("Number of addresses and ratios must match");
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Sweeping funds to {} addresses, refreshing wallet first",
|
||||||
|
addresses.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
self.refresh_blocking()?;
|
||||||
|
|
||||||
|
let balance = self.unlocked_balance();
|
||||||
|
|
||||||
|
// Since we're using "subtract fee from outputs", we distribute the full balance
|
||||||
|
// The underlying transaction creation will subtract the fee proportionally from each output
|
||||||
|
let amounts = FfiWallet::distribute(balance, ratios)?;
|
||||||
|
|
||||||
|
tracing::debug!(%balance, num_outputs = addresses.len(), outputs=?amounts, "Distributing funds to outputs");
|
||||||
|
|
||||||
|
// Build a C++ vector of destination addresses
|
||||||
|
let mut cxx_addrs: UniquePtr<CxxVector<CxxString>> = CxxVector::<CxxString>::new();
|
||||||
|
for addr in addresses {
|
||||||
|
let_cxx_string!(s = addr.to_string());
|
||||||
|
ffi::vector_string_push_back(cxx_addrs.pin_mut(), &s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a C++ vector of amounts
|
||||||
|
let mut cxx_amounts: UniquePtr<CxxVector<u64>> = CxxVector::<u64>::new();
|
||||||
|
for &amount in &amounts {
|
||||||
|
cxx_amounts.pin_mut().push(amount.as_pico());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the multi-sweep pending transaction
|
||||||
|
let raw_tx = ffi::createTransactionMultiDest(
|
||||||
|
self.inner.pinned(),
|
||||||
|
cxx_addrs.as_ref().unwrap(),
|
||||||
|
cxx_amounts.as_ref().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if raw_tx.is_null() {
|
||||||
|
self.check_error()
|
||||||
|
.context("Failed to create multi-sweep transaction")?;
|
||||||
|
anyhow::bail!("Failed to create multi-sweep transaction");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pending_tx = PendingTransaction(raw_tx);
|
||||||
|
|
||||||
|
// Get the txids from the pending transaction before we publish,
|
||||||
|
// otherwise it might be null.
|
||||||
|
let txids: Vec<String> = ffi::pendingTransactionTxIds(&pending_tx)
|
||||||
|
.context("Failed to get txids of pending transaction: FFI call failed with exception")?
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Publish the transaction
|
||||||
|
let result = pending_tx
|
||||||
|
.publish()
|
||||||
|
.context("Failed to publish transaction");
|
||||||
|
|
||||||
|
// Dispose of the transaction to avoid leaking memory.
|
||||||
|
self.dispose_transaction(pending_tx);
|
||||||
|
|
||||||
|
// Check for errors only after cleaning up the memory.
|
||||||
|
result.context("Failed to publish transaction")?;
|
||||||
|
|
||||||
|
// Get the receipts for the transactions.
|
||||||
|
let mut receipts = Vec::new();
|
||||||
|
|
||||||
|
for txid in txids {
|
||||||
|
let_cxx_string!(txid_cxx = &txid);
|
||||||
|
|
||||||
|
let tx_key = ffi::walletGetTxKey(&self.inner, &txid_cxx)
|
||||||
|
.context("Failed to get tx key from wallet: FFI call failed with exception")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let height = self.blockchain_height();
|
||||||
|
|
||||||
|
receipts.push(TxReceipt {
|
||||||
|
txid: txid.clone(),
|
||||||
|
tx_key,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(receipts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distribute the funds in the wallet to a set of addresses with a set of percentages,
|
||||||
|
/// such that the complete balance is spent (takes fee into account).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `balance` - The total balance to distribute
|
||||||
|
/// * `percentages` - A slice of percentages that must sum to 100.0
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of Monero amounts proportional to the input percentages.
|
||||||
|
/// The last amount gets any remainder to ensure exact distribution.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if:
|
||||||
|
/// - Percentages don't sum to 100.0
|
||||||
|
/// - Balance is zero
|
||||||
|
/// - There are more outputs than piconeros in balance
|
||||||
|
fn distribute(balance: monero::Amount, percentages: &[f64]) -> Result<Vec<monero::Amount>> {
|
||||||
|
if percentages.is_empty() {
|
||||||
|
bail!("No ratios to distribute to");
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOLERANCE: f64 = 1e-6;
|
||||||
|
let sum: f64 = percentages.iter().sum();
|
||||||
|
if (sum - 100.0).abs() > TOLERANCE {
|
||||||
|
bail!("Percentages must sum to 100 (actual sum: {})", sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the case where distributable amount is zero
|
||||||
|
if balance.as_pico() == 0 {
|
||||||
|
bail!("Zero balance to distribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the distributable amount is enough to cover at least one piconero per output
|
||||||
|
if balance.as_pico() < percentages.len() as u64 {
|
||||||
|
bail!("More outputs than piconeros in balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut amounts = Vec::new();
|
||||||
|
let mut total = Amount::ZERO;
|
||||||
|
|
||||||
|
// Distribute amounts according to ratios, except for the last one
|
||||||
|
for &percentage in &percentages[..percentages.len() - 1] {
|
||||||
|
let amount_pico = ((balance.as_pico() as f64) * percentage / 100.0).floor() as u64;
|
||||||
|
let amount = Amount::from_pico(amount_pico);
|
||||||
|
amounts.push(amount);
|
||||||
|
total += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the remainder to the last recipient to ensure exact distribution
|
||||||
|
let remainder = balance.checked_sub(total).context(format!(
|
||||||
|
"Underflow when calculating rest (unexpected) - balance {}, distributed: {}",
|
||||||
|
balance, total,
|
||||||
|
))?;
|
||||||
|
amounts.push(remainder);
|
||||||
|
|
||||||
|
Ok(amounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispose (deallocate) a pending transaction object.
|
/// Dispose (deallocate) a pending transaction object.
|
||||||
|
|
@ -1685,3 +1903,176 @@ fn backoff(
|
||||||
.with_max_interval(max_interval)
|
.with_max_interval(max_interval)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use quickcheck::TestResult;
|
||||||
|
use quickcheck_macros::quickcheck;
|
||||||
|
|
||||||
|
#[quickcheck]
|
||||||
|
fn prop_distribute_sum_equals_balance(balance_pico: u64, percentages: Vec<f64>) -> TestResult {
|
||||||
|
// Filter out invalid inputs
|
||||||
|
if percentages.is_empty() || balance_pico == 0 {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure percentages are valid (non-negative and sum to approximately 100.0)
|
||||||
|
if percentages.iter().any(|&p| p < 0.0 || p > 100.0) {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
let percentage_sum: f64 = percentages.iter().sum();
|
||||||
|
if (percentage_sum - 100.0).abs() > 1e-6 {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance = monero::Amount::from_pico(balance_pico);
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages);
|
||||||
|
|
||||||
|
// Property: sum of distributed amounts should equal balance
|
||||||
|
let total_distributed: u64 = amounts.unwrap().iter().map(|a| a.as_pico()).sum();
|
||||||
|
let expected = balance.as_pico();
|
||||||
|
|
||||||
|
TestResult::from_bool(total_distributed == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[quickcheck]
|
||||||
|
fn prop_distribute_count_matches_percentages(
|
||||||
|
balance_pico: u64,
|
||||||
|
percentages: Vec<f64>,
|
||||||
|
) -> TestResult {
|
||||||
|
if percentages.is_empty() || balance_pico == 0 {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
if percentages.iter().any(|&p| p < 0.0 || p > 100.0) {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
let percentage_sum: f64 = percentages.iter().sum();
|
||||||
|
if (percentage_sum - 100.0).abs() > 1e-6 {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance = monero::Amount::from_pico(balance_pico);
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages).unwrap();
|
||||||
|
|
||||||
|
// Property: number of amounts should equal number of percentages
|
||||||
|
TestResult::from_bool(amounts.len() == percentages.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[quickcheck]
|
||||||
|
fn prop_distribute_respects_percentages(
|
||||||
|
balance_pico: u64,
|
||||||
|
percentages: Vec<f64>,
|
||||||
|
) -> TestResult {
|
||||||
|
if percentages.len() < 2 || balance_pico == 0 {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
if percentages.iter().any(|&p| p < 0.0 || p > 100.0) {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
let percentage_sum: f64 = percentages.iter().sum();
|
||||||
|
if (percentage_sum - 100.0).abs() > 1e-6 {
|
||||||
|
return TestResult::discard();
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance = monero::Amount::from_pico(balance_pico);
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages).unwrap();
|
||||||
|
|
||||||
|
// Property: percentages should be approximately respected (except for rounding)
|
||||||
|
// We check all but the last amount since the last one gets the remainder
|
||||||
|
let mut percentages_respected = true;
|
||||||
|
for i in 0..percentages.len() - 1 {
|
||||||
|
let expected_amount =
|
||||||
|
((balance.as_pico() as f64) * percentages[i] / 100.0).floor() as u64;
|
||||||
|
if amounts[i].as_pico() != expected_amount {
|
||||||
|
percentages_respected = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestResult::from_bool(percentages_respected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distribute_empty_percentages() {
|
||||||
|
let balance = monero::Amount::from_pico(1000);
|
||||||
|
let percentages: Vec<f64> = vec![];
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages);
|
||||||
|
assert!(amounts.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distribute_zero_balance() {
|
||||||
|
let balance = monero::Amount::from_pico(0);
|
||||||
|
let percentages = vec![50.0, 50.0];
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages);
|
||||||
|
assert!(amounts.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distribute_insufficient_balance_for_outputs() {
|
||||||
|
let balance = monero::Amount::from_pico(2);
|
||||||
|
let percentages = vec![30.0, 30.0, 40.0]; // 3 outputs but only 2 piconeros
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages);
|
||||||
|
assert!(amounts.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distribute_simple_case() {
|
||||||
|
let balance = monero::Amount::from_pico(1000);
|
||||||
|
let percentages = vec![50.0, 30.0, 20.0];
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(amounts.len(), 3);
|
||||||
|
|
||||||
|
// Total should equal balance
|
||||||
|
let total: u64 = amounts.iter().map(|a| a.as_pico()).sum();
|
||||||
|
assert_eq!(total, 1000);
|
||||||
|
|
||||||
|
// First two amounts should respect percentages exactly
|
||||||
|
assert_eq!(amounts[0].as_pico(), 500); // 50% of 1000
|
||||||
|
assert_eq!(amounts[1].as_pico(), 300); // 30% of 1000
|
||||||
|
// Last amount gets remainder: 1000 - 500 - 300 = 200
|
||||||
|
assert_eq!(amounts[2].as_pico(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distribute_small_donation() {
|
||||||
|
let balance = monero::Amount::from_pico(1000);
|
||||||
|
let percentages = vec![99.9, 0.1];
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(amounts.len(), 2);
|
||||||
|
|
||||||
|
// Total should equal balance
|
||||||
|
let total: u64 = amounts.iter().map(|a| a.as_pico()).sum();
|
||||||
|
assert_eq!(total, 1000);
|
||||||
|
|
||||||
|
// First amount should respect percentage exactly
|
||||||
|
assert_eq!(amounts[0].as_pico(), 999); // 99.9% of 1000 (floored)
|
||||||
|
// Last amount gets remainder: 1000 - 999 = 1
|
||||||
|
assert_eq!(amounts[1].as_pico(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distribute_percentages_not_sum_to_100() {
|
||||||
|
let balance = monero::Amount::from_pico(1000);
|
||||||
|
let percentages = vec![50.0, 30.0]; // Only sums to 80%
|
||||||
|
|
||||||
|
let amounts = FfiWallet::distribute(balance, &percentages);
|
||||||
|
assert!(amounts.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
# You can configure the address of a locally running testnet asb. It'll displayed in the GUI. This is useful for testing
|
# You can configure the address of a locally running testnet asb. It'll displayed in the GUI. This is useful for testing
|
||||||
VITE_TESTNET_STUB_PROVIDER_ADDRESS=/onion3/dmdrgmy27szmps3p5zqh4ujd7twoi2a5ao7mouugfg6owyj4ikd2h5yd:9939/p2p/12D3KooWCa6vLE6SFhEBs3EhsC5tCBoHKBLoLEo1riDDmcExr5BW
|
# VITE_TESTNET_STUB_PROVIDER_ADDRESS=/onion3/dmdrgmy27szmps3p5zqh4ujd7twoi2a5ao7mouugfg6owyj4ikd2h5yd:9939/p2p/12D3KooWCa6vLE6SFhEBs3EhsC5tCBoHKBLoLEo1riDDmcExr5BW
|
||||||
|
|
@ -3,7 +3,7 @@ import { ReactNode } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id?: string;
|
id?: string;
|
||||||
title: ReactNode;
|
title: ReactNode | null;
|
||||||
mainContent: ReactNode;
|
mainContent: ReactNode;
|
||||||
additionalContent: ReactNode;
|
additionalContent: ReactNode;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
@ -30,7 +30,7 @@ export default function InfoBox({
|
||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle1">{title}</Typography>
|
{title ? <Typography variant="subtitle1">{title}</Typography> : null}
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
{icon}
|
{icon}
|
||||||
{mainContent}
|
{mainContent}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Link, Typography } from "@mui/material";
|
import { Box, Link, Typography } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import InfoBox from "./InfoBox";
|
import InfoBox from "./InfoBox";
|
||||||
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
|
|
||||||
export type TransactionInfoBoxProps = {
|
export type TransactionInfoBoxProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -24,12 +25,14 @@ export default function TransactionInfoBox({
|
||||||
title={title}
|
title={title}
|
||||||
mainContent={
|
mainContent={
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
{txId ?? "Transaction ID not available"}
|
<TruncatedText truncateMiddle limit={40}>
|
||||||
|
{txId ?? "Transaction ID not available"}
|
||||||
|
</TruncatedText>
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
additionalContent={
|
additionalContent={
|
||||||
<>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
<Typography variant="subtitle2">{additionalContent}</Typography>
|
<Typography variant="subtitle2">{additionalContent}</Typography>
|
||||||
{explorerUrlCreator != null &&
|
{explorerUrlCreator != null &&
|
||||||
txId != null && ( // Only show the link if the txId is not null and we have a creator for the explorer URL
|
txId != null && ( // Only show the link if the txId is not null and we have a creator for the explorer URL
|
||||||
|
|
@ -39,7 +42,7 @@ export default function TransactionInfoBox({
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</>
|
</Box>
|
||||||
}
|
}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, DialogContentText } from "@mui/material";
|
import { Box, DialogContentText, Typography } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
||||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
||||||
|
|
@ -11,8 +11,8 @@ export default function XmrRedeemInMempoolPage(
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
The swap was successful and the Monero has been sent to the address you
|
The swap was successful and the Monero has been sent to the following
|
||||||
specified. The swap is completed and you may exit the application now.
|
address(es). The swap is completed and you may exit the application now.
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -24,7 +24,55 @@ export default function XmrRedeemInMempoolPage(
|
||||||
<MoneroTransactionInfoBox
|
<MoneroTransactionInfoBox
|
||||||
title="Monero Redeem Transaction"
|
title="Monero Redeem Transaction"
|
||||||
txId={xmr_redeem_txid}
|
txId={xmr_redeem_txid}
|
||||||
additionalContent={`The funds have been sent to the address ${state.xmr_redeem_address}`}
|
additionalContent={
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
{state.xmr_receive_pool.map((pool, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 0.5,
|
||||||
|
padding: 1,
|
||||||
|
border: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor: (theme) => theme.palette.action.hover,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{pool.label} ({pool.percentage}%)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
color: (theme) => theme.palette.text.secondary,
|
||||||
|
wordBreak: "break-all",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pool.address}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
loading={false}
|
loading={false}
|
||||||
/>
|
/>
|
||||||
<FeedbackInfoBox />
|
<FeedbackInfoBox />
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,14 @@ import {
|
||||||
PendingLockBitcoinApprovalRequest,
|
PendingLockBitcoinApprovalRequest,
|
||||||
TauriSwapProgressEventContent,
|
TauriSwapProgressEventContent,
|
||||||
} from "models/tauriModelExt";
|
} from "models/tauriModelExt";
|
||||||
import {
|
import { SatsAmount, PiconeroAmount } from "renderer/components/other/Units";
|
||||||
SatsAmount,
|
|
||||||
PiconeroAmount,
|
|
||||||
MoneroBitcoinExchangeRateFromAmounts,
|
|
||||||
} from "renderer/components/other/Units";
|
|
||||||
import { Box, Typography, Divider } from "@mui/material";
|
import { Box, Typography, Divider } from "@mui/material";
|
||||||
import { useActiveSwapId, usePendingLockBitcoinApproval } from "store/hooks";
|
import { useActiveSwapId, usePendingLockBitcoinApproval } from "store/hooks";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
|
||||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
||||||
import CheckIcon from "@mui/icons-material/Check";
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
|
import ArrowRightAltIcon from "@mui/icons-material/ArrowRightAlt";
|
||||||
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
|
|
||||||
/// A hook that returns the LockBitcoin confirmation request for the active swap
|
/// A hook that returns the LockBitcoin confirmation request for the active swap
|
||||||
/// Returns null if no confirmation request is found
|
/// Returns null if no confirmation request is found
|
||||||
|
|
@ -63,107 +60,412 @@ export default function SwapSetupInflightPage({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { btc_network_fee, xmr_receive_amount } =
|
const { btc_network_fee, monero_receive_pool, xmr_receive_amount } =
|
||||||
request.content.details.content;
|
request.content.details.content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoBox
|
<>
|
||||||
title="Approve Swap"
|
{/* Grid layout for perfect alignment */}
|
||||||
icon={<></>}
|
<Box
|
||||||
loading={false}
|
sx={{
|
||||||
mainContent={
|
display: "grid",
|
||||||
<>
|
gridTemplateColumns: "max-content auto max-content",
|
||||||
<Divider />
|
gap: "1.5rem",
|
||||||
|
alignItems: "stretch",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Row 1: Bitcoin box */}
|
||||||
|
<Box>
|
||||||
|
<BitcoinMainBox btc_lock_amount={btc_lock_amount} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Row 1: Animated arrow */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatedArrow />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Row 1: Monero main box */}
|
||||||
|
<Box>
|
||||||
|
<MoneroMainBox
|
||||||
|
monero_receive_pool={monero_receive_pool}
|
||||||
|
xmr_receive_amount={xmr_receive_amount}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Row 2: Empty space */}
|
||||||
|
<Box />
|
||||||
|
|
||||||
|
{/* Row 2: Empty space */}
|
||||||
|
<Box />
|
||||||
|
|
||||||
|
{/* Row 2: Secondary content */}
|
||||||
|
<Box>
|
||||||
|
<MoneroSecondaryContent
|
||||||
|
monero_receive_pool={monero_receive_pool}
|
||||||
|
xmr_receive_amount={xmr_receive_amount}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
variant="text"
|
||||||
|
size="large"
|
||||||
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
|
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
||||||
|
displayErrorSnackbar
|
||||||
|
requiresContext
|
||||||
|
>
|
||||||
|
Deny
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
|
||||||
|
<PromiseInvokeButton
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onInvoke={() => resolveApproval(request.content.request_id, true)}
|
||||||
|
displayErrorSnackbar
|
||||||
|
requiresContext
|
||||||
|
endIcon={<CheckIcon />}
|
||||||
|
>
|
||||||
|
{`Confirm (${timeLeft}s)`}
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure presentational components -------------------------------------------------
|
||||||
|
* They live in the same file to avoid additional imports yet keep
|
||||||
|
* JSX for the main page tidy. All styling values are kept identical
|
||||||
|
* to their previous inline counterparts so that the visual appearance
|
||||||
|
* stays exactly the same while making the code easier to reason about.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface BitcoinSendSectionProps {
|
||||||
|
btc_lock_amount: number;
|
||||||
|
btc_network_fee: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BitcoinMainBox = ({ btc_lock_amount }: { btc_lock_amount: number }) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 1.5,
|
||||||
|
border: 1,
|
||||||
|
gap: "0.5rem 1rem",
|
||||||
|
borderColor: "warning.main",
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor: (theme) => theme.palette.warning.light + "10",
|
||||||
|
background: (theme) =>
|
||||||
|
`linear-gradient(135deg, ${theme.palette.warning.light}20, ${theme.palette.warning.light}05)`,
|
||||||
|
flex: "1 1 0",
|
||||||
|
height: "100%", // Match the height of the Monero box
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
You send
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.palette.warning.dark,
|
||||||
|
textShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SatsAmount amount={btc_lock_amount} />
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface PoolBreakdownProps {
|
||||||
|
monero_receive_pool: Array<{
|
||||||
|
address: string;
|
||||||
|
label: string;
|
||||||
|
percentage: number;
|
||||||
|
}>;
|
||||||
|
xmr_receive_amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PoolBreakdown = ({
|
||||||
|
monero_receive_pool,
|
||||||
|
xmr_receive_amount,
|
||||||
|
}: PoolBreakdownProps) => {
|
||||||
|
// Find the pool entry with the highest percentage to exclude it (since it's shown in main box)
|
||||||
|
const highestPercentagePool = monero_receive_pool.reduce((prev, current) =>
|
||||||
|
prev.percentage > current.percentage ? prev : current,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out the highest percentage pool since it's already displayed in the main box
|
||||||
|
const remainingPools = monero_receive_pool.filter(
|
||||||
|
(pool) => pool !== highestPercentagePool,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", flexDirection: "column", gap: 1, width: "100%" }}
|
||||||
|
>
|
||||||
|
{remainingPools.map((pool) => (
|
||||||
|
<Box
|
||||||
|
key={pool.address}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "stretch",
|
||||||
|
padding: pool.percentage >= 5 ? 1.5 : 1.2,
|
||||||
|
border: 1,
|
||||||
|
borderColor:
|
||||||
|
pool.percentage >= 5 ? "success.main" : "success.light",
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
pool.percentage >= 5
|
||||||
|
? theme.palette.success.light + "10"
|
||||||
|
: theme.palette.action.hover,
|
||||||
|
width: "100%", // Ensure full width
|
||||||
|
minWidth: 0,
|
||||||
|
opacity: pool.percentage >= 5 ? 1 : 0.75,
|
||||||
|
transform: pool.percentage >= 5 ? "scale(1)" : "scale(0.95)",
|
||||||
|
animation:
|
||||||
|
pool.percentage >= 5
|
||||||
|
? "poolPulse 2s ease-in-out infinite"
|
||||||
|
: "none",
|
||||||
|
"@keyframes poolPulse": {
|
||||||
|
"0%": {
|
||||||
|
transform: "scale(1)",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
transform: "scale(1.02)",
|
||||||
|
opacity: 0.95,
|
||||||
|
},
|
||||||
|
"100%": {
|
||||||
|
transform: "scale(1)",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "flex",
|
||||||
gridTemplateColumns: "auto 1fr",
|
flexDirection: "column",
|
||||||
rowGap: 1,
|
gap: 0.5,
|
||||||
columnGap: 2,
|
flex: "1 1 0",
|
||||||
alignItems: "center",
|
minWidth: 0,
|
||||||
marginBlock: 2,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
variant="body2"
|
||||||
>
|
|
||||||
You send
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
<SatsAmount amount={btc_lock_amount} />
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
|
||||||
>
|
|
||||||
Bitcoin network fees
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
<SatsAmount amount={btc_network_fee} />
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
|
||||||
>
|
|
||||||
You receive
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
fontWeight: "bold",
|
color: theme.palette.text.primary,
|
||||||
color: theme.palette.success.main,
|
fontSize: "0.75rem",
|
||||||
|
fontWeight: 600,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<PiconeroAmount amount={xmr_receive_amount} />
|
{pool.label === "user address" ? "Your Wallet" : pool.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: (theme) => theme.palette.text.secondary,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Exchange rate
|
<TruncatedText truncateMiddle limit={15}>
|
||||||
</Typography>
|
{pool.address}
|
||||||
<Typography>
|
</TruncatedText>
|
||||||
<MoneroBitcoinExchangeRateFromAmounts
|
|
||||||
satsAmount={btc_lock_amount}
|
|
||||||
piconeroAmount={xmr_receive_amount}
|
|
||||||
displayMarkup
|
|
||||||
/>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
<Box
|
||||||
}
|
sx={{
|
||||||
additionalContent={
|
display: "flex",
|
||||||
<Box
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: 0.5,
|
||||||
|
flex: "0 0 auto",
|
||||||
|
minWidth: 140,
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pool.percentage >= 5 && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<PiconeroAmount
|
||||||
|
amount={(pool.percentage * Number(xmr_receive_amount)) / 100}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{pool.percentage}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MoneroReceiveSectionProps {
|
||||||
|
monero_receive_pool: PoolBreakdownProps["monero_receive_pool"];
|
||||||
|
xmr_receive_amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MoneroMainBox = ({
|
||||||
|
monero_receive_pool,
|
||||||
|
xmr_receive_amount,
|
||||||
|
}: MoneroReceiveSectionProps) => {
|
||||||
|
// Find the pool entry with the highest percentage
|
||||||
|
const highestPercentagePool = monero_receive_pool.reduce((prev, current) =>
|
||||||
|
prev.percentage > current.percentage ? prev : current,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 1.5,
|
||||||
|
border: 1,
|
||||||
|
gap: "0.5rem 1rem",
|
||||||
|
borderColor: "success.main",
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor: (theme) => theme.palette.success.light + "10",
|
||||||
|
background: (theme) =>
|
||||||
|
`linear-gradient(135deg, ${theme.palette.success.light}20, ${theme.palette.success.light}05)`,
|
||||||
|
flex: "1 1 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.25 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{highestPercentagePool.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 2,
|
fontFamily: "monospace",
|
||||||
display: "flex",
|
fontSize: "0.65rem",
|
||||||
justifyContent: "flex-end",
|
color: (theme) => theme.palette.text.secondary,
|
||||||
gap: 2,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PromiseInvokeButton
|
<TruncatedText truncateMiddle limit={15}>
|
||||||
variant="text"
|
{highestPercentagePool.address}
|
||||||
size="large"
|
</TruncatedText>
|
||||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
</Typography>
|
||||||
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
</Box>
|
||||||
displayErrorSnackbar
|
<Box
|
||||||
requiresContext
|
sx={{
|
||||||
>
|
display: "flex",
|
||||||
Deny
|
flexDirection: "column",
|
||||||
</PromiseInvokeButton>
|
alignItems: "flex-end",
|
||||||
|
justifyContent: "center",
|
||||||
<PromiseInvokeButton
|
}}
|
||||||
variant="contained"
|
>
|
||||||
color="primary"
|
<Typography
|
||||||
size="large"
|
variant="h5"
|
||||||
onInvoke={() => resolveApproval(request.content.request_id, true)}
|
sx={(theme) => ({
|
||||||
displayErrorSnackbar
|
fontWeight: "bold",
|
||||||
requiresContext
|
color: theme.palette.success.dark,
|
||||||
endIcon={<CheckIcon />}
|
textShadow: "0 1px 2px rgba(0,0,0,0.1)",
|
||||||
>
|
})}
|
||||||
{`Confirm & lock BTC (${timeLeft}s)`}
|
>
|
||||||
</PromiseInvokeButton>
|
<PiconeroAmount
|
||||||
</Box>
|
amount={
|
||||||
}
|
(highestPercentagePool.percentage * Number(xmr_receive_amount)) /
|
||||||
/>
|
100
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const MoneroSecondaryContent = ({
|
||||||
|
monero_receive_pool,
|
||||||
|
xmr_receive_amount,
|
||||||
|
}: MoneroReceiveSectionProps) => (
|
||||||
|
<PoolBreakdown
|
||||||
|
monero_receive_pool={monero_receive_pool}
|
||||||
|
xmr_receive_amount={xmr_receive_amount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Arrow animation styling extracted for reuse
|
||||||
|
const arrowSx = {
|
||||||
|
fontSize: "3rem",
|
||||||
|
color: (theme: any) => theme.palette.primary.main,
|
||||||
|
animation: "slideArrow 2s infinite",
|
||||||
|
"@keyframes slideArrow": {
|
||||||
|
"0%": {
|
||||||
|
opacity: 0.6,
|
||||||
|
transform: "translateX(-8px)",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
opacity: 1,
|
||||||
|
transform: "translateX(8px)",
|
||||||
|
},
|
||||||
|
"100%": {
|
||||||
|
opacity: 0.6,
|
||||||
|
transform: "translateX(-8px)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const AnimatedArrow = () => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignSelf: "center",
|
||||||
|
flex: "0 0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRightAltIcon sx={arrowSx} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTe
|
||||||
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
|
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { buyXmr } from "renderer/rpc";
|
import { buyXmr } from "renderer/rpc";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector, useSettings } from "store/hooks";
|
||||||
|
|
||||||
export default function InitPage() {
|
export default function InitPage() {
|
||||||
const [redeemAddress, setRedeemAddress] = useState("");
|
const [redeemAddress, setRedeemAddress] = useState("");
|
||||||
|
|
@ -18,12 +18,14 @@ export default function InitPage() {
|
||||||
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
||||||
|
|
||||||
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
||||||
|
const donationRatio = useSettings((s) => s.donateToDevelopment);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await buyXmr(
|
await buyXmr(
|
||||||
selectedMaker,
|
selectedMaker,
|
||||||
useExternalRefundAddress ? refundAddress : null,
|
useExternalRefundAddress ? refundAddress : null,
|
||||||
redeemAddress,
|
redeemAddress,
|
||||||
|
donationRatio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,14 @@ import {
|
||||||
useTheme,
|
useTheme,
|
||||||
Switch,
|
Switch,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
TextField,
|
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
Chip,
|
|
||||||
LinearProgress,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
addNode,
|
addNode,
|
||||||
addRendezvousPoint,
|
addRendezvousPoint,
|
||||||
Blockchain,
|
Blockchain,
|
||||||
|
DonateToDevelopmentTip,
|
||||||
FiatCurrency,
|
FiatCurrency,
|
||||||
moveUpNode,
|
moveUpNode,
|
||||||
Network,
|
Network,
|
||||||
|
|
@ -41,12 +39,12 @@ import {
|
||||||
setTheme,
|
setTheme,
|
||||||
setTorEnabled,
|
setTorEnabled,
|
||||||
setUseMoneroRpcPool,
|
setUseMoneroRpcPool,
|
||||||
|
setDonateToDevelopment,
|
||||||
} from "store/features/settingsSlice";
|
} from "store/features/settingsSlice";
|
||||||
import { useAppDispatch, useNodes, useSettings } from "store/hooks";
|
import { useAppDispatch, useNodes, useSettings } from "store/hooks";
|
||||||
import ValidatedTextField from "renderer/components/other/ValidatedTextField";
|
import ValidatedTextField from "renderer/components/other/ValidatedTextField";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
|
||||||
import HelpIcon from "@mui/icons-material/HelpOutline";
|
import HelpIcon from "@mui/icons-material/HelpOutline";
|
||||||
import { ReactNode, useState, useEffect } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { Theme } from "renderer/components/theme";
|
import { Theme } from "renderer/components/theme";
|
||||||
import {
|
import {
|
||||||
Add,
|
Add,
|
||||||
|
|
@ -61,8 +59,6 @@ import { getNetwork } from "store/config";
|
||||||
import { currencySymbol } from "utils/formatUtils";
|
import { currencySymbol } from "utils/formatUtils";
|
||||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||||
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import { getNodeStatus } from "renderer/rpc";
|
import { getNodeStatus } from "renderer/rpc";
|
||||||
import { setStatus } from "store/features/nodesSlice";
|
import { setStatus } from "store/features/nodesSlice";
|
||||||
|
|
||||||
|
|
@ -95,6 +91,7 @@ export default function SettingsBox() {
|
||||||
<Table>
|
<Table>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TorSettings />
|
<TorSettings />
|
||||||
|
<DonationTipSetting />
|
||||||
<ElectrumRpcUrlSetting />
|
<ElectrumRpcUrlSetting />
|
||||||
<MoneroRpcPoolSetting />
|
<MoneroRpcPoolSetting />
|
||||||
<MoneroNodeUrlSetting />
|
<MoneroNodeUrlSetting />
|
||||||
|
|
@ -835,3 +832,127 @@ function RendezvousPointsSetting() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setting that allows you to set a development donation tip amount
|
||||||
|
*/
|
||||||
|
function DonationTipSetting() {
|
||||||
|
const donateToDevelopment = useSettings((s) => s.donateToDevelopment);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleTipSelect = (tipAmount: DonateToDevelopmentTip) => {
|
||||||
|
dispatch(setDonateToDevelopment(tipAmount));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTipLabel = (tip: DonateToDevelopmentTip) => {
|
||||||
|
if (tip === false) return "0%";
|
||||||
|
return `${(tip * 100).toFixed(2)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTipButtonColor = (
|
||||||
|
tip: DonateToDevelopmentTip,
|
||||||
|
isSelected: boolean,
|
||||||
|
) => {
|
||||||
|
// Only show colored if selected and > 0
|
||||||
|
if (isSelected && tip !== false) {
|
||||||
|
return "#198754"; // Green for any tip > 0
|
||||||
|
}
|
||||||
|
return "#6c757d"; // Gray for all unselected or no tip
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTipButtonSelectedColor = (tip: DonateToDevelopmentTip) => {
|
||||||
|
if (tip === false) return "#5c636a"; // Darker gray
|
||||||
|
return "#146c43"; // Darker green for any tip > 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<SettingLabel
|
||||||
|
label="Tip to the developers"
|
||||||
|
tooltip="Support the development of UnstoppableSwap by donating a small percentage of your swaps. Donations go directly to paying for infrastructure costs and developers"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={donateToDevelopment}
|
||||||
|
exclusive
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
if (newValue !== null) {
|
||||||
|
handleTipSelect(newValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="Development tip amount"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
gap: 1,
|
||||||
|
"& .MuiToggleButton-root": {
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: "8px",
|
||||||
|
fontWeight: "600",
|
||||||
|
textTransform: "none",
|
||||||
|
border: "2px solid",
|
||||||
|
"&:not(:first-of-type)": {
|
||||||
|
marginLeft: "8px",
|
||||||
|
borderLeft: "2px solid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{([false, 0.0005, 0.0075] as const).map((tipAmount) => (
|
||||||
|
<ToggleButton
|
||||||
|
key={String(tipAmount)}
|
||||||
|
value={tipAmount}
|
||||||
|
sx={{
|
||||||
|
borderColor: `${getTipButtonColor(tipAmount, donateToDevelopment === tipAmount)} !important`,
|
||||||
|
color:
|
||||||
|
donateToDevelopment === tipAmount
|
||||||
|
? "white"
|
||||||
|
: getTipButtonColor(
|
||||||
|
tipAmount,
|
||||||
|
donateToDevelopment === tipAmount,
|
||||||
|
),
|
||||||
|
backgroundColor:
|
||||||
|
donateToDevelopment === tipAmount
|
||||||
|
? getTipButtonColor(
|
||||||
|
tipAmount,
|
||||||
|
donateToDevelopment === tipAmount,
|
||||||
|
)
|
||||||
|
: "transparent",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: `${getTipButtonSelectedColor(tipAmount)} !important`,
|
||||||
|
color: "white !important",
|
||||||
|
},
|
||||||
|
"&.Mui-selected": {
|
||||||
|
backgroundColor: `${getTipButtonColor(tipAmount, true)} !important`,
|
||||||
|
color: "white !important",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: `${getTipButtonSelectedColor(tipAmount)} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatTipLabel(tipAmount)}
|
||||||
|
</ToggleButton>
|
||||||
|
))}
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
<Typography variant="subtitle2">
|
||||||
|
<ul style={{ margin: 0, padding: "0 1.5rem" }}>
|
||||||
|
<li>
|
||||||
|
Tips go <strong>directly</strong> towards paying for
|
||||||
|
infrastructure costs and developers
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Only ever sent for <strong>successful</strong> swaps
|
||||||
|
</li>{" "}
|
||||||
|
(refunds are not counted)
|
||||||
|
<li>Monero is used for the tips, giving you full anonymity</li>
|
||||||
|
</ul>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,12 @@ import {
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { OpenInNew } from "@mui/icons-material";
|
|
||||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||||
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||||
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
|
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
|
||||||
import {
|
import {
|
||||||
MoneroBitcoinExchangeRate,
|
|
||||||
MoneroBitcoinExchangeRateFromAmounts,
|
MoneroBitcoinExchangeRateFromAmounts,
|
||||||
PiconeroAmount,
|
PiconeroAmount,
|
||||||
SatsAmount,
|
SatsAmount,
|
||||||
|
|
@ -109,6 +108,48 @@ export default function HistoryRowExpanded({
|
||||||
</Link>
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Monero receive pool</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
{swap.monero_receive_pool.map((pool, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 0.5,
|
||||||
|
padding: 1,
|
||||||
|
border: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor: (theme) => theme.palette.action.hover,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{pool.label} ({pool.percentage}%)
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
color: (theme) => theme.palette.text.secondary,
|
||||||
|
wordBreak: "break-all",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pool.address}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
ResolveApprovalResponse,
|
ResolveApprovalResponse,
|
||||||
RedactArgs,
|
RedactArgs,
|
||||||
RedactResponse,
|
RedactResponse,
|
||||||
|
LabeledMoneroAddress,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
|
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
|
||||||
import { store } from "./store/storeRenderer";
|
import { store } from "./store/storeRenderer";
|
||||||
|
|
@ -36,14 +37,46 @@ import { MoneroRecoveryResponse } from "models/rpcModel";
|
||||||
import { ListSellersResponse } from "../models/tauriModel";
|
import { ListSellersResponse } from "../models/tauriModel";
|
||||||
import logger from "utils/logger";
|
import logger from "utils/logger";
|
||||||
import { getNetwork, isTestnet } from "store/config";
|
import { getNetwork, isTestnet } from "store/config";
|
||||||
import { Blockchain, Network } from "store/features/settingsSlice";
|
import {
|
||||||
|
Blockchain,
|
||||||
|
DonateToDevelopmentTip,
|
||||||
|
Network,
|
||||||
|
} from "store/features/settingsSlice";
|
||||||
import { setStatus } from "store/features/nodesSlice";
|
import { setStatus } from "store/features/nodesSlice";
|
||||||
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
|
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
|
||||||
import { CliLog } from "models/cliModel";
|
import { CliLog } from "models/cliModel";
|
||||||
import { logsToRawString, parseLogsFromString } from "utils/parseUtils";
|
import { logsToRawString, parseLogsFromString } from "utils/parseUtils";
|
||||||
|
|
||||||
|
/// These are the official donation address for the UnstoppableSwap/core project
|
||||||
|
const DONATION_ADDRESS_MAINNET =
|
||||||
|
"49LEH26DJGuCyr8xzRAzWPUryzp7bpccC7Hie1DiwyfJEyUKvMFAethRLybDYrFdU1eHaMkKQpUPebY4WT3cSjEvThmpjPa";
|
||||||
|
const DONATION_ADDRESS_STAGENET =
|
||||||
|
"56E274CJxTyVuuFG651dLURKyneoJ5LsSA5jMq4By9z9GBNYQKG8y5ejTYkcvZxarZW6if14ve8xXav2byK4aRnvNdKyVxp";
|
||||||
|
|
||||||
|
/// Signature by binarybaron for the donation address
|
||||||
|
/// https://github.com/binarybaron/
|
||||||
|
///
|
||||||
|
/// Get the key from:
|
||||||
|
/// - https://github.com/UnstoppableSwap/core/blob/master/utils/gpg_keys/binarybaron.asc
|
||||||
|
/// - https://unstoppableswap.net/binarybaron.asc
|
||||||
|
const DONATION_ADDRESS_MAINNET_SIG = `
|
||||||
|
-----BEGIN PGP SIGNED MESSAGE-----
|
||||||
|
Hash: SHA512
|
||||||
|
|
||||||
|
56E274CJxTyVuuFG651dLURKyneoJ5LsSA5jMq4By9z9GBNYQKG8y5ejTYkcvZxarZW6if14ve8xXav2byK4aRnvNdKyVxp is our donation address (signed by binarybaron)
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iHUEARYKAB0WIQQ1qETX9LVbxE4YD/GZt10+FHaibgUCaFvzWQAKCRCZt10+FHai
|
||||||
|
bvC6APoCzCto6RsNYwUr7j1ou3xeVNiwMkUQbE0erKt70pT+tQD/fAvPxHtPyb56
|
||||||
|
XGFQ0pxL1PKzMd9npBGmGJhC4aTljQ4=
|
||||||
|
=OUK4
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
|
`;
|
||||||
|
|
||||||
export const PRESET_RENDEZVOUS_POINTS = [
|
export const PRESET_RENDEZVOUS_POINTS = [
|
||||||
"/dnsaddr/xxmr.cheap/p2p/12D3KooWMk3QyPS8D1d1vpHZoY7y2MnXdPE5yV6iyPvyuj4zcdxT",
|
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||||
|
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
||||||
|
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU",
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function fetchSellersAtPresetRendezvousPoints() {
|
export async function fetchSellersAtPresetRendezvousPoints() {
|
||||||
|
|
@ -142,20 +175,39 @@ export async function buyXmr(
|
||||||
seller: Maker,
|
seller: Maker,
|
||||||
bitcoin_change_address: string | null,
|
bitcoin_change_address: string | null,
|
||||||
monero_receive_address: string,
|
monero_receive_address: string,
|
||||||
|
donation_percentage: DonateToDevelopmentTip,
|
||||||
) {
|
) {
|
||||||
await invoke<BuyXmrArgs, BuyXmrResponse>(
|
const address_pool: LabeledMoneroAddress[] = [];
|
||||||
"buy_xmr",
|
if (donation_percentage !== false) {
|
||||||
bitcoin_change_address == null
|
const donation_address = isTestnet()
|
||||||
? {
|
? DONATION_ADDRESS_STAGENET
|
||||||
seller: providerToConcatenatedMultiAddr(seller),
|
: DONATION_ADDRESS_MAINNET;
|
||||||
monero_receive_address,
|
|
||||||
}
|
address_pool.push(
|
||||||
: {
|
{
|
||||||
seller: providerToConcatenatedMultiAddr(seller),
|
address: monero_receive_address,
|
||||||
monero_receive_address,
|
percentage: 100 - donation_percentage * 100,
|
||||||
bitcoin_change_address,
|
label: "Your wallet",
|
||||||
},
|
},
|
||||||
);
|
{
|
||||||
|
address: donation_address,
|
||||||
|
percentage: donation_percentage * 100,
|
||||||
|
label: "Tip to the developers",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
address_pool.push({
|
||||||
|
address: monero_receive_address,
|
||||||
|
percentage: 100,
|
||||||
|
label: "Your wallet",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await invoke<BuyXmrArgs, BuyXmrResponse>("buy_xmr", {
|
||||||
|
seller: providerToConcatenatedMultiAddr(seller),
|
||||||
|
monero_receive_pool: address_pool,
|
||||||
|
bitcoin_change_address,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resumeSwap(swapId: string) {
|
export async function resumeSwap(swapId: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { Theme } from "renderer/components/theme";
|
import { Theme } from "renderer/components/theme";
|
||||||
|
|
||||||
|
export type DonateToDevelopmentTip = false | 0.0005 | 0.0075;
|
||||||
|
|
||||||
const DEFAULT_RENDEZVOUS_POINTS = [
|
const DEFAULT_RENDEZVOUS_POINTS = [
|
||||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||||
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
||||||
|
|
@ -22,6 +24,9 @@ export interface SettingsState {
|
||||||
userHasSeenIntroduction: boolean;
|
userHasSeenIntroduction: boolean;
|
||||||
/// List of rendezvous points
|
/// List of rendezvous points
|
||||||
rendezvousPoints: string[];
|
rendezvousPoints: string[];
|
||||||
|
/// Does the user want to donate parts of his swaps to funding the development
|
||||||
|
/// of the project?
|
||||||
|
donateToDevelopment: DonateToDevelopmentTip;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FiatCurrency {
|
export enum FiatCurrency {
|
||||||
|
|
@ -124,6 +129,7 @@ const initialState: SettingsState = {
|
||||||
useMoneroRpcPool: true, // Default to using RPC pool
|
useMoneroRpcPool: true, // Default to using RPC pool
|
||||||
userHasSeenIntroduction: false,
|
userHasSeenIntroduction: false,
|
||||||
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
|
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
|
||||||
|
donateToDevelopment: false, // Default to no donation
|
||||||
};
|
};
|
||||||
|
|
||||||
const alertsSlice = createSlice({
|
const alertsSlice = createSlice({
|
||||||
|
|
@ -212,6 +218,12 @@ const alertsSlice = createSlice({
|
||||||
setUseMoneroRpcPool(slice, action: PayloadAction<boolean>) {
|
setUseMoneroRpcPool(slice, action: PayloadAction<boolean>) {
|
||||||
slice.useMoneroRpcPool = action.payload;
|
slice.useMoneroRpcPool = action.payload;
|
||||||
},
|
},
|
||||||
|
setDonateToDevelopment(
|
||||||
|
slice,
|
||||||
|
action: PayloadAction<DonateToDevelopmentTip>,
|
||||||
|
) {
|
||||||
|
slice.donateToDevelopment = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -228,6 +240,7 @@ export const {
|
||||||
setUserHasSeenIntroduction,
|
setUserHasSeenIntroduction,
|
||||||
addRendezvousPoint,
|
addRendezvousPoint,
|
||||||
removeRendezvousPoint,
|
removeRendezvousPoint,
|
||||||
|
setDonateToDevelopment,
|
||||||
} = alertsSlice.actions;
|
} = alertsSlice.actions;
|
||||||
|
|
||||||
export default alertsSlice.reducer;
|
export default alertsSlice.reducer;
|
||||||
|
|
|
||||||
1
swap/.gitignore
vendored
1
swap/.gitignore
vendored
|
|
@ -1,2 +1 @@
|
||||||
tempdb
|
tempdb
|
||||||
.sqlx
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6"
|
"hash": "081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [true]
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c"
|
"hash": "0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n insert into monero_addresses (\n swap_id,\n address\n ) values (?, ?);\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0"
|
|
||||||
}
|
|
||||||
|
|
@ -17,10 +17,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [true, true]
|
||||||
true,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "5cc61dd0315571bc198401a354cd9431ee68360941f341386cbacf44ea598de8"
|
"hash": "5cc61dd0315571bc198401a354cd9431ee68360941f341386cbacf44ea598de8"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false, false]
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6"
|
"hash": "6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
swap/.sqlx/query-7c37de52b3bb2ccd0868ccb861127416848d85eaebe8245c58d5beac7d537087.json
generated
Normal file
12
swap/.sqlx/query-7c37de52b3bb2ccd0868ccb861127416848d85eaebe8245c58d5beac7d537087.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n insert into monero_addresses (\n swap_id,\n address,\n percentage,\n label\n ) values (?, ?, ?, ?);\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "7c37de52b3bb2ccd0868ccb861127416848d85eaebe8245c58d5beac7d537087"
|
||||||
|
}
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf"
|
"hash": "88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "98a8b7f4971e0eb4ab8f5aa688aa22e7fdc6b925de211f7784782f051c2dcd8c"
|
"hash": "98a8b7f4971e0eb4ab8f5aa688aa22e7fdc6b925de211f7784782f051c2dcd8c"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"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
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "ce270dd4a4b9615695a79864240c5401e2122077365e5e5a19408c068c7f9454"
|
|
||||||
}
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2"
|
"hash": "d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
swap/.sqlx/query-dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b.json
generated
Normal file
28
swap/.sqlx/query-dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b.json
generated
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT address, percentage, label\n FROM monero_addresses\n WHERE swap_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "percentage",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Float"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "label",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [false, false, false]
|
||||||
|
},
|
||||||
|
"hash": "dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b"
|
||||||
|
}
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646"
|
"hash": "e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae"
|
"hash": "e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- The user can now have multiple monero receive addresses
|
||||||
|
-- for a single swap
|
||||||
|
-- Each address has a percentage (0 to 1) of the amount they'll receive of the total of the swap amount
|
||||||
|
-- The sum of the percentages must for a single swap MUST be 1
|
||||||
|
-- Add percentage column with default value of 1.0
|
||||||
|
ALTER TABLE monero_addresses ADD COLUMN percentage REAL NOT NULL DEFAULT 1.0;
|
||||||
|
|
||||||
|
-- SQLite doesn't support dropping PRIMARY KEY constraint directly
|
||||||
|
-- We need to recreate the table without the PRIMARY KEY on swap_id
|
||||||
|
CREATE TABLE monero_addresses_temp
|
||||||
|
(
|
||||||
|
swap_id TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
percentage REAL NOT NULL DEFAULT 1.0,
|
||||||
|
label TEXT NOT NULL DEFAULT 'user address'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Copy data from the original table
|
||||||
|
INSERT INTO monero_addresses_temp (swap_id, address, percentage)
|
||||||
|
SELECT swap_id, address, percentage FROM monero_addresses;
|
||||||
|
|
||||||
|
-- Drop the original table
|
||||||
|
DROP TABLE monero_addresses;
|
||||||
|
|
||||||
|
-- Rename the temporary table
|
||||||
|
ALTER TABLE monero_addresses_temp RENAME TO monero_addresses;
|
||||||
|
|
||||||
|
-- Create an index on swap_id for performance
|
||||||
|
CREATE INDEX idx_monero_addresses_swap_id ON monero_addresses(swap_id);
|
||||||
|
|
@ -190,6 +190,7 @@ pub struct Context {
|
||||||
bitcoin_wallet: Option<Arc<bitcoin::Wallet>>,
|
bitcoin_wallet: Option<Arc<bitcoin::Wallet>>,
|
||||||
monero_manager: Option<Arc<monero::Wallets>>,
|
monero_manager: Option<Arc<monero::Wallets>>,
|
||||||
tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
|
tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
monero_rpc_pool_handle: Option<Arc<monero_rpc_pool::PoolHandle>>,
|
monero_rpc_pool_handle: Option<Arc<monero_rpc_pool::PoolHandle>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
|
||||||
use crate::common::{get_logs, redact};
|
use crate::common::{get_logs, redact};
|
||||||
use crate::libp2p_ext::MultiAddrExt;
|
use crate::libp2p_ext::MultiAddrExt;
|
||||||
use crate::monero::wallet_rpc::MoneroDaemon;
|
use crate::monero::wallet_rpc::MoneroDaemon;
|
||||||
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
|
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
|
||||||
use crate::network::swarm;
|
use crate::network::swarm;
|
||||||
use crate::protocol::bob::{BobState, Swap};
|
use crate::protocol::bob::{BobState, Swap};
|
||||||
|
|
@ -59,8 +60,7 @@ pub struct BuyXmrArgs {
|
||||||
pub seller: Multiaddr,
|
pub seller: Multiaddr,
|
||||||
#[typeshare(serialized_as = "Option<string>")]
|
#[typeshare(serialized_as = "Option<string>")]
|
||||||
pub bitcoin_change_address: Option<bitcoin::Address<NetworkUnchecked>>,
|
pub bitcoin_change_address: Option<bitcoin::Address<NetworkUnchecked>>,
|
||||||
#[typeshare(serialized_as = "string")]
|
pub monero_receive_pool: MoneroAddressPool,
|
||||||
pub monero_receive_address: monero::Address,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
|
|
@ -231,6 +231,7 @@ pub struct GetSwapInfoResponse {
|
||||||
pub cancel_timelock: CancelTimelock,
|
pub cancel_timelock: CancelTimelock,
|
||||||
pub punish_timelock: PunishTimelock,
|
pub punish_timelock: PunishTimelock,
|
||||||
pub timelock: Option<ExpiredTimelocks>,
|
pub timelock: Option<ExpiredTimelocks>,
|
||||||
|
pub monero_receive_pool: MoneroAddressPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request for GetSwapInfoArgs {
|
impl Request for GetSwapInfoArgs {
|
||||||
|
|
@ -559,6 +560,8 @@ pub async fn get_swap_info(
|
||||||
|
|
||||||
let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?;
|
let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?;
|
||||||
|
|
||||||
|
let monero_receive_pool = context.db.get_monero_address_pool(args.swap_id).await?;
|
||||||
|
|
||||||
Ok(GetSwapInfoResponse {
|
Ok(GetSwapInfoResponse {
|
||||||
swap_id: args.swap_id,
|
swap_id: args.swap_id,
|
||||||
seller: AliceAddress {
|
seller: AliceAddress {
|
||||||
|
|
@ -578,6 +581,7 @@ pub async fn get_swap_info(
|
||||||
cancel_timelock,
|
cancel_timelock,
|
||||||
punish_timelock,
|
punish_timelock,
|
||||||
timelock,
|
timelock,
|
||||||
|
monero_receive_pool,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -590,9 +594,11 @@ pub async fn buy_xmr(
|
||||||
let BuyXmrArgs {
|
let BuyXmrArgs {
|
||||||
seller,
|
seller,
|
||||||
bitcoin_change_address,
|
bitcoin_change_address,
|
||||||
monero_receive_address,
|
monero_receive_pool,
|
||||||
} = buy_xmr;
|
} = buy_xmr;
|
||||||
|
|
||||||
|
monero_receive_pool.assert_network(context.config.env_config.monero_network)?;
|
||||||
|
|
||||||
let bitcoin_wallet = Arc::clone(
|
let bitcoin_wallet = Arc::clone(
|
||||||
context
|
context
|
||||||
.bitcoin_wallet
|
.bitcoin_wallet
|
||||||
|
|
@ -653,7 +659,7 @@ pub async fn buy_xmr(
|
||||||
|
|
||||||
context
|
context
|
||||||
.db
|
.db
|
||||||
.insert_monero_address(swap_id, monero_receive_address)
|
.insert_monero_address_pool(swap_id, monero_receive_pool.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
||||||
|
|
@ -769,7 +775,7 @@ pub async fn buy_xmr(
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
env_config,
|
env_config,
|
||||||
event_loop_handle,
|
event_loop_handle,
|
||||||
monero_receive_address,
|
monero_receive_pool.clone(),
|
||||||
bitcoin_change_address,
|
bitcoin_change_address,
|
||||||
tx_lock_amount,
|
tx_lock_amount,
|
||||||
tx_lock_fee
|
tx_lock_fee
|
||||||
|
|
@ -847,7 +853,7 @@ pub async fn resume_swap(
|
||||||
let (event_loop, event_loop_handle) =
|
let (event_loop, event_loop_handle) =
|
||||||
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?;
|
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?;
|
||||||
|
|
||||||
let monero_receive_address = context.db.get_monero_address(swap_id).await?;
|
let monero_receive_pool = context.db.get_monero_address_pool(swap_id).await?;
|
||||||
|
|
||||||
let swap = Swap::from_db(
|
let swap = Swap::from_db(
|
||||||
Arc::clone(&context.db),
|
Arc::clone(&context.db),
|
||||||
|
|
@ -865,7 +871,7 @@ pub async fn resume_swap(
|
||||||
.clone(),
|
.clone(),
|
||||||
context.config.env_config,
|
context.config.env_config,
|
||||||
event_loop_handle,
|
event_loop_handle,
|
||||||
monero_receive_address,
|
monero_receive_pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.with_event_emitter(context.tauri_handle.clone());
|
.with_event_emitter(context.tauri_handle.clone());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::request::BalanceResponse;
|
use super::request::BalanceResponse;
|
||||||
use crate::bitcoin;
|
use crate::bitcoin;
|
||||||
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use bitcoin::Txid;
|
use bitcoin::Txid;
|
||||||
|
|
@ -43,6 +44,7 @@ pub struct LockBitcoinDetails {
|
||||||
pub btc_network_fee: bitcoin::Amount,
|
pub btc_network_fee: bitcoin::Amount,
|
||||||
#[typeshare(serialized_as = "number")]
|
#[typeshare(serialized_as = "number")]
|
||||||
pub xmr_receive_amount: monero::Amount,
|
pub xmr_receive_amount: monero::Amount,
|
||||||
|
pub monero_receive_pool: MoneroAddressPool,
|
||||||
#[typeshare(serialized_as = "string")]
|
#[typeshare(serialized_as = "string")]
|
||||||
pub swap_id: Uuid,
|
pub swap_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
@ -629,8 +631,7 @@ pub enum TauriSwapProgressEvent {
|
||||||
XmrRedeemInMempool {
|
XmrRedeemInMempool {
|
||||||
#[typeshare(serialized_as = "Vec<string>")]
|
#[typeshare(serialized_as = "Vec<string>")]
|
||||||
xmr_redeem_txids: Vec<monero::TxHash>,
|
xmr_redeem_txids: Vec<monero::TxHash>,
|
||||||
#[typeshare(serialized_as = "string")]
|
xmr_receive_pool: MoneroAddressPool,
|
||||||
xmr_redeem_address: monero::Address,
|
|
||||||
},
|
},
|
||||||
CancelTimelockExpired,
|
CancelTimelockExpired,
|
||||||
BtcCancelled {
|
BtcCancelled {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use crate::cli::api::request::{
|
||||||
GetHistoryArgs, ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs,
|
GetHistoryArgs, ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs,
|
||||||
};
|
};
|
||||||
use crate::cli::api::Context;
|
use crate::cli::api::Context;
|
||||||
use crate::monero;
|
|
||||||
use crate::monero::monero_address;
|
use crate::monero::monero_address;
|
||||||
|
use crate::monero::{self, MoneroAddressPool};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bitcoin::address::NetworkUnchecked;
|
use bitcoin::address::NetworkUnchecked;
|
||||||
use libp2p::core::Multiaddr;
|
use libp2p::core::Multiaddr;
|
||||||
|
|
@ -68,8 +68,8 @@ where
|
||||||
monero_receive_address,
|
monero_receive_address,
|
||||||
tor,
|
tor,
|
||||||
} => {
|
} => {
|
||||||
let monero_receive_address =
|
let monero_receive_pool: MoneroAddressPool =
|
||||||
monero_address::validate_is_testnet(monero_receive_address, is_testnet)?;
|
monero_address::validate_is_testnet(monero_receive_address, is_testnet)?.into();
|
||||||
|
|
||||||
let bitcoin_change_address = bitcoin_change_address
|
let bitcoin_change_address = bitcoin_change_address
|
||||||
.map(|address| bitcoin_address::validate(address, is_testnet))
|
.map(|address| bitcoin_address::validate(address, is_testnet))
|
||||||
|
|
@ -91,7 +91,7 @@ where
|
||||||
BuyXmrArgs {
|
BuyXmrArgs {
|
||||||
seller,
|
seller,
|
||||||
bitcoin_change_address,
|
bitcoin_change_address,
|
||||||
monero_receive_address,
|
monero_receive_pool,
|
||||||
}
|
}
|
||||||
.request(context.clone())
|
.request(context.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
use crate::cli::api::tauri_bindings::TauriEmitter;
|
use crate::cli::api::tauri_bindings::TauriEmitter;
|
||||||
use crate::cli::api::tauri_bindings::TauriHandle;
|
use crate::cli::api::tauri_bindings::TauriHandle;
|
||||||
use crate::database::Swap;
|
use crate::database::Swap;
|
||||||
use crate::monero::{Address, TransferProof};
|
use crate::monero::LabeledMoneroAddress;
|
||||||
|
use crate::monero::MoneroAddressPool;
|
||||||
|
use crate::monero::TransferProof;
|
||||||
use crate::protocol::{Database, State};
|
use crate::protocol::{Database, State};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use libp2p::{Multiaddr, PeerId};
|
use libp2p::{Multiaddr, PeerId};
|
||||||
|
use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
use sqlx::sqlite::{Sqlite, SqliteConnectOptions};
|
use sqlx::sqlite::{Sqlite, SqliteConnectOptions};
|
||||||
use sqlx::{ConnectOptions, Pool, SqlitePool};
|
use sqlx::{ConnectOptions, Pool, SqlitePool};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -96,43 +100,76 @@ impl Database for SqliteDatabase {
|
||||||
Ok(peer_id)
|
Ok(peer_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn insert_monero_address(&self, swap_id: Uuid, address: Address) -> Result<()> {
|
async fn insert_monero_address_pool(
|
||||||
|
&self,
|
||||||
|
swap_id: Uuid,
|
||||||
|
address: MoneroAddressPool,
|
||||||
|
) -> Result<()> {
|
||||||
let swap_id = swap_id.to_string();
|
let swap_id = swap_id.to_string();
|
||||||
let address = address.to_string();
|
|
||||||
|
|
||||||
sqlx::query!(
|
for labeled_address in address.iter() {
|
||||||
r#"
|
let address_str = labeled_address.address().to_string();
|
||||||
insert into monero_addresses (
|
let percentage_f64 = labeled_address
|
||||||
swap_id,
|
.percentage()
|
||||||
address
|
.to_f64()
|
||||||
) values (?, ?);
|
.expect("Decimal should convert to f64");
|
||||||
"#,
|
let label_str = labeled_address.label();
|
||||||
swap_id,
|
|
||||||
address
|
sqlx::query!(
|
||||||
)
|
r#"
|
||||||
.execute(&self.pool)
|
insert into monero_addresses (
|
||||||
.await?;
|
swap_id,
|
||||||
|
address,
|
||||||
|
percentage,
|
||||||
|
label
|
||||||
|
) values (?, ?, ?, ?);
|
||||||
|
"#,
|
||||||
|
swap_id,
|
||||||
|
address_str,
|
||||||
|
percentage_f64,
|
||||||
|
label_str
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_monero_address(&self, swap_id: Uuid) -> Result<Address> {
|
async fn get_monero_address_pool(&self, swap_id: Uuid) -> Result<MoneroAddressPool> {
|
||||||
let swap_id = swap_id.to_string();
|
let swap_id = swap_id.to_string();
|
||||||
|
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT address
|
SELECT address, percentage, label
|
||||||
FROM monero_addresses
|
FROM monero_addresses
|
||||||
WHERE swap_id = ?
|
WHERE swap_id = ?
|
||||||
"#,
|
"#,
|
||||||
swap_id
|
swap_id
|
||||||
)
|
)
|
||||||
.fetch_one(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let address = row.address.parse()?;
|
if row.is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"No Monero address pool found for swap ID: {}",
|
||||||
|
swap_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(address)
|
let addresses = row
|
||||||
|
.iter()
|
||||||
|
.map(|row| -> Result<LabeledMoneroAddress> {
|
||||||
|
let address = row.address.parse()?;
|
||||||
|
let percentage = Decimal::from_f64(row.percentage).expect("Invalid percentage");
|
||||||
|
let label = row.label.clone();
|
||||||
|
|
||||||
|
LabeledMoneroAddress::new(address, percentage, label)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid percentage in database: {}", e))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
Ok(MoneroAddressPool::new(addresses))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>> {
|
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>> {
|
||||||
|
|
@ -484,17 +521,55 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_insert_load_monero_address() -> Result<()> {
|
async fn test_insert_and_load_monero_address_pool() -> Result<()> {
|
||||||
|
use crate::monero::{LabeledMoneroAddress, MoneroAddressPool};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
let db = setup_test_db().await?;
|
let db = setup_test_db().await?;
|
||||||
|
|
||||||
let swap_id = Uuid::new_v4();
|
let swap_id = Uuid::new_v4();
|
||||||
let monero_address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?;
|
|
||||||
|
|
||||||
db.insert_monero_address(swap_id, monero_address).await?;
|
// Create multiple labeled addresses with valid percentages that sum to 1
|
||||||
|
let address1 = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?; // Stagenet address
|
||||||
|
let address2 = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa".parse()?; // Mainnet address
|
||||||
|
let address3 = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?; // Same as address1 for simplicity
|
||||||
|
|
||||||
let loaded_monero_address = db.get_monero_address(swap_id).await?;
|
let labeled_addresses = vec![
|
||||||
|
LabeledMoneroAddress::new(address1, Decimal::new(5, 1), "Primary".to_string())
|
||||||
|
.map_err(|e| anyhow!(e))?, // 0.5
|
||||||
|
LabeledMoneroAddress::new(address2, Decimal::new(3, 1), "Secondary".to_string())
|
||||||
|
.map_err(|e| anyhow!(e))?, // 0.3
|
||||||
|
LabeledMoneroAddress::new(address3, Decimal::new(2, 1), "Tertiary".to_string())
|
||||||
|
.map_err(|e| anyhow!(e))?, // 0.2
|
||||||
|
];
|
||||||
|
|
||||||
assert_eq!(monero_address, loaded_monero_address);
|
let address_pool = MoneroAddressPool::new(labeled_addresses);
|
||||||
|
|
||||||
|
// Insert the address pool
|
||||||
|
db.insert_monero_address_pool(swap_id, address_pool.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Load the address pool back
|
||||||
|
let loaded_address_pool = db.get_monero_address_pool(swap_id).await?;
|
||||||
|
|
||||||
|
// Verify they are equal
|
||||||
|
assert_eq!(address_pool.addresses(), loaded_address_pool.addresses());
|
||||||
|
assert_eq!(
|
||||||
|
address_pool.percentages(),
|
||||||
|
loaded_address_pool.percentages()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify each labeled address individually
|
||||||
|
let original_addresses: Vec<_> = address_pool.iter().collect();
|
||||||
|
let loaded_addresses: Vec<_> = loaded_address_pool.iter().collect();
|
||||||
|
|
||||||
|
assert_eq!(original_addresses.len(), loaded_addresses.len());
|
||||||
|
|
||||||
|
for (orig, loaded) in original_addresses.iter().zip(loaded_addresses.iter()) {
|
||||||
|
assert_eq!(orig.address(), loaded.address());
|
||||||
|
assert_eq!(orig.percentage(), loaded.percentage());
|
||||||
|
assert_eq!(orig.label(), loaded.label());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,142 @@ impl Amount {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A Monero address with an associated percentage and human-readable label.
|
||||||
|
///
|
||||||
|
/// This structure represents a destination address for Monero transactions
|
||||||
|
/// along with the percentage of funds it should receive and a descriptive label.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct LabeledMoneroAddress {
|
||||||
|
#[typeshare(serialized_as = "string")]
|
||||||
|
address: monero::Address,
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
percentage: Decimal,
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LabeledMoneroAddress {
|
||||||
|
/// Creates a new labeled Monero address.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `address` - The Monero address
|
||||||
|
/// * `percentage` - The percentage of funds (between 0.0 and 1.0)
|
||||||
|
/// * `label` - A human-readable label for this address
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the percentage is not between 0.0 and 1.0 inclusive.
|
||||||
|
pub fn new(
|
||||||
|
address: monero::Address,
|
||||||
|
percentage: Decimal,
|
||||||
|
label: String,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
if percentage < Decimal::ZERO || percentage > Decimal::ONE {
|
||||||
|
return Err(format!(
|
||||||
|
"Percentage must be between 0 and 1 inclusive, got: {}",
|
||||||
|
percentage
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
address,
|
||||||
|
percentage,
|
||||||
|
label,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Monero address.
|
||||||
|
pub fn address(&self) -> monero::Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the percentage as a decimal.
|
||||||
|
pub fn percentage(&self) -> Decimal {
|
||||||
|
self.percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the human-readable label.
|
||||||
|
pub fn label(&self) -> &str {
|
||||||
|
&self.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of labeled Monero addresses that can receive funds in a transaction.
|
||||||
|
///
|
||||||
|
/// This structure manages multiple destination addresses with their associated
|
||||||
|
/// percentages and labels. It's used for splitting Monero transactions across
|
||||||
|
/// multiple recipients, such as for donations or multi-destination swaps.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct MoneroAddressPool(Vec<LabeledMoneroAddress>);
|
||||||
|
|
||||||
|
use rust_decimal::prelude::ToPrimitive;
|
||||||
|
|
||||||
|
impl MoneroAddressPool {
|
||||||
|
/// Creates a new address pool from a vector of labeled addresses.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `addresses` - Vector of labeled Monero addresses
|
||||||
|
pub fn new(addresses: Vec<LabeledMoneroAddress>) -> Self {
|
||||||
|
Self(addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vector of all Monero addresses in the pool.
|
||||||
|
pub fn addresses(&self) -> Vec<monero::Address> {
|
||||||
|
self.0.iter().map(|address| address.address()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vector of all percentages as f64 values.
|
||||||
|
pub fn percentages(&self) -> Vec<f64> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.map(|address| {
|
||||||
|
address
|
||||||
|
.percentage()
|
||||||
|
.to_f64()
|
||||||
|
.expect("Decimal should convert to f64")
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the labeled addresses.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &LabeledMoneroAddress> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that all addresses in the pool are on the expected network.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `network` - The expected Monero network
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if any address is on a different network than expected.
|
||||||
|
pub fn assert_network(&self, network: Network) -> Result<()> {
|
||||||
|
for address in self.0.iter() {
|
||||||
|
if address.address().network != network {
|
||||||
|
bail!("Address pool contains addresses on the wrong network (address {} is on {:?}, expected {:?})", address.address(), address.address().network, network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<::monero::Address> for MoneroAddressPool {
|
||||||
|
fn from(address: ::monero::Address) -> Self {
|
||||||
|
Self(vec![LabeledMoneroAddress::new(
|
||||||
|
address,
|
||||||
|
Decimal::from(1),
|
||||||
|
"user address".to_string(),
|
||||||
|
)
|
||||||
|
.expect("Percentage 1 is always valid")])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Add for Amount {
|
impl Add for Amount {
|
||||||
type Output = Amount;
|
type Output = Amount;
|
||||||
|
|
||||||
|
|
@ -760,4 +896,27 @@ mod tests {
|
||||||
let min_balance = large_amount.min_conservative_balance_to_spend();
|
let min_balance = large_amount.min_conservative_balance_to_spend();
|
||||||
assert!(min_balance > large_amount);
|
assert!(min_balance > large_amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn labeled_monero_address_percentage_validation() {
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
|
let address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse().unwrap();
|
||||||
|
|
||||||
|
// Valid percentages should work
|
||||||
|
assert!(LabeledMoneroAddress::new(address, Decimal::ZERO, "test".to_string()).is_ok());
|
||||||
|
assert!(LabeledMoneroAddress::new(address, Decimal::ONE, "test".to_string()).is_ok());
|
||||||
|
assert!(LabeledMoneroAddress::new(address, Decimal::new(5, 1), "test".to_string()).is_ok()); // 0.5
|
||||||
|
|
||||||
|
// Invalid percentages should fail
|
||||||
|
assert!(
|
||||||
|
LabeledMoneroAddress::new(address, Decimal::new(-1, 0), "test".to_string()).is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
LabeledMoneroAddress::new(address, Decimal::new(11, 1), "test".to_string()).is_err()
|
||||||
|
); // 1.1
|
||||||
|
assert!(
|
||||||
|
LabeledMoneroAddress::new(address, Decimal::new(2, 0), "test".to_string()).is_err()
|
||||||
|
); // 2.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::protocol::alice::swap::is_complete as alice_is_complete;
|
use crate::protocol::alice::swap::is_complete as alice_is_complete;
|
||||||
use crate::protocol::alice::AliceState;
|
use crate::protocol::alice::AliceState;
|
||||||
use crate::protocol::bob::swap::is_complete as bob_is_complete;
|
use crate::protocol::bob::swap::is_complete as bob_is_complete;
|
||||||
|
|
@ -139,8 +140,12 @@ impl TryInto<AliceState> for State {
|
||||||
pub trait Database {
|
pub trait Database {
|
||||||
async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()>;
|
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 get_peer_id(&self, swap_id: Uuid) -> Result<PeerId>;
|
||||||
async fn insert_monero_address(&self, swap_id: Uuid, address: monero::Address) -> Result<()>;
|
async fn insert_monero_address_pool(
|
||||||
async fn get_monero_address(&self, swap_id: Uuid) -> Result<monero::Address>;
|
&self,
|
||||||
|
swap_id: Uuid,
|
||||||
|
address: MoneroAddressPool,
|
||||||
|
) -> Result<()>;
|
||||||
|
async fn get_monero_address_pool(&self, swap_id: Uuid) -> Result<MoneroAddressPool>;
|
||||||
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>>;
|
async fn get_monero_addresses(&self) -> Result<Vec<monero::Address>>;
|
||||||
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>;
|
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>;
|
||||||
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>>;
|
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>>;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use anyhow::Result;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::cli::api::tauri_bindings::TauriHandle;
|
use crate::cli::api::tauri_bindings::TauriHandle;
|
||||||
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::protocol::Database;
|
use crate::protocol::Database;
|
||||||
use crate::{bitcoin, cli, env, monero};
|
use crate::{bitcoin, cli, env, monero};
|
||||||
|
|
||||||
|
|
@ -22,7 +23,7 @@ pub struct Swap {
|
||||||
pub monero_wallet: Arc<monero::Wallets>,
|
pub monero_wallet: Arc<monero::Wallets>,
|
||||||
pub env_config: env::Config,
|
pub env_config: env::Config,
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub monero_receive_address: monero::Address,
|
pub monero_receive_pool: MoneroAddressPool,
|
||||||
pub event_emitter: Option<TauriHandle>,
|
pub event_emitter: Option<TauriHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +36,7 @@ impl Swap {
|
||||||
monero_wallet: Arc<monero::Wallets>,
|
monero_wallet: Arc<monero::Wallets>,
|
||||||
env_config: env::Config,
|
env_config: env::Config,
|
||||||
event_loop_handle: cli::EventLoopHandle,
|
event_loop_handle: cli::EventLoopHandle,
|
||||||
monero_receive_address: monero::Address,
|
monero_receive_pool: MoneroAddressPool,
|
||||||
bitcoin_change_address: bitcoin::Address,
|
bitcoin_change_address: bitcoin::Address,
|
||||||
btc_amount: bitcoin::Amount,
|
btc_amount: bitcoin::Amount,
|
||||||
tx_lock_fee: bitcoin::Amount,
|
tx_lock_fee: bitcoin::Amount,
|
||||||
|
|
@ -52,7 +53,7 @@ impl Swap {
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
env_config,
|
env_config,
|
||||||
id,
|
id,
|
||||||
monero_receive_address,
|
monero_receive_pool,
|
||||||
event_emitter: None,
|
event_emitter: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +66,7 @@ impl Swap {
|
||||||
monero_wallet: Arc<monero::Wallets>,
|
monero_wallet: Arc<monero::Wallets>,
|
||||||
env_config: env::Config,
|
env_config: env::Config,
|
||||||
event_loop_handle: cli::EventLoopHandle,
|
event_loop_handle: cli::EventLoopHandle,
|
||||||
monero_receive_address: monero::Address,
|
monero_receive_pool: MoneroAddressPool,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let state = db.get_state(id).await?.try_into()?;
|
let state = db.get_state(id).await?.try_into()?;
|
||||||
|
|
||||||
|
|
@ -77,7 +78,7 @@ impl Swap {
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
env_config,
|
env_config,
|
||||||
id,
|
id,
|
||||||
monero_receive_address,
|
monero_receive_pool,
|
||||||
event_emitter: None,
|
event_emitter: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::bitcoin::{
|
||||||
TxLock, Txid, Wallet,
|
TxLock, Txid, Wallet,
|
||||||
};
|
};
|
||||||
use crate::monero::wallet::WatchRequest;
|
use crate::monero::wallet::WatchRequest;
|
||||||
use crate::monero::{self, TxHash};
|
use crate::monero::{self, MoneroAddressPool, TxHash};
|
||||||
use crate::monero::{monero_private_key, TransferProof};
|
use crate::monero::{monero_private_key, TransferProof};
|
||||||
use crate::monero_ext::ScalarExt;
|
use crate::monero_ext::ScalarExt;
|
||||||
use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM};
|
use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM};
|
||||||
|
|
@ -717,7 +717,7 @@ impl State5 {
|
||||||
&self,
|
&self,
|
||||||
monero_wallet: &monero::Wallets,
|
monero_wallet: &monero::Wallets,
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
monero_receive_address: monero::Address,
|
monero_receive_pool: MoneroAddressPool,
|
||||||
) -> Result<Vec<TxHash>> {
|
) -> Result<Vec<TxHash>> {
|
||||||
let (spend_key, view_key) = self.xmr_keys();
|
let (spend_key, view_key) = self.xmr_keys();
|
||||||
|
|
||||||
|
|
@ -742,15 +742,17 @@ impl State5 {
|
||||||
.await
|
.await
|
||||||
.context("Couldn't get Monero blockheight")?;
|
.context("Couldn't get Monero blockheight")?;
|
||||||
|
|
||||||
tracing::debug!(%swap_id, receive_address=%monero_receive_address, "Sweeping Monero to receive address");
|
tracing::debug!(%swap_id, receive_address=?monero_receive_pool, "Sweeping Monero to receive address");
|
||||||
|
|
||||||
let tx_hashes = wallet
|
let tx_hashes = wallet
|
||||||
.clone()
|
.sweep_multi(
|
||||||
.sweep(&monero_receive_address.clone())
|
&monero_receive_pool.addresses(),
|
||||||
|
&monero_receive_pool.percentages(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.context("Failed to redeem Monero")?
|
.context("Failed to redeem Monero")?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(TxHash)
|
.map(|tx_receipt| TxHash(tx_receipt.txid))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
tracing::info!(%swap_id, txids=?tx_hashes, "Monero sweep completed");
|
tracing::info!(%swap_id, txids=?tx_hashes, "Monero sweep completed");
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use crate::cli::api::tauri_bindings::{
|
||||||
};
|
};
|
||||||
use crate::cli::EventLoopHandle;
|
use crate::cli::EventLoopHandle;
|
||||||
use crate::common::retry;
|
use crate::common::retry;
|
||||||
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
|
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
|
||||||
use crate::network::swap_setup::bob::NewSwap;
|
use crate::network::swap_setup::bob::NewSwap;
|
||||||
use crate::protocol::bob::state::*;
|
use crate::protocol::bob::state::*;
|
||||||
|
|
@ -17,7 +18,7 @@ use std::time::Duration;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS: u64 = 120;
|
const PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS: u64 = 60 * 3;
|
||||||
|
|
||||||
pub fn is_complete(state: &BobState) -> bool {
|
pub fn is_complete(state: &BobState) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
|
|
@ -75,7 +76,7 @@ pub async fn run_until(
|
||||||
swap.db.clone(),
|
swap.db.clone(),
|
||||||
swap.bitcoin_wallet.as_ref(),
|
swap.bitcoin_wallet.as_ref(),
|
||||||
swap.monero_wallet.clone(),
|
swap.monero_wallet.clone(),
|
||||||
swap.monero_receive_address,
|
swap.monero_receive_pool.clone(),
|
||||||
swap.event_emitter.clone(),
|
swap.event_emitter.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -102,7 +103,7 @@ async fn next_state(
|
||||||
db: Arc<dyn Database + Send + Sync>,
|
db: Arc<dyn Database + Send + Sync>,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
monero_wallet: Arc<monero::Wallets>,
|
monero_wallet: Arc<monero::Wallets>,
|
||||||
monero_receive_address: monero::Address,
|
monero_receive_pool: MoneroAddressPool,
|
||||||
event_emitter: Option<TauriHandle>,
|
event_emitter: Option<TauriHandle>,
|
||||||
) -> Result<BobState> {
|
) -> Result<BobState> {
|
||||||
tracing::debug!(%state, "Advancing state");
|
tracing::debug!(%state, "Advancing state");
|
||||||
|
|
@ -167,6 +168,7 @@ async fn next_state(
|
||||||
btc_lock_amount,
|
btc_lock_amount,
|
||||||
btc_network_fee,
|
btc_network_fee,
|
||||||
xmr_receive_amount,
|
xmr_receive_amount,
|
||||||
|
monero_receive_pool,
|
||||||
swap_id,
|
swap_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -285,14 +287,34 @@ async fn next_state(
|
||||||
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
||||||
let cancel_timelock_expires = tx_lock_status.wait_until(|status| {
|
let cancel_timelock_expires = tx_lock_status.wait_until(|status| {
|
||||||
// Emit a tauri event on new confirmations
|
// Emit a tauri event on new confirmations
|
||||||
if let ScriptStatus::Confirmed(confirmed) = status {
|
match status {
|
||||||
event_emitter.emit_swap_progress_event(
|
ScriptStatus::Confirmed(confirmed) => {
|
||||||
swap_id,
|
event_emitter.emit_swap_progress_event(
|
||||||
TauriSwapProgressEvent::BtcLockTxInMempool {
|
swap_id,
|
||||||
btc_lock_txid: state3.tx_lock_id(),
|
TauriSwapProgressEvent::BtcLockTxInMempool {
|
||||||
btc_lock_confirmations: Some(u64::from(confirmed.confirmations())),
|
btc_lock_txid: state3.tx_lock_id(),
|
||||||
},
|
btc_lock_confirmations: Some(u64::from(confirmed.confirmations())),
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ScriptStatus::InMempool => {
|
||||||
|
event_emitter.emit_swap_progress_event(
|
||||||
|
swap_id,
|
||||||
|
TauriSwapProgressEvent::BtcLockTxInMempool {
|
||||||
|
btc_lock_txid: state3.tx_lock_id(),
|
||||||
|
btc_lock_confirmations: Some(0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ScriptStatus::Unseen | ScriptStatus::Retrying => {
|
||||||
|
event_emitter.emit_swap_progress_event(
|
||||||
|
swap_id,
|
||||||
|
TauriSwapProgressEvent::BtcLockTxInMempool {
|
||||||
|
btc_lock_txid: state3.tx_lock_id(),
|
||||||
|
btc_lock_confirmations: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop when the cancel timelock expires
|
// Stop when the cancel timelock expires
|
||||||
|
|
@ -518,7 +540,7 @@ async fn next_state(
|
||||||
"Refund Monero",
|
"Refund Monero",
|
||||||
|| async {
|
|| async {
|
||||||
state
|
state
|
||||||
.redeem_xmr(&monero_wallet, swap_id, monero_receive_address)
|
.redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::transient)
|
.map_err(backoff::Error::transient)
|
||||||
},
|
},
|
||||||
|
|
@ -532,7 +554,7 @@ async fn next_state(
|
||||||
swap_id,
|
swap_id,
|
||||||
TauriSwapProgressEvent::XmrRedeemInMempool {
|
TauriSwapProgressEvent::XmrRedeemInMempool {
|
||||||
xmr_redeem_txids,
|
xmr_redeem_txids,
|
||||||
xmr_redeem_address: monero_receive_address,
|
xmr_receive_pool: monero_receive_pool.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -730,7 +752,7 @@ async fn next_state(
|
||||||
"Redeeming Monero",
|
"Redeeming Monero",
|
||||||
|| async {
|
|| async {
|
||||||
state5
|
state5
|
||||||
.redeem_xmr(&monero_wallet, swap_id, monero_receive_address)
|
.redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(backoff::Error::transient)
|
.map_err(backoff::Error::transient)
|
||||||
},
|
},
|
||||||
|
|
@ -745,7 +767,7 @@ async fn next_state(
|
||||||
swap_id,
|
swap_id,
|
||||||
TauriSwapProgressEvent::XmrRedeemInMempool {
|
TauriSwapProgressEvent::XmrRedeemInMempool {
|
||||||
xmr_redeem_txids,
|
xmr_redeem_txids,
|
||||||
xmr_redeem_address: monero_receive_address,
|
xmr_receive_pool: monero_receive_pool.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -812,7 +834,7 @@ async fn next_state(
|
||||||
// We don't have the txids of the redeem transaction here, so we can't emit them
|
// We don't have the txids of the redeem transaction here, so we can't emit them
|
||||||
// We return an empty array instead
|
// We return an empty array instead
|
||||||
xmr_redeem_txids: vec![],
|
xmr_redeem_txids: vec![],
|
||||||
xmr_redeem_address: monero_receive_address,
|
xmr_receive_pool: monero_receive_pool.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
BobState::XmrRedeemed { tx_lock_id }
|
BobState::XmrRedeemed { tx_lock_id }
|
||||||
|
|
|
||||||
BIN
swap/tempdb
BIN
swap/tempdb
Binary file not shown.
|
|
@ -492,7 +492,12 @@ impl BobParams {
|
||||||
self.monero_wallet.clone(),
|
self.monero_wallet.clone(),
|
||||||
self.env_config,
|
self.env_config,
|
||||||
handle,
|
handle,
|
||||||
self.monero_wallet.main_wallet().await.main_address().await,
|
self.monero_wallet
|
||||||
|
.main_wallet()
|
||||||
|
.await
|
||||||
|
.main_address()
|
||||||
|
.await
|
||||||
|
.into(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -524,7 +529,12 @@ impl BobParams {
|
||||||
self.monero_wallet.clone(),
|
self.monero_wallet.clone(),
|
||||||
self.env_config,
|
self.env_config,
|
||||||
handle,
|
handle,
|
||||||
self.monero_wallet.main_wallet().await.main_address().await,
|
self.monero_wallet
|
||||||
|
.main_wallet()
|
||||||
|
.await
|
||||||
|
.main_address()
|
||||||
|
.await
|
||||||
|
.into(),
|
||||||
self.bitcoin_wallet.new_address().await?,
|
self.bitcoin_wallet.new_address().await?,
|
||||||
btc_amount,
|
btc_amount,
|
||||||
bitcoin::Amount::from_sat(1000), // Fixed fee of 1000 satoshis for now
|
bitcoin::Amount::from_sat(1000), // Fixed fee of 1000 satoshis for now
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue