From 143fecb51b6b249dc8b3f92a41591930569bbabe Mon Sep 17 00:00:00 2001 From: Rushil Kapoor Date: Tue, 2 Jul 2024 19:34:00 +0530 Subject: [PATCH 1/4] feat: add `sweepV1` function in IPC main handler BTC-1224 --- electron/main/index.ts | 19 +++++++++++++++++++ electron/preload/index.ts | 4 ++++ src/preload.d.ts | 1 + 3 files changed, 24 insertions(+) diff --git a/electron/main/index.ts b/electron/main/index.ts index d8358725..ecc68808 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -427,6 +427,25 @@ async function createWindow() { } } ); + + ipcMain.handle( + 'sweepV1', + async (event, coin, parameters) => { + switch (coin) { + case 'btc': + case 'tbtc': { + const coinInstance = sdk.coin(coin) as + | Btc + | Tbtc + return coinInstance.sweepV1(parameters); + } + default: + return new Error( + `Coin: ${coin} does not support v1 wallets sweep` + ); + } + } + ); } void app.whenReady().then(createWindow); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index c25689b8..6185ee2e 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -46,6 +46,7 @@ type Commands = { | createDotBroadcastableSweepTransactionParameters | createSolBroadcastableSweepTransactionParameters ): Promise; + sweepV1(coin: string, parameters); recoverConsolidations( coin: string, params: @@ -159,6 +160,9 @@ const commands: Commands = { parameters ); }, + sweepV1(coin, parameters) { + return ipcRenderer.invoke('sweepV1', coin, parameters); + }, recoverConsolidations( coin: string, params: diff --git a/src/preload.d.ts b/src/preload.d.ts index 0bdc58d0..b52cca6b 100644 --- a/src/preload.d.ts +++ b/src/preload.d.ts @@ -45,6 +45,7 @@ type Commands = { | createDotBroadcastableSweepTransactionParameters | createSolBroadcastableSweepTransactionParameters ): Promise; + sweepV1(coin: string, parameters); recoverConsolidations( coin: string, params: From 1e6f2ef0c69841b1fabc834a4778112ebc56018e Mon Sep 17 00:00:00 2001 From: Rushil Kapoor Date: Tue, 2 Jul 2024 19:34:28 +0530 Subject: [PATCH 2/4] feat: add flow for sweeping funds from a V1 BTC wallet Given a list of unspents to sweep. BTC-1224 --- electron/main/index.ts | 6 +- src/containers/App.tsx | 13 +++ src/containers/Home.tsx | 6 ++ src/containers/V1BtcSweep/V1BtcSweepForm.tsx | 103 +++++++++++++++++++ src/containers/V1BtcSweep/index.tsx | 84 +++++++++++++++ 5 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 src/containers/V1BtcSweep/V1BtcSweepForm.tsx create mode 100644 src/containers/V1BtcSweep/index.tsx diff --git a/electron/main/index.ts b/electron/main/index.ts index ecc68808..2ddd3835 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -434,10 +434,8 @@ async function createWindow() { switch (coin) { case 'btc': case 'tbtc': { - const coinInstance = sdk.coin(coin) as - | Btc - | Tbtc - return coinInstance.sweepV1(parameters); + const coinInstance = sdk.coin(coin) as AbstractUtxoCoin; + return await coinInstance.sweepV1(parameters); } default: return new Error( diff --git a/src/containers/App.tsx b/src/containers/App.tsx index 701cc5ad..7ba506d5 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -15,6 +15,7 @@ import { CreateBroadcastableTransactionIndex } from '~/containers/CreateBroadcas import { BroadcastTransactionIndex } from './BroadcastTransactionIndex'; import { SuccessfulBroadcastTransaction } from './SuccessfulBroadcastTransaction'; import { BroadcastTransactionCoin } from './BroadcastTransactionCoin'; +import {V1BtcSweep} from "~/containers/V1BtcSweep"; export default function App() { return ( @@ -114,6 +115,18 @@ export default function App() { } /> } /> + + } + > + } /> + } /> + ); } diff --git a/src/containers/Home.tsx b/src/containers/Home.tsx index 514ee0b5..80117a28 100644 --- a/src/containers/Home.tsx +++ b/src/containers/Home.tsx @@ -115,6 +115,12 @@ export function Home() { Title="Wrong Chain Recoveries" Description="Recover funds sent to the wrong chain, such as BTC sent to a LTC address." /> + diff --git a/src/containers/V1BtcSweep/V1BtcSweepForm.tsx b/src/containers/V1BtcSweep/V1BtcSweepForm.tsx new file mode 100644 index 00000000..e2f7d489 --- /dev/null +++ b/src/containers/V1BtcSweep/V1BtcSweepForm.tsx @@ -0,0 +1,103 @@ +import { Form, FormikHelpers, FormikProvider, useFormik } from 'formik'; +import { Link } from 'react-router-dom'; +import * as Yup from 'yup'; +import { Button, FormikPasswordfield, FormikTextfield } from '~/components'; +import { FormikFilefield } from "~/components/FormikFilefield"; + +const validationSchema = Yup.object({ + walletId: Yup.string().required(), + walletPassphrase: Yup.string().required(), + unspents: Yup.mixed().required(), + destinationAddress: Yup.string().required(), + encryptedUserKey: Yup.string().required(), +}).required(); + +export type V1BtcSweepFormProps = { + onSubmit: ( + values: V1BtcSweepFormValues, + formikHelpers: FormikHelpers + ) => void | Promise; +}; + +type V1BtcSweepFormValues = Yup.Asserts; + +export function V1BtcSweepForm({onSubmit}: V1BtcSweepFormProps) { + const formik = useFormik({ + onSubmit, + initialValues: { + walletId: '', + walletPassphrase: '', + unspents: undefined, + destinationAddress: '', + encryptedUserKey: '', + }, + validationSchema, + }); + + return ( + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ { + formik + .setFieldValue('unspents', event.currentTarget.files?.[0]) + .catch(console.error); + }} + /> +
+ +
+ + +
+
+
+ ); +} diff --git a/src/containers/V1BtcSweep/index.tsx b/src/containers/V1BtcSweep/index.tsx new file mode 100644 index 00000000..60e17d62 --- /dev/null +++ b/src/containers/V1BtcSweep/index.tsx @@ -0,0 +1,84 @@ +import {useNavigate, useParams} from 'react-router-dom'; +import {useAlertBanner} from '~/contexts'; +import {safeEnv} from '~/helpers'; +import {V1BtcSweepForm} from "~/containers/V1BtcSweep/V1BtcSweepForm"; +import { BitGoV1Unspent } from "@bitgo/abstract-utxo"; + + +export function V1BtcSweep() { + const {env} = useParams<'env'>(); + const navigate = useNavigate(); + const [_, setAlert] = useAlertBanner(); + + const environment = safeEnv(env); + const coin = env === 'test' ? 'tbtc' : 'btc'; + + return ( + { + if (!values.unspents) { + setAlert('Unspents file is required'); + return; + } + + setAlert(undefined); + const isSdkAuth = await window.queries.isSdkAuthenticated(); + if (!isSdkAuth) { + setAlert('Not logged in'); + setSubmitting(false); + navigate(`/${environment}/v1btc-sweep`); + return; + } + + const fileReader = new FileReader(); + fileReader.readAsText(values.unspents, 'UTF-8'); + fileReader.onload = async event => { + try { + const unspents = JSON.parse(event.target?.result as string) as BitGoV1Unspent[]; + setSubmitting(true); + const chainData = await window.queries.getChain(coin); + const result = await window.commands.sweepV1(coin, { + walletId: values.walletId, + walletPassphrase: values.walletPassphrase, + recoveryDestination: values.destinationAddress, + userKey: values.encryptedUserKey, + unspents, + }); + setSubmitting(false); + + const showSaveDialogData = await window.commands.showSaveDialog({ + filters: [ + { + name: 'Custom File Type', + extensions: ['json'], + }, + ], + defaultPath: `~/${chainData}-fullsigned-tx-${Date.now()}.json`, + }); + if (!showSaveDialogData.filePath) { + throw new Error('No file path selected'); + } + + await window.commands.writeFile( + showSaveDialogData.filePath, + JSON.stringify(result, null, 2), + { encoding: 'utf8' } + ); + navigate( + `/${environment}/v1btc-sweep/${coin}/success` + ); + } catch (error) { + if (error instanceof Error) { + setFieldError('unspents', error.message); + setAlert(error.message); + } else { + console.log(error); + } + } + setSubmitting(false); + }; + + }} + /> + ); +} From 3d3426dbe68c11bdedbe1a418811e4c26d37e2ad Mon Sep 17 00:00:00 2001 From: Rushil Kapoor Date: Wed, 17 Jul 2024 16:48:27 +0530 Subject: [PATCH 3/4] feat: add `unlock` function in ipc handler BTC-1236 --- electron/main/index.ts | 5 +++++ electron/preload/index.ts | 4 ++++ src/preload.d.ts | 1 + 3 files changed, 10 insertions(+) diff --git a/electron/main/index.ts b/electron/main/index.ts index 2ddd3835..95a6e358 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -428,6 +428,11 @@ async function createWindow() { } ); + ipcMain.handle('unlock', async (event, otp) => { + const response = await sdk.unlock({ otp }); + return response; + }); + ipcMain.handle( 'sweepV1', async (event, coin, parameters) => { diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 6185ee2e..46c20599 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -46,6 +46,7 @@ type Commands = { | createDotBroadcastableSweepTransactionParameters | createSolBroadcastableSweepTransactionParameters ): Promise; + unlock(otp: string); sweepV1(coin: string, parameters); recoverConsolidations( coin: string, @@ -160,6 +161,9 @@ const commands: Commands = { parameters ); }, + unlock(otp: string) { + return ipcRenderer.invoke('unlock', otp); + }, sweepV1(coin, parameters) { return ipcRenderer.invoke('sweepV1', coin, parameters); }, diff --git a/src/preload.d.ts b/src/preload.d.ts index b52cca6b..c4e9b2bf 100644 --- a/src/preload.d.ts +++ b/src/preload.d.ts @@ -45,6 +45,7 @@ type Commands = { | createDotBroadcastableSweepTransactionParameters | createSolBroadcastableSweepTransactionParameters ): Promise; + unlock(otp: string); sweepV1(coin: string, parameters); recoverConsolidations( coin: string, From b3f1b6decc213d35701fa586f581cacca58c5fbc Mon Sep 17 00:00:00 2001 From: Rushil Kapoor Date: Wed, 17 Jul 2024 16:49:55 +0530 Subject: [PATCH 4/4] feat: accept `otp` in the withdrawal form and pass it down to send API BTC-1236 --- src/containers/V1BtcSweep/V1BtcSweepForm.tsx | 10 ++++++++++ src/containers/V1BtcSweep/index.tsx | 9 +++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/containers/V1BtcSweep/V1BtcSweepForm.tsx b/src/containers/V1BtcSweep/V1BtcSweepForm.tsx index e2f7d489..4e80bbee 100644 --- a/src/containers/V1BtcSweep/V1BtcSweepForm.tsx +++ b/src/containers/V1BtcSweep/V1BtcSweepForm.tsx @@ -10,6 +10,7 @@ const validationSchema = Yup.object({ unspents: Yup.mixed().required(), destinationAddress: Yup.string().required(), encryptedUserKey: Yup.string().required(), + otp: Yup.string().required(), }).required(); export type V1BtcSweepFormProps = { @@ -30,6 +31,7 @@ export function V1BtcSweepForm({onSubmit}: V1BtcSweepFormProps) { unspents: undefined, destinationAddress: '', encryptedUserKey: '', + otp: '', }, validationSchema, }); @@ -82,6 +84,14 @@ export function V1BtcSweepForm({onSubmit}: V1BtcSweepFormProps) { }} /> +
+ +