diff --git a/Cargo.lock b/Cargo.lock
index bde56ff4..0e9d4fc9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
diff --git a/src-gui/src/assets/dfx-logo.svg b/src-gui/src/assets/dfx-logo.svg
new file mode 100644
index 00000000..1cb68c9d
--- /dev/null
+++ b/src-gui/src/assets/dfx-logo.svg
@@ -0,0 +1,24 @@
+
diff --git a/src-gui/src/renderer/components/pages/monero/components/DFXWidget.tsx b/src-gui/src/renderer/components/pages/monero/components/DFXWidget.tsx
new file mode 100644
index 00000000..d32f967e
--- /dev/null
+++ b/src-gui/src/renderer/components/pages/monero/components/DFXWidget.tsx
@@ -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 (
+
+
+
+ );
+}
+
+// Component for DFX button and modal
+export default function DfxButton() {
+ const [dfxUrl, setDfxUrl] = useState(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 (
+ <>
+
+ }
+ label="Buy Monero"
+ clickable
+ onClick={handleOpenDfx}
+ />
+
+
+
+ >
+ );
+}
diff --git a/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx b/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx
index b428679e..5cb44bb7 100644
--- a/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx
+++ b/src-gui/src/renderer/components/pages/monero/components/SendAmountInput.tsx
@@ -60,7 +60,6 @@ export default function SendAmountInput({
const handleMaxAmount = () => {
if (disabled) return;
-
if (onMaxToggled) {
onMaxToggled();
} else if (onMaxClicked) {
diff --git a/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx b/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx
index 1cd4ddbb..0f65b780 100644
--- a/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx
+++ b/src-gui/src/renderer/components/pages/monero/components/SendTransactionContent.tsx
@@ -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 !== "") {
setIsMaxSelected(false);
}
diff --git a/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx b/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx
index 77d0cd63..80ffd900 100644
--- a/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx
+++ b/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx
@@ -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 (
-
- );
-}
-
export default function WalletActionButtons({
balance,
}: WalletActionButtonsProps) {
@@ -121,6 +83,7 @@ export default function WalletActionButtons({
variant="button"
clickable
/>
+
diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts
index 595b8a56..ae1cbde9 100644
--- a/src-gui/src/renderer/rpc.ts
+++ b/src-gui/src/renderer/rpc.ts
@@ -38,6 +38,7 @@ import {
SendMoneroResponse,
GetMoneroSyncProgressResponse,
GetPendingApprovalsResponse,
+ DfxAuthenticateResponse,
RejectApprovalArgs,
RejectApprovalResponse,
SetRestoreHeightArgs,
@@ -621,3 +622,7 @@ export async function saveFilesInDialog(files: Record) {
files,
});
}
+
+export async function dfxAuthenticate(): Promise {
+ return await invokeNoArgs("dfx_authenticate");
+}
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 9cae259e..c6f03bda 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -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"
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 343fa7da..075f93e0 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -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 {
+ 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)>(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,
+ })
+}
diff --git a/swap/Cargo.toml b/swap/Cargo.toml
index 28fa1243..f94791b5 100644
--- a/swap/Cargo.toml
+++ b/swap/Cargo.toml
@@ -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"
diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs
index e9f86af8..29323be9 100644
--- a/swap/src/cli/api.rs
+++ b/swap/src/cli/api.rs
@@ -188,7 +188,7 @@ pub struct Context {
pub tasks: Arc,
tauri_handle: Option,
bitcoin_wallet: Option>,
- monero_manager: Option>,
+ pub monero_manager: Option>,
tor_client: Option>>,
#[allow(dead_code)]
monero_rpc_pool_handle: Option>,
diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs
index a12975d1..d98e0a2e 100644
--- a/swap/src/cli/api/request.rs
+++ b/swap/src/cli/api/request.rs
@@ -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,
}
+
+#[typeshare]
+#[derive(Serialize, Deserialize, Debug)]
+pub struct DfxAuthenticateResponse {
+ pub access_token: String,
+ pub kyc_url: String,
+}
diff --git a/swap/src/common/mod.rs b/swap/src/common/mod.rs
index ecaae413..a0065a62 100644
--- a/swap/src/common/mod.rs
+++ b/swap/src/common/mod.rs
@@ -1,3 +1,4 @@
+pub mod throttle;
pub mod tor;
pub mod tracing_util;
diff --git a/swap/src/common/throttle.rs b/swap/src/common/throttle.rs
new file mode 100644
index 00000000..c092d643
--- /dev/null
+++ b/swap/src/common/throttle.rs
@@ -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(closure: F, delay: Duration) -> Throttle
+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 {
+ closure: Pin () + Send + Sync + 'static>>,
+ delay: Duration,
+}
+impl Drop for ThrottleConfig {
+ fn drop(&mut self) {
+ tracing::debug!("drop ThrottleConfig {:?}", format!("{:p}", self));
+ }
+}
+
+#[allow(dead_code)]
+pub struct Throttle {
+ sender: Option>>>>,
+ thread: Option>,
+ throttle_config: Arc>>,
+}
+impl Throttle {
+ 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 Drop for Throttle {
+ 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);
+ }
+ }
+}
diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs
index d9b260c0..c4709d8b 100644
--- a/swap/src/monero/wallet.rs
+++ b/swap/src/monero/wallet.rs
@@ -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::{