Skip to content

Commit

Permalink
Merge pull request #455 from BitGo/v1-sweep
Browse files Browse the repository at this point in the history
feat: add flow for sweeping funds from a V1 BTC wallet
  • Loading branch information
rushilbg authored Jul 18, 2024
2 parents d8539ed + b3f1b6d commit faf1a8f
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 0 deletions.
22 changes: 22 additions & 0 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,28 @@ async function createWindow() {
}
}
);

ipcMain.handle('unlock', async (event, otp) => {
const response = await sdk.unlock({ otp });
return response;
});

ipcMain.handle(
'sweepV1',
async (event, coin, parameters) => {
switch (coin) {
case 'btc':
case 'tbtc': {
const coinInstance = sdk.coin(coin) as AbstractUtxoCoin;
return await coinInstance.sweepV1(parameters);
}
default:
return new Error(
`Coin: ${coin} does not support v1 wallets sweep`
);
}
}
);
}

void app.whenReady().then(createWindow);
Expand Down
8 changes: 8 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type Commands = {
| createDotBroadcastableSweepTransactionParameters
| createSolBroadcastableSweepTransactionParameters
): Promise<Error | BroadcastableSweepTransaction>;
unlock(otp: string);
sweepV1(coin: string, parameters);
recoverConsolidations(
coin: string,
params:
Expand Down Expand Up @@ -159,6 +161,12 @@ const commands: Commands = {
parameters
);
},
unlock(otp: string) {
return ipcRenderer.invoke('unlock', otp);
},
sweepV1(coin, parameters) {
return ipcRenderer.invoke('sweepV1', coin, parameters);
},
recoverConsolidations(
coin: string,
params:
Expand Down
13 changes: 13 additions & 0 deletions src/containers/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -114,6 +115,18 @@ export default function App() {
<Route index element={<WrongChainRecovery />} />
<Route path=":coin/success" element={<SuccessfulRecovery />} />
</Route>
<Route
path="/:env/v1btc-sweep"
element={
<AuthenticatedPageLayout
Title="V1BTC Sweep"
Description="Create a full-signed sweep transaction using User and BitGo key."
/>
}
>
<Route index element={<V1BtcSweep />} />
<Route path=":coin/success" element={<SuccessfulRecovery />} />
</Route>
</Routes>
);
}
6 changes: 6 additions & 0 deletions src/containers/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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."
/>
<LinkCardItem
Tag={Link}
to={`/${env}/v1btc-sweep`}
Title="V1BTC Sweep"
Description="Create a full-signed sweep transaction using User and BitGo key."
/>
</LinkCard>
</div>
</div>
Expand Down
113 changes: 113 additions & 0 deletions src/containers/V1BtcSweep/V1BtcSweepForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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(),
otp: Yup.string().required(),
}).required();

export type V1BtcSweepFormProps = {
onSubmit: (
values: V1BtcSweepFormValues,
formikHelpers: FormikHelpers<V1BtcSweepFormValues>
) => void | Promise<void>;
};

type V1BtcSweepFormValues = Yup.Asserts<typeof validationSchema>;

export function V1BtcSweepForm({onSubmit}: V1BtcSweepFormProps) {
const formik = useFormik<V1BtcSweepFormValues>({
onSubmit,
initialValues: {
walletId: '',
walletPassphrase: '',
unspents: undefined,
destinationAddress: '',
encryptedUserKey: '',
otp: '',
},
validationSchema,
});

return (
<FormikProvider value={formik}>
<Form>
<div className="tw-mb-4">
<FormikTextfield
HelperText={`This is the wallet ID of the wallet from where to sweep.`}
Label="Wallet ID"
name="walletId"
Width="fill"
/>
</div>
<div className="tw-mb-4">
<FormikPasswordfield
HelperText={`The wallet passphrase of the wallet from where to sweep.`}
Label="Wallet Passphrase"
name="walletPassphrase"
Width="fill"
/>
</div>
<div className="tw-mb-4">
<FormikTextfield
HelperText={`The encrypted user private key for the wallet.`}
Label="Encrypted User Private Key"
name="encryptedUserKey"
Width="fill"
/>
</div>
<div className="tw-mb-4">
<FormikTextfield
HelperText={`The address your sweep transaction will send to.`}
Label="Destination Address"
name="destinationAddress"
Width="fill"
/>
</div>
<div className="tw-mb-4">
<FormikFilefield
name="tx"
accept=".json"
Width="fill"
Label="Upload unspents file"
onChange={event => {
formik
.setFieldValue('unspents', event.currentTarget.files?.[0])
.catch(console.error);
}}
/>
</div>
<div className="tw-mb-4">
<FormikTextfield
HelperText={`2FA`}
Label="2FA Code"
name="otp"
Width="fill"
/>
</div>

<div className="tw-flex tw-flex-col-reverse sm:tw-justify-between sm:tw-flex-row tw-gap-1 tw-mt-4">
<Button Tag={Link} to="/" Variant="secondary" Width="hug">
Cancel
</Button>
<Button
Variant="primary"
Width="hug"
type="submit"
Disabled={formik.isSubmitting}
disabled={formik.isSubmitting}
>
{formik.isSubmitting ? 'Sweeping...' : 'Sweep Funds'}
</Button>
</div>
</Form>
</FormikProvider>
);
}
85 changes: 85 additions & 0 deletions src/containers/V1BtcSweep/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 (
<V1BtcSweepForm
onSubmit={async (values, {setFieldError, setSubmitting}) => {
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);
await window.commands.unlock(values.otp);
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) {
setSubmitting(false);
console.log(error);
if (error instanceof Error) {
setAlert(error.message);
}
} finally {
setSubmitting(false);
}
};

}}
/>
);
}
2 changes: 2 additions & 0 deletions src/preload.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type Commands = {
| createDotBroadcastableSweepTransactionParameters
| createSolBroadcastableSweepTransactionParameters
): Promise<Error | BroadcastableSweepTransaction>;
unlock(otp: string);
sweepV1(coin: string, parameters);
recoverConsolidations(
coin: string,
params:
Expand Down

0 comments on commit faf1a8f

Please sign in to comment.