feat(gui): Monero wallet (#442)

* 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

* 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

* refactor(gui): Remove isLoading from wallet slice

* feat(gui): Add success dialog after send transaction was approved

* fix(gui): Floor piconero amount in sendMoneroTransaction

* feat(gui): Allow view on explorer button on send success modal

* feat(backend): save the wallet state on events

* fix(structure): move throttle into its own crate

* fix(log): remove spammy logs

* fix(logs): log folder in confid

* remove "sync progress: " log

* small refactors

* save wallet at most every 60s

* remove useless logs

* underscore unused variables

* feat(gui): Add timestamp of the tx

* feat(gui): Add the legacy wallet init option

* legac ybutton

* Fix(gui, asb): reverse the log config
remove log in bridge.h
cleanup

* use none for .store(..)

* display dot for running swap

---------

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-18 15:08:36 +02:00 committed by GitHub
parent eb0dc10489
commit a7823d7489
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
118 changed files with 7857 additions and 3456 deletions

View file

@ -31,16 +31,31 @@ import {
RedactResponse,
GetCurrentSwapResponse,
LabeledMoneroAddress,
GetPendingApprovalsArgs,
GetMoneroHistoryResponse,
GetMoneroMainAddressResponse,
GetMoneroBalanceResponse,
SendMoneroArgs,
SendMoneroResponse,
GetMoneroSyncProgressResponse,
GetPendingApprovalsResponse,
RejectApprovalArgs,
RejectApprovalResponse,
SetRestoreHeightArgs,
SetRestoreHeightResponse,
GetRestoreHeightResponse,
} from "models/tauriModel";
import {
rpcSetBalance,
rpcSetSwapInfo,
approvalRequestsReplaced,
} from "store/features/rpcSlice";
import {
setMainAddress,
setBalance,
setSyncProgress,
setHistory,
} from "store/features/walletSlice";
import { store } from "./store/storeRenderer";
import { Maker } from "models/apiModel";
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
import { MoneroRecoveryResponse } from "models/rpcModel";
import { ListSellersResponse } from "../models/tauriModel";
@ -417,6 +432,129 @@ export async function getMoneroAddresses(): Promise<GetMoneroAddressesResponse>
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
}
export async function getRestoreHeight(): Promise<GetRestoreHeightResponse> {
return await invokeNoArgs<GetRestoreHeightResponse>("get_restore_height");
}
export async function setMoneroRestoreHeight(
height: number | Date,
): Promise<SetRestoreHeightResponse> {
const args: SetRestoreHeightArgs =
typeof height === "number"
? { type: "Height", height: height }
: {
type: "Date",
height: {
year: height.getFullYear(),
month: height.getMonth() + 1, // JavaScript months are 0-indexed, but we want 1-indexed
day: height.getDate(),
},
};
return await invoke<SetRestoreHeightArgs, SetRestoreHeightResponse>(
"set_monero_restore_height",
args,
);
}
export async function getMoneroHistory(): Promise<GetMoneroHistoryResponse> {
return await invokeNoArgs<GetMoneroHistoryResponse>("get_monero_history");
}
export async function getMoneroMainAddress(): Promise<GetMoneroMainAddressResponse> {
return await invokeNoArgs<GetMoneroMainAddressResponse>(
"get_monero_main_address",
);
}
export async function getMoneroBalance(): Promise<GetMoneroBalanceResponse> {
return await invokeNoArgs<GetMoneroBalanceResponse>("get_monero_balance");
}
export async function sendMonero(
args: SendMoneroArgs,
): Promise<SendMoneroResponse> {
return await invoke<SendMoneroArgs, SendMoneroResponse>("send_monero", args);
}
export async function getMoneroSyncProgress(): Promise<GetMoneroSyncProgressResponse> {
return await invokeNoArgs<GetMoneroSyncProgressResponse>(
"get_monero_sync_progress",
);
}
// Wallet management functions that handle Redux dispatching
export async function initializeMoneroWallet() {
try {
const [
addressResponse,
balanceResponse,
syncProgressResponse,
historyResponse,
] = await Promise.all([
getMoneroMainAddress(),
getMoneroBalance(),
getMoneroSyncProgress(),
getMoneroHistory(),
]);
store.dispatch(setMainAddress(addressResponse.address));
store.dispatch(setBalance(balanceResponse));
store.dispatch(setSyncProgress(syncProgressResponse));
store.dispatch(setHistory(historyResponse));
} catch (err) {
console.error("Failed to fetch Monero wallet data:", err);
}
}
export async function sendMoneroTransaction(
args: SendMoneroArgs,
): Promise<SendMoneroResponse> {
try {
const response = await sendMonero(args);
// Refresh balance and history after sending - but don't let this block the response
Promise.all([
getMoneroBalance(),
getMoneroHistory(),
]).then(([newBalance, newHistory]) => {
store.dispatch(setBalance(newBalance));
store.dispatch(setHistory(newHistory));
}).catch(refreshErr => {
console.error("Failed to refresh wallet data after send:", refreshErr);
// Could emit a toast notification here
});
return response;
} catch (err) {
console.error("Failed to send Monero:", err);
throw err; // ✅ Re-throw so caller can handle appropriately
}
}
async function refreshWalletDataAfterTransaction() {
try {
const [newBalance, newHistory] = await Promise.all([
getMoneroBalance(),
getMoneroHistory(),
]);
store.dispatch(setBalance(newBalance));
store.dispatch(setHistory(newHistory));
} catch (err) {
console.error("Failed to refresh wallet data after transaction:", err);
// Maybe show a non-blocking notification to user
}
}
export async function updateMoneroSyncProgress() {
try {
const response = await getMoneroSyncProgress();
store.dispatch(setSyncProgress(response));
} catch (err) {
console.error("Failed to fetch sync progress:", err);
}
}
export async function getDataDir(): Promise<string> {
const testnet = isTestnet();
return await invoke<GetDataDirArgs, string>("get_data_dir", {
@ -424,22 +562,37 @@ export async function getDataDir(): Promise<string> {
});
}
export async function resolveApproval(
export async function resolveApproval<T>(
requestId: string,
accept: object,
accept: T,
): Promise<void> {
try {
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
"resolve_approval_request",
{ request_id: requestId, accept },
{ request_id: requestId, accept: accept as object },
);
} catch (error) {
// Refresh approval list when resolve fails to keep UI in sync
} finally {
// Always refresh the approval list
await refreshApprovals();
throw error;
// Refresh the approval list a few miliseconds later to again
// Just to make sure :)
setTimeout(() => {
refreshApprovals();
}, 200);
}
}
export async function rejectApproval<T>(
requestId: string,
reject: T,
): Promise<void> {
await invoke<RejectApprovalArgs, RejectApprovalResponse>(
"reject_approval_request",
{ request_id: requestId },
);
}
export async function refreshApprovals(): Promise<void> {
const response = await invokeNoArgs<GetPendingApprovalsResponse>(
"get_pending_approvals",