11import * as _ from 'lodash' ;
22import BigNumber from 'bignumber.js' ;
33import blake2b from '@bitgo/blake2b' ;
4+ import assert from 'assert' ;
45import axios from 'axios' ;
56import { TransactionClause , Transaction as VetTransaction } from '@vechain/sdk-core' ;
67import {
@@ -29,12 +30,12 @@ import {
2930 BaseBroadcastTransactionResult ,
3031} from '@bitgo/sdk-core' ;
3132import * as mpc from '@bitgo/sdk-lib-mpc' ;
32- import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics' ;
33+ import { BaseCoin as StaticsBaseCoin , coins } from '@bitgo/statics' ;
3334import utils from './lib/utils' ;
3435import { bip32 } from '@bitgo/secp256k1' ;
3536import { randomBytes , Hash } from 'crypto' ;
3637import { KeyPair as EthKeyPair } from '@bitgo/abstract-eth' ;
37- import { Transaction , TransactionBuilderFactory } from './lib' ;
38+ import { TokenTransaction , Transaction , TransactionBuilderFactory } from './lib' ;
3839import {
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