From 86394491e6c7a9405fea6d4cad05edea098c0465 Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Tue, 10 Jun 2025 14:18:30 -0300 Subject: [PATCH 1/8] feat: recovery handler router on mbe --- src/masterBitgoExpress/recoveryWallet.ts | 67 +++++++++++++++++++ .../routers/masterApiSpec.ts | 56 ++++++++++++++-- 2 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 src/masterBitgoExpress/recoveryWallet.ts diff --git a/src/masterBitgoExpress/recoveryWallet.ts b/src/masterBitgoExpress/recoveryWallet.ts new file mode 100644 index 0000000..a5e3e71 --- /dev/null +++ b/src/masterBitgoExpress/recoveryWallet.ts @@ -0,0 +1,67 @@ +// export async function handleRecoveryWalletOnPrem( +// req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>, +// ) { +// console.log(req); +// } + +import { AbstractEthLikeNewCoins, RecoverOptions } from '@bitgo/abstract-eth'; +import { BitGoRequest } from '../types/request'; +import { createEnclavedExpressClient } from './enclavedExpressClient'; + +export async function handleRecoveryWalletOnPrem(req: BitGoRequest) { + const bitgo = req.bitgo; + const coin = req.params.coin; + // const { rootAddress, recoveryDestinationAddress, userPubKey, backupPubKey, coinSpecificParams } = + // req.body; + + //TODO: delete this part + const userPubKey = ''; + const backupPubKey = ''; + const apiKey = ''; + const walletContractAddress = ''; + const recoveryDestinationAddress = ''; + + const baseCoin = bitgo.coin(coin); + + const enclavedExpressClient = createEnclavedExpressClient(req.config, coin); + if (!enclavedExpressClient) { + throw new Error( + 'Enclaved express client not configured - enclaved express features will be disabled', + ); + } + + // what's this? isEVM + if (baseCoin.isEVM()) { + let sdkCoin; + //TODO: do we need this cast to call recover? + if (true) { + sdkCoin = baseCoin as unknown as AbstractEthLikeNewCoins; + // } else if (isStxCoin(baseCoin)) { + // //TODO: what's the abstract coin class for stx, eos, btc, etc? + // sdkCoin = baseCoin as unknown as AbstractStxCoin; + } else { + throw new Error('Unsupported coin type for recovery: ' + coin); + } + + // Is the other class for xtz, eos, btc ==> AbstractUtxoCoin or do we have more specialization than that? + + try { + // const { apiKey, walletContractAddress } = coinSpecificParams; + + // recover also ask for gasPrice, gasLimit, replayProtectionOptions, etc + // should we bring those from the coinSpecificParams or just let them empty? + const unsignedTx = await sdkCoin.recover({ + userKey: userPubKey, + backupKey: backupPubKey, + walletContractAddress, + recoveryDestination: recoveryDestinationAddress, + apiKey, + walletPassphrase: '^.u0UWaTI;cIx!xi9Ya1', + } as any as RecoverOptions); + console.log('unsigned tx payload'); + console.log(JSON.stringify(unsignedTx)); + } catch (err) { + console.log(err); + } + } +} diff --git a/src/masterBitgoExpress/routers/masterApiSpec.ts b/src/masterBitgoExpress/routers/masterApiSpec.ts index 4c5d5fc..8dafd47 100644 --- a/src/masterBitgoExpress/routers/masterApiSpec.ts +++ b/src/masterBitgoExpress/routers/masterApiSpec.ts @@ -1,24 +1,25 @@ -import * as t from 'io-ts'; import { apiSpec, - httpRoute, + Method as HttpMethod, httpRequest, HttpResponse, - Method as HttpMethod, + httpRoute, } from '@api-ts/io-ts-http'; +import { Response } from '@api-ts/response'; import { createRouter, TypedRequestHandler, type WrappedRouter, } from '@api-ts/typed-express-router'; -import { Response } from '@api-ts/response'; import express from 'express'; -import { BitGoRequest } from '../../types/request'; +import * as t from 'io-ts'; import { MasterExpressConfig } from '../../initConfig'; -import { handleGenerateWalletOnPrem } from '../generateWallet'; import { prepareBitGo, responseHandler } from '../../shared/middleware'; +import { BitGoRequest } from '../../types/request'; +import { handleGenerateWalletOnPrem } from '../generateWallet'; import { handleSendMany } from '../handleSendMany'; import { validateMasterExpressConfig } from '../middleware'; +import { handleRecoveryWalletOnPrem } from '../recoveryWallet'; // Middleware functions export function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) { @@ -79,6 +80,7 @@ export const SendManyRequest = { export const SendManyResponse: HttpResponse = { // TODO: Get type from public types repo / Wallet Platform + 200: t.any, 500: t.type({ error: t.string, @@ -86,6 +88,26 @@ export const SendManyResponse: HttpResponse = { }), }; +// Response type for /recovery endpoint +const RecoveryWalletResponse: HttpResponse = { + // TODO: Get type from public types repo + 200: t.any, + 500: t.type({ + error: t.string, + details: t.string, + }), +}; + +// Request type for /generate endpoint +const RecoveryWalletRequest = { + // TODO: complete the type + // label: t.string, + // multisigType: t.union([t.undefined, t.literal('onchain'), t.literal('tss')]), + // enterprise: t.string, + // disableTransactionNotifications: t.union([t.undefined, t.boolean]), + // isDistributedCustody: t.union([t.undefined, t.boolean]), +}; + // API Specification export const MasterApiSpec = apiSpec({ 'v1.wallet.generate': { @@ -117,6 +139,20 @@ export const MasterApiSpec = apiSpec({ description: 'Send many transactions', }), }, + 'v1.wallet.recovery': { + post: httpRoute({ + method: 'POST', + path: '/{coin}/wallet/recovery', + request: httpRequest({ + params: { + coin: t.string, + }, + body: RecoveryWalletRequest, + }), + response: RecoveryWalletResponse, + description: 'Recover an existing wallet', + }), + }, }); export type MasterApiSpec = typeof MasterApiSpec; @@ -161,5 +197,13 @@ export function createMasterApiRouter( }), ]); + router.post('v1.wallet.recovery', [ + responseHandler(async (req: express.Request) => { + const typedReq = req as GenericMasterApiSpecRouteRequest; + const result = await handleRecoveryWalletOnPrem(typedReq); + return Response.ok(result); + }), + ]); + return router; } From ed414f713737d6b42b631b92f6a3ee94f33434ae Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Wed, 11 Jun 2025 12:59:02 -0300 Subject: [PATCH 2/8] feat: recovery for eth and other musig2 coins wip --- .../eth-recovery-sign-half.json | 15 ++ .../musig-eth-recovery-sign-full.json | 3 + .../musig-eth-recovery-unsigned.json | 24 +++ src/masterBitgoExpress/recoveryWallet.ts | 181 ++++++++++++++---- src/shared/coinUtils.ts | 66 +++++++ 5 files changed, 253 insertions(+), 36 deletions(-) create mode 100644 mocks/wallet-recovery/eth-recovery-sign-half.json create mode 100644 mocks/wallet-recovery/musig-eth-recovery-sign-full.json create mode 100644 mocks/wallet-recovery/musig-eth-recovery-unsigned.json create mode 100644 src/shared/coinUtils.ts diff --git a/mocks/wallet-recovery/eth-recovery-sign-half.json b/mocks/wallet-recovery/eth-recovery-sign-half.json new file mode 100644 index 0000000..4d27a48 --- /dev/null +++ b/mocks/wallet-recovery/eth-recovery-sign-half.json @@ -0,0 +1,15 @@ +{ + "halfSigned": { + "recipients": [ + { + "address": "0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20", + "amount": "1000000000000000000" + } + ], + "expireTime": 1750182870, + "contractSequenceId": 1, + "operationHash": "0x92d3a28bd75dfa559c60e679b98fddfcb7dcaeb579c25cab3f9442b25fd270e2", + "signature": "0x62c594b62ce2fc9f1d2e82a105668ed53528eb02635b8ad73206fe75ed26b26923450ed54b87980296c362fe03c7bc8e156d1ab38bfe9a682ba585e7d92d88e31b", + "backupKeyNonce": 0 + } +} diff --git a/mocks/wallet-recovery/musig-eth-recovery-sign-full.json b/mocks/wallet-recovery/musig-eth-recovery-sign-full.json new file mode 100644 index 0000000..b9bc5e6 --- /dev/null +++ b/mocks/wallet-recovery/musig-eth-recovery-sign-full.json @@ -0,0 +1,3 @@ +{ + "txHex": "f901c380808094223fe2adcc8f28d8a46f72f7f355117d2727554d80b9016439125215000000000000000000000000e7d07af8e3e7472ea8391a3372ab98d04ac4df200000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000006851b0bf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041917bcebd0b1f43a25b72b161dbd4db539c282af9f3856fc60f701471f0df22e22b5428593f5affc1a88944a5c5255c6c2d0df87a0668864d551a5409ec9f82ca1c000000000000000000000000000000000000000000000000000000000000001ca0c1e0750ac2c3c1cc98997dd1a14f96f1ba65929503bbdbd96ffa00fdeea2e51fa05cca504d869dcf2935c46dc5a733c0c3d8483ce2b452cae2bf89b7417d7f80b9" +} diff --git a/mocks/wallet-recovery/musig-eth-recovery-unsigned.json b/mocks/wallet-recovery/musig-eth-recovery-unsigned.json new file mode 100644 index 0000000..a74ddd4 --- /dev/null +++ b/mocks/wallet-recovery/musig-eth-recovery-unsigned.json @@ -0,0 +1,24 @@ +{ + "tx": "f9012b808504a817c8008307a12094223fe2adcc8f28d8a46f72f7f355117d2727554d80b9010439125215000000000000000000000000e7d07af8e3e7472ea8391a3372ab98d04ac4df200000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000006851a693000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808080", + "userKey": "xpub661MyMwAqRbcFigezGWEYSbCPVuaUmvnp1u7iEpH9YsKU6uYQtPANvudjgAo82QRHXsUieMqKeB1xEj89VUKU1ugtmyAZ3xzNEbHPexxgKK", + "backupKey": "xpub661MyMwAqRbcGbCirzmQsUJT2eidt9tFLw2m77w6FiKco6TKu49CP3GkHF88xGCpvqkP93SYMAarfyWAn8UWevQtNT6pDo8xH7xmf6GqK6e", + "coin": "hteth", + "gasPrice": "20000000000", + "gasLimit": "500000", + "recipients": [ + { + "address": "0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20", + "amount": "1000000000000000000" + } + ], + "walletContractAddress": "0x223fe2adcc8f28d8a46f72f7f355117d2727554d", + "amount": "1000000000000000000", + "backupKeyNonce": 0, + "recipient": { + "address": "0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20", + "amount": "1000000000000000000" + }, + "expireTime": 1750181523, + "contractSequenceId": 1, + "nextContractSequenceId": 1 +} diff --git a/src/masterBitgoExpress/recoveryWallet.ts b/src/masterBitgoExpress/recoveryWallet.ts index a5e3e71..6dbe1fc 100644 --- a/src/masterBitgoExpress/recoveryWallet.ts +++ b/src/masterBitgoExpress/recoveryWallet.ts @@ -1,28 +1,33 @@ +// TODO: type the handler with something like this // export async function handleRecoveryWalletOnPrem( // req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>, // ) { -// console.log(req); // } -import { AbstractEthLikeNewCoins, RecoverOptions } from '@bitgo/abstract-eth'; +import { SignFinalOptions } from '@bitgo/abstract-eth'; +import { isEosCoin, isEthCoin, isStxCoin, isUtxoCoin, isXtzCoin } from '../shared/coinUtils'; import { BitGoRequest } from '../types/request'; import { createEnclavedExpressClient } from './enclavedExpressClient'; +// TODO: this is gonna be present on eve so we can remove this +const userEncryptedPrv = ''; +const backupEncryptedPrv = ''; +const passphrase = ''; +// TODO: ---end remove vars + export async function handleRecoveryWalletOnPrem(req: BitGoRequest) { const bitgo = req.bitgo; const coin = req.params.coin; - // const { rootAddress, recoveryDestinationAddress, userPubKey, backupPubKey, coinSpecificParams } = - // req.body; - //TODO: delete this part - const userPubKey = ''; - const backupPubKey = ''; - const apiKey = ''; - const walletContractAddress = ''; - const recoveryDestinationAddress = ''; + const { + userPub, + backupPub, + walletContractAddress, + recoveryDestinationAddress, + coinSpecificParams, + } = req.body; const baseCoin = bitgo.coin(coin); - const enclavedExpressClient = createEnclavedExpressClient(req.config, coin); if (!enclavedExpressClient) { throw new Error( @@ -30,38 +35,142 @@ export async function handleRecoveryWalletOnPrem(req: BitGoRequest) { ); } - // what's this? isEVM + const sdkCoin = baseCoin; + const commonRecoverParams = { + userKey: userPub, + backupKey: backupPub, + walletContractAddress, + recoveryDestination: recoveryDestinationAddress, + // TODO: add api key here, currently configured on bitgo obj + // apiKey, + }; if (baseCoin.isEVM()) { - let sdkCoin; - //TODO: do we need this cast to call recover? - if (true) { - sdkCoin = baseCoin as unknown as AbstractEthLikeNewCoins; - // } else if (isStxCoin(baseCoin)) { - // //TODO: what's the abstract coin class for stx, eos, btc, etc? - // sdkCoin = baseCoin as unknown as AbstractStxCoin; + if (isEthCoin(sdkCoin)) { + try { + // TODO: populate coinSpecificParams with things like replayProtectionOptions + // coinSpecificParams type could be "recoverOptions" + const unsignedTx = await sdkCoin.recover({ + ...commonRecoverParams, + walletPassphrase: passphrase, + }); + + const halfSignedTx = await sdkCoin.signTransaction({ + txPrebuild: { ...unsignedTx } as unknown as SignFinalOptions, + prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }), + }); + + const { halfSigned } = halfSignedTx as any; + const fullSignedTx = await sdkCoin.signTransaction({ + isLastSignature: true, + signingKeyNonce: halfSigned.signingKeyNonce ?? 0, + backupKeyNonce: halfSigned.backupKeyNonce ?? 0, + txPrebuild: { + ...halfSigned, + txHex: halfSigned.signatures, + halfSigned, + } as unknown as SignFinalOptions, + prv: bitgo.decrypt({ password: passphrase, input: backupEncryptedPrv }), + recipients: halfSigned.recipients ?? [], + walletContractAddress: walletContractAddress, + }); + } catch (err) { + console.log(err); + throw err; + } } else { throw new Error('Unsupported coin type for recovery: ' + coin); } + } else { + // TODO (can't advance): XTZ throws a method not implemented on recover. + if (isXtzCoin(sdkCoin)) { + try { + const unsignedTx = await sdkCoin.recover({ + ...commonRecoverParams, + }); + + //TODO: fill this fields, check output from recover when recover implemented on sdk for xtz + const txHex = ''; + const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; + const addressInfo = 'addressInfo' in unsignedTx ? unsignedTx.addressInfo : undefined; + const feeInfo = 'feeInfo' in unsignedTx ? unsignedTx.feeInfo : undefined; + const source = ''; + const dataToSign = ''; - // Is the other class for xtz, eos, btc ==> AbstractUtxoCoin or do we have more specialization than that? + const halfSignedTx = await sdkCoin.signTransaction({ + txPrebuild: { + txHex, + txInfo, + addressInfo, + feeInfo, + source, + dataToSign, + }, + prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }), + }); + } catch (err) { + console.log(err); + throw err; + } + } else if (isStxCoin(sdkCoin)) { + //TODO: (implementation untested): prioritize eth and btc instead of stc, when the other couple finished, go back to STX + try { + const unsignedTx = await sdkCoin.recover({ + ...commonRecoverParams, + rootAddress: walletContractAddress, // TODO: is a root address the same as wallet contract address? where does root address comes from if not? + }); + } catch (err) { + console.log(err); + throw err; + } + } else if (isEosCoin(sdkCoin)) { + // TODO (implementation untested): we need some funds but faucets not working + try { + const unsignedTx = await sdkCoin.recover({ + ...commonRecoverParams, + }); + } catch (err) { + console.log(err); + throw err; + } + } else if (isUtxoCoin(sdkCoin)) { + //TODO (implementation untested): we need an API key to complete/test btc flow + //TODO: do we need a special case for BTC or is another UTXO-based coin? - try { - // const { apiKey, walletContractAddress } = coinSpecificParams; + const { bitgoPub } = coinSpecificParams || ''; + try { + const unsignedTx = await sdkCoin.recover({ + ...commonRecoverParams, + bitgoKey: bitgoPub, + ignoreAddressTypes: coinSpecificParams?.ignoreAddressTypes || [], + }); - // recover also ask for gasPrice, gasLimit, replayProtectionOptions, etc - // should we bring those from the coinSpecificParams or just let them empty? - const unsignedTx = await sdkCoin.recover({ - userKey: userPubKey, - backupKey: backupPubKey, - walletContractAddress, - recoveryDestination: recoveryDestinationAddress, - apiKey, - walletPassphrase: '^.u0UWaTI;cIx!xi9Ya1', - } as any as RecoverOptions); - console.log('unsigned tx payload'); - console.log(JSON.stringify(unsignedTx)); - } catch (err) { - console.log(err); + // some guards as the types have some imcompatibilities issues + const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; + const txHex = 'txHex' in unsignedTx ? unsignedTx.txHex : ''; + + const halfSignedTx = await sdkCoin.signTransaction({ + txPrebuild: { + txHex, + txInfo, + }, + prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }), + }); + + const fullSignedTx = await sdkCoin.signTransaction({ + //TODO: check the body of this based on halfSignedTx output + isLastSignature: true, + txPrebuild: { + txHex, + txInfo, + }, + signingStep: 'cosignerNonce', + }); + } catch (err) { + console.log(err); + throw err; + } + } else { + throw new Error('Unsupported coin type for recovery: ' + coin); } } } diff --git a/src/shared/coinUtils.ts b/src/shared/coinUtils.ts new file mode 100644 index 0000000..5b7a69f --- /dev/null +++ b/src/shared/coinUtils.ts @@ -0,0 +1,66 @@ +import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth'; +import { BaseCoin } from 'bitgo'; +import { AbstractUtxoCoin, Eos, Stx, Xtz } from 'bitgo/dist/types/src/v2/coins'; + +export function isEthCoin(coin: BaseCoin): coin is AbstractEthLikeNewCoins { + const isEthPure = + isFamily(coin, 'eth', 'gteth') || + isFamily(coin, 'eth', 'hteth') || + isFamily(coin, 'ethw', 'tethw'); + + const isEthLike = + isFamily(coin, 'rbtc', 'trbtc') || + isFamily(coin, 'etc', 'tetc') || + isFamily(coin, 'avaxc', 'tavaxc') || + isFamily(coin, 'polygon', 'tpolygon') || + isFamily(coin, 'arbeth', 'tarbeth') || + isFamily(coin, 'opeth', 'topeth') || + isFamily(coin, 'bsc', 'tbsc') || + isFamily(coin, 'baseeth', 'tbaseeth') || + isFamily(coin, 'coredao', 'tcoredao') || + isFamily(coin, 'oas', 'toas') || + isFamily(coin, 'flr', 'tflr') || + isFamily(coin, 'sgb', 'tsgb') || + isFamily(coin, 'wemix', 'twemix') || + isFamily(coin, 'xdc', 'txdc'); + + return isEthPure || isEthLike; +} + +export function isUtxoCoin(coin: BaseCoin): coin is AbstractUtxoCoin { + // how to check if coin is UTXO? so many families + const isBtc = isFamily(coin, 'btc', 'tbtc'); + + const isBtcLike = + isFamily(coin, 'ltc', 'tltc') || + isFamily(coin, 'bch', 'tbch') || + isFamily(coin, 'zec', 'tzec') || + isFamily(coin, 'dash', 'tdash') || + isFamily(coin, 'doge', 'tdoge') || + isFamily(coin, 'btg', 'tbtg'); + + return isBtc || isBtcLike; +} + +//look for those on OVC repo +//https://github.com/BitGo/offline-vault-console/blob/7f850cdd10c89ceb850c69759349b9e0bbfb56db/frontend/src/pkg/bitgo/transaction-utils.ts#L595 +export function isEosCoin(coin: BaseCoin): coin is Eos { + return isFamily(coin, 'eos', 'teos'); +} + +export function isStxCoin(coin: BaseCoin): coin is Stx { + return isFamily(coin, 'stx', 'tstx'); +} + +export function isXtzCoin(coin: BaseCoin): coin is Xtz { + // Tezos faucet: https://faucet.ghostnet.teztnets.com/ + return isFamily(coin, 'xtz', 'txtz'); +} + +function isFamily(coin: BaseCoin, coinFamily: string, testFamily: string) { + if (!coin) { + return false; + } + const family = coin.getFamily(); + return family === coinFamily || family === testFamily; +} From efaab2b4325ddea72fea6596185499fe0a7562a6 Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Wed, 11 Jun 2025 16:09:18 -0300 Subject: [PATCH 3/8] feat: added partial typing - eve calls from mbe --- .../enclaved/recoveryMultisigTransaction.ts | 223 ++++++++++++++++++ .../routers/enclavedApiSpec.ts | 58 ++++- src/masterBitgoExpress/recoveryWallet.ts | 194 +++------------ 3 files changed, 305 insertions(+), 170 deletions(-) create mode 100644 src/api/enclaved/recoveryMultisigTransaction.ts diff --git a/src/api/enclaved/recoveryMultisigTransaction.ts b/src/api/enclaved/recoveryMultisigTransaction.ts new file mode 100644 index 0000000..019cf79 --- /dev/null +++ b/src/api/enclaved/recoveryMultisigTransaction.ts @@ -0,0 +1,223 @@ +// TODO: based on the original signMultisigTransaction.ts file. +// Added this one because things like verify transaction doesn't seems to be present during recovery (in my limited experience) +// But I want to commit something that could be fused later on with the normal signing + +import { SignFinalOptions } from '@bitgo/abstract-eth'; +import { MethodNotImplementedError } from 'bitgo'; +import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec'; +import { KmsClient } from '../../kms/kmsClient'; +import logger from '../../logger'; +import { isEosCoin, isEthCoin, isStxCoin, isUtxoCoin, isXtzCoin } from '../../shared/coinUtils'; + +export async function recoveryMultisigTransaction( + req: EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>, +): Promise { + const { + userPub, + backupPub, + walletContractAddress, + recoveryDestinationAddress, + recoveryParams, + apiKey, + } = req.body; + + //fetch prv and check that pub are valid + const userPrv = await retrieveKmsKey({ pub: userPub, source: 'user' }); + const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'user' }); + + if (!userPrv || !backupPrv) { + const errorMsg = `Error while recovery wallet, missing prv keys for user or backup on pub keys user=${userPub}, backup=${backupPub}`; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + const bitgo = req.bitgo; + const coin = bitgo.coin(req.params.coin); + + //construct a common payload for the recovery that it's repeated in any kind of recovery + const commonRecoveryParams = { + userKey: userPub, + backupKey: backupPub, + walletContractAddress, + recoveryDestination: recoveryDestinationAddress, + // TODO: api key is not used so far because of a missconfig error on the bitgo obj + apiKey, + }; + + // The signed transaction format depends on the coin type so we do this check as a guard + // If you check the type of coin before and after the "if", you may see "BaseCoin" vs "AbstractEthLikeCoin" + if (coin.isEVM()) { + // Every recovery method on every coin family varies one from another so we need to ensure with a guard. + if (isEthCoin(coin)) { + // TODO: populate coinSpecificParams with things like replayProtectionOptions + // coinSpecificParams type could be "recoverOptions" + try { + const unsignedTx = await coin.recover({ + ...commonRecoveryParams, + //TODO: it's needed for keycard debugging, the walletPassphrase + //walletPassphrase: passphrase, + }); + + const halfSignedTx = await coin.signTransaction({ + isLastSignature: false, + prv: userPrv, + txPrebuild: { ...unsignedTx } as unknown as SignFinalOptions, + }); + + const { halfSigned } = halfSignedTx as any; + const fullSignedTx = await coin.signTransaction({ + isLastSignature: true, + prv: backupPrv, + txPrebuild: { + ...halfSignedTx, + txHex: halfSigned.signatures, + halfSigned, + }, + signingKeyNonce: halfSigned.signingKeyNonce ?? 0, + backupKeyNonce: halfSigned.backupKeyNonce ?? 0, + recipients: halfSigned.recipients ?? [], + }); + + return fullSignedTx; + } catch (error) { + logger.error('error while recovering wallet transaction:', error); + throw error; + } + } else { + const errorMsg = 'Unsupported coin type for recovery: ' + req.params.coin; + logger.error(errorMsg); + throw new Error(errorMsg); + } + } else { + // TODO: from now on, this part isn't tested as we're lacking funds/apiKeys/etc + // TODO: WIP + // TODO (can't advance): XTZ throws a method not implemented on recover. + if (isXtzCoin(coin)) { + try { + const unsignedTx = await coin.recover({ + ...commonRecoveryParams, + }); + + //TODO: fill this fields, check output from recover when recover implemented on sdk for xtz + const txHex = ''; + const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; + const addressInfo = 'addressInfo' in unsignedTx ? unsignedTx.addressInfo : undefined; + const feeInfo = 'feeInfo' in unsignedTx ? unsignedTx.feeInfo : undefined; + const source = ''; + const dataToSign = ''; + + const halfSignedTx = await coin.signTransaction({ + txPrebuild: { + txHex, + txInfo, + addressInfo, + feeInfo, + source, + dataToSign, + }, + prv: userPrv, + }); + //TODO: continue with full sign and return that + // still needs to be tested in order to deduce min payload + return halfSignedTx; + } catch (err) { + console.log(err); + throw err; + } + } else if (isStxCoin(coin)) { + //TODO: (implementation untested): prioritize eth and btc instead of stc, when the other couple finished, go back to STX + try { + const unsignedTx = await coin.recover({ + ...commonRecoveryParams, + rootAddress: walletContractAddress, // TODO: is a root address the same as wallet contract address? where does root address comes from if not? + }); + //TODO: continue with half sign and return that + return unsignedTx; + } catch (err) { + console.log(err); + throw err; + } + } else if (isEosCoin(coin)) { + // TODO (implementation untested): we need some funds but faucets not working + try { + const unsignedTx = await coin.recover({ + ...commonRecoveryParams, + }); + + //TODO: continue with half sign and return that + return unsignedTx; + } catch (err) { + console.log(err); + throw err; + } + } else if (isUtxoCoin(coin)) { + //TODO (implementation untested): we need an API key to complete/test btc flow + //TODO: do we need a special case for BTC or is another UTXO-based coin? + + const { bitgoPub } = recoveryParams; + if (!bitgoPub) { + logger.error('Missing bitgoPub in recoveryParams for UTXO coin recovery'); + throw new Error('Missing bitgoPub in recoveryParams for UTXO coin recovery'); + } + try { + const unsignedTx = await coin.recover({ + ...commonRecoveryParams, + bitgoKey: bitgoPub, + ignoreAddressTypes: recoveryParams.ignoreAddressTypes || [], + }); + + // some guards as the types have some imcompatibilities issues + const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; + const txHex = 'txHex' in unsignedTx ? unsignedTx.txHex : ''; + + const halfSignedTx = await coin.signTransaction({ + txPrebuild: { + txHex, + txInfo, + }, + prv: userPrv, + }); + + const fullSignedTx = await coin.signTransaction({ + //TODO: check the body of this based on halfSignedTx output + isLastSignature: true, + txPrebuild: { + txHex, + txInfo, + }, + signingStep: 'cosignerNonce', + }); + + console.log(halfSignedTx); + throw new MethodNotImplementedError( + 'Full signing for UTXO coins is not implemented in recovery yet. Please implement it.', + ); + + return fullSignedTx; + } catch (err) { + console.log(err); + throw err; + } + } else { + throw new Error('Unsupported coin type for recovery: ' + coin); + } + } +} + +// TODO: this function is duplicated in multisigTransactioSign.ts but as hardcoded. +// move both to an utils file +async function retrieveKmsKey({ pub, source }: { pub: string; source: string }): Promise { + const kms = new KmsClient(); + // Retrieve the private key from KMS + let prv: string; + try { + const res = await kms.getKey({ pub, source }); + prv = res.prv; + return prv; + } catch (error: any) { + throw { + status: error.status || 500, + message: error.message || 'Failed to retrieve key from KMS', + }; + } +} diff --git a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts index 663c78e..e90c108 100644 --- a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts +++ b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts @@ -1,23 +1,24 @@ -import * as t from 'io-ts'; import { apiSpec, - httpRoute, + Method as HttpMethod, httpRequest, HttpResponse, - Method as HttpMethod, + httpRoute, } from '@api-ts/io-ts-http'; +import { Response } from '@api-ts/response'; import { createRouter, - type WrappedRouter, TypedRequestHandler, + type WrappedRouter, } from '@api-ts/typed-express-router'; -import { Response } from '@api-ts/response'; import express from 'express'; -import { BitGoRequest } from '../../types/request'; -import { EnclavedConfig } from '../../types'; +import * as t from 'io-ts'; import { postIndependentKey } from '../../api/enclaved/postIndependentKey'; +import { recoveryMultisigTransaction } from '../../api/enclaved/recoveryMultisigTransaction'; import { signMultisigTransaction } from '../../api/enclaved/signMultisigTransaction'; import { prepareBitGo, responseHandler } from '../../shared/middleware'; +import { EnclavedConfig } from '../../types'; +import { BitGoRequest } from '../../types/request'; // Request type for /key/independent endpoint const IndependentKeyRequest = { @@ -39,7 +40,7 @@ const IndependentKeyResponse: HttpResponse = { const SignMultisigRequest = { source: t.string, pub: t.string, - txPrebuild: t.any, // TransactionPrebuild type from BitGo + txPrebuild: t.any, }; // Response type for /multisig/sign endpoint @@ -52,6 +53,25 @@ const SignMultisigResponse: HttpResponse = { }), }; +// Request type for /multisig/recovery endpoint +const RecoveryMultisigRequest = { + userPub: t.string, + backupPub: t.string, + walletContractAddress: t.string, + recoveryDestinationAddress: t.string, + recoveryParams: t.any, // TODO: add more precise typing +}; + +// Response type for /multisig/recovery endpoint +const RecoveryMultisigResponse: HttpResponse = { + // TODO: Define proper response type for recovery multisig transaction + 200: t.any, // the full signed tx + 500: t.type({ + error: t.string, + details: t.string, + }), +}; + // API Specification export const EnclavedAPiSpec = apiSpec({ 'v1.multisig.sign': { @@ -68,6 +88,20 @@ export const EnclavedAPiSpec = apiSpec({ description: 'Sign a multisig transaction', }), }, + 'v1.multisig.recovery': { + post: httpRoute({ + method: 'POST', + path: '/{coin}/multisig/recovery', + request: httpRequest({ + params: { + coin: t.string, + }, + body: RecoveryMultisigRequest, + }), + response: RecoveryMultisigResponse, + description: 'Recover a multisig transaction', + }), + }, 'v1.key.independent': { post: httpRoute({ method: 'POST', @@ -121,5 +155,13 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter(async (req) => { + const typedReq = req as EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>; + const result = await recoveryMultisigTransaction(typedReq); + return Response.ok(result); + }), + ]); + return router; } diff --git a/src/masterBitgoExpress/recoveryWallet.ts b/src/masterBitgoExpress/recoveryWallet.ts index 6dbe1fc..2db45c1 100644 --- a/src/masterBitgoExpress/recoveryWallet.ts +++ b/src/masterBitgoExpress/recoveryWallet.ts @@ -1,33 +1,16 @@ -// TODO: type the handler with something like this -// export async function handleRecoveryWalletOnPrem( -// req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>, -// ) { -// } - -import { SignFinalOptions } from '@bitgo/abstract-eth'; -import { isEosCoin, isEthCoin, isStxCoin, isUtxoCoin, isXtzCoin } from '../shared/coinUtils'; -import { BitGoRequest } from '../types/request'; +import assert from 'assert'; +import { isMasterExpressConfig } from '../types'; import { createEnclavedExpressClient } from './enclavedExpressClient'; +import { MasterApiSpecRouteRequest } from './routers/masterApiSpec'; -// TODO: this is gonna be present on eve so we can remove this -const userEncryptedPrv = ''; -const backupEncryptedPrv = ''; -const passphrase = ''; -// TODO: ---end remove vars - -export async function handleRecoveryWalletOnPrem(req: BitGoRequest) { - const bitgo = req.bitgo; +export async function handleRecoveryWalletOnPrem( + req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>, +) { const coin = req.params.coin; - - const { - userPub, - backupPub, - walletContractAddress, - recoveryDestinationAddress, - coinSpecificParams, - } = req.body; - - const baseCoin = bitgo.coin(coin); + assert( + isMasterExpressConfig(req.config), + 'Expected req.config to be of type MasterExpressConfig', + ); const enclavedExpressClient = createEnclavedExpressClient(req.config, coin); if (!enclavedExpressClient) { throw new Error( @@ -35,142 +18,29 @@ export async function handleRecoveryWalletOnPrem(req: BitGoRequest) { ); } - const sdkCoin = baseCoin; - const commonRecoverParams = { - userKey: userPub, - backupKey: backupPub, + const { + userPub, + backupPub, walletContractAddress, - recoveryDestination: recoveryDestinationAddress, - // TODO: add api key here, currently configured on bitgo obj - // apiKey, - }; - if (baseCoin.isEVM()) { - if (isEthCoin(sdkCoin)) { - try { - // TODO: populate coinSpecificParams with things like replayProtectionOptions - // coinSpecificParams type could be "recoverOptions" - const unsignedTx = await sdkCoin.recover({ - ...commonRecoverParams, - walletPassphrase: passphrase, - }); - - const halfSignedTx = await sdkCoin.signTransaction({ - txPrebuild: { ...unsignedTx } as unknown as SignFinalOptions, - prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }), - }); - - const { halfSigned } = halfSignedTx as any; - const fullSignedTx = await sdkCoin.signTransaction({ - isLastSignature: true, - signingKeyNonce: halfSigned.signingKeyNonce ?? 0, - backupKeyNonce: halfSigned.backupKeyNonce ?? 0, - txPrebuild: { - ...halfSigned, - txHex: halfSigned.signatures, - halfSigned, - } as unknown as SignFinalOptions, - prv: bitgo.decrypt({ password: passphrase, input: backupEncryptedPrv }), - recipients: halfSigned.recipients ?? [], - walletContractAddress: walletContractAddress, - }); - } catch (err) { - console.log(err); - throw err; - } - } else { - throw new Error('Unsupported coin type for recovery: ' + coin); - } - } else { - // TODO (can't advance): XTZ throws a method not implemented on recover. - if (isXtzCoin(sdkCoin)) { - try { - const unsignedTx = await sdkCoin.recover({ - ...commonRecoverParams, - }); - - //TODO: fill this fields, check output from recover when recover implemented on sdk for xtz - const txHex = ''; - const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; - const addressInfo = 'addressInfo' in unsignedTx ? unsignedTx.addressInfo : undefined; - const feeInfo = 'feeInfo' in unsignedTx ? unsignedTx.feeInfo : undefined; - const source = ''; - const dataToSign = ''; - - const halfSignedTx = await sdkCoin.signTransaction({ - txPrebuild: { - txHex, - txInfo, - addressInfo, - feeInfo, - source, - dataToSign, - }, - prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }), - }); - } catch (err) { - console.log(err); - throw err; - } - } else if (isStxCoin(sdkCoin)) { - //TODO: (implementation untested): prioritize eth and btc instead of stc, when the other couple finished, go back to STX - try { - const unsignedTx = await sdkCoin.recover({ - ...commonRecoverParams, - rootAddress: walletContractAddress, // TODO: is a root address the same as wallet contract address? where does root address comes from if not? - }); - } catch (err) { - console.log(err); - throw err; - } - } else if (isEosCoin(sdkCoin)) { - // TODO (implementation untested): we need some funds but faucets not working - try { - const unsignedTx = await sdkCoin.recover({ - ...commonRecoverParams, - }); - } catch (err) { - console.log(err); - throw err; - } - } else if (isUtxoCoin(sdkCoin)) { - //TODO (implementation untested): we need an API key to complete/test btc flow - //TODO: do we need a special case for BTC or is another UTXO-based coin? - - const { bitgoPub } = coinSpecificParams || ''; - try { - const unsignedTx = await sdkCoin.recover({ - ...commonRecoverParams, - bitgoKey: bitgoPub, - ignoreAddressTypes: coinSpecificParams?.ignoreAddressTypes || [], - }); - - // some guards as the types have some imcompatibilities issues - const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; - const txHex = 'txHex' in unsignedTx ? unsignedTx.txHex : ''; - - const halfSignedTx = await sdkCoin.signTransaction({ - txPrebuild: { - txHex, - txInfo, - }, - prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }), - }); + recoveryDestinationAddress, + recoveryParams, + apiKey, + } = req.body; - const fullSignedTx = await sdkCoin.signTransaction({ - //TODO: check the body of this based on halfSignedTx output - isLastSignature: true, - txPrebuild: { - txHex, - txInfo, - }, - signingStep: 'cosignerNonce', - }); - } catch (err) { - console.log(err); - throw err; - } - } else { - throw new Error('Unsupported coin type for recovery: ' + coin); - } + try { + const fullSignedRecoveryTx = await enclavedExpressClient.multisigRecovery({ + coin, + userPub, + backupPub, + walletContractAddress, + recoveryDestination: recoveryDestinationAddress, + apiKey, + recoveryParams, + }); + + return fullSignedRecoveryTx; + } catch (err) { + //TODO: check other error handling for ref on mbe + throw err; } } From 3642a2a35df0007626ca3269f1c9eda4484f4bee Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Thu, 12 Jun 2025 10:30:11 -0300 Subject: [PATCH 4/8] feat: call route handler on eve client for recovery --- .../enclaved/recoveryMultisigTransaction.ts | 4 -- .../enclavedExpressClient.ts | 42 ++++++++++++++++--- src/masterBitgoExpress/recoveryWallet.ts | 5 +-- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/api/enclaved/recoveryMultisigTransaction.ts b/src/api/enclaved/recoveryMultisigTransaction.ts index 019cf79..77c238b 100644 --- a/src/api/enclaved/recoveryMultisigTransaction.ts +++ b/src/api/enclaved/recoveryMultisigTransaction.ts @@ -1,7 +1,3 @@ -// TODO: based on the original signMultisigTransaction.ts file. -// Added this one because things like verify transaction doesn't seems to be present during recovery (in my limited experience) -// But I want to commit something that could be fused later on with the normal signing - import { SignFinalOptions } from '@bitgo/abstract-eth'; import { MethodNotImplementedError } from 'bitgo'; import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec'; diff --git a/src/masterBitgoExpress/enclavedExpressClient.ts b/src/masterBitgoExpress/enclavedExpressClient.ts index 0b95695..059b274 100644 --- a/src/masterBitgoExpress/enclavedExpressClient.ts +++ b/src/masterBitgoExpress/enclavedExpressClient.ts @@ -1,9 +1,8 @@ -import superagent from 'superagent'; -import https from 'https'; -import debug from 'debug'; -import { MasterExpressConfig } from '../types'; -import { TlsMode } from '../types'; import { SignedTransaction, TransactionPrebuild } from '@bitgo/sdk-core'; +import debug from 'debug'; +import https from 'https'; +import superagent from 'superagent'; +import { MasterExpressConfig, TlsMode } from '../types'; const debugLogger = debug('bitgo:express:enclavedExpressClient'); @@ -29,6 +28,18 @@ interface SignMultisigOptions { pub: string; } +interface RecoveryMultisigOptions { + userPub: string; + backupPub: string; + walletContractAddress: string; + recoveryDestinationAddress: string; + apiKey: string; + recoveryParams?: { + bitgoPub?: string; + ignoreAddressTypes: string[]; + }; +} + export class EnclavedExpressClient { private readonly baseUrl: string; private readonly enclavedExpressCert: string; @@ -134,6 +145,27 @@ export class EnclavedExpressClient { throw err; } } + + /** + * Recovery a multisig transaction + */ + async recoveryMultisig(params: RecoveryMultisigOptions): Promise { + if (!this.coin) { + throw new Error('Coin must be specified to recovery a multisig'); + } + + try { + const res = await this.configureRequest( + superagent.post(`${this.baseUrl}/api/${this.coin}/multisig/recovery`).type('json'), + ).send(params); + + return res.body; + } catch (error) { + const err = error as Error; + debugLogger('Failed to recover multisig: %s', err.message); + throw err; + } + } } /** diff --git a/src/masterBitgoExpress/recoveryWallet.ts b/src/masterBitgoExpress/recoveryWallet.ts index 2db45c1..fcaad59 100644 --- a/src/masterBitgoExpress/recoveryWallet.ts +++ b/src/masterBitgoExpress/recoveryWallet.ts @@ -28,12 +28,11 @@ export async function handleRecoveryWalletOnPrem( } = req.body; try { - const fullSignedRecoveryTx = await enclavedExpressClient.multisigRecovery({ - coin, + const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({ userPub, backupPub, walletContractAddress, - recoveryDestination: recoveryDestinationAddress, + recoveryDestinationAddress, apiKey, recoveryParams, }); From f84a7996f765412bfc871042e6c89de69b6bbeb9 Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Thu, 12 Jun 2025 11:09:06 -0300 Subject: [PATCH 5/8] feat: typed mbe recovery endpoint req params --- .../enclaved/recoveryMultisigTransaction.ts | 170 ++---------------- src/api/enclaved/utils.ts | 23 +++ .../routers/enclavedApiSpec.ts | 13 +- .../enclavedExpressClient.ts | 10 +- src/masterBitgoExpress/recoveryWallet.ts | 51 ++++-- .../routers/masterApiSpec.ts | 21 ++- 6 files changed, 99 insertions(+), 189 deletions(-) create mode 100644 src/api/enclaved/utils.ts diff --git a/src/api/enclaved/recoveryMultisigTransaction.ts b/src/api/enclaved/recoveryMultisigTransaction.ts index 77c238b..6576963 100644 --- a/src/api/enclaved/recoveryMultisigTransaction.ts +++ b/src/api/enclaved/recoveryMultisigTransaction.ts @@ -1,25 +1,17 @@ import { SignFinalOptions } from '@bitgo/abstract-eth'; -import { MethodNotImplementedError } from 'bitgo'; import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec'; -import { KmsClient } from '../../kms/kmsClient'; import logger from '../../logger'; -import { isEosCoin, isEthCoin, isStxCoin, isUtxoCoin, isXtzCoin } from '../../shared/coinUtils'; +import { isEthCoin } from '../../shared/coinUtils'; +import { retrieveKmsKey } from './utils'; export async function recoveryMultisigTransaction( req: EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>, ): Promise { - const { - userPub, - backupPub, - walletContractAddress, - recoveryDestinationAddress, - recoveryParams, - apiKey, - } = req.body; + const { userPub, backupPub, unsignedSweepPrebuildTx, walletContractAddress } = req.body; //fetch prv and check that pub are valid const userPrv = await retrieveKmsKey({ pub: userPub, source: 'user' }); - const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'user' }); + const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'backup' }); if (!userPrv || !backupPrv) { const errorMsg = `Error while recovery wallet, missing prv keys for user or backup on pub keys user=${userPub}, backup=${backupPub}`; @@ -30,16 +22,6 @@ export async function recoveryMultisigTransaction( const bitgo = req.bitgo; const coin = bitgo.coin(req.params.coin); - //construct a common payload for the recovery that it's repeated in any kind of recovery - const commonRecoveryParams = { - userKey: userPub, - backupKey: backupPub, - walletContractAddress, - recoveryDestination: recoveryDestinationAddress, - // TODO: api key is not used so far because of a missconfig error on the bitgo obj - apiKey, - }; - // The signed transaction format depends on the coin type so we do this check as a guard // If you check the type of coin before and after the "if", you may see "BaseCoin" vs "AbstractEthLikeCoin" if (coin.isEVM()) { @@ -48,16 +30,11 @@ export async function recoveryMultisigTransaction( // TODO: populate coinSpecificParams with things like replayProtectionOptions // coinSpecificParams type could be "recoverOptions" try { - const unsignedTx = await coin.recover({ - ...commonRecoveryParams, - //TODO: it's needed for keycard debugging, the walletPassphrase - //walletPassphrase: passphrase, - }); - const halfSignedTx = await coin.signTransaction({ isLastSignature: false, prv: userPrv, - txPrebuild: { ...unsignedTx } as unknown as SignFinalOptions, + txPrebuild: { ...unsignedSweepPrebuildTx } as unknown as SignFinalOptions, + walletContractAddress, }); const { halfSigned } = halfSignedTx as any; @@ -68,7 +45,9 @@ export async function recoveryMultisigTransaction( ...halfSignedTx, txHex: halfSigned.signatures, halfSigned, - }, + recipients: halfSigned.recipients ?? [], + } as unknown as SignFinalOptions, + walletContractAddress, signingKeyNonce: halfSigned.signingKeyNonce ?? 0, backupKeyNonce: halfSigned.backupKeyNonce ?? 0, recipients: halfSigned.recipients ?? [], @@ -85,135 +64,6 @@ export async function recoveryMultisigTransaction( throw new Error(errorMsg); } } else { - // TODO: from now on, this part isn't tested as we're lacking funds/apiKeys/etc - // TODO: WIP - // TODO (can't advance): XTZ throws a method not implemented on recover. - if (isXtzCoin(coin)) { - try { - const unsignedTx = await coin.recover({ - ...commonRecoveryParams, - }); - - //TODO: fill this fields, check output from recover when recover implemented on sdk for xtz - const txHex = ''; - const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; - const addressInfo = 'addressInfo' in unsignedTx ? unsignedTx.addressInfo : undefined; - const feeInfo = 'feeInfo' in unsignedTx ? unsignedTx.feeInfo : undefined; - const source = ''; - const dataToSign = ''; - - const halfSignedTx = await coin.signTransaction({ - txPrebuild: { - txHex, - txInfo, - addressInfo, - feeInfo, - source, - dataToSign, - }, - prv: userPrv, - }); - //TODO: continue with full sign and return that - // still needs to be tested in order to deduce min payload - return halfSignedTx; - } catch (err) { - console.log(err); - throw err; - } - } else if (isStxCoin(coin)) { - //TODO: (implementation untested): prioritize eth and btc instead of stc, when the other couple finished, go back to STX - try { - const unsignedTx = await coin.recover({ - ...commonRecoveryParams, - rootAddress: walletContractAddress, // TODO: is a root address the same as wallet contract address? where does root address comes from if not? - }); - //TODO: continue with half sign and return that - return unsignedTx; - } catch (err) { - console.log(err); - throw err; - } - } else if (isEosCoin(coin)) { - // TODO (implementation untested): we need some funds but faucets not working - try { - const unsignedTx = await coin.recover({ - ...commonRecoveryParams, - }); - - //TODO: continue with half sign and return that - return unsignedTx; - } catch (err) { - console.log(err); - throw err; - } - } else if (isUtxoCoin(coin)) { - //TODO (implementation untested): we need an API key to complete/test btc flow - //TODO: do we need a special case for BTC or is another UTXO-based coin? - - const { bitgoPub } = recoveryParams; - if (!bitgoPub) { - logger.error('Missing bitgoPub in recoveryParams for UTXO coin recovery'); - throw new Error('Missing bitgoPub in recoveryParams for UTXO coin recovery'); - } - try { - const unsignedTx = await coin.recover({ - ...commonRecoveryParams, - bitgoKey: bitgoPub, - ignoreAddressTypes: recoveryParams.ignoreAddressTypes || [], - }); - - // some guards as the types have some imcompatibilities issues - const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined; - const txHex = 'txHex' in unsignedTx ? unsignedTx.txHex : ''; - - const halfSignedTx = await coin.signTransaction({ - txPrebuild: { - txHex, - txInfo, - }, - prv: userPrv, - }); - - const fullSignedTx = await coin.signTransaction({ - //TODO: check the body of this based on halfSignedTx output - isLastSignature: true, - txPrebuild: { - txHex, - txInfo, - }, - signingStep: 'cosignerNonce', - }); - - console.log(halfSignedTx); - throw new MethodNotImplementedError( - 'Full signing for UTXO coins is not implemented in recovery yet. Please implement it.', - ); - - return fullSignedTx; - } catch (err) { - console.log(err); - throw err; - } - } else { - throw new Error('Unsupported coin type for recovery: ' + coin); - } - } -} - -// TODO: this function is duplicated in multisigTransactioSign.ts but as hardcoded. -// move both to an utils file -async function retrieveKmsKey({ pub, source }: { pub: string; source: string }): Promise { - const kms = new KmsClient(); - // Retrieve the private key from KMS - let prv: string; - try { - const res = await kms.getKey({ pub, source }); - prv = res.prv; - return prv; - } catch (error: any) { - throw { - status: error.status || 500, - message: error.message || 'Failed to retrieve key from KMS', - }; + throw new Error('Unsupported coin type for recovery: ' + coin); } } diff --git a/src/api/enclaved/utils.ts b/src/api/enclaved/utils.ts new file mode 100644 index 0000000..8dd1241 --- /dev/null +++ b/src/api/enclaved/utils.ts @@ -0,0 +1,23 @@ +// TODO: this function is duplicated in multisigTransactioSign.ts but as hardcoded. Replace that code later with this call (to avoid merge conflicts/duplication) +import { KmsClient } from '../../kms/kmsClient'; +export async function retrieveKmsKey({ + pub, + source, +}: { + pub: string; + source: string; +}): Promise { + const kms = new KmsClient(); + // Retrieve the private key from KMS + let prv: string; + try { + const res = await kms.getKey({ pub, source }); + prv = res.prv; + return prv; + } catch (error: any) { + throw { + status: error.status || 500, + message: error.message || 'Failed to retrieve key from KMS', + }; + } +} diff --git a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts index e90c108..11ee96a 100644 --- a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts +++ b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts @@ -57,9 +57,16 @@ const SignMultisigResponse: HttpResponse = { const RecoveryMultisigRequest = { userPub: t.string, backupPub: t.string, - walletContractAddress: t.string, - recoveryDestinationAddress: t.string, - recoveryParams: t.any, // TODO: add more precise typing + apiKey: t.string, + // TODO: best typing for this, they come from sdk TS types + unsignedSweepPrebuildTx: t.any, + coinSpecificParams: t.union([ + t.undefined, + t.partial({ + bitgoPub: t.union([t.undefined, t.string]), + ignoreAddressTypes: t.union([t.undefined, t.array(t.string)]), + }), + ]), }; // Response type for /multisig/recovery endpoint diff --git a/src/masterBitgoExpress/enclavedExpressClient.ts b/src/masterBitgoExpress/enclavedExpressClient.ts index 059b274..bcd76e4 100644 --- a/src/masterBitgoExpress/enclavedExpressClient.ts +++ b/src/masterBitgoExpress/enclavedExpressClient.ts @@ -1,3 +1,4 @@ +import { OfflineVaultTxInfo, RecoveryInfo, UnsignedSweepTxMPCv2 } from '@bitgo/sdk-coin-eth'; import { SignedTransaction, TransactionPrebuild } from '@bitgo/sdk-core'; import debug from 'debug'; import https from 'https'; @@ -31,13 +32,14 @@ interface SignMultisigOptions { interface RecoveryMultisigOptions { userPub: string; backupPub: string; - walletContractAddress: string; - recoveryDestinationAddress: string; + unsignedSweepPrebuildTx: RecoveryInfo | OfflineVaultTxInfo | UnsignedSweepTxMPCv2; apiKey: string; - recoveryParams?: { + walletContractAddress: string; + coinSpecificParams?: { bitgoPub?: string; - ignoreAddressTypes: string[]; + ignoreAddressTypes?: string[]; }; + // recoveryDestinationAddress: string; } export class EnclavedExpressClient { diff --git a/src/masterBitgoExpress/recoveryWallet.ts b/src/masterBitgoExpress/recoveryWallet.ts index fcaad59..4b29cea 100644 --- a/src/masterBitgoExpress/recoveryWallet.ts +++ b/src/masterBitgoExpress/recoveryWallet.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import { isEthCoin } from '../shared/coinUtils'; import { isMasterExpressConfig } from '../types'; import { createEnclavedExpressClient } from './enclavedExpressClient'; import { MasterApiSpecRouteRequest } from './routers/masterApiSpec'; @@ -6,6 +7,7 @@ import { MasterApiSpecRouteRequest } from './routers/masterApiSpec'; export async function handleRecoveryWalletOnPrem( req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>, ) { + const bitgo = req.bitgo; const coin = req.params.coin; assert( isMasterExpressConfig(req.config), @@ -23,23 +25,44 @@ export async function handleRecoveryWalletOnPrem( backupPub, walletContractAddress, recoveryDestinationAddress, - recoveryParams, + coinSpecificParams, apiKey, } = req.body; - try { - const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({ - userPub, - backupPub, - walletContractAddress, - recoveryDestinationAddress, - apiKey, - recoveryParams, - }); + //construct a common payload for the recovery that it's repeated in any kind of recovery + const commonRecoveryParams = { + userKey: userPub, + backupKey: backupPub, + walletContractAddress, + recoveryDestination: recoveryDestinationAddress, + apiKey, + }; + + const sdkCoin = bitgo.coin(coin); + + if (isEthCoin(sdkCoin)) { + try { + const unsignedSweepPrebuildTx = await sdkCoin.recover({ + ...commonRecoveryParams, + //TODO: DELETE. it's needed for keycard debugging, the walletPassphrase + //walletPassphrase: passphrase, + }); + const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({ + userPub, + backupPub, + apiKey, + unsignedSweepPrebuildTx, + coinSpecificParams, + walletContractAddress, + // recoveryDestinationAddress, + }); - return fullSignedRecoveryTx; - } catch (err) { - //TODO: check other error handling for ref on mbe - throw err; + return fullSignedRecoveryTx; + } catch (err) { + //TODO: check other error handling for ref on mbe + throw err; + } + } else { + throw new Error('Recovery wallet is not supported for this coin: ' + coin); } } diff --git a/src/masterBitgoExpress/routers/masterApiSpec.ts b/src/masterBitgoExpress/routers/masterApiSpec.ts index 8dafd47..8fdf709 100644 --- a/src/masterBitgoExpress/routers/masterApiSpec.ts +++ b/src/masterBitgoExpress/routers/masterApiSpec.ts @@ -80,7 +80,6 @@ export const SendManyRequest = { export const SendManyResponse: HttpResponse = { // TODO: Get type from public types repo / Wallet Platform - 200: t.any, 500: t.type({ error: t.string, @@ -98,14 +97,20 @@ const RecoveryWalletResponse: HttpResponse = { }), }; -// Request type for /generate endpoint +// Request type for /recovery endpoint const RecoveryWalletRequest = { - // TODO: complete the type - // label: t.string, - // multisigType: t.union([t.undefined, t.literal('onchain'), t.literal('tss')]), - // enterprise: t.string, - // disableTransactionNotifications: t.union([t.undefined, t.boolean]), - // isDistributedCustody: t.union([t.undefined, t.boolean]), + userPub: t.string, + backupPub: t.string, + walletContractAddress: t.string, + recoveryDestinationAddress: t.string, + apiKey: t.string, + coinSpecificParams: t.union([ + t.undefined, + t.partial({ + bitgoPub: t.union([t.undefined, t.string]), + ignoreAddressTypes: t.union([t.undefined, t.array(t.string)]), + }), + ]), }; // API Specification From 0ec107ab768abbd3fc91de1ab83286042576cbbe Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Fri, 13 Jun 2025 11:18:23 -0300 Subject: [PATCH 6/8] feat: eth like coins family and follow up feedback --- .../enclaved/recoveryMultisigTransaction.ts | 11 +-- src/masterBitgoExpress/recoveryWallet.ts | 14 ++-- src/shared/coinUtils.ts | 69 ++++++++----------- 3 files changed, 42 insertions(+), 52 deletions(-) diff --git a/src/api/enclaved/recoveryMultisigTransaction.ts b/src/api/enclaved/recoveryMultisigTransaction.ts index 6576963..553fe15 100644 --- a/src/api/enclaved/recoveryMultisigTransaction.ts +++ b/src/api/enclaved/recoveryMultisigTransaction.ts @@ -1,7 +1,8 @@ import { SignFinalOptions } from '@bitgo/abstract-eth'; +import { MethodNotImplementedError } from 'bitgo'; import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec'; import logger from '../../logger'; -import { isEthCoin } from '../../shared/coinUtils'; +import { isEthLikeCoin } from '../../shared/coinUtils'; import { retrieveKmsKey } from './utils'; export async function recoveryMultisigTransaction( @@ -20,13 +21,13 @@ export async function recoveryMultisigTransaction( } const bitgo = req.bitgo; - const coin = bitgo.coin(req.params.coin); + const coin = bitgo.coin(req.decoded.coin); // The signed transaction format depends on the coin type so we do this check as a guard // If you check the type of coin before and after the "if", you may see "BaseCoin" vs "AbstractEthLikeCoin" if (coin.isEVM()) { // Every recovery method on every coin family varies one from another so we need to ensure with a guard. - if (isEthCoin(coin)) { + if (isEthLikeCoin(coin)) { // TODO: populate coinSpecificParams with things like replayProtectionOptions // coinSpecificParams type could be "recoverOptions" try { @@ -59,11 +60,11 @@ export async function recoveryMultisigTransaction( throw error; } } else { - const errorMsg = 'Unsupported coin type for recovery: ' + req.params.coin; + const errorMsg = 'Unsupported coin type for recovery: ' + req.decoded.coin; logger.error(errorMsg); throw new Error(errorMsg); } } else { - throw new Error('Unsupported coin type for recovery: ' + coin); + throw new MethodNotImplementedError('Unsupported coin type for recovery: ' + coin); } } diff --git a/src/masterBitgoExpress/recoveryWallet.ts b/src/masterBitgoExpress/recoveryWallet.ts index 4b29cea..d1aee0a 100644 --- a/src/masterBitgoExpress/recoveryWallet.ts +++ b/src/masterBitgoExpress/recoveryWallet.ts @@ -1,5 +1,6 @@ import assert from 'assert'; -import { isEthCoin } from '../shared/coinUtils'; +import { MethodNotImplementedError } from 'bitgo'; +import { isEthLikeCoin } from '../shared/coinUtils'; import { isMasterExpressConfig } from '../types'; import { createEnclavedExpressClient } from './enclavedExpressClient'; import { MasterApiSpecRouteRequest } from './routers/masterApiSpec'; @@ -8,7 +9,7 @@ export async function handleRecoveryWalletOnPrem( req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>, ) { const bitgo = req.bitgo; - const coin = req.params.coin; + const coin = req.decoded.coin; assert( isMasterExpressConfig(req.config), 'Expected req.config to be of type MasterExpressConfig', @@ -27,7 +28,7 @@ export async function handleRecoveryWalletOnPrem( recoveryDestinationAddress, coinSpecificParams, apiKey, - } = req.body; + } = req.decoded; //construct a common payload for the recovery that it's repeated in any kind of recovery const commonRecoveryParams = { @@ -40,12 +41,10 @@ export async function handleRecoveryWalletOnPrem( const sdkCoin = bitgo.coin(coin); - if (isEthCoin(sdkCoin)) { + if (isEthLikeCoin(sdkCoin)) { try { const unsignedSweepPrebuildTx = await sdkCoin.recover({ ...commonRecoveryParams, - //TODO: DELETE. it's needed for keycard debugging, the walletPassphrase - //walletPassphrase: passphrase, }); const fullSignedRecoveryTx = await enclavedExpressClient.recoveryMultisig({ userPub, @@ -59,10 +58,9 @@ export async function handleRecoveryWalletOnPrem( return fullSignedRecoveryTx; } catch (err) { - //TODO: check other error handling for ref on mbe throw err; } } else { - throw new Error('Recovery wallet is not supported for this coin: ' + coin); + throw new MethodNotImplementedError('Recovery wallet is not supported for this coin: ' + coin); } } diff --git a/src/shared/coinUtils.ts b/src/shared/coinUtils.ts index 5b7a69f..c3822cb 100644 --- a/src/shared/coinUtils.ts +++ b/src/shared/coinUtils.ts @@ -1,66 +1,57 @@ import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth'; +import { CoinFamily } from '@bitgo/statics'; import { BaseCoin } from 'bitgo'; import { AbstractUtxoCoin, Eos, Stx, Xtz } from 'bitgo/dist/types/src/v2/coins'; -export function isEthCoin(coin: BaseCoin): coin is AbstractEthLikeNewCoins { - const isEthPure = - isFamily(coin, 'eth', 'gteth') || - isFamily(coin, 'eth', 'hteth') || - isFamily(coin, 'ethw', 'tethw'); +export function isEthLikeCoin(coin: BaseCoin): coin is AbstractEthLikeNewCoins { + const isEthPure = isFamily(coin, CoinFamily.ETH); const isEthLike = - isFamily(coin, 'rbtc', 'trbtc') || - isFamily(coin, 'etc', 'tetc') || - isFamily(coin, 'avaxc', 'tavaxc') || - isFamily(coin, 'polygon', 'tpolygon') || - isFamily(coin, 'arbeth', 'tarbeth') || - isFamily(coin, 'opeth', 'topeth') || - isFamily(coin, 'bsc', 'tbsc') || - isFamily(coin, 'baseeth', 'tbaseeth') || - isFamily(coin, 'coredao', 'tcoredao') || - isFamily(coin, 'oas', 'toas') || - isFamily(coin, 'flr', 'tflr') || - isFamily(coin, 'sgb', 'tsgb') || - isFamily(coin, 'wemix', 'twemix') || - isFamily(coin, 'xdc', 'txdc'); + isFamily(coin, CoinFamily.ETHW) || // ethw has its own family. as the others + isFamily(coin, CoinFamily.RBTC) || + isFamily(coin, CoinFamily.ETC) || + isFamily(coin, CoinFamily.AVAXC) || + isFamily(coin, CoinFamily.POLYGON) || + isFamily(coin, CoinFamily.ARBETH) || + isFamily(coin, CoinFamily.OPETH) || + isFamily(coin, CoinFamily.BSC) || + isFamily(coin, CoinFamily.BASEETH) || + isFamily(coin, CoinFamily.COREDAO) || + isFamily(coin, CoinFamily.OAS) || + isFamily(coin, CoinFamily.FLR) || + isFamily(coin, CoinFamily.SGB) || + isFamily(coin, CoinFamily.WEMIX) || + isFamily(coin, CoinFamily.XDC); return isEthPure || isEthLike; } export function isUtxoCoin(coin: BaseCoin): coin is AbstractUtxoCoin { - // how to check if coin is UTXO? so many families - const isBtc = isFamily(coin, 'btc', 'tbtc'); + const isBtc = isFamily(coin, CoinFamily.BTC); const isBtcLike = - isFamily(coin, 'ltc', 'tltc') || - isFamily(coin, 'bch', 'tbch') || - isFamily(coin, 'zec', 'tzec') || - isFamily(coin, 'dash', 'tdash') || - isFamily(coin, 'doge', 'tdoge') || - isFamily(coin, 'btg', 'tbtg'); + isFamily(coin, CoinFamily.LTC) || + isFamily(coin, CoinFamily.BCH) || + isFamily(coin, CoinFamily.ZEC) || + isFamily(coin, CoinFamily.DASH) || + isFamily(coin, CoinFamily.DASH) || + isFamily(coin, CoinFamily.BTG); return isBtc || isBtcLike; } -//look for those on OVC repo -//https://github.com/BitGo/offline-vault-console/blob/7f850cdd10c89ceb850c69759349b9e0bbfb56db/frontend/src/pkg/bitgo/transaction-utils.ts#L595 export function isEosCoin(coin: BaseCoin): coin is Eos { - return isFamily(coin, 'eos', 'teos'); + return isFamily(coin, CoinFamily.EOS); } export function isStxCoin(coin: BaseCoin): coin is Stx { - return isFamily(coin, 'stx', 'tstx'); + return isFamily(coin, CoinFamily.STX); } export function isXtzCoin(coin: BaseCoin): coin is Xtz { - // Tezos faucet: https://faucet.ghostnet.teztnets.com/ - return isFamily(coin, 'xtz', 'txtz'); + return isFamily(coin, CoinFamily.XTZ); } -function isFamily(coin: BaseCoin, coinFamily: string, testFamily: string) { - if (!coin) { - return false; - } - const family = coin.getFamily(); - return family === coinFamily || family === testFamily; +function isFamily(coin: BaseCoin, family: CoinFamily) { + return Boolean(coin && coin.getFamily() === family); } From a5ff857cdcd28c99ac77d93bb00a34063e48ec2d Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Fri, 13 Jun 2025 11:41:36 -0300 Subject: [PATCH 7/8] fix: missing cfg prop on kms client call post rebase master --- src/api/enclaved/recoveryMultisigTransaction.ts | 4 ++-- src/api/enclaved/utils.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api/enclaved/recoveryMultisigTransaction.ts b/src/api/enclaved/recoveryMultisigTransaction.ts index 553fe15..9d69444 100644 --- a/src/api/enclaved/recoveryMultisigTransaction.ts +++ b/src/api/enclaved/recoveryMultisigTransaction.ts @@ -11,8 +11,8 @@ export async function recoveryMultisigTransaction( const { userPub, backupPub, unsignedSweepPrebuildTx, walletContractAddress } = req.body; //fetch prv and check that pub are valid - const userPrv = await retrieveKmsKey({ pub: userPub, source: 'user' }); - const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'backup' }); + const userPrv = await retrieveKmsKey({ pub: userPub, source: 'user', cfg: req.config }); + const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'backup', cfg: req.config }); if (!userPrv || !backupPrv) { const errorMsg = `Error while recovery wallet, missing prv keys for user or backup on pub keys user=${userPub}, backup=${backupPub}`; diff --git a/src/api/enclaved/utils.ts b/src/api/enclaved/utils.ts index 8dd1241..ac4d1ae 100644 --- a/src/api/enclaved/utils.ts +++ b/src/api/enclaved/utils.ts @@ -1,13 +1,16 @@ // TODO: this function is duplicated in multisigTransactioSign.ts but as hardcoded. Replace that code later with this call (to avoid merge conflicts/duplication) import { KmsClient } from '../../kms/kmsClient'; +import { EnclavedConfig } from '../../types'; export async function retrieveKmsKey({ pub, source, + cfg, }: { pub: string; source: string; + cfg: EnclavedConfig; }): Promise { - const kms = new KmsClient(); + const kms = new KmsClient(cfg); // Retrieve the private key from KMS let prv: string; try { From bbf2bf4b84cc153557e9297a983610a3fd73728d Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Tue, 17 Jun 2025 11:38:06 -0300 Subject: [PATCH 8/8] feat: behavior adjustments based on reviews --- src/api/enclaved/recoveryMultisigTransaction.ts | 2 -- src/masterBitgoExpress/enclavedExpressClient.ts | 5 ++--- src/masterBitgoExpress/recoveryWallet.ts | 15 +-------------- src/masterBitgoExpress/routers/masterApiSpec.ts | 2 +- src/shared/coinUtils.ts | 1 - 5 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/api/enclaved/recoveryMultisigTransaction.ts b/src/api/enclaved/recoveryMultisigTransaction.ts index 9d69444..bd6c3f3 100644 --- a/src/api/enclaved/recoveryMultisigTransaction.ts +++ b/src/api/enclaved/recoveryMultisigTransaction.ts @@ -28,8 +28,6 @@ export async function recoveryMultisigTransaction( if (coin.isEVM()) { // Every recovery method on every coin family varies one from another so we need to ensure with a guard. if (isEthLikeCoin(coin)) { - // TODO: populate coinSpecificParams with things like replayProtectionOptions - // coinSpecificParams type could be "recoverOptions" try { const halfSignedTx = await coin.signTransaction({ isLastSignature: false, diff --git a/src/masterBitgoExpress/enclavedExpressClient.ts b/src/masterBitgoExpress/enclavedExpressClient.ts index bcd76e4..64d2331 100644 --- a/src/masterBitgoExpress/enclavedExpressClient.ts +++ b/src/masterBitgoExpress/enclavedExpressClient.ts @@ -39,7 +39,6 @@ interface RecoveryMultisigOptions { bitgoPub?: string; ignoreAddressTypes?: string[]; }; - // recoveryDestinationAddress: string; } export class EnclavedExpressClient { @@ -149,11 +148,11 @@ export class EnclavedExpressClient { } /** - * Recovery a multisig transaction + * Recover a multisig transaction */ async recoveryMultisig(params: RecoveryMultisigOptions): Promise { if (!this.coin) { - throw new Error('Coin must be specified to recovery a multisig'); + throw new Error('Coin must be specified to recover a multisig'); } try { diff --git a/src/masterBitgoExpress/recoveryWallet.ts b/src/masterBitgoExpress/recoveryWallet.ts index d1aee0a..3052c8f 100644 --- a/src/masterBitgoExpress/recoveryWallet.ts +++ b/src/masterBitgoExpress/recoveryWallet.ts @@ -1,8 +1,5 @@ -import assert from 'assert'; import { MethodNotImplementedError } from 'bitgo'; import { isEthLikeCoin } from '../shared/coinUtils'; -import { isMasterExpressConfig } from '../types'; -import { createEnclavedExpressClient } from './enclavedExpressClient'; import { MasterApiSpecRouteRequest } from './routers/masterApiSpec'; export async function handleRecoveryWalletOnPrem( @@ -10,16 +7,7 @@ export async function handleRecoveryWalletOnPrem( ) { const bitgo = req.bitgo; const coin = req.decoded.coin; - assert( - isMasterExpressConfig(req.config), - 'Expected req.config to be of type MasterExpressConfig', - ); - const enclavedExpressClient = createEnclavedExpressClient(req.config, coin); - if (!enclavedExpressClient) { - throw new Error( - 'Enclaved express client not configured - enclaved express features will be disabled', - ); - } + const enclavedExpressClient = req.enclavedExpressClient; const { userPub, @@ -53,7 +41,6 @@ export async function handleRecoveryWalletOnPrem( unsignedSweepPrebuildTx, coinSpecificParams, walletContractAddress, - // recoveryDestinationAddress, }); return fullSignedRecoveryTx; diff --git a/src/masterBitgoExpress/routers/masterApiSpec.ts b/src/masterBitgoExpress/routers/masterApiSpec.ts index 8fdf709..db8b248 100644 --- a/src/masterBitgoExpress/routers/masterApiSpec.ts +++ b/src/masterBitgoExpress/routers/masterApiSpec.ts @@ -147,7 +147,7 @@ export const MasterApiSpec = apiSpec({ 'v1.wallet.recovery': { post: httpRoute({ method: 'POST', - path: '/{coin}/wallet/recovery', + path: '/api/{coin}/wallet/recovery', request: httpRequest({ params: { coin: t.string, diff --git a/src/shared/coinUtils.ts b/src/shared/coinUtils.ts index c3822cb..8a37a60 100644 --- a/src/shared/coinUtils.ts +++ b/src/shared/coinUtils.ts @@ -34,7 +34,6 @@ export function isUtxoCoin(coin: BaseCoin): coin is AbstractUtxoCoin { isFamily(coin, CoinFamily.BCH) || isFamily(coin, CoinFamily.ZEC) || isFamily(coin, CoinFamily.DASH) || - isFamily(coin, CoinFamily.DASH) || isFamily(coin, CoinFamily.BTG); return isBtc || isBtcLike;