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

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");
}