mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-26 10:46:23 -05:00
feat: Recover from / write down seed (#439)
* vendor: seed crate from monero-serai * refactor: make approval type generic over respone from frontend * progress * Raphaels Progress * feat(gui): seed import flow skeleton * fix(gui): Seed import in the development version * fix(gui): specify the imported seed type * remove initializeHandle, make tauri handle non optional in state, dont allow closing seed dialog * feat(gui): check if seed is valid dynamically * fix(gui): refine the dynamic seed validation * push * progress * progress * Fix pending trimeout --------- Co-authored-by: Maksim Kirillov <artist@eduroam-141-23-183-184.wlan.tu-berlin.de> Co-authored-by: Maksim Kirillov <artist@eduroam-141-23-189-144.wlan.tu-berlin.de> Co-authored-by: Maksim Kirillov <maksim.kirillov@staticlabs.de>
This commit is contained in:
parent
b8982b5ac2
commit
7606982de3
39 changed files with 22465 additions and 171 deletions
|
|
@ -253,27 +253,38 @@ export function isGetSwapInfoResponseWithTimelock(
|
|||
return response.timelock !== null;
|
||||
}
|
||||
|
||||
export type PendingApprovalRequest = Extract<
|
||||
ApprovalRequest,
|
||||
{ state: "Pending" }
|
||||
>;
|
||||
export type PendingApprovalRequest = ApprovalRequest & {
|
||||
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||
};
|
||||
|
||||
export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & {
|
||||
content: {
|
||||
details: { type: "LockBitcoin" };
|
||||
};
|
||||
export type PendingLockBitcoinApprovalRequest = ApprovalRequest & {
|
||||
request: Extract<ApprovalRequest["request"], { type: "LockBitcoin" }>;
|
||||
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||
};
|
||||
|
||||
export type PendingSeedSelectionApprovalRequest = ApprovalRequest & {
|
||||
type: "SeedSelection";
|
||||
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||
};
|
||||
|
||||
export function isPendingLockBitcoinApprovalEvent(
|
||||
event: ApprovalRequest,
|
||||
): event is PendingLockBitcoinApprovalRequest {
|
||||
// Check if the request is pending
|
||||
if (event.state !== "Pending") {
|
||||
return false;
|
||||
}
|
||||
// Check if the request is a LockBitcoin request and is pending
|
||||
return (
|
||||
event.request.type === "LockBitcoin" &&
|
||||
event.request_status.state === "Pending"
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the request is a LockBitcoin request
|
||||
return event.content.details.type === "LockBitcoin";
|
||||
export function isPendingSeedSelectionApprovalEvent(
|
||||
event: ApprovalRequest,
|
||||
): event is PendingSeedSelectionApprovalRequest {
|
||||
// Check if the request is a SeedSelection request and is pending
|
||||
return (
|
||||
event.request.type === "SeedSelection" &&
|
||||
event.request_status.state === "Pending"
|
||||
);
|
||||
}
|
||||
|
||||
export function isPendingBackgroundProcess(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { setupBackgroundTasks } from "renderer/background";
|
|||
import "@fontsource/roboto";
|
||||
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
||||
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
||||
import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
|
||||
|
||||
declare module "@mui/material/styles" {
|
||||
interface Theme {
|
||||
|
|
@ -46,6 +47,7 @@ export default function App() {
|
|||
<CssBaseline />
|
||||
<GlobalSnackbarProvider>
|
||||
<IntroductionModal />
|
||||
<SeedSelectionDialog />
|
||||
<Router>
|
||||
<Navigation />
|
||||
<InnerContent />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePendingSeedSelectionApproval } from "store/hooks";
|
||||
import { resolveApproval, checkSeed } from "renderer/rpc";
|
||||
|
||||
export default function SeedSelectionDialog() {
|
||||
const pendingApprovals = usePendingSeedSelectionApproval();
|
||||
const [selectedOption, setSelectedOption] = useState<string>("RandomSeed");
|
||||
const [customSeed, setCustomSeed] = useState<string>("");
|
||||
const [isSeedValid, setIsSeedValid] = useState<boolean>(false);
|
||||
const approval = pendingApprovals[0]; // Handle the first pending approval
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOption === "FromSeed" && customSeed.trim()) {
|
||||
checkSeed(customSeed.trim())
|
||||
.then((valid) => {
|
||||
setIsSeedValid(valid);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSeedValid(false);
|
||||
});
|
||||
} else {
|
||||
setIsSeedValid(false);
|
||||
}
|
||||
}, [customSeed, selectedOption]);
|
||||
|
||||
const handleClose = async (accept: boolean) => {
|
||||
if (!approval) return;
|
||||
|
||||
if (accept) {
|
||||
const seedChoice =
|
||||
selectedOption === "RandomSeed"
|
||||
? { type: "RandomSeed" }
|
||||
: { type: "FromSeed", content: { seed: customSeed } };
|
||||
|
||||
await resolveApproval(approval.request_id, seedChoice);
|
||||
} else {
|
||||
// On reject, just close without approval
|
||||
await resolveApproval(approval.request_id, { type: "RandomSeed" });
|
||||
}
|
||||
};
|
||||
|
||||
if (!approval) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={true} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Monero Wallet</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
Choose what seed to use for the wallet.
|
||||
</Typography>
|
||||
|
||||
<FormControl component="fieldset">
|
||||
<RadioGroup
|
||||
value={selectedOption}
|
||||
onChange={(e) => setSelectedOption(e.target.value)}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="RandomSeed"
|
||||
control={<Radio />}
|
||||
label="Create a new wallet"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="FromSeed"
|
||||
control={<Radio />}
|
||||
label="Restore wallet from seed"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
{selectedOption === "FromSeed" && (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="Enter your seed phrase"
|
||||
value={customSeed}
|
||||
onChange={(e) => setCustomSeed(e.target.value)}
|
||||
sx={{ mt: 2 }}
|
||||
placeholder="Enter your Monero 25 words seed phrase..."
|
||||
error={!isSeedValid && customSeed.length > 0}
|
||||
helperText={
|
||||
isSeedValid
|
||||
? "Seed is valid"
|
||||
: customSeed.length > 0
|
||||
? "Seed is invalid"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => handleClose(true)}
|
||||
variant="contained"
|
||||
disabled={
|
||||
selectedOption === "FromSeed"
|
||||
? !customSeed.trim() || !isSeedValid
|
||||
: false
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,9 +20,7 @@ function useActiveLockBitcoinApprovalRequest(): PendingLockBitcoinApprovalReques
|
|||
const activeSwapId = useActiveSwapId();
|
||||
|
||||
return (
|
||||
approvals?.find(
|
||||
(r) => r.content.details.content.swap_id === activeSwapId,
|
||||
) || null
|
||||
approvals?.find((r) => r.request.content.swap_id === activeSwapId) || null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -32,10 +30,18 @@ export default function SwapSetupInflightPage({
|
|||
const request = useActiveLockBitcoinApprovalRequest();
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||
|
||||
const expiresAtMs = request?.content.expiration_ts * 1000 || 0;
|
||||
const expirationTs =
|
||||
request?.request_status.state === "Pending"
|
||||
? request.request_status.content.expiration_ts
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (expirationTs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAtMs = expirationTs * 1000 || 0;
|
||||
|
||||
const tick = () => {
|
||||
const remainingMs = Math.max(expiresAtMs - Date.now(), 0);
|
||||
setTimeLeft(Math.ceil(remainingMs / 1000));
|
||||
|
|
@ -44,11 +50,11 @@ export default function SwapSetupInflightPage({
|
|||
tick();
|
||||
const id = setInterval(tick, 250);
|
||||
return () => clearInterval(id);
|
||||
}, [expiresAtMs]);
|
||||
}, [request, expirationTs]);
|
||||
|
||||
// If we do not have an approval request yet for the Bitcoin lock transaction, we haven't received the offer from Alice yet
|
||||
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
||||
if (!request) {
|
||||
if (request == null) {
|
||||
return (
|
||||
<CircularProgressWithSubtitle
|
||||
description={
|
||||
|
|
@ -61,7 +67,7 @@ export default function SwapSetupInflightPage({
|
|||
}
|
||||
|
||||
const { btc_network_fee, monero_receive_pool, xmr_receive_amount } =
|
||||
request.content.details.content;
|
||||
request.request.content;
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -124,7 +130,9 @@ export default function SwapSetupInflightPage({
|
|||
variant="text"
|
||||
size="large"
|
||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
||||
onInvoke={() =>
|
||||
resolveApproval(request.request_id, false as unknown as object)
|
||||
}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
>
|
||||
|
|
@ -135,7 +143,9 @@ export default function SwapSetupInflightPage({
|
|||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
onInvoke={() => resolveApproval(request.content.request_id, true)}
|
||||
onInvoke={() =>
|
||||
resolveApproval(request.request_id, true as unknown as object)
|
||||
}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
endIcon={<CheckIcon />}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import {
|
|||
GetSwapInfoArgs,
|
||||
ExportBitcoinWalletResponse,
|
||||
CheckMoneroNodeArgs,
|
||||
CheckSeedArgs,
|
||||
CheckSeedResponse,
|
||||
CheckMoneroNodeResponse,
|
||||
TauriSettings,
|
||||
CheckElectrumNodeArgs,
|
||||
|
|
@ -304,10 +306,16 @@ export async function initializeContext() {
|
|||
|
||||
logger.info("Initializing context with settings", tauriSettings);
|
||||
|
||||
await invokeUnsafe<void>("initialize_context", {
|
||||
settings: tauriSettings,
|
||||
testnet,
|
||||
});
|
||||
try {
|
||||
await invokeUnsafe<void>("initialize_context", {
|
||||
settings: tauriSettings,
|
||||
testnet,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error("Couldn't initialize context: " + error);
|
||||
}
|
||||
|
||||
logger.info("Initialized context");
|
||||
}
|
||||
|
||||
export async function getWalletDescriptor() {
|
||||
|
|
@ -395,7 +403,7 @@ export async function getDataDir(): Promise<string> {
|
|||
|
||||
export async function resolveApproval(
|
||||
requestId: string,
|
||||
accept: boolean,
|
||||
accept: object,
|
||||
): Promise<void> {
|
||||
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
||||
"resolve_approval_request",
|
||||
|
|
@ -403,6 +411,16 @@ export async function resolveApproval(
|
|||
);
|
||||
}
|
||||
|
||||
export async function checkSeed(seed: string): Promise<boolean> {
|
||||
const response = await invoke<CheckSeedArgs, CheckSeedResponse>(
|
||||
"check_seed",
|
||||
{
|
||||
seed,
|
||||
},
|
||||
);
|
||||
return response.available;
|
||||
}
|
||||
|
||||
export async function saveLogFiles(
|
||||
zipFileName: string,
|
||||
content: Record<string, string>,
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ export const rpcSlice = createSlice({
|
|||
},
|
||||
approvalEventReceived(slice, action: PayloadAction<ApprovalRequest>) {
|
||||
const event = action.payload;
|
||||
const requestId = event.content.request_id;
|
||||
const requestId = event.request_id;
|
||||
slice.state.approvalRequests[requestId] = event;
|
||||
},
|
||||
backgroundProgressEventReceived(
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import {
|
|||
isBitcoinSyncProgress,
|
||||
isPendingBackgroundProcess,
|
||||
isPendingLockBitcoinApprovalEvent,
|
||||
isPendingSeedSelectionApprovalEvent,
|
||||
PendingApprovalRequest,
|
||||
PendingLockBitcoinApprovalRequest,
|
||||
PendingSeedSelectionApprovalRequest,
|
||||
} from "models/tauriModelExt";
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||
|
|
@ -155,7 +157,9 @@ export function useNodes<T>(selector: (nodes: NodesSlice) => T): T {
|
|||
|
||||
export function usePendingApprovals(): PendingApprovalRequest[] {
|
||||
const approvals = useAppSelector((state) => state.rpc.state.approvalRequests);
|
||||
return Object.values(approvals).filter((c) => c.state === "Pending");
|
||||
return Object.values(approvals).filter(
|
||||
(c) => c.request_status.state === "Pending",
|
||||
) as PendingApprovalRequest[];
|
||||
}
|
||||
|
||||
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {
|
||||
|
|
@ -163,6 +167,11 @@ export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalReque
|
|||
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
||||
}
|
||||
|
||||
export function usePendingSeedSelectionApproval(): PendingSeedSelectionApprovalRequest[] {
|
||||
const approvals = usePendingApprovals();
|
||||
return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c));
|
||||
}
|
||||
|
||||
/// Returns all the pending background processes
|
||||
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
|
||||
export function usePendingBackgroundProcesses(): [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue