mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-25 18:26:28 -05:00
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:
parent
0f2c406915
commit
3f4cbddf23
68 changed files with 5002 additions and 2692 deletions
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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]}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue