From 934fc0b477061ea8bcf1668ae01a4b818cc08daa Mon Sep 17 00:00:00 2001 From: Aditya Tiwari Date: Thu, 23 Oct 2025 02:47:24 +0530 Subject: [PATCH] feat: add unsigned sweep logic for vet Ticket: COIN-6042 --- modules/bitgo/test/v2/lib/recovery-nocks.ts | 10 ++-- modules/bitgo/test/v2/unit/recovery.ts | 61 ++++++--------------- modules/sdk-coin-vet/src/lib/types.ts | 6 +- modules/sdk-coin-vet/src/vet.ts | 26 ++++++++- 4 files changed, 50 insertions(+), 53 deletions(-) diff --git a/modules/bitgo/test/v2/lib/recovery-nocks.ts b/modules/bitgo/test/v2/lib/recovery-nocks.ts index e7a1e272c3..ef74b91b18 100644 --- a/modules/bitgo/test/v2/lib/recovery-nocks.ts +++ b/modules/bitgo/test/v2/lib/recovery-nocks.ts @@ -416,10 +416,10 @@ module.exports.nockEthLikeRecovery = function (bitgo, nockData = nockEthData) { }); }; -module.exports.nockVetRecovery = function (bitgo) { +module.exports.nockVetRecovery = function (bitgo, baseAddress) { // nock for account balance const url = Environments[bitgo.getEnv()].vetNodeUrl; - nock(url).get('/accounts/0x88c2ab227908d39f6afdb85203dca3e937bb77af').reply(200, { + nock(url).get(`/accounts/${baseAddress}`).reply(200, { balance: '0x8ac7230489e80000', energy: '0x5969b539627800', hasCode: false, @@ -456,7 +456,7 @@ module.exports.nockVetRecovery = function (bitgo) { data: '0x', }, ], - caller: '0x88c2ab227908d39f6afdb85203dca3e937bb77af', + caller: `${baseAddress}`, }) .reply(200, [ { @@ -464,7 +464,7 @@ module.exports.nockVetRecovery = function (bitgo) { events: [], transfers: [ { - sender: '0x880ff4718587d678e78fc7803b3634bd12ecf019', + sender: `${baseAddress}`, recipient: '0xac05da78464520aa7c9d4c19bd7a440b111b3054', amount: '0x8ac7230489e80000', }, @@ -481,7 +481,7 @@ module.exports.nockVetRecovery = function (bitgo) { { to: '0x0000000000000000000000000000456E65726779', value: '0x0', - data: '0x70a0823100000000000000000000000088c2ab227908d39f6afdb85203dca3e937bb77af', + data: `0x70a08231000000000000000000000000${baseAddress.slice(2)}`, }, ], }) diff --git a/modules/bitgo/test/v2/unit/recovery.ts b/modules/bitgo/test/v2/unit/recovery.ts index 94f3329a4b..3329ce1e46 100644 --- a/modules/bitgo/test/v2/unit/recovery.ts +++ b/modules/bitgo/test/v2/unit/recovery.ts @@ -1465,9 +1465,9 @@ describe('Recovery:', function () { let recoveryParams; it('should construct a recovery tx with MPCv2 TSS', async function () { - recoveryNocks.nockVetRecovery(bitgo); const basecoin = bitgo.coin('tvet'); const baseAddress = ethLikeDKLSKeycard.senderAddress; + recoveryNocks.nockVetRecovery(bitgo, baseAddress); recoveryParams = { userKey: ethLikeDKLSKeycard.userKey, backupKey: ethLikeDKLSKeycard.backupKey, @@ -1484,48 +1484,21 @@ describe('Recovery:', function () { recovery.should.have.property('tx'); }); - // it('should construct an unsigned sweep tx with TSS', async function () { - // recoveryNocks.nockEthLikeRecovery(bitgo, nockUnsignedSweepTSSData); - // - // const basecoin = bitgo.coin('hteth'); - // - // const userKey = - // '0234eb39b22fed523ece7c78da29ba1f1de5b64a6e48013e0914de793bc1df0570e779de04758732734d97e54b782c8b336283811af6a2c57bd81438798e1c2446'; - // const backupKey = - // '0234eb39b22fed523ece7c78da29ba1f1de5b64a6e48013e0914de793bc1df0570e779de04758732734d97e54b782c8b336283811af6a2c57bd81438798e1c2446'; - // - // recoveryParams = { - // userKey: userKey, - // backupKey: backupKey, - // walletContractAddress: '0xe7406dc43d13f698fb41a345c7783d39a4c2d191', - // recoveryDestination: '0xac05da78464520aa7c9d4c19bd7a440b111b3054', - // walletPassphrase: TestBitGo.V2.TEST_RECOVERY_PASSCODE, - // isTss: true, - // gasPrice: '20000000000', - // gasLimit: '500000', - // replayProtectionOptions: { - // chain: 42, - // hardfork: 'london', - // }, - // }; - // - // const transaction = await basecoin.recover(recoveryParams); - // should.exist(transaction); - // const output = transaction as unknown as UnsignedSweepTxMPCv2; - // output.should.have.property('txRequests'); - // output.txRequests.should.have.length(1); - // output.txRequests[0].should.have.property('transactions'); - // output.txRequests[0].transactions.should.have.length(1); - // output.txRequests[0].should.have.property('walletCoin'); - // output.txRequests[0].transactions[0].should.have.property('unsignedTx'); - // output.txRequests[0].transactions[0].unsignedTx.should.have.property('serializedTxHex'); - // output.txRequests[0].transactions[0].unsignedTx.should.have.property('signableHex'); - // output.txRequests[0].transactions[0].unsignedTx.should.have.property('derivationPath'); - // output.txRequests[0].transactions[0].unsignedTx.should.have.property('feeInfo'); - // output.txRequests[0].transactions[0].unsignedTx.should.have.property('parsedTx'); - // const parsedTx = output.txRequests[0].transactions[0].unsignedTx.parsedTx as { spendAmount: string }; - // parsedTx.should.have.property('spendAmount'); - // (output.txRequests[0].transactions[0].unsignedTx.parsedTx as { outputs: any[] }).should.have.property('outputs'); - // }); + it('should construct an unsigned sweep tx with TSS', async function () { + recoveryNocks.nockVetRecovery(bitgo, '0xad848d2c97a08b2cd5e7f28f76ecd45dd0f82e0e'); + const basecoin = bitgo.coin('tvet'); + + const unsignedSweepRecoveryParams = { + bitgoKey: + '03f54983c529802697d9a2320ded23eb7f15118fcba01156356c2264f04d32b4caa77fcf8cf3f73547078e984f28787c4c1e694586214b609e45b6de9cc32ad6e5', + recoveryDestination: ethLikeDKLSKeycard.destinationAddress, + }; + + const recovery = await basecoin.recover(unsignedSweepRecoveryParams); + should.exist(recovery); + recovery.should.have.property('txHex'); + recovery.should.have.property('coin'); + recovery.coin.should.equal('tvet'); + }); }); }); diff --git a/modules/sdk-coin-vet/src/lib/types.ts b/modules/sdk-coin-vet/src/lib/types.ts index ac04838487..7f87275b1b 100644 --- a/modules/sdk-coin-vet/src/lib/types.ts +++ b/modules/sdk-coin-vet/src/lib/types.ts @@ -17,12 +17,12 @@ export interface ClaimRewardsData { } export type RecoverOptions = { - userKey: string; - backupKey: string; + userKey?: string; + backupKey?: string; walletPassphrase?: string; - walletContractAddress: string; // use this as walletBaseAddress for TSS recoveryDestination: string; isUnsignedSweep?: boolean; // specify if this is an unsigned recovery + bitgoKey?: string; }; export interface RecoveryTransaction { diff --git a/modules/sdk-coin-vet/src/vet.ts b/modules/sdk-coin-vet/src/vet.ts index cd4f2b328e..3cde6e7592 100644 --- a/modules/sdk-coin-vet/src/vet.ts +++ b/modules/sdk-coin-vet/src/vet.ts @@ -43,6 +43,7 @@ import { } from './lib/types'; import { VetTransactionExplanation } from './lib/iface'; import { AVG_GAS_UNITS, COEF_DIVISOR, EXPIRATION, GAS_PRICE_COEF, GAS_UNIT_PRICE } from './lib/constants'; +import * as mpc from '@bitgo/sdk-lib-mpc'; interface FeeEstimateData { gas: string; @@ -331,7 +332,22 @@ export class Vet extends BaseCoin { const MPC = new Ecdsa(); if (isUnsignedSweep) { - throw new Error('Unsigned sweep recovery is not supported for VET'); + const bitgoKey = params.bitgoKey; + if (!bitgoKey) { + throw new Error('missing bitgoKey'); + } + + const hdTree = new mpc.Secp256k1Bip32HdTree(); + const derivationPath = 'm/0'; + const derivedPub = hdTree.publicDerive( + { + pk: mpc.bigIntFromBufferBE(Buffer.from(bitgoKey.slice(0, 66), 'hex')), + chaincode: mpc.bigIntFromBufferBE(Buffer.from(bitgoKey.slice(66), 'hex')), + }, + derivationPath + ); + + publicKey = mpc.bigIntToBufferBE(derivedPub.pk).toString('hex'); } else { if (!params.userKey) { throw new Error('missing userKey'); @@ -370,6 +386,14 @@ export class Vet extends BaseCoin { const signableHex = await tx.signablePayload; const serializedTxHex = await tx.toBroadcastFormat(); + + if (isUnsignedSweep) { + return { + txHex: serializedTxHex, + coin: this.getChain(), + }; + } + const signableMessage = this.getHashFunction().update(signableHex).digest(); const signatureObj = await ECDSAUtils.signRecoveryMpcV2(