upgrade(swap): Concurrent syncing, bdk upgrade, refactors (#180)

* upgrade sqlx to 0.8, add bdk_wallet and bdk_electrum

The new dependencies are part of the bdk upgrade and
include the improved wallet code.
They, too, depend on sqlite3.
However, they use a newer version than we currently use via sqlx.
This necessitated the sqlx upgrade.
This entailed trivial changes (use Pool directly instead of pool.acquire()).
We might have to fix the CI as well, I kept getting compile
errors from the macro until I ran swap/sqlx_dev_setup.sh.

* move old wallet code to extra module

* fix fee estimation for old client

* bump bitcoin crate, add new wallet constructor

* remove unused old Client, move code around for better readibility

* make Wallet generic over Persister (database) and move more code around for readibility

* add script history, start reimplementing client methods

* update some imports

* cargo fmt

* Add comments, fix fee estimation, address generation and status_of_script

* redo state update and wallet sync

* fix bitcoin address validation and more imports, use Amount everywhere

* fix tx cancel, lock, punish, redeem, refund

* fix bitcoin::Address de-/serialisation

* fix more address validation

* fix more address parsing and validation, also some more imports

* cargo fmt

* fix wallet initialization, start wallet migration

* fail test instead of ignoring it

* perform full scan on creation, load from db if it exists

* add more wallet info, fix wallet initialization

* fix: default to null in config

* migrate from old wallet if needed

* change something

* fix some tests

* temporarily patch bdk_wallet and bdk_electrum

* fix more tests

* fix missing rustls

* asb: only start tor client if register_hidden_service=true in the config

* fix: use p2wsh_signature_hash instead of p2wpkh_signature_hash

* fix some bitcoin address parsing and fee rate parsing

* dprint fmt

* add bitcoin-harness to this project and update to the new bitcoin version

* fix max_givible again

* create electrum client separately from wallet, clean up some code

* add comment

* ignore .env.development

* log config file path on ./asb config

* feat(monero-sys): Initial commit. Regtest integration test. Wrapper around basic Wallet functions, depends on monero#9464

* Revert "feat(monero-sys): Initial commit. Regtest integration test. Wrapper around basic Wallet functions, depends on monero#9464"

This reverts commit 14a5b4c348a109d2524657ffeba306422458ea44.

* upgrade to rust toolchain 1.81

* Use new bdk update for code from master

* fix

* remove

* fix: add empty .gitmodules file to fix Docker build

* fix: clean up submodule references

* fix: properly declare monero submodule with ignore flag

* fix(wallet, bdk): only reveal new address if absolutely necessary

* fix: private keys not loaded into bdk wallet

* refactor: sync wallet progress log

* dprint fmt

* refactor: move bitcoin-harness to outside repo

* refactor: remove redundant log message

* Display sync progress

* Remove redundant arg to  swap/tests/harness/mod.rs function

* fix: call rustls::crypto:💍:default_provider()

* dprint fmt

* refactor: remove debug code

* refactor: move old bdk wallet export to own function, clear log messages

* remove old migr for testnets (checksum mismatch), remove balance and stringified last revealed addresses from migration export

* use revalidate_network function, remove redundant drop

* Display progress of background tasks, TauriBackgroundProgressHandle struct

* fix: almost satisfy clippy

* fix: gen-bindings error

* feat: add BackgroundRefund background type

* feat: use builder pattern for constructing Bitcoin wallet

* dprint ftm

* sync electrum in seperate thread

* do not allow user to start sync while sync is in progress

* remove redundant log message

* display random buffer in AlertWithLinearProgress progress

* fix: use TauriContextStatusEvent.Available), dont show syncing wallet spinner if not syncing

* differentiate between TestWalletBuilder and WalletBuilder

* satisfy clippy

* remove custom BackgroundRefund event, move into background process architecture

* refactor

* dprint fmt

* progress: get unit tests compiling

* fix: bitcoin unit tests specify const values like sync_interval

* fix: get unit tests passing

* make clippy happy

* feat: display full sync progress, fix unit test import issues

* dprint fmt

* make clippy happy, use u32 for target_block and not usize

* always spawn tor for asb

* refactor: remove gen_background_progress_id and just use Uuid::new_v4()

* refactor(hooks.ts): clarify comment on useConservativeBitcoinSyncProgress

* fix typo

* refactor: do not let WalletBuilder take entire env struct

* dprint fmt

* refactor: remove default feature from workspace patch of bdk

* first try for concurrent syncing

* refactor: concurrent syncing

* fix(wallet.rs): Safely convert FeeRate from btc / kb to sats / kwu

* feat(wallet.rs): persist published Bitcoin transactions without requiring re-scan

This allows us to compute an updated Bitcoin balance without requiring a re-scan

* refactor(wallet.rs): use just 5 concurrent sync requests

* refactor: display snackbar error when Wallet refresh fails

* fix: add missing space

* dprint fmt

* refactor: fancy traits for the CumulativeProgress struct, allow limiting amount of callback calls

* make clippy happy

* dprint fmt

* refactor: clearly differntiate between SyncMutex and TokioMutex, use traits for converting to Arc<Mutex<_>>, move sync_ext into own moid

* fix: skip syncing if no spks in wallet

* fix: update bdk.sh to test migration from old wallet (pre 1.0.0 bdk) to new bdk

* fix: increase bitcoin_lock_confirmed_timeout in RegTest env to 5 minutes

* refactor: avoid usize where possible, create persistence only after full scan, transmit assumed_total for full scan to tauri, add some icons to progress displays

* make clippy happy

* fix(ci): change rust toolchain 1.81

* fix(cross compilation arm): use ring instead of aws-lc-rs

* fmt

* ignore failing rendezvous tests

* fix printing_status_change_doesnt_spam_on_same_status

* fix: given_bitcoin_address_network_mismatch_then_error test

* ignore list_sellers_should_report_all_registered_asbs_with_a_quote test

* feat: add tor icon

* refactor(wallet.rs): reorder struct by abstraction level

* refactor(bitcoin wallet): chunk size for syncing

* fix(integration tests): decrease sync interval to 3s

* fix(integration tests): parse_rpc_err method to take new bdk error, not old one

* add changelog entry

---------

Co-authored-by: Binarybaron <binarybaron@protonmail.com>
Co-authored-by: Mohan <86064887+binarybaron@users.noreply.github.com>
This commit is contained in:
Raphael 2025-05-18 22:54:03 +02:00 committed by GitHub
parent 0f2c406915
commit 3f4cbddf23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 5002 additions and 2692 deletions

View file

@ -3,6 +3,8 @@ import {
ApprovalRequest,
ExpiredTimelocks,
GetSwapInfoResponse,
PendingCompleted,
TauriBackgroundProgress,
TauriSwapProgressEvent,
} from "./tauriModel";
@ -230,3 +232,17 @@ export function isPendingLockBitcoinApprovalEvent(
// Check if the request is a LockBitcoin request
return event.content.details.type === "LockBitcoin";
}
export function isPendingBackgroundProcess(
process: TauriBackgroundProgress,
): process is TauriBackgroundProgress {
return process.progress.type === "Pending";
}
export type TauriBitcoinSyncProgress = Extract<TauriBackgroundProgress, { componentName: "SyncingBitcoinWallet" }>;
export function isBitcoinSyncProgress(
progress: TauriBackgroundProgress,
): progress is TauriBitcoinSyncProgress {
return progress.componentName === "SyncingBitcoinWallet";
}

View file

@ -1,11 +1,14 @@
import { listen } from "@tauri-apps/api/event";
import { TauriSwapProgressEventWrapper, TauriContextStatusEvent, TauriLogEvent, BalanceResponse, TauriDatabaseStateEvent, TauriTimelockChangeEvent, TauriBackgroundRefundEvent, ApprovalRequest } from "models/tauriModel";
import { contextStatusEventReceived, receivedCliLog, rpcSetBalance, timelockChangeEventReceived, rpcSetBackgroundRefundState, approvalEventReceived } from "store/features/rpcSlice";
import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel";
import { contextStatusEventReceived, receivedCliLog, rpcSetBalance, timelockChangeEventReceived, approvalEventReceived, backgroundProgressEventReceived } from "store/features/rpcSlice";
import { swapProgressEventReceived } from "store/features/swapSlice";
import logger from "utils/logger";
import { fetchAllConversations, updateAlerts, updatePublicRegistry, updateRates } from "./api";
import { checkContextAvailability, getSwapInfo, initializeContext, updateAllNodeStatuses } from "./rpc";
import { store } from "./store/storeRenderer";
import { exhaustiveGuard } from "utils/typescriptUtils";
const TAURI_UNIFIED_EVENT_CHANNEL_NAME = "tauri-unified-event";
// Update the public registry every 5 minutes
const PROVIDER_UPDATE_INTERVAL = 5 * 60 * 1_000;
@ -25,7 +28,7 @@ function setIntervalImmediate(callback: () => void, interval: number): void {
}
export async function setupBackgroundTasks(): Promise<void> {
// // Setup periodic fetch tasks
// Setup periodic fetch tasks
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
@ -34,11 +37,10 @@ export async function setupBackgroundTasks(): Promise<void> {
// Fetch all alerts
updateAlerts();
// // Setup Tauri event listeners
// Setup Tauri event listeners
// Check if the context is already available. This is to prevent unnecessary re-initialization
if (await checkContextAvailability()) {
store.dispatch(contextStatusEventReceived({ type: "Available" }));
store.dispatch(contextStatusEventReceived(TauriContextStatusEvent.Available));
} else {
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
initializeContext().catch((e) => {
@ -52,47 +54,50 @@ export async function setupBackgroundTasks(): Promise<void> {
});
}
listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => {
logger.info("Received swap progress event", event.payload);
store.dispatch(swapProgressEventReceived(event.payload));
});
listen<TauriContextStatusEvent>("context-init-progress-update", (event) => {
logger.info("Received context init progress event", event.payload);
store.dispatch(contextStatusEventReceived(event.payload));
});
listen<TauriLogEvent>("cli-log-emitted", (event) => {
store.dispatch(receivedCliLog(event.payload));
});
listen<BalanceResponse>("balance-change", (event) => {
logger.info("Received balance change event", event.payload);
store.dispatch(rpcSetBalance(event.payload.balance));
});
listen<TauriDatabaseStateEvent>("swap-database-state-update", (event) => {
logger.info("Received swap database state update event", event.payload);
getSwapInfo(event.payload.swap_id);
// This is ugly but it's the best we can do for now
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
// in the database. So we wait a bit before fetching the new state
setTimeout(() => getSwapInfo(event.payload.swap_id), 3000);
});
listen<TauriTimelockChangeEvent>('timelock-change', (event) => {
logger.info('Received timelock change event', event.payload);
store.dispatch(timelockChangeEventReceived(event.payload));
})
listen<TauriBackgroundRefundEvent>('background-refund', (event) => {
logger.info('Received background refund event', event.payload);
store.dispatch(rpcSetBackgroundRefundState(event.payload));
})
listen<ApprovalRequest>("approval_event", (event) => {
logger.info("Received approval_event:", event.payload);
store.dispatch(approvalEventReceived(event.payload));
// Listen for the unified event
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
const { channelName, event: eventData } = event.payload;
switch (channelName) {
case "SwapProgress":
store.dispatch(swapProgressEventReceived(eventData));
break;
case "ContextInitProgress":
store.dispatch(contextStatusEventReceived(eventData));
break;
case "CliLog":
store.dispatch(receivedCliLog(eventData));
break;
case "BalanceChange":
store.dispatch(rpcSetBalance((eventData).balance));
break;
case "SwapDatabaseStateUpdate":
getSwapInfo(eventData.swap_id);
// This is ugly but it's the best we can do for now
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
// in the database. So we wait a bit before fetching the new state
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
break;
case "TimelockChange":
store.dispatch(timelockChangeEventReceived(eventData));
break;
case "Approval":
store.dispatch(approvalEventReceived(eventData));
break;
case "BackgroundProgress":
store.dispatch(backgroundProgressEventReceived(eventData));
break;
default:
exhaustiveGuard(channelName);
}
});
}

View file

@ -1,11 +1,16 @@
import { Box, Button, LinearProgress, makeStyles } from "@material-ui/core";
import { Box, Button, LinearProgress, makeStyles, Badge } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useNavigate } from "react-router-dom";
import { useAppSelector } from "store/hooks";
import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
import { exhaustiveGuard } from "utils/typescriptUtils";
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
import { bytesToMb } from "utils/conversionUtils";
import { TauriPartialInitProgress } from "models/tauriModel";
import { TauriBackgroundProgress, TauriContextStatusEvent } from "models/tauriModel";
import { useEffect, useState } from "react";
import TruncatedText from "../other/TruncatedText";
import BitcoinIcon from "../icons/BitcoinIcon";
import MoneroIcon from "../icons/MoneroIcon";
import TorIcon from "../icons/TorIcon";
const useStyles = makeStyles((theme) => ({
innerAlert: {
@ -15,8 +20,46 @@ const useStyles = makeStyles((theme) => ({
},
}));
function PartialInitStatus({ status, classes }: {
status: TauriPartialInitProgress,
function AlertWithLinearProgress({ title, progress, icon, count }: {
title: React.ReactNode,
progress: number | null,
icon?: React.ReactNode | null,
count?: number
}) {
const BUFFER_PROGRESS_ADDITION_MAX = 20;
const [bufferProgressAddition, setBufferProgressAddition] = useState(Math.random() * BUFFER_PROGRESS_ADDITION_MAX);
useEffect(() => {
setBufferProgressAddition(Math.random() * BUFFER_PROGRESS_ADDITION_MAX);
}, [progress]);
let displayIcon = icon ?? null;
if (icon && count && count > 1) {
displayIcon = (
<Badge badgeContent={count} color="error">
{icon}
</Badge>
);
}
// If the progress is already at 100%, but not finished yet we show an indeterminate progress bar
// as it'd be confusing to show a 100% progress bar for longer than a second or so.
return <Alert severity="info" icon={displayIcon}>
<Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
{title}
{(progress === null || progress === 0 || progress >= 100) ? (
<LinearProgress variant="indeterminate" />
) : (
<LinearProgress variant="buffer" value={progress} valueBuffer={Math.min(progress + bufferProgressAddition, 100)} />
)}
</Box>
</Alert>
}
function PartialInitStatus({ status, totalOfType, classes }: {
status: TauriBackgroundProgress,
totalOfType: number,
classes: ReturnType<typeof useStyles>
}) {
if (status.progress.type === "Completed") {
@ -24,69 +67,115 @@ function PartialInitStatus({ status, classes }: {
}
switch (status.componentName) {
case "EstablishingTorCircuits":
return (
<AlertWithLinearProgress
title={
<>
Establishing Tor circuits
</>
}
progress={status.progress.content.frac * 100}
count={totalOfType}
icon={<TorIcon />}
/>
);
case "SyncingBitcoinWallet":
const progressValue =
status.progress.content?.type === "Known" ?
(status.progress.content?.content?.consumed / status.progress.content?.content?.total) * 100 : null;
return (
<AlertWithLinearProgress
title={
<>
Syncing Bitcoin wallet
</>
}
progress={progressValue}
icon={<BitcoinIcon />}
count={totalOfType}
/>
);
case "FullScanningBitcoinWallet":
const fullScanProgressValue = status.progress.content?.type === "Known" ? (status.progress.content?.content?.current_index / status.progress.content?.content?.assumed_total) * 100 : null;
return (
<AlertWithLinearProgress
title={
<>
Full scan of Bitcoin wallet (one time operation)
</>
}
progress={fullScanProgressValue}
icon={<BitcoinIcon />}
count={totalOfType}
/>
);
case "OpeningBitcoinWallet":
return (
<LoadingSpinnerAlert severity="warning">
Syncing internal Bitcoin wallet
<LoadingSpinnerAlert severity="info">
<>
Opening Bitcoin wallet
</>
</LoadingSpinnerAlert>
);
case "DownloadingMoneroWalletRpc":
const moneroRpcTitle = `Downloading and verifying the Monero wallet RPC (${bytesToMb(status.progress.content.size).toFixed(2)} MB)`;
return (
<LoadingSpinnerAlert severity="warning">
<Box className={classes.innerAlert}>
<Box>
Downloading and verifying the Monero wallet RPC (
{bytesToMb(status.progress.content.size).toFixed(2)} MB)
</Box>
<LinearProgress variant="determinate" value={status.progress.content.progress} />
</Box>
</LoadingSpinnerAlert>
<AlertWithLinearProgress
title={
<>
{moneroRpcTitle}
</>
}
progress={status.progress.content.progress}
icon={<MoneroIcon />}
count={totalOfType}
/>
);
case "OpeningMoneroWallet":
return (
<LoadingSpinnerAlert severity="warning">
Opening the Monero wallet
<LoadingSpinnerAlert severity="info">
<>
Opening the Monero wallet
</>
</LoadingSpinnerAlert>
);
case "OpeningDatabase":
return (
<LoadingSpinnerAlert severity="warning">
Opening the local database
<LoadingSpinnerAlert severity="info">
<>
Opening the local database
</>
</LoadingSpinnerAlert>
);
case "EstablishingTorCircuits":
case "BackgroundRefund":
return (
<LoadingSpinnerAlert severity="warning">
Establishing Tor circuits
<LoadingSpinnerAlert severity="info">
<>
Refunding swap <TruncatedText limit={10}>{status.progress.content.swap_id}</TruncatedText>
</>
</LoadingSpinnerAlert>
)
);
default:
return null;
return exhaustiveGuard(status);
}
}
export default function DaemonStatusAlert() {
const classes = useStyles();
const contextStatus = useAppSelector((s) => s.rpc.status);
const navigate = useNavigate();
if (contextStatus === null || contextStatus.type === "NotInitialized") {
if (contextStatus === null || contextStatus === TauriContextStatusEvent.NotInitialized) {
return <LoadingSpinnerAlert severity="warning">Checking for available remote nodes</LoadingSpinnerAlert>;
}
switch (contextStatus.type) {
case "Initializing":
return contextStatus.content
.map((status) => (
<PartialInitStatus
key={status.componentName}
status={status}
classes={classes}
/>
))
case "Available":
switch (contextStatus) {
case TauriContextStatusEvent.Initializing:
return <LoadingSpinnerAlert severity="warning">Core components are loading</LoadingSpinnerAlert>;
case TauriContextStatusEvent.Available:
return <Alert severity="success">The daemon is running</Alert>;
case "Failed":
case TauriContextStatusEvent.Failed:
return (
<Alert
severity="error"
@ -94,7 +183,7 @@ export default function DaemonStatusAlert() {
<Button
size="small"
variant="outlined"
onClick={() => navigate("/help#daemon-control-box")}
onClick={() => navigate("/settings#daemon-control-box")}
>
View Logs
</Button>
@ -107,3 +196,35 @@ export default function DaemonStatusAlert() {
return exhaustiveGuard(contextStatus);
}
}
export function BackgroundProgressAlerts() {
const backgroundProgress = usePendingBackgroundProcesses();
const classes = useStyles();
if (backgroundProgress.length === 0) {
return null;
}
const componentCounts: Record<string, number> = {};
backgroundProgress.forEach(([, status]) => {
componentCounts[status.componentName] = (componentCounts[status.componentName] || 0) + 1;
});
const renderedComponentNames = new Set<string>();
const uniqueBackgroundProcesses = backgroundProgress.filter(([, status]) => {
if (!renderedComponentNames.has(status.componentName)) {
renderedComponentNames.add(status.componentName);
return true;
}
return false;
});
return uniqueBackgroundProcesses.map(([id, status]) => (
<PartialInitStatus
key={id}
status={status}
classes={classes}
totalOfType={componentCounts[status.componentName]}
/>
));
}

View file

@ -1,6 +1,7 @@
import {
Box,
CircularProgress,
LinearProgress,
makeStyles,
Typography,
} from "@material-ui/core";
@ -33,3 +34,24 @@ export default function CircularProgressWithSubtitle({
</Box>
);
}
export function LinearProgressWithSubtitle({
description,
value,
}: {
description: string | ReactNode;
value: number;
}) {
const classes = useStyles();
return (
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" style={{ gap: "0.5rem" }}>
<Typography variant="subtitle2" className={classes.subtitle}>
{description}
</Typography>
<Box width="10rem">
<LinearProgress variant="determinate" value={value} />
</Box>
</Box>
);
}

View file

@ -1,7 +1,22 @@
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
import { useConservativeBitcoinSyncProgress, usePendingBackgroundProcesses } from "store/hooks";
import CircularProgressWithSubtitle, { LinearProgressWithSubtitle } from "../../CircularProgressWithSubtitle";
export default function ReceivedQuotePage() {
return (
<CircularProgressWithSubtitle description="Syncing local wallet" />
);
const syncProgress = useConservativeBitcoinSyncProgress();
if (syncProgress?.type === "Known") {
const percentage = Math.round((syncProgress.content.consumed / syncProgress.content.total) * 100);
return (
<LinearProgressWithSubtitle description={`Syncing Bitcoin wallet (${percentage}%)`} value={percentage} />
);
}
if (syncProgress?.type === "Unknown") {
return (
<CircularProgressWithSubtitle description="Syncing Bitcoin wallet" />
);
}
return <CircularProgressWithSubtitle description="Processing offer" />;
}

View file

@ -40,7 +40,7 @@ export default function WaitingForBtcDepositPage({
<ul>
{max_giveable > 0 ? (
<li>
You have already deposited enough funds to swap
You have already deposited enough funds to swap{' '}
<SatsAmount amount={max_giveable} />. However, that is below the minimum amount required to start the swap.
</li>
) : null}

View file

@ -1,13 +1,13 @@
import { Box, makeStyles, Tooltip } from "@material-ui/core";
import GitHubIcon from "@material-ui/icons/GitHub";
import DaemonStatusAlert from "../alert/DaemonStatusAlert";
import DaemonStatusAlert, { BackgroundProgressAlerts } from "../alert/DaemonStatusAlert";
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert";
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
import LinkIconButton from "../icons/LinkIconButton";
import BackgroundRefundAlert from "../alert/BackgroundRefundAlert";
import MatrixIcon from "../icons/MatrixIcon";
import { BookRounded, MenuBook } from "@material-ui/icons";
import { MenuBook } from "@material-ui/icons";
const useStyles = makeStyles((theme) => ({
outer: {
@ -31,6 +31,7 @@ export default function NavigationFooter() {
<UnfinishedSwapsAlert />
<BackgroundRefundAlert />
<DaemonStatusAlert />
<BackgroundProgressAlerts />
<MoneroWalletRpcUpdatingAlert />
<Box className={classes.linksOuter}>
<Tooltip title="Check out the GitHub repository">

View file

@ -239,7 +239,6 @@ function ConversationModal({ open, onClose, feedbackId }: { open: boolean, onClo
enqueueSnackbar('Message sent successfully!', { variant: 'success' });
fetchAllConversations();
} catch (e) {
logger.error(e, 'Send failed');
enqueueSnackbar('Failed to send message. Please try again.', { variant: 'error' });
} finally {
setLoading(false);

View file

@ -78,7 +78,7 @@ function WalletDescriptorModal({
onClose: () => void;
walletDescriptor: ExportBitcoinWalletResponse;
}) {
const parsedDescriptor = JSON.parse(walletDescriptor.wallet_descriptor.descriptor);
const parsedDescriptor = JSON.parse(walletDescriptor.wallet_descriptor["descriptor"]);
const stringifiedDescriptor = JSON.stringify(parsedDescriptor, null, 4);
return (

View file

@ -1,13 +1,18 @@
import RefreshIcon from "@material-ui/icons/Refresh";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { checkBitcoinBalance } from "renderer/rpc";
import { isSyncingBitcoin } from "store/hooks";
export default function WalletRefreshButton() {
const isSyncing = isSyncingBitcoin();
return (
<PromiseInvokeButton
endIcon={<RefreshIcon />}
isIconButton
isLoadingOverride={isSyncing}
onInvoke={() => checkBitcoinBalance()}
displayErrorSnackbar
size="small"
/>
);

View file

@ -55,8 +55,7 @@ export async function fetchSellersAtPresetRendezvousPoints() {
store.dispatch(discoveredMakersByRendezvous(response.sellers));
logger.info(`Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`);
}),
);
}));
}
async function invoke<ARGS, RESPONSE>(
@ -73,6 +72,12 @@ async function invokeNoArgs<RESPONSE>(command: string): Promise<RESPONSE> {
}
export async function checkBitcoinBalance() {
// If we are already syncing, don't start a new sync
if (Object.values(store.getState().rpc?.state.background ?? {}).some(progress => progress.componentName === "SyncingBitcoinWallet" && progress.progress.type === "Pending")) {
console.log("checkBitcoinBalance() was called but we are already syncing Bitcoin, skipping");
return;
}
const response = await invoke<BalanceArgs, BalanceResponse>("get_balance", {
force_refresh: true,
});
@ -80,6 +85,14 @@ export async function checkBitcoinBalance() {
store.dispatch(rpcSetBalance(response.balance));
}
export async function cheapCheckBitcoinBalance() {
const response = await invoke<BalanceArgs, BalanceResponse>("get_balance", {
force_refresh: false,
});
store.dispatch(rpcSetBalance(response.balance));
}
export async function getAllSwapInfos() {
const response =
await invokeNoArgs<GetSwapInfoResponse[]>("get_swap_infos_all");
@ -109,6 +122,10 @@ export async function withdrawBtc(address: string): Promise<string> {
},
);
// We check the balance, this is cheap and does not sync the wallet
// but instead uses our local cached balance
await cheapCheckBitcoinBalance();
return response.txid;
}
@ -176,7 +193,6 @@ export async function redactLogs(
text: logsToRawString(logs)
})
console.log(response.text.split("\n").length)
return parseLogsFromString(response.text);
}

View file

@ -7,6 +7,8 @@ import {
TauriTimelockChangeEvent,
BackgroundRefundState,
ApprovalRequest,
TauriBackgroundProgressWrapper,
TauriBackgroundProgress,
} from "models/tauriModel";
import { MoneroRecoveryResponse } from "../../models/rpcModel";
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
@ -17,7 +19,7 @@ import logger from "utils/logger";
interface State {
balance: number | null;
withdrawTxId: string | null;
rendezvous_discovered_sellers: (ExtendedMakerStatus | MakerStatus)[];
rendezvousDiscoveredSellers: (ExtendedMakerStatus | MakerStatus)[];
swapInfos: {
[swapId: string]: GetSwapInfoResponseExt;
};
@ -25,10 +27,6 @@ interface State {
swapId: string;
keys: MoneroRecoveryResponse;
} | null;
moneroWalletRpc: {
// TODO: Reimplement this using Tauri
updateState: false;
};
backgroundRefund: {
swapId: string;
state: BackgroundRefundState;
@ -37,6 +35,9 @@ interface State {
// Store the full event, keyed by request_id
[requestId: string]: ApprovalRequest;
};
background: {
[key: string]: TauriBackgroundProgress;
}
}
export interface RPCSlice {
@ -50,12 +51,10 @@ const initialState: RPCSlice = {
state: {
balance: null,
withdrawTxId: null,
rendezvous_discovered_sellers: [],
rendezvousDiscoveredSellers: [],
swapInfos: {},
moneroRecovery: null,
moneroWalletRpc: {
updateState: false,
},
background: {},
backgroundRefund: null,
approvalRequests: {},
},
@ -76,23 +75,7 @@ export const rpcSlice = createSlice({
slice,
action: PayloadAction<TauriContextStatusEvent>,
) {
// If we are already initializing, and we receive a new partial status, we update the existing status
if (slice.status?.type === "Initializing" && action.payload.type === "Initializing") {
for (const partialStatus of action.payload.content) {
// We find the existing status with the same type
const existingStatus = slice.status.content.find(s => s.componentName === partialStatus.componentName);
if (existingStatus) {
// If we find it, we update the content
existingStatus.progress = partialStatus.progress;
} else {
// Otherwise, we add the new partial status
slice.status.content.push(partialStatus);
}
}
} else {
// Otherwise, we replace the whole status
slice.status = action.payload;
}
slice.status = action.payload;
},
timelockChangeEventReceived(
slice: RPCSlice,
@ -114,7 +97,7 @@ export const rpcSlice = createSlice({
slice,
action: PayloadAction<(ExtendedMakerStatus | MakerStatus)[]>,
) {
slice.state.rendezvous_discovered_sellers = action.payload;
slice.state.rendezvousDiscoveredSellers = action.payload;
},
rpcResetWithdrawTxId(slice) {
slice.state.withdrawTxId = null;
@ -149,6 +132,12 @@ export const rpcSlice = createSlice({
const requestId = event.content.request_id;
slice.state.approvalRequests[requestId] = event;
},
backgroundProgressEventReceived(slice, action: PayloadAction<TauriBackgroundProgressWrapper>) {
slice.state.background[action.payload.id] = action.payload.event;
},
backgroundProgressEventRemoved(slice, action: PayloadAction<string>) {
delete slice.state.background[action.payload];
},
},
});
@ -165,6 +154,8 @@ export const {
rpcSetBackgroundRefundState,
timelockChangeEventReceived,
approvalEventReceived,
backgroundProgressEventReceived,
backgroundProgressEventRemoved,
} = rpcSlice.actions;
export default rpcSlice.reducer;

View file

@ -1,5 +1,5 @@
import { sortBy } from "lodash";
import { BobStateName, GetSwapInfoResponseExt, PendingApprovalRequest, PendingLockBitcoinApprovalRequest } from "models/tauriModelExt";
import { sortBy, sum } from "lodash";
import { BobStateName, GetSwapInfoResponseExt, isBitcoinSyncProgress, isPendingBackgroundProcess, isPendingLockBitcoinApprovalEvent, PendingApprovalRequest, PendingLockBitcoinApprovalRequest } from "models/tauriModelExt";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
import { parseDateString } from "utils/parseUtils";
@ -9,6 +9,7 @@ import { SettingsState } from "./features/settingsSlice";
import { NodesSlice } from "./features/nodesSlice";
import { RatesState } from "./features/ratesSlice";
import { sortMakerList } from "utils/sortUtils";
import { TauriBackgroundProgress, TauriBitcoinSyncProgress, TauriContextStatusEvent } from "models/tauriModel";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
@ -47,7 +48,7 @@ export function useIsSwapRunning() {
}
export function useIsContextAvailable() {
return useAppSelector((state) => state.rpc.status?.type === "Available");
return useAppSelector((state) => state.rpc.status === TauriContextStatusEvent.Available);
}
/// We do not use a sanity check here, as opposed to the other useSwapInfo hooks,
@ -145,7 +146,53 @@ export function usePendingApprovals(): PendingApprovalRequest[] {
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {
const approvals = usePendingApprovals();
return approvals.filter((c) => c.content.details.type === "LockBitcoin");
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
}
/// Returns all the pending background processes
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
export function usePendingBackgroundProcesses(): [string, TauriBackgroundProgress][] {
const background = useAppSelector((state) => state.rpc.state.background);
return Object.entries(background).filter(([_, c]) => isPendingBackgroundProcess(c));
}
export function useBitcoinSyncProgress(): TauriBitcoinSyncProgress[] {
const pendingProcesses = usePendingBackgroundProcesses();
const syncingProcesses = pendingProcesses.map(([_, c]) => c).filter(isBitcoinSyncProgress);
return syncingProcesses.map((c) => c.progress.content);
}
export function isSyncingBitcoin(): boolean {
const syncProgress = useBitcoinSyncProgress();
return syncProgress.length > 0;
}
/// This function returns the cumulative sync progress of all currently running Bitcoin wallet syncs
/// If all syncs are unknown, it returns {type: "Unknown"}
/// If at least one sync is known, it returns {type: "Known", content: {consumed, total}}
/// where consumed and total are the sum of all the consumed and total values of the syncs
export function useConservativeBitcoinSyncProgress(): TauriBitcoinSyncProgress | null {
const syncingProcesses = useBitcoinSyncProgress();
const progressValues = syncingProcesses.map((c) => c.content?.consumed ?? 0);
const totalValues = syncingProcesses.map((c) => c.content?.total ?? 0);
const progress = sum(progressValues);
const total = sum(totalValues);
// If either the progress or the total is 0, we consider the sync to be unknown
if (progress === 0 || total === 0) {
return {
type: "Unknown",
};
}
return {
type: "Known",
content: {
consumed: progress,
total: total,
},
};
}
/**

View file

@ -7,6 +7,7 @@ import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api";
import { store } from "renderer/store/storeRenderer";
import { swapProgressEventReceived } from "store/features/swapSlice";
import { addFeedbackId, setConversation } from "store/features/conversationsSlice";
import { TauriContextStatusEvent } from "models/tauriModel";
export function createMainListeners() {
const listener = createListenerMiddleware();
@ -18,10 +19,10 @@ export function createMainListeners() {
effect: async (action) => {
const status = action.payload;
// If the context is available, check the bitcoin balance and fetch all swap infos
if (status.type === "Available") {
// If the context is available, check the Bitcoin balance and fetch all swap infos
if (status === TauriContextStatusEvent.Available) {
logger.debug(
"Context is available, checking bitcoin balance and history",
"Context is available, checking Bitcoin balance and history",
);
await Promise.allSettled([
checkBitcoinBalance(),