diff --git a/electron/main/index.ts b/electron/main/index.ts index 2ba5b9b1..fa99d613 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,4 +1,4 @@ -import { TrxConsolidationRecoveryOptions } from '../types'; +import { EthRecoveryConsolidationRecoveryOptions, TrxConsolidationRecoveryOptions } from '../types'; import EthereumCommon from '@ethereumjs/common'; process.env.DIST_ELECTRON = join(__dirname, '../..'); @@ -12,7 +12,7 @@ import { AbstractUtxoCoin } from '@bitgo/abstract-utxo'; import { BitGoAPI } from '@bitgo/sdk-api'; import { Ada, Tada } from '@bitgo/sdk-coin-ada'; import { Dot, Tdot } from '@bitgo/sdk-coin-dot'; -import { AbstractEthLikeNewCoins } from '@bitgo/sdk-coin-eth'; +import { AbstractEthLikeNewCoins, Eth, Hteth } from '@bitgo/sdk-coin-eth'; import { Sol, Tsol, SolToken } from '@bitgo/sdk-coin-sol'; import { Trx, Ttrx, TrxToken } from '@bitgo/sdk-coin-trx'; import { BaseCoin } from '@bitgo/sdk-core'; @@ -270,6 +270,11 @@ async function createWindow() { params as TrxConsolidationRecoveryOptions ); } + case 'eth': + case 'hteth': { + const ethLikeCoin = sdk.coin(coin) as unknown as Eth | Hteth; + return await ethLikeCoin.recoverConsolidations(params as EthRecoveryConsolidationRecoveryOptions); + } default: throw new Error( `Coin: ${coin} does not support consolidation recovery` diff --git a/electron/preload/index.ts b/electron/preload/index.ts index e61da43e..3abd1128 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -33,6 +33,8 @@ import { SolRecoveryConsolidationRecoveryOptions, SuiRecoverConsolidationRecoveryBatch, SuiRecoveryConsolidationRecoveryOptions, + EthRecoveryConsolidationRecoveryOptions, + EthRecoverConsolidationRecoveryBatch, } from '../types'; import type * as EthLikeCommon from '@ethereumjs/common'; @@ -61,6 +63,7 @@ type Commands = { | TaoRecoveryConsolidationRecoveryOptions | SolRecoveryConsolidationRecoveryOptions | SuiRecoveryConsolidationRecoveryOptions + | EthRecoveryConsolidationRecoveryOptions ): Promise< | Error | TrxConsolidationRecoveryBatch @@ -69,6 +72,7 @@ type Commands = { | TaoRecoverConsolidationRecoveryBatch | SolRecoverConsolidationRecoveryBatch | SuiRecoverConsolidationRecoveryBatch + | EthRecoverConsolidationRecoveryBatch >; writeFile( file: string, @@ -185,6 +189,7 @@ const commands: Commands = { | DotRecoveryConsolidationRecoveryOptions | TaoRecoveryConsolidationRecoveryOptions | SolRecoveryConsolidationRecoveryOptions + | EthRecoveryConsolidationRecoveryOptions ): Promise< | Error | TrxConsolidationRecoveryBatch @@ -192,6 +197,7 @@ const commands: Commands = { | DotRecoverConsolidationRecoveryBatch | TaoRecoverConsolidationRecoveryBatch | SolRecoverConsolidationRecoveryBatch + | EthRecoverConsolidationRecoveryBatch > { return ipcRenderer.invoke('recoverConsolidations', coin, params); }, diff --git a/electron/types.ts b/electron/types.ts index 0308b811..0b73679c 100644 --- a/electron/types.ts +++ b/electron/types.ts @@ -69,6 +69,10 @@ export type SuiRecoveryConsolidationRecoveryOptions = | Parameters[0]; export type TrxConsolidationRecoveryOptions = ConsolidationRecoveryOptions; +export type EthRecoveryConsolidationRecoveryOptions = + | Parameters[0] + | Parameters[0]; + export type AdaRecoveryConsolidationRecoveryBatch = Awaited< ReturnType >; @@ -85,3 +89,7 @@ export type SuiRecoverConsolidationRecoveryBatch = Awaited< ReturnType >; export type TrxConsolidationRecoveryBatch = ConsolidationRecoveryBatch; + +export type EthRecoverConsolidationRecoveryBatch = Awaited< + ReturnType +>; diff --git a/src/containers/BuildUnsignedConsolidation/BuildUnsignedConsolidationCoin.tsx b/src/containers/BuildUnsignedConsolidation/BuildUnsignedConsolidationCoin.tsx index a86d1e8e..bb6aeec4 100644 --- a/src/containers/BuildUnsignedConsolidation/BuildUnsignedConsolidationCoin.tsx +++ b/src/containers/BuildUnsignedConsolidation/BuildUnsignedConsolidationCoin.tsx @@ -2,6 +2,8 @@ import { useNavigate, useParams } from 'react-router-dom'; import { assert, BitgoEnv, + getEip1559Params, + getEthLikeRecoveryChainId, includePubsFor, safeEnv, updateKeysFromIds, @@ -17,6 +19,7 @@ import { GenericEcdsaForm } from '~/containers/BuildUnsignedConsolidation/Generi import { SolForm } from '~/containers/BuildUnsignedConsolidation/SolForm'; import { SolTokenForm } from '~/containers/BuildUnsignedConsolidation/SolTokenForm'; import { SuiTokenForm } from '~/containers/BuildUnsignedConsolidation/SuiTokenForm'; +import { EthLikeForm } from './EthLikeForm'; type ConsolidationFormProps = { coin?: string; @@ -361,11 +364,11 @@ function ConsolidationForm({ coin, environment }: ConsolidationFormProps) { const parentCoin = tokenParentCoins[coin]; const chainData = await window.queries.getChain(parentCoin); const consolidateData = await window.commands.recoverConsolidations(parentCoin, { - ...(await updateKeysFromIds(parentCoin, values)), - bitgoKey: values.bitgoKey.replace(/\s+/g, ''), - tokenContractAddress: values.packageId, - seed: values.seed, - }); + ...(await updateKeysFromIds(parentCoin, values)), + bitgoKey: values.bitgoKey.replace(/\s+/g, ''), + tokenContractAddress: values.packageId, + seed: values.seed, + }); if (consolidateData instanceof Error) { throw consolidateData; @@ -399,6 +402,80 @@ function ConsolidationForm({ coin, environment }: ConsolidationFormProps) { console.log(e); } + setSubmitting(false); + } + }} + /> + ); + case 'eth': + case 'hteth': + return ( + { + setAlert(undefined); + setSubmitting(true); + try { + await window.commands.setBitGoEnvironment( + environment, + coin, + values.apiKey + ); + const chainData = await window.queries.getChain(coin); + + const { maxFeePerGas, maxPriorityFeePerGas, ...rest } = + await updateKeysFromIds(coin, values); + const recoverData = await window.commands.recoverConsolidations( + coin, + { + ...rest, + coinName: coin, + eip1559: getEip1559Params( + coin, + maxFeePerGas, + maxPriorityFeePerGas + ), + replayProtectionOptions: { + chain: getEthLikeRecoveryChainId(coin, environment), + hardfork: 'london', + }, + bitgoKey: '', + ignoreAddressTypes: [], + } + ); + + if (recoverData instanceof Error) { + throw recoverData; + } + + const showSaveDialogData = await window.commands.showSaveDialog({ + filters: [ + { + name: 'Custom File Type', + extensions: ['json'], + }, + ], + defaultPath: `~/${chainData}-unsigned-sweep-${Date.now()}.json`, + }); + + if (!showSaveDialogData.filePath) { + throw new Error('No file path selected'); + } + + await window.commands.writeFile( + showSaveDialogData.filePath, + JSON.stringify(recoverData, null, 2), + { encoding: 'utf-8' } + ); + + navigate(`/${environment}/build-unsigned-sweep/${coin}/success`); + } catch (err) { + if (err instanceof Error) { + setAlert(err.message); + } else { + console.error(err); + } setSubmitting(false); } }} diff --git a/src/containers/BuildUnsignedConsolidation/EthLikeForm.tsx b/src/containers/BuildUnsignedConsolidation/EthLikeForm.tsx new file mode 100644 index 00000000..f5ce6a87 --- /dev/null +++ b/src/containers/BuildUnsignedConsolidation/EthLikeForm.tsx @@ -0,0 +1,211 @@ +import { Field, Form, FormikHelpers, FormikProvider, useFormik } from 'formik'; +import { Link } from 'react-router-dom'; +import * as Yup from 'yup'; +import { Button, FormikTextfield } from '~/components'; +import { allCoinMetas } from '~/helpers/config'; + +const validationSchema = Yup.object({ + apiKey: Yup.string().required(), + backupKey: Yup.string().required(), + backupKeyId: Yup.string(), + gasLimit: Yup.number() + .typeError('Gas limit must be a number') + .integer() + .positive('Gas limit must be a positive integer') + .required(), + maxFeePerGas: Yup.number().required(), + maxPriorityFeePerGas: Yup.number().required(), + recoveryDestination: Yup.string().required(), + userKey: Yup.string().required(), + userKeyId: Yup.string(), + walletContractAddress: Yup.string().required(), + derivationSeed: Yup.string(), + isTss: Yup.boolean(), + startingScanIndex: Yup.number().required(), + endingScanIndex: Yup.number().required(), + bitgoFeeAddress: Yup.string().required(), +}).required(); + +export type EthLikeFormProps = { + coinName: string; + onSubmit: ( + values: EthLikeFormValues, + formikHelpers: FormikHelpers + ) => void | Promise; +}; + +type EthLikeFormValues = Yup.Asserts; + +export function EthLikeForm({ onSubmit, coinName }: EthLikeFormProps) { + const formik = useFormik({ + onSubmit, + initialValues: { + apiKey: '', + backupKey: '', + backupKeyId: '', + gasLimit: allCoinMetas[coinName]?.defaultGasLimitNum ?? 500000, + maxFeePerGas: 20, + maxPriorityFeePerGas: 10, + recoveryDestination: '', + userKey: '', + userKeyId: '', + walletContractAddress: '', + derivationSeed: '', + isTss: false, + startingScanIndex: 1, + endingScanIndex: 21, + bitgoFeeAddress: '', + }, + validationSchema, + }); + + return ( + +
+

+ Self-managed cold wallet details +

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ {allCoinMetas[coinName].isTssSupported && ( +
+ +
+ )} +
+ + +
+
+
+ ); +} diff --git a/src/helpers/config.ts b/src/helpers/config.ts index a9f7bee2..0d748638 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -901,6 +901,7 @@ export const buildUnsignedConsolidationCoins: Record< allCoinMetas.solToken, allCoinMetas.sui, allCoinMetas.suiToken, + allCoinMetas.eth, ], test: [ allCoinMetas.ttrx, @@ -912,6 +913,7 @@ export const buildUnsignedConsolidationCoins: Record< allCoinMetas.tsolToken, allCoinMetas.tsui, allCoinMetas.tsuiToken, + allCoinMetas.hteth, ], }; diff --git a/src/preload.d.ts b/src/preload.d.ts index 1fb1246f..264a5125 100644 --- a/src/preload.d.ts +++ b/src/preload.d.ts @@ -38,6 +38,8 @@ import { SuiRecoveryConsolidationRecoveryOptions, TrxConsolidationRecoveryBatch, TrxConsolidationRecoveryOptions, + EthRecoveryConsolidationRecoveryOptions, + EthRecoverConsolidationRecoveryBatch, } from '~/utils/types'; import type * as EthLikeCommon from '@ethereumjs/common'; import { EvmCcrNonBitgoCoinConfigType } from '~/helpers/config'; @@ -72,6 +74,7 @@ type Commands = { | TaoRecoveryConsolidationRecoveryOptions | SolRecoveryConsolidationRecoveryOptions | SuiRecoveryConsolidationRecoveryOptions + | EthRecoveryConsolidationRecoveryOptions ): Promise< | Error | TrxConsolidationRecoveryBatch @@ -80,6 +83,7 @@ type Commands = { | TaoRecoverConsolidationRecoveryBatch | SolRecoverConsolidationRecoveryBatch | SuiRecoverConsolidationRecoveryBatch + | EthRecoverConsolidationRecoveryBatch >; writeFile( file: string, @@ -187,6 +191,7 @@ const commands: Commands = { | TaoRecoveryConsolidationRecoveryOptions | SolRecoveryConsolidationRecoveryOptions | SuiRecoveryConsolidationRecoveryOptions + | EthRecoveryConsolidationRecoveryOptions ): Promise< | Error | TrxConsolidationRecoveryBatch @@ -195,6 +200,7 @@ const commands: Commands = { | TaoRecoverConsolidationRecoveryBatch | SolRecoverConsolidationRecoveryBatch | SuiRecoverConsolidationRecoveryBatch + | EthRecoveryConsolidationRecoveryOptions > { return ipcRenderer.invoke('recoverConsolidations', coin, params); }, diff --git a/src/utils/types.ts b/src/utils/types.ts index 8af02a7f..ac8b04ec 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -10,6 +10,7 @@ import { import { Hbar, Thbar } from '@bitgo/sdk-coin-hbar'; import { Algo, Talgo } from '@bitgo/sdk-coin-algo'; import { Sui, Tsui } from '@bitgo/sdk-coin-sui'; +import { Eth, Hteth } from '@bitgo/sdk-coin-eth'; export type createAdaBroadcastableSweepTransactionParameters = | Parameters[0] @@ -87,6 +88,10 @@ export type SuiRecoveryConsolidationRecoveryOptions = | Parameters[0]; export type TrxConsolidationRecoveryOptions = ConsolidationRecoveryOptions; +export type EthRecoveryConsolidationRecoveryOptions = + | Parameters[0] + | Parameters[0]; + export type AdaRecoveryConsolidationRecoveryBatch = Awaited< ReturnType >; @@ -103,3 +108,7 @@ export type SuiRecoverConsolidationRecoveryBatch = Awaited< ReturnType >; export type TrxConsolidationRecoveryBatch = ConsolidationRecoveryBatch; + +export type EthRecoverConsolidationRecoveryBatch = Awaited< + ReturnType +>;