feat(gui): DFX.swiss integration (#451)

* feat(gui): Monero wallet

* progress

* refactor

* progress, dont delete wallet, re-fetch approvals and background periodically

* show transaction history correctly

* Enable fetching tx hashes

* Try add the wallet listener event callbacks, not working

* fix: Redeem XMR to internal main wallet, not temp wallet

* feat(monero-sys): Support signing messages

* feat(gui): DFX.swiss integration

* refactor: format, slight refactorings

* progress

* type safety

* refactoring of callback system

* make free floating functions generic

* refactor: Format files

* refactor(gui): Split wallet components and redesign balanceOverview component

* refactor(gui): Add action buttons and transaction section

* wrapper event listener

* progress, compiles

* works!

* WORKS! Event received on balance change

* refactor: format and slight refactorings and comments

* refactor(gui): Start with implementation of send dialog

- new number input
- new button variant and size

* add @tauri-apps/plugin-dialog

* feat(gui): Add permissions for file dialog

* fix(monero-harness): Compile issue

* feat(gui): Extract seed from Monero wallet and use for derivation, allow opening existing wallet file

* feat(gui): Always refresh the approval list from frontend when resolving

* fix(monero-rpc-pool): Implement Into<String> for ServerInfo

* fix(monero-sys): Use oneshot channel for all wallets

* feat(gui, monero-sys): Display recently opened wallets

* small refactors

* fix(gui): Enable background_sync, display temp "Loading..." if values are null

* feat(gui): Remove headers from pages, show selected navigation item

* feat(gui): Explicitly tell user if no swaps have been made yet

* feat(gui): send sync and history updates

* feat(gui): Fetch monero wallet details when context becomes availiable

* feat(gui): Display Monero primary address without modal

* feat(gui): Make "swap" button on wallet page take you to "/swap"

* feat(gui): Rework send modal, adjust number input, added send to field

* feat(gui): set block restore height, not working

* refactor(gui): Optimize number input and add support for switching between currency

* feat(gui): Display real fiat currency prices in send modal

* feat(gui): Add error message for too high send amount

* feat(gui): Modern UI for SeedSelectionDialog

* feat(gui): Wrap MoneroWalletActions

* wip

* refactoring approval callback

* feat(gui): Send Direction of Transaction in History to Frontend

* feat(gui): Let user approve transaction before publishing

* feat: Display 8 digits for Monero amounts by default

* feat(monero-sys): Store pending (non published) transactions in Mutex map inside wallet thread

This allows seperating signing and publishing transactions cleanly

* dprint fmt

* fix(gui): Refresh Monero wallet history C++ struct before serializing

* feat(monero-rpc-pool): Fail after three JSON-RPC errors

* feat(monero-sys): Add wrapper around verify_wallet_password

* feat(gui): Allow opening password-protected Wallets

* refactor: fmt, remove receive button

* fix(gui): Convert to XMR before converting into Fiat

* feat(gui): Add dialog for setting restore height

* feat(gui): block height can be changed, blocks when too low

* refactor(monero-sys): Remove old WalletListener code

* feat(gui): Continually ask for user to select wallet and enter password, if user rejects, offer to select different wallet

* refactor(swap): Extract "select Monero wallet" into own function

* refactor(tauri): Dont kill monero-wallet-rpc

* refactor(tauri): Avoid multiple concurrent Contexts starting

* refactor: Change "Cancel" to "Change wallet" on PasswordEntryDialog

* feat(gui): show curent block height, fix blockage

* Cargo.lock update

* refactor(monero-sys): Use match instead of is_err() and expect(...)

* refactor: better context for WalletHandle constructor method errors handling

* refactor(monero-sys): Common open_with<F>(path: String, daemon: Daemon, wallet_op: F) function

* feat: check empty password before requeston password for wallet

* feat: Remove "Checking for available remote nodes" from frontend

* feat(gui): Allow sweeping entire Monero balance

* feat(monero-rpc-pool): Keep alive TCP connections, do not record JSON-RPC errors as failure if >=3 nodes failed

If >=3 nodes failed we assume it was an actual issue on our side, not an issue with the node

* refactor(swap): Remove dead code

* add comment to WalletHandleListener::on_refreshed{...}

* feat(gui): show current block height in the field

* refactor: remove unused UserCancelledError;

* refactor: No Arc<Mutex<_>> for Pending TXs map

* refactor: remove redundant } catch (error) {

* feat: add our new crates to `OUR_CRATES` in tracing util

* fix(gui): Add math.ceil to piconero conversion to ensure integer

* fix(gui): Close menu when option is clicked

* review and improve/reduce uses of unsafe, also remove unique_ptr wrapper around TransactionHistory to avoid double free

* fix(gui): Use monero amount from units.tsx

* fix(gui): Use PromiseInvokeButton for simplification for approving of send transaction

* update comment, rename function

* refactor(gui): Fix alignment of amounts

* refactor(gui): Remove sending and refreshing states from wallet

* fix(cli, gui): use old seed flow on no tauri, fix minor issues in gui

* fix: use the new named function

* refactor(gui): Add skeletons for monero wallet when still loading

* fix

* get working

* feat(gui): Add tooltip to buy monero button

* refactor: Format files

* refactor(gui): Do not store logs in redux-persist

---------

Co-authored-by: Maksim Kirillov <maksim.kirillov@staticlabs.de>
Co-authored-by: b-enedict <benedict.seuss@gmail.com>
Co-authored-by: einliterflasche <einliterflasche@pm.me>
This commit is contained in:
Mohan 2025-07-28 11:00:33 +02:00 committed by GitHub
parent 591d0b8e20
commit 69ddd2486d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 658 additions and 81 deletions

252
Cargo.lock generated
View file

@ -524,7 +524,7 @@ dependencies = [
"futures-lite",
"parking",
"polling",
"rustix 1.0.7",
"rustix 1.0.8",
"slab",
"tracing",
"windows-sys 0.59.0",
@ -556,7 +556,7 @@ dependencies = [
"cfg-if",
"event-listener",
"futures-lite",
"rustix 1.0.7",
"rustix 1.0.8",
"tracing",
]
@ -583,7 +583,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
"rustix 1.0.7",
"rustix 1.0.8",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
@ -777,7 +777,7 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tower 0.5.2",
"tower-layer",
@ -800,7 +800,7 @@ dependencies = [
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tower-layer",
"tower-service",
"tracing",
@ -1096,7 +1096,7 @@ dependencies = [
"hmac",
"jsonrpc_client",
"rand 0.8.5",
"reqwest",
"reqwest 0.12.22",
"serde",
"serde_json",
"sha2 0.10.9",
@ -1912,7 +1912,7 @@ dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"foreign-types 0.5.0",
"libc",
]
@ -2578,6 +2578,32 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "dfx-swiss-sdk"
version = "1.0.0"
source = "git+https://github.com/eigenwallet/dfx-swiss-rs#0b7d5dc88e7c6481c527fb8fb246e863b415e45f"
dependencies = [
"dfx-swiss-sdk-raw",
"serde",
"serde_derive",
"serde_json",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "dfx-swiss-sdk-raw"
version = "1.0.0"
source = "git+https://github.com/eigenwallet/dfx-swiss-rs#0b7d5dc88e7c6481c527fb8fb246e863b415e45f"
dependencies = [
"reqwest 0.11.27",
"serde",
"serde_derive",
"serde_json",
"url",
"uuid",
]
[[package]]
name = "dialoguer"
version = "0.11.0"
@ -3273,6 +3299,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@ -3280,7 +3315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
"foreign-types-shared 0.3.1",
]
[[package]]
@ -3294,6 +3329,12 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@ -4359,6 +4400,19 @@ dependencies = [
"webpki-roots 1.0.1",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyper-util"
version = "0.1.15"
@ -4558,7 +4612,7 @@ dependencies = [
"netlink-proto",
"netlink-sys",
"rtnetlink",
"system-configuration",
"system-configuration 0.6.1",
"tokio",
"windows 0.53.0",
]
@ -4881,7 +4935,7 @@ source = "git+https://github.com/delta1/rust-jsonrpc-client.git?rev=3b6081697cd6
dependencies = [
"async-trait",
"jsonrpc_client_macro",
"reqwest",
"reqwest 0.12.22",
"serde",
"serde_json",
"url",
@ -5972,7 +6026,7 @@ dependencies = [
"monero-rpc",
"monero-sys",
"rand 0.8.5",
"reqwest",
"reqwest 0.12.22",
"testcontainers",
"tokio",
"tracing",
@ -6013,7 +6067,7 @@ dependencies = [
"monero",
"monero-epee-bin-serde",
"rand 0.8.5",
"reqwest",
"reqwest 0.12.22",
"rust_decimal",
"serde",
"serde_json",
@ -6034,7 +6088,7 @@ dependencies = [
"monero-rpc",
"rand 0.8.5",
"regex",
"reqwest",
"reqwest 0.12.22",
"serde",
"serde_json",
"sqlx",
@ -6165,6 +6219,23 @@ dependencies = [
"unsigned-varint 0.7.2",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework 2.11.1",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.9.0"
@ -6753,12 +6824,50 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -7281,7 +7390,7 @@ dependencies = [
"concurrent-queue",
"hermit-abi 0.5.2",
"pin-project-lite",
"rustix 1.0.7",
"rustix 1.0.8",
"tracing",
"windows-sys 0.59.0",
]
@ -7955,6 +8064,47 @@ dependencies = [
"bytecheck",
]
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.27",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration 0.5.1",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
]
[[package]]
name = "reqwest"
version = "0.12.22"
@ -7983,7 +8133,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tokio-rustls 0.26.2",
"tokio-util",
@ -8234,15 +8384,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -8321,6 +8471,15 @@ dependencies = [
"security-framework 3.2.0",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
@ -9264,7 +9423,7 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
"foreign-types",
"foreign-types 0.5.0",
"js-sys",
"log",
"objc2 0.5.2",
@ -9744,6 +9903,7 @@ dependencies = [
"curve25519-dalek-ng",
"data-encoding",
"derive_builder",
"dfx-swiss-sdk",
"dialoguer",
"ecdsa_fun",
"ed25519-dalek 1.0.1",
@ -9768,7 +9928,7 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"regex",
"reqwest",
"reqwest 0.12.22",
"rust_decimal",
"rust_decimal_macros",
"rustls 0.23.29",
@ -9911,6 +10071,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@ -9943,6 +10109,17 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
@ -9951,7 +10128,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.9.4",
"system-configuration-sys",
"system-configuration-sys 0.6.0",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
@ -10084,7 +10271,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.12.22",
"serde",
"serde_json",
"serde_repr",
@ -10356,7 +10543,7 @@ dependencies = [
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"reqwest 0.12.22",
"semver",
"serde",
"serde_json",
@ -10480,7 +10667,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.7",
"rustix 1.0.8",
"windows-sys 0.59.0",
]
@ -10707,6 +10894,16 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.22.0"
@ -11890,7 +12087,7 @@ dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tower-layer",
"tower-service",
@ -12363,6 +12560,7 @@ name = "unstoppableswap-gui-rs"
version = "3.0.0-beta.2"
dependencies = [
"anyhow",
"dfx-swiss-sdk",
"monero-rpc-pool",
"rustls 0.23.29",
"serde",
@ -12379,6 +12577,8 @@ dependencies = [
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-updater",
"tokio",
"tokio-util",
"tracing",
"uuid",
"zip 4.3.0",
@ -13667,7 +13867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
dependencies = [
"libc",
"rustix 1.0.7",
"rustix 1.0.8",
]
[[package]]

View file

@ -0,0 +1,24 @@
<svg width="544" height="170" viewBox="0 0 544 170" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4704_494)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.5031 0H124.245C170.646 0 208.267 36.5427 208.267 84.0393C208.267 131.536 169.767 170.018 122.288 170.018H61.5031V135.504H114.046C141.825 135.504 164.541 112.789 164.541 85.009C164.541 57.2293 141.825 34.5136 114.046 34.5136H61.5031V0ZM266.25 31.5686V76.4973H338.294V108.066H266.25V170H226.906V0H355.389V31.5686H266.25ZM495.76 170L454.71 110.975L414.396 170H369.216L432.12 83.5365L372.395 0H417.072L456.183 55.1283L494.557 0H537.061L477.803 82.082L541.191 170H495.778H495.76Z" fill="#072440"/>
<path d="M86.1582 126.274C109.821 126.274 129.004 107.092 129.004 83.4287C129.004 59.7657 109.821 40.583 86.1582 40.583C62.4952 40.583 43.3126 59.7657 43.3126 83.4287C43.3126 107.092 62.4952 126.274 86.1582 126.274Z" fill="url(#paint0_linear_4704_494)"/>
<path d="M47.1374 132.146C73.1707 132.146 94.2748 111.042 94.2748 85.009C94.2748 58.9757 73.1707 37.8716 47.1374 37.8716C21.1041 37.8716 0 58.9757 0 85.009C0 111.042 21.1041 132.146 47.1374 132.146Z" fill="url(#paint1_linear_4704_494)"/>
</g>
<defs>
<linearGradient id="paint0_linear_4704_494" x1="122.111" y1="64.6777" x2="45.9618" y2="103.949" gradientUnits="userSpaceOnUse">
<stop offset="0.04" stop-color="#F5516C"/>
<stop offset="0.14" stop-color="#C74863"/>
<stop offset="0.31" stop-color="#853B57"/>
<stop offset="0.44" stop-color="#55324E"/>
<stop offset="0.55" stop-color="#382D49"/>
<stop offset="0.61" stop-color="#2D2B47"/>
</linearGradient>
<linearGradient id="paint1_linear_4704_494" x1="75.8868" y1="50.7468" x2="15.2815" y2="122.952" gradientUnits="userSpaceOnUse">
<stop offset="0.2" stop-color="#F5516C"/>
<stop offset="1" stop-color="#6B3753"/>
</linearGradient>
<clipPath id="clip0_4704_494">
<rect width="541.174" height="170" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,105 @@
import {
Box,
Dialog,
DialogTitle,
Button,
DialogContent,
Chip,
Tooltip,
} from "@mui/material";
import { EuroSymbol as EuroIcon } from "@mui/icons-material";
import DFXSwissLogo from "assets/dfx-logo.svg";
import { useState } from "react";
import { dfxAuthenticate } from "renderer/rpc";
function DFXLogo({ height = 24 }: { height?: number }) {
return (
<Box
sx={{
backgroundColor: "white",
borderRadius: 1,
display: "flex",
alignItems: "center",
padding: 1,
height,
}}
>
<img
src={DFXSwissLogo}
alt="DFX Swiss"
style={{ height: "100%", flex: 1 }}
/>
</Box>
);
}
// Component for DFX button and modal
export default function DfxButton() {
const [dfxUrl, setDfxUrl] = useState<string | null>(null);
const handleOpenDfx = async () => {
try {
// Get authentication token and URL (this will initialize DFX if needed)
const response = await dfxAuthenticate();
setDfxUrl(response.kyc_url);
return response;
} catch (error) {
console.error("DFX authentication failed:", error);
// TODO: Show error snackbar if needed
throw error;
}
};
const handleCloseModal = () => {
setDfxUrl(null);
};
return (
<>
<Tooltip title="Buy Monero with fiat using DFX" enterDelay={500}>
<Chip
variant="button"
icon={<EuroIcon />}
label="Buy Monero"
clickable
onClick={handleOpenDfx}
/>
</Tooltip>
<Dialog
open={dfxUrl != null}
onClose={handleCloseModal}
maxWidth="lg"
fullWidth
>
<DialogTitle>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<DFXLogo />
<Button onClick={handleCloseModal} variant="outlined">
Close
</Button>
</Box>
</DialogTitle>
<DialogContent sx={{ p: 0, height: "min(40rem, 80vh)" }}>
{dfxUrl && (
<iframe
src={dfxUrl}
style={{
width: "100%",
height: "100%",
border: "none",
}}
title="DFX Swiss"
/>
)}
</DialogContent>
</Dialog>
</>
);
}

View file

@ -60,7 +60,6 @@ export default function SendAmountInput({
const handleMaxAmount = () => {
if (disabled) return;
if (onMaxToggled) {
onMaxToggled();
} else if (onMaxClicked) {

View file

@ -61,7 +61,6 @@ export default function SendTransactionContent({
const handleMaxToggled = () => {
if (isSending) return;
if (isMaxSelected) {
// Disable MAX mode - restore previous amount
setIsMaxSelected(false);
@ -76,7 +75,6 @@ export default function SendTransactionContent({
const handleAmountChange = (newAmount: string) => {
if (isSending) return;
if (newAmount !== "<MAX>") {
setIsMaxSelected(false);
}

View file

@ -25,6 +25,7 @@ import SendTransactionModal from "../SendTransactionModal";
import { useNavigate } from "react-router-dom";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import SetRestoreHeightModal from "../SetRestoreHeightModal";
import DfxButton from "./DFXWidget";
interface WalletActionButtonsProps {
balance: {
@ -32,45 +33,6 @@ interface WalletActionButtonsProps {
};
}
function RestoreHeightDialog({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
const [restoreHeight, setRestoreHeight] = useState(0);
const handleRestoreHeight = async () => {
await setMoneroRestoreHeight(restoreHeight);
onClose();
};
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Restore Height</DialogTitle>
<DialogContent>
<TextField
label="Restore Height"
type="number"
value={restoreHeight}
onChange={(e) => setRestoreHeight(Number(e.target.value))}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<PromiseInvokeButton
onInvoke={handleRestoreHeight}
displayErrorSnackbar={true}
variant="contained"
>
Restore
</PromiseInvokeButton>
</DialogActions>
</Dialog>
);
}
export default function WalletActionButtons({
balance,
}: WalletActionButtonsProps) {
@ -121,6 +83,7 @@ export default function WalletActionButtons({
variant="button"
clickable
/>
<DfxButton />
<IconButton onClick={handleMenuClick}>
<MoreHorizIcon />

View file

@ -38,6 +38,7 @@ import {
SendMoneroResponse,
GetMoneroSyncProgressResponse,
GetPendingApprovalsResponse,
DfxAuthenticateResponse,
RejectApprovalArgs,
RejectApprovalResponse,
SetRestoreHeightArgs,
@ -621,3 +622,7 @@ export async function saveFilesInDialog(files: Record<string, string>) {
files,
});
}
export async function dfxAuthenticate(): Promise<DfxAuthenticateResponse> {
return await invokeNoArgs<DfxAuthenticateResponse>("dfx_authenticate");
}

View file

@ -29,9 +29,12 @@ tauri-plugin-process = "^2.0.0"
tauri-plugin-shell = "^2.0.0"
tauri-plugin-store = "^2.0.0"
tauri-plugin-updater = "^2.0.0"
tokio = { workspace = true, features = ["rt"] }
tokio-util = { version = "0.7", features = ["rt"] }
tracing = { workspace = true }
uuid = { workspace = true }
zip = "4.0.0"
dfx-swiss-sdk = { git = "https://github.com/eigenwallet/dfx-swiss-rs", subdir = "dfx-swiss-sdk" }
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-cli = "^2.0.0"

View file

@ -8,13 +8,14 @@ use swap::cli::{
request::{
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs,
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs,
GetMoneroHistoryArgs, GetMoneroMainAddressArgs, GetMoneroSyncProgressArgs,
GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs,
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
CheckSeedResponse, DfxAuthenticateResponse, ExportBitcoinWalletArgs,
GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs, GetLogsArgs,
GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
GetMoneroMainAddressArgs, GetMoneroSyncProgressArgs, GetPendingApprovalsResponse,
GetRestoreHeightArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs,
MoneroRecoveryArgs, RedactArgs, RejectApprovalArgs, RejectApprovalResponse,
ResolveApprovalArgs, ResumeSwapArgs, SendMoneroArgs, SetRestoreHeightArgs,
SuspendCurrentSwapArgs, WithdrawBtcArgs,
},
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
Context, ContextBuilder,
@ -209,7 +210,8 @@ pub fn run() {
get_pending_approvals,
set_monero_restore_height,
reject_approval_request,
get_restore_height
get_restore_height,
dfx_authenticate,
])
.setup(setup)
.build(tauri::generate_context!())
@ -461,3 +463,92 @@ async fn initialize_context(
}
}
}
#[tauri::command]
async fn dfx_authenticate(
state: tauri::State<'_, State>,
) -> Result<DfxAuthenticateResponse, String> {
use dfx_swiss_sdk::{DfxClient, SignRequest};
use tokio::sync::{mpsc, oneshot};
use tokio_util::task::AbortOnDropHandle;
let context = state.try_get_context()?;
// Get the monero wallet manager
let monero_manager = context
.monero_manager
.as_ref()
.ok_or("Monero wallet manager not available for DFX authentication")?;
let wallet = monero_manager.main_wallet().await;
let address = wallet.main_address().await.to_string();
// Create channel for authentication
let (auth_tx, mut auth_rx) = mpsc::channel::<(SignRequest, oneshot::Sender<String>)>(10);
// Create DFX client
let mut client = DfxClient::new(address, Some("https://api.dfx.swiss".to_string()), auth_tx);
// Start signing task with AbortOnDropHandle
let signing_task = tokio::spawn(async move {
tracing::info!("DFX signing service started and listening for requests");
while let Some((sign_request, response_tx)) = auth_rx.recv().await {
tracing::debug!(
message = %sign_request.message,
blockchains = ?sign_request.blockchains,
"Received DFX signing request"
);
// Sign the message using the main Monero wallet
let signature = match wallet
.sign_message(&sign_request.message, None, false)
.await
{
Ok(sig) => {
tracing::debug!(
signature_preview = %&sig[..std::cmp::min(50, sig.len())],
"Message signed successfully for DFX"
);
sig
}
Err(e) => {
tracing::error!(error = ?e, "Failed to sign message for DFX");
continue;
}
};
// Send signature back to DFX client
if let Err(_) = response_tx.send(signature) {
tracing::warn!("Failed to send signature response through channel to DFX client");
}
}
tracing::info!("DFX signing service stopped");
});
// Create AbortOnDropHandle so the task gets cleaned up
let _abort_handle = AbortOnDropHandle::new(signing_task);
// Authenticate with DFX
tracing::info!("Starting DFX authentication...");
client
.authenticate()
.await
.map_err(|e| format!("Failed to authenticate with DFX: {}", e))?;
let access_token = client
.access_token
.as_ref()
.ok_or("No access token available after authentication")?
.clone();
let kyc_url = format!("https://app.dfx.swiss/buy?session={}", access_token);
tracing::info!("DFX authentication completed successfully");
Ok(DfxAuthenticateResponse {
access_token,
kyc_url,
})
}

View file

@ -9,7 +9,7 @@ description = "XMR/BTC trustless atomic swaps."
name = "swap"
[features]
tauri = ["dep:tauri"]
tauri = ["dep:tauri", "dep:dfx-swiss-sdk"]
[dependencies]
anyhow = { workspace = true }
@ -33,6 +33,7 @@ conquer-once = "0.4"
curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" }
data-encoding = "2.6"
derive_builder = "0.20.2"
dfx-swiss-sdk = { git = "https://github.com/eigenwallet/dfx-swiss-rs", subdir = "dfx-swiss-sdk", optional = true }
dialoguer = "0.11"
ecdsa_fun = { version = "0.10", default-features = false, features = ["libsecp_compat", "serde", "adaptor"] }
ed25519-dalek = "1"

View file

@ -188,7 +188,7 @@ pub struct Context {
pub tasks: Arc<PendingTaskList>,
tauri_handle: Option<TauriHandle>,
bitcoin_wallet: Option<Arc<bitcoin::Wallet>>,
monero_manager: Option<Arc<monero::Wallets>>,
pub monero_manager: Option<Arc<monero::Wallets>>,
tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
#[allow(dead_code)]
monero_rpc_pool_handle: Option<Arc<monero_rpc_pool::PoolHandle>>,

View file

@ -594,7 +594,6 @@ impl Request for SetRestoreHeightArgs {
let year: u16 = date.year;
let month: u8 = date.month;
let day: u8 = date.day;
// Validate ranges
if month < 1 || month > 12 {
bail!("Month must be between 1 and 12");
@ -632,7 +631,6 @@ impl Request for SetRestoreHeightArgs {
};
wallet.set_restore_height(height).await?;
wallet.pause_refresh().await;
wallet.stop().await;
tracing::debug!("Background refresh stopped");
@ -2001,3 +1999,10 @@ impl Request for GetMoneroSyncProgressArgs {
pub struct GetPendingApprovalsResponse {
pub approvals: Vec<crate::cli::api::tauri_bindings::ApprovalRequest>,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct DfxAuthenticateResponse {
pub access_token: String,
pub kyc_url: String,
}

View file

@ -1,3 +1,4 @@
pub mod throttle;
pub mod tor;
pub mod tracing_util;

182
swap/src/common/throttle.rs Normal file
View file

@ -0,0 +1,182 @@
// copied from: https://github.com/cargo-crates/fns
// MIT License
use std::pin::Pin;
use std::sync::{mpsc, Arc, Mutex};
use std::time::{self, /* SystemTime, UNIX_EPOCH, */ Duration};
pub fn throttle<F, T>(closure: F, delay: Duration) -> Throttle<T>
where
F: Fn(T) -> () + Send + Sync + 'static,
T: Send + Sync + 'static,
{
let (sender, receiver) = mpsc::channel();
let sender = Arc::new(Mutex::new(sender));
let throttle_config = Arc::new(Mutex::new(ThrottleConfig {
closure: Box::pin(closure),
delay,
}));
let dup_throttle_config = throttle_config.clone();
let throttle = Throttle {
sender: Some(sender),
thread: Some(std::thread::spawn(move || {
let throttle_config = dup_throttle_config;
let mut current_param = None; // 最后被保存为执行的参数
let mut closure_time = None; // 闭包最后执行时间
loop {
if current_param.is_none() {
let message = receiver.recv();
let now = time::Instant::now();
match message {
Ok(param) => {
if let Some(param) = param {
let throttle_config = throttle_config.lock().unwrap();
if closure_time.is_none()
|| now.duration_since(closure_time.unwrap())
>= throttle_config.delay
{
current_param = None;
closure_time = Some(now);
(*throttle_config.closure)(param);
} else {
current_param = Some(param);
}
} else {
current_param = None;
}
}
Err(_) => {
break;
}
}
} else {
let message = receiver.recv_timeout((*throttle_config.lock().unwrap()).delay);
let now = time::Instant::now();
match message {
Ok(param) => {
if let Some(param) = param {
let throttle_config = throttle_config.lock().unwrap();
if closure_time.is_none()
|| now.duration_since(closure_time.unwrap())
>= throttle_config.delay
{
(*throttle_config.closure)(param);
current_param = None;
closure_time = Some(now);
} else {
current_param = Some(param);
}
} else {
current_param = None;
}
}
Err(err) => {
match err {
mpsc::RecvTimeoutError::Timeout => {
if let Some(param) = current_param.take() {
(throttle_config.lock().unwrap().closure)(param);
current_param = None;
closure_time = None; // 超时执行为额外的执行, 不影响的下一次执行
}
}
mpsc::RecvTimeoutError::Disconnected => {
break;
}
}
}
}
}
}
})),
throttle_config,
};
throttle
}
struct ThrottleConfig<T> {
closure: Pin<Box<dyn Fn(T) -> () + Send + Sync + 'static>>,
delay: Duration,
}
impl<T> Drop for ThrottleConfig<T> {
fn drop(&mut self) {
tracing::debug!("drop ThrottleConfig {:?}", format!("{:p}", self));
}
}
#[allow(dead_code)]
pub struct Throttle<T> {
sender: Option<Arc<Mutex<mpsc::Sender<Option<T>>>>>,
thread: Option<std::thread::JoinHandle<()>>,
throttle_config: Arc<Mutex<ThrottleConfig<T>>>,
}
impl<T> Throttle<T> {
pub fn call(&self, param: T) {
self.sender
.as_ref()
.unwrap()
.lock()
.unwrap()
.send(Some(param))
.unwrap();
}
pub fn terminate(&self) {
self.sender
.as_ref()
.unwrap()
.lock()
.unwrap()
.send(None)
.unwrap();
}
}
impl<T> Drop for Throttle<T> {
fn drop(&mut self) {
self.terminate();
tracing::debug!("drop Throttle {:?}", format!("{:p}", self));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let effect_run_times = Arc::new(Mutex::new(0));
let param = Arc::new(Mutex::new(0));
let dup_effect_run_times = effect_run_times.clone();
let dup_param = param.clone();
let throttle_fn = throttle(
move |param| {
*dup_effect_run_times.lock().unwrap() += 1;
*dup_param.lock().unwrap() = param;
},
std::time::Duration::from_millis(100),
);
{
throttle_fn.call(1);
throttle_fn.call(2);
throttle_fn.call(3);
std::thread::sleep(std::time::Duration::from_millis(200));
assert_eq!(*effect_run_times.lock().unwrap(), 2); // delay后执行最有一个参数
assert_eq!(*param.lock().unwrap(), 3);
}
{
throttle_fn.call(4);
std::thread::sleep(std::time::Duration::from_millis(200));
assert_eq!(*effect_run_times.lock().unwrap(), 3);
assert_eq!(*param.lock().unwrap(), 4);
}
{
throttle_fn.call(5);
throttle_fn.call(6);
throttle_fn.terminate(); // 终止最后一次执行
std::thread::sleep(std::time::Duration::from_millis(200));
assert_eq!(*effect_run_times.lock().unwrap(), 4);
assert_eq!(*param.lock().unwrap(), 5);
}
}
}

View file

@ -7,11 +7,11 @@
use std::{path::PathBuf, sync::Arc, time::Duration};
use crate::common::throttle::{throttle, Throttle};
use anyhow::{Context, Result};
use monero::{Address, Network};
use monero_sys::WalletEventListener;
pub use monero_sys::{Daemon, WalletHandle as Wallet, WalletHandleListener};
use throttle::{throttle, Throttle};
use uuid::Uuid;
use crate::cli::api::{