Skip to content

Commit 9b564dc

Browse files
authored
Merge pull request #7322 from BitGo/coin-6042-1
fix: handle vet token recovery in coin class
2 parents fe96f7f + 4ceb282 commit 9b564dc

File tree

3 files changed

+186
-189
lines changed

3 files changed

+186
-189
lines changed

modules/bitgo/test/v2/unit/recovery.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,7 +1502,7 @@ describe('Recovery:', function () {
15021502
});
15031503

15041504
it('should construct a token(vtho) recovery tx with MPCv2 TSS', async function () {
1505-
const basecoin = bitgo.coin('tvet:vtho');
1505+
const basecoin = bitgo.coin('tvet');
15061506
const baseAddress = ethLikeDKLSKeycard.senderAddress;
15071507
recoveryNocks.nockVetTokenRecovery(bitgo, baseAddress);
15081508
recoveryParams = {
@@ -1523,7 +1523,7 @@ describe('Recovery:', function () {
15231523

15241524
it('should construct an unsigned sweep token tx(vtho) with TSS', async function () {
15251525
recoveryNocks.nockVetTokenRecovery(bitgo, '0xad848d2c97a08b2cd5e7f28f76ecd45dd0f82e0e');
1526-
const basecoin = bitgo.coin('tvet:vtho');
1526+
const basecoin = bitgo.coin('tvet');
15271527

15281528
const unsignedSweepRecoveryParams = {
15291529
bitgoKey:
@@ -1536,7 +1536,7 @@ describe('Recovery:', function () {
15361536
should.exist(recovery);
15371537
recovery.should.have.property('txHex');
15381538
recovery.should.have.property('coin');
1539-
recovery.coin.should.equal('tvet:vtho');
1539+
recovery.coin.should.equal('tvet');
15401540
});
15411541
});
15421542
});

modules/sdk-coin-vet/src/vet.ts

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as _ from 'lodash';
22
import BigNumber from 'bignumber.js';
33
import blake2b from '@bitgo/blake2b';
4+
import assert from 'assert';
45
import axios from 'axios';
56
import { TransactionClause, Transaction as VetTransaction } from '@vechain/sdk-core';
67
import {
@@ -29,12 +30,12 @@ import {
2930
BaseBroadcastTransactionResult,
3031
} from '@bitgo/sdk-core';
3132
import * as mpc from '@bitgo/sdk-lib-mpc';
32-
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
33+
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
3334
import utils from './lib/utils';
3435
import { bip32 } from '@bitgo/secp256k1';
3536
import { randomBytes, Hash } from 'crypto';
3637
import { KeyPair as EthKeyPair } from '@bitgo/abstract-eth';
37-
import { Transaction, TransactionBuilderFactory } from './lib';
38+
import { TokenTransaction, Transaction, TransactionBuilderFactory } from './lib';
3839
import {
3940
ExplainTransactionOptions,
4041
RecoverOptions,
@@ -322,6 +323,9 @@ export class Vet extends BaseCoin {
322323

323324
/** @inheritDoc */
324325
async recover(params: RecoverOptions): Promise<RecoveryTransaction | UnsignedSweepRecoveryTransaction> {
326+
if (params.tokenContractAddress) {
327+
return this.recoverTokens(params);
328+
}
325329
try {
326330
if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
327331
throw new Error('invalid recoveryDestination');
@@ -425,7 +429,7 @@ export class Vet extends BaseCoin {
425429
* Returns the public node URL for the VeChain network.
426430
* @returns {string} The URL of the public VeChain node.
427431
*/
428-
protected getPublicNodeUrl(): string {
432+
private getPublicNodeUrl(): string {
429433
return Environments[this.bitgo.getEnv()].vetNodeUrl;
430434
}
431435

@@ -435,7 +439,7 @@ export class Vet extends BaseCoin {
435439
* @param {BigNumber} estimatedGasLimit - The estimated gas limit for the transaction.
436440
* @returns {BigNumber} The calculated transaction fee.
437441
*/
438-
protected calculateFee(feeEstimateData: FeeEstimateData, estimatedGasLimit: BigNumber): BigNumber {
442+
private calculateFee(feeEstimateData: FeeEstimateData, estimatedGasLimit: BigNumber): BigNumber {
439443
const gasLimit = estimatedGasLimit;
440444
const adjustmentFactor = new BigNumber(1).plus(
441445
new BigNumber(feeEstimateData.gasPriceCoef)
@@ -618,7 +622,7 @@ export class Vet extends BaseCoin {
618622
* @returns {Promise<Transaction>} A promise that resolves to the built recovery transaction.
619623
* @throws {Error} If there's no VET balance to recover or if there's an error building the transaction.
620624
*/
621-
protected async buildRecoveryTransaction(buildParams: {
625+
private async buildRecoveryTransaction(buildParams: {
622626
baseAddress: string;
623627
params: RecoverOptions;
624628
}): Promise<Transaction> {
@@ -665,4 +669,177 @@ export class Vet extends BaseCoin {
665669

666670
return tx;
667671
}
672+
673+
async recoverTokens(params: RecoverOptions): Promise<RecoveryTransaction | UnsignedSweepRecoveryTransaction> {
674+
try {
675+
if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
676+
throw new Error('invalid recoveryDestination');
677+
}
678+
if (!params.tokenContractAddress || !this.isValidAddress(params.tokenContractAddress)) {
679+
throw new Error('invalid tokenContractAddress');
680+
}
681+
682+
const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
683+
684+
let publicKey: string | undefined;
685+
let userKeyShare, backupKeyShare, commonKeyChain;
686+
const MPC = new Ecdsa();
687+
688+
if (isUnsignedSweep) {
689+
const bitgoKey = params.bitgoKey;
690+
if (!bitgoKey) {
691+
throw new Error('missing bitgoKey');
692+
}
693+
694+
const hdTree = new mpc.Secp256k1Bip32HdTree();
695+
const derivationPath = 'm/0';
696+
const derivedPub = hdTree.publicDerive(
697+
{
698+
pk: mpc.bigIntFromBufferBE(Buffer.from(bitgoKey.slice(0, 66), 'hex')),
699+
chaincode: mpc.bigIntFromBufferBE(Buffer.from(bitgoKey.slice(66), 'hex')),
700+
},
701+
derivationPath
702+
);
703+
704+
publicKey = mpc.bigIntToBufferBE(derivedPub.pk).toString('hex');
705+
} else {
706+
if (!params.userKey) {
707+
throw new Error('missing userKey');
708+
}
709+
710+
if (!params.backupKey) {
711+
throw new Error('missing backupKey');
712+
}
713+
714+
if (!params.walletPassphrase) {
715+
throw new Error('missing wallet passphrase');
716+
}
717+
718+
const userKey = params.userKey.replace(/\s/g, '');
719+
const backupKey = params.backupKey.replace(/\s/g, '');
720+
721+
({ userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares(
722+
userKey,
723+
backupKey,
724+
params.walletPassphrase
725+
));
726+
publicKey = MPC.deriveUnhardened(commonKeyChain, 'm/0').slice(0, 66);
727+
}
728+
729+
if (!publicKey) {
730+
throw new Error('failed to derive public key');
731+
}
732+
733+
const backupKeyPair = new EthKeyPair({ pub: publicKey });
734+
const baseAddress = backupKeyPair.getAddress();
735+
736+
const tx = await this.buildTokenRecoveryTransaction({
737+
baseAddress,
738+
params,
739+
});
740+
741+
const signableHex = await tx.signablePayload;
742+
const serializedTxHex = await tx.toBroadcastFormat();
743+
744+
if (isUnsignedSweep) {
745+
return {
746+
txHex: serializedTxHex,
747+
coin: this.getChain(),
748+
};
749+
}
750+
751+
const signableMessage = this.getHashFunction().update(signableHex).digest();
752+
753+
const signatureObj = await ECDSAUtils.signRecoveryMpcV2(
754+
signableMessage,
755+
userKeyShare,
756+
backupKeyShare,
757+
commonKeyChain
758+
);
759+
const signature = Buffer.from(signatureObj.r + signatureObj.s + (signatureObj.recid === 0 ? '00' : '01'), 'hex');
760+
const tokenTransaction = new TokenTransaction(coins.get(this.getChain()));
761+
const txBuilder = this.getTxBuilderFactory().getTokenTransactionBuilder(tokenTransaction);
762+
await txBuilder.from(serializedTxHex);
763+
txBuilder.isRecovery(true);
764+
await txBuilder.addSenderSignature(signature);
765+
766+
const signedTx = await txBuilder.build();
767+
768+
return {
769+
id: signedTx.id,
770+
tx: signedTx.toBroadcastFormat(),
771+
};
772+
} catch (error) {
773+
throw new Error(`Error during Vechain token recovery: ${error.message || error}`);
774+
}
775+
}
776+
777+
private async buildTokenRecoveryTransaction(buildParams: {
778+
baseAddress: string;
779+
params: RecoverOptions;
780+
}): Promise<Transaction> {
781+
const { baseAddress, params } = buildParams;
782+
const tokenContractAddress = params.tokenContractAddress;
783+
assert(tokenContractAddress, 'tokenContractAddress is required for token recovery');
784+
785+
const balance = await this.getBalance(baseAddress, tokenContractAddress);
786+
//replace with get balance function
787+
788+
if (balance.isLessThanOrEqualTo(0)) {
789+
throw new Error(
790+
`no token balance to recover for address ${baseAddress} contract address ${tokenContractAddress}`
791+
);
792+
}
793+
794+
// create the recipients here so that we can build the clauses for gas estimation
795+
const roughFeeEstimate = this.calculateFee(feeEstimateData, new BigNumber(51390));
796+
let recipients = [
797+
{
798+
address: params.recoveryDestination,
799+
amount: balance.minus(roughFeeEstimate).toString(),
800+
},
801+
];
802+
803+
const blockRef = await this.getBlockRef();
804+
805+
const tokenTransaction = new TokenTransaction(coins.get(this.getChain()));
806+
const txBuilder = this.getTxBuilderFactory().getTokenTransactionBuilder(tokenTransaction);
807+
808+
txBuilder.tokenAddress(tokenContractAddress);
809+
txBuilder.chainTag(this.bitgo.getEnv() === 'prod' ? 0x4a : 0x27);
810+
txBuilder.recipients(recipients);
811+
txBuilder.sender(baseAddress);
812+
txBuilder.addFeePayerAddress(baseAddress);
813+
txBuilder.gas(Number(AVG_GAS_UNITS));
814+
txBuilder.blockRef(blockRef);
815+
txBuilder.expiration(EXPIRATION);
816+
txBuilder.gasPriceCoef(Number(GAS_PRICE_COEF));
817+
txBuilder.nonce(this.getRandomNonce());
818+
txBuilder.isRecovery(true);
819+
820+
let tx = (await txBuilder.build()) as Transaction;
821+
822+
const clauses = tx.clauses;
823+
824+
const actualGasUnits = await this.estimateGas(clauses, baseAddress);
825+
826+
await this.ensureVthoBalanceForFee(baseAddress, actualGasUnits);
827+
828+
const requiredFee = this.calculateFee(feeEstimateData, actualGasUnits);
829+
830+
// create the final recipients with the fee deducted
831+
recipients = [
832+
{
833+
address: params.recoveryDestination,
834+
amount: balance.minus(requiredFee).toString(),
835+
},
836+
];
837+
838+
txBuilder.recipients(recipients);
839+
txBuilder.gas(actualGasUnits.toNumber());
840+
841+
tx = (await txBuilder.build()) as Transaction;
842+
843+
return tx;
844+
}
668845
}

0 commit comments

Comments
 (0)