diff --git a/modules/bitgo/test/v2/lib/recovery-nocks.ts b/modules/bitgo/test/v2/lib/recovery-nocks.ts index d6d8570a88..ef74b91b18 100644 --- a/modules/bitgo/test/v2/lib/recovery-nocks.ts +++ b/modules/bitgo/test/v2/lib/recovery-nocks.ts @@ -416,6 +416,96 @@ module.exports.nockEthLikeRecovery = function (bitgo, nockData = nockEthData) { }); }; +module.exports.nockVetRecovery = function (bitgo, baseAddress) { + // nock for account balance + const url = Environments[bitgo.getEnv()].vetNodeUrl; + nock(url).get(`/accounts/${baseAddress}`).reply(200, { + balance: '0x8ac7230489e80000', + energy: '0x5969b539627800', + hasCode: false, + }); + + nock(url).get('/blocks/best').reply(200, { + number: 23107826, + id: '0x016098f2a6779c3ad2bb52ef0a3f57c770af55a77bfa1b2837266f752118ad8d', + size: 368, + parentID: '0x016098f1acffb0125ffeca9b3e2491d31574d14b55a15e912e45e8081e063e0e', + timestamp: 1761116630, + gasLimit: 40000000, + beneficiary: '0xae99cb89767a09d53e589a40cb4016974aba4b94', + gasUsed: 0, + totalScore: 218523577, + txsRoot: '0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', + txsFeatures: 1, + stateRoot: '0x7a5e7b3b8b89958e7fdd5e14acbc79dbc419672e84d02376a43b3beebe555e33', + receiptsRoot: '0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', + com: true, + signer: '0xae99cb89767a09d53e589a40cb4016974aba4b94', + isTrunk: true, + isFinalized: false, + baseFeePerGas: '0x9184e72a000', + transactions: [], + }); + + nock(url) + .post('/accounts/*', { + clauses: [ + { + to: '0xac05da78464520aa7c9d4c19bd7a440b111b3054', + value: '10000000000000000000', + data: '0x', + }, + ], + caller: `${baseAddress}`, + }) + .reply(200, [ + { + data: '0x', + events: [], + transfers: [ + { + sender: `${baseAddress}`, + recipient: '0xac05da78464520aa7c9d4c19bd7a440b111b3054', + amount: '0x8ac7230489e80000', + }, + ], + gasUsed: 0, + reverted: false, + vmError: '', + }, + ]); + // nock for vtho balance for gas + nock(url) + .post('/accounts/*', { + clauses: [ + { + to: '0x0000000000000000000000000000456E65726779', + value: '0x0', + data: `0x70a08231000000000000000000000000${baseAddress.slice(2)}`, + }, + ], + }) + .reply(200, [ + { + data: '0x000000000000000000000000000000000000000000000007e982789f8fe0cf0a', + events: [], + transfers: [], + gasUsed: 870, + reverted: false, + vmError: '', + }, + ]); + + nock(url) + .post('/transactions', { + raw: /^0x[0-9a-f]+$/i, + }) + .reply(200, { + id: '0x' + 'a'.repeat(64), // A fake transaction ID + reverted: false, + }); +}; + module.exports.nockEtherscanRateLimitError = function () { const response = { status: '0', diff --git a/modules/bitgo/test/v2/unit/recovery.ts b/modules/bitgo/test/v2/unit/recovery.ts index 10bacabdd4..3329ce1e46 100644 --- a/modules/bitgo/test/v2/unit/recovery.ts +++ b/modules/bitgo/test/v2/unit/recovery.ts @@ -1457,4 +1457,48 @@ describe('Recovery:', function () { (output.txRequests[0].transactions[0].unsignedTx.parsedTx as { outputs: any[] }).should.have.property('outputs'); }); }); + + describe('RecoverVet', function () { + beforeEach(() => { + nock.cleanAll(); + }); + let recoveryParams; + + it('should construct a recovery tx with MPCv2 TSS', async function () { + const basecoin = bitgo.coin('tvet'); + const baseAddress = ethLikeDKLSKeycard.senderAddress; + recoveryNocks.nockVetRecovery(bitgo, baseAddress); + recoveryParams = { + userKey: ethLikeDKLSKeycard.userKey, + backupKey: ethLikeDKLSKeycard.backupKey, + walletContractAddress: baseAddress, + recoveryDestination: ethLikeDKLSKeycard.destinationAddress, + walletPassphrase: ethLikeDKLSKeycard.walletPassphrase, + isTss: true, + }; + + const recovery = await basecoin.recover(recoveryParams); + + should.exist(recovery); + recovery.should.have.property('id'); + recovery.should.have.property('tx'); + }); + + 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/package.json b/modules/sdk-coin-vet/package.json index 7fb731e043..69dbf014c5 100644 --- a/modules/sdk-coin-vet/package.json +++ b/modules/sdk-coin-vet/package.json @@ -45,6 +45,7 @@ "@bitgo/sdk-core": "^36.15.0", "@bitgo/secp256k1": "^1.6.0", "@bitgo/statics": "^58.7.0", + "@bitgo/sdk-lib-mpc": "^10.8.1", "@noble/curves": "1.8.1", "@vechain/sdk-core": "^1.2.0-rc.3", "bignumber.js": "^9.1.1", diff --git a/modules/sdk-coin-vet/src/lib/constants.ts b/modules/sdk-coin-vet/src/lib/constants.ts index bb2bf298ca..c9349a1250 100644 --- a/modules/sdk-coin-vet/src/lib/constants.ts +++ b/modules/sdk-coin-vet/src/lib/constants.ts @@ -15,3 +15,9 @@ export const STARGATE_DELEGATION_ADDRESS = '0x4cb1c9ef05b529c093371264fab2c93cc6 export const STARGATE_NFT_ADDRESS_TESTNET = '0x1ec1d168574603ec35b9d229843b7c2b44bcb770'; export const STARGATE_DELEGATION_ADDRESS_TESTNET = '0x7240e3bc0d26431512d5b67dbd26d199205bffe8'; + +export const AVG_GAS_UNITS = '21000'; +export const EXPIRATION = 400; +export const GAS_PRICE_COEF = '128'; +export const GAS_UNIT_PRICE = '10000000000000'; // vechain has fixed gas unit price of 10^13 wei +export const COEF_DIVISOR = '255'; diff --git a/modules/sdk-coin-vet/src/lib/transaction/transaction.ts b/modules/sdk-coin-vet/src/lib/transaction/transaction.ts index eba7cb7ef9..34860fee9f 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/transaction.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/transaction.ts @@ -32,6 +32,7 @@ export class Transaction extends BaseTransaction { private _senderSignature: Buffer | null; private _feePayerAddress: string; private _feePayerSignature: Buffer | null; + private _isRecovery: boolean; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -47,6 +48,7 @@ export class Transaction extends BaseTransaction { this._recipients = []; this._senderSignature = null; this._feePayerSignature = null; + this._isRecovery = false; } public get id(): string { @@ -198,6 +200,14 @@ export class Transaction extends BaseTransaction { this._transactionData = transactionData; } + get isRecovery(): boolean { + return this._isRecovery; + } + + set isRecovery(isRecovery: boolean) { + this._isRecovery = isRecovery; + } + /** * Get all signatures associated with this transaction * Required by BaseTransaction @@ -337,6 +347,9 @@ export class Transaction extends BaseTransaction { if (halfSignedTransaction.signature) { this._rawTransaction = halfSignedTransaction; this._sender = halfSignedTransaction.origin.toString().toLowerCase(); + if (this.isRecovery) { + this._id = halfSignedTransaction.id.toString(); + } } else { return; } @@ -372,7 +385,7 @@ export class Transaction extends BaseTransaction { }; if ( - this.type === TransactionType.Send || + (this.type === TransactionType.Send && !this.isRecovery) || this.type === TransactionType.SendToken || this.type === TransactionType.SendNFT || this.type === TransactionType.ContractCall || diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/transactionBuilder.ts index 1b1a57d593..6afacec71f 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/transactionBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/transactionBuilder.ts @@ -45,6 +45,11 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { return this; } + isRecovery(isRecovery: boolean): this { + this._transaction.isRecovery = isRecovery; + return this; + } + /** * Sets the sender of this transaction. * diff --git a/modules/sdk-coin-vet/src/lib/types.ts b/modules/sdk-coin-vet/src/lib/types.ts index 6cd98044dc..7f87275b1b 100644 --- a/modules/sdk-coin-vet/src/lib/types.ts +++ b/modules/sdk-coin-vet/src/lib/types.ts @@ -15,3 +15,22 @@ export interface ClaimRewardsData { claimBaseRewards?: boolean; claimStakingRewards?: boolean; } + +export type RecoverOptions = { + userKey?: string; + backupKey?: string; + walletPassphrase?: string; + recoveryDestination: string; + isUnsignedSweep?: boolean; // specify if this is an unsigned recovery + bitgoKey?: string; +}; + +export interface RecoveryTransaction { + id: string; + tx: string; +} + +export interface UnsignedSweepRecoveryTransaction { + txHex: string; + coin: string; +} diff --git a/modules/sdk-coin-vet/src/vet.ts b/modules/sdk-coin-vet/src/vet.ts index 9ca8eca7e5..7a96980a37 100644 --- a/modules/sdk-coin-vet/src/vet.ts +++ b/modules/sdk-coin-vet/src/vet.ts @@ -1,6 +1,8 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; import blake2b from '@bitgo/blake2b'; +import axios from 'axios'; +import { TransactionClause, Transaction as VetTransaction } from '@vechain/sdk-core'; import { AuditDecryptedKeyParams, BaseCoin, @@ -20,15 +22,42 @@ import { VerifyAddressOptions, VerifyTransactionOptions, TokenType, + Ecdsa, + ECDSAUtils, + Environments, + BaseBroadcastTransactionOptions, + BaseBroadcastTransactionResult, } from '@bitgo/sdk-core'; +import * as mpc from '@bitgo/sdk-lib-mpc'; import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import utils from './lib/utils'; import { bip32 } from '@bitgo/secp256k1'; import { randomBytes, Hash } from 'crypto'; import { KeyPair as EthKeyPair } from '@bitgo/abstract-eth'; -import { TransactionBuilderFactory } from './lib'; -import { ExplainTransactionOptions, VetParseTransactionOptions } from './lib/types'; +import { Transaction, TransactionBuilderFactory } from './lib'; +import { + ExplainTransactionOptions, + RecoverOptions, + RecoveryTransaction, + UnsignedSweepRecoveryTransaction, + VetParseTransactionOptions, +} from './lib/types'; import { VetTransactionExplanation } from './lib/iface'; +import { AVG_GAS_UNITS, COEF_DIVISOR, EXPIRATION, GAS_PRICE_COEF, GAS_UNIT_PRICE } from './lib/constants'; + +interface FeeEstimateData { + gas: string; + gasUnitPrice: string; + gasPriceCoef: string; + coefDivisor: string; +} + +const feeEstimateData: FeeEstimateData = { + gas: AVG_GAS_UNITS, + gasUnitPrice: GAS_UNIT_PRICE, + gasPriceCoef: GAS_PRICE_COEF, + coefDivisor: COEF_DIVISOR, +}; /** * Full Name: Vechain @@ -272,4 +301,380 @@ export class Vet extends BaseCoin { throw new NotImplementedError(`NFT type ${params.type} not supported on ${this.getChain()}`); } } + + /** + * Broadcasts a signed transaction to the VeChain network. + * + * @param {BaseBroadcastTransactionOptions} payload - The payload containing the serialized signed transaction. + * @param {string} payload.serializedSignedTransaction - The serialized signed transaction to broadcast. + * @returns {Promise} A promise that resolves to an empty object if the broadcast is successful. + * @throws {Error} If the broadcast fails, an error is thrown with the failure message. + */ + public async broadcastTransaction(payload: BaseBroadcastTransactionOptions): Promise { + const baseUrl = this.getPublicNodeUrl(); + const url = `${baseUrl}/transactions`; + + // The body should be a JSON object with a 'raw' key + const requestBody = { + raw: payload.serializedSignedTransaction, + }; + + try { + await axios.post(url, requestBody); + return {}; + } catch (error) { + throw new Error(`Failed to broadcast transaction: ${error.message}`); + } + } + + /** @inheritDoc */ + async recover(params: RecoverOptions): Promise { + try { + if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) { + throw new Error('invalid recoveryDestination'); + } + + const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase; + + let publicKey: string | undefined; + let userKeyShare, backupKeyShare, commonKeyChain; + const MPC = new Ecdsa(); + + if (isUnsignedSweep) { + 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'); + } + + if (!params.backupKey) { + throw new Error('missing backupKey'); + } + + if (!params.walletPassphrase) { + throw new Error('missing wallet passphrase'); + } + + const userKey = params.userKey.replace(/\s/g, ''); + const backupKey = params.backupKey.replace(/\s/g, ''); + + ({ userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares( + userKey, + backupKey, + params.walletPassphrase + )); + publicKey = MPC.deriveUnhardened(commonKeyChain, 'm/0').slice(0, 66); + } + + if (!publicKey) { + throw new Error('failed to derive public key'); + } + + const backupKeyPair = new EthKeyPair({ pub: publicKey }); + const baseAddress = backupKeyPair.getAddress(); + + const tx = await this.buildRecoveryTransaction({ + baseAddress, + params, + }); + + 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( + signableMessage, + userKeyShare, + backupKeyShare, + commonKeyChain + ); + const signature = Buffer.from(signatureObj.r + signatureObj.s + (signatureObj.recid === 0 ? '00' : '01'), 'hex'); + const txBuilder = this.getTxBuilderFactory().getTransferBuilder(); + await txBuilder.from(serializedTxHex); + txBuilder.isRecovery(true); + await txBuilder.addSenderSignature(signature); + + const signedTx = await txBuilder.build(); + + // broadcast this transaction + await this.broadcastTransaction({ + serializedSignedTransaction: signedTx.toBroadcastFormat(), + }); + + return { + id: signedTx.id, + tx: signedTx.toBroadcastFormat(), + }; + } catch (error) { + throw new Error(`Error during Vechain recovery: ${error.message || error}`); + } + } + + /** + * Returns the public node URL for the VeChain network. + * @returns {string} The URL of the public VeChain node. + */ + protected getPublicNodeUrl(): string { + return Environments[this.bitgo.getEnv()].vetNodeUrl; + } + + /** + * Calculates the transaction fee based on the estimated gas limit and fee estimate data. + * @param {FeeEstimateData} feeEstimateData - The fee estimate data. + * @param {BigNumber} estimatedGasLimit - The estimated gas limit for the transaction. + * @returns {BigNumber} The calculated transaction fee. + */ + private calculateFee(feeEstimateData: FeeEstimateData, estimatedGasLimit: BigNumber): BigNumber { + const gasLimit = estimatedGasLimit; + const adjustmentFactor = new BigNumber(1).plus( + new BigNumber(feeEstimateData.gasPriceCoef) + .dividedBy(new BigNumber(feeEstimateData.coefDivisor)) + .decimalPlaces(18, BigNumber.ROUND_DOWN) + ); + const adjustedGasPrice = new BigNumber(feeEstimateData.gasUnitPrice).times(adjustmentFactor); + return gasLimit.times(adjustedGasPrice).integerValue(BigNumber.ROUND_CEIL); + } + + /** + * Ensures that the given address has sufficient VTHO balance to cover the transaction fee. + * @param {string} baseAddress - The address to check for VTHO balance. + * @param {BigNumber} requiredGasUnits - The required gas units for the transaction. + * @throws {Error} If the VTHO balance is insufficient or if there's an error checking the balance. + */ + async ensureVthoBalanceForFee(baseAddress: string, requiredGasUnits: BigNumber): Promise { + const vthoTokenAddress = '0x0000000000000000000000000000456E65726779'; // VTHO token contract address + try { + const vthoBalance = await this.getBalance(baseAddress, vthoTokenAddress); + + const requiredFee = this.calculateFee(feeEstimateData, requiredGasUnits); + + if (vthoBalance.isLessThan(requiredFee)) { + throw new Error( + `Insufficient VTHO balance for fees. Required: ${requiredFee.toString()}, Available: ${vthoBalance.toString()}` + ); + } + } catch (error) { + throw new Error(`Failed to ensure VTHO balance: ${error.message}`); + } + } + + /** + * Fetches the balance for a given Vechain address. + * + * @param address The Vechain address (e.g., "0x...") to check. + * @param tokenContractAddress (Optional) The contract address of a VIP180 token. + * @returns A Promise that resolves to a BigNumber instance of the balance. + */ + async getBalance(address: string, tokenContractAddress?: string): Promise { + const baseUrl = this.getPublicNodeUrl(); + + if (!tokenContractAddress) { + const url = `${baseUrl}/accounts/${address}`; + + try { + const response = await axios.get(url); + + // The 'balance' is returned as a hex string. + const balance = new BigNumber(response.data.balance); + + return balance; + } catch (error) { + throw new Error('Failed to get native balance.'); + } + } + + const url = `${baseUrl}/accounts/*`; + + // Construct the ABI-encoded data for the 'balanceOf(address)' call + // 1. Function selector for 'balanceOf(address)': '0x70a08231' + // 2. Padded address: The address, stripped of '0x', left-padded with zeros to 64 chars + const paddedAddress = address.startsWith('0x') ? address.substring(2).padStart(64, '0') : address.padStart(64, '0'); + const data = `0x70a08231${paddedAddress}`; + + const requestBody = { + clauses: [ + { + to: tokenContractAddress, // The token contract address + value: '0x0', + data: data, // The 'balanceOf' call + }, + ], + }; + + try { + const response = await axios.post(url, requestBody); + + const simResponse = response.data; + + // Validate response and extract the balance data + if (!simResponse || !Array.isArray(simResponse) || simResponse.length === 0 || !simResponse[0].data) { + throw new Error('Invalid simulation response from VeChain node'); + } + + // The returned data is the hex-encoded balance + return new BigNumber(simResponse[0].data); + } catch (error) { + console.error('Error fetching token balance:', error); + throw new Error(`Failed to get token balance: ${error.message}`); + } + } + + /** + * Retrieves the block reference from the VeChain network. + * @returns {Promise} A promise that resolves to the block reference string. + * @throws {Error} If there's an error fetching the block reference or if the response is invalid. + */ + public async getBlockRef(): Promise { + const baseUrl = this.getPublicNodeUrl(); + const url = `${baseUrl}/blocks/best`; + + try { + const response = await axios.get(url); + + const data = response.data; + + // Validate the response data + if (!data || !data.id) { + throw new Error('Invalid response from the VeChain node'); + } + + // Return the first 18 characters of the block ID + return data.id.slice(0, 18); + } catch (error) { + // Rethrow or return a sensible default + throw new Error('Failed to get block ref: '); + } + } + + /** + * Generates a random nonce for use in transactions. + * @returns {string} A hexadecimal string representing the random nonce. + */ + getRandomNonce(): string { + return '0x' + randomBytes(8).toString('hex'); + } + + /** + * Estimates the gas required for a set of transaction clauses. + * @param {TransactionClause[]} clauses - An array of transaction clauses. + * @param {string} caller - The address of the transaction caller. + * @returns {Promise} A promise that resolves to the estimated gas amount. + * @throws {Error} If the clauses are invalid, the caller is not provided, or if there's an error in gas estimation. + */ + public async estimateGas(clauses: TransactionClause[], caller: string): Promise { + if (!clauses || !Array.isArray(clauses) || clauses.length === 0) { + throw new Error('Clauses must be a non-empty array'); + } + + if (!caller) { + throw new Error('Caller address is required'); + } + + const baseUrl = this.getPublicNodeUrl(); + const url = `${baseUrl}/accounts/*`; + + const requestBody = { + clauses: clauses, + caller: caller, + }; + + try { + const response = await axios.post(url, requestBody); + + const simResponse = response.data; + + if (!simResponse || !Array.isArray(simResponse)) { + throw new Error('Invalid simulation response from VeChain node'); + } + + const totalSimulatedGas = simResponse.reduce((sum, result) => sum + (result.gasUsed || 0), 0); + + const intrinsicGas = Number(VetTransaction.intrinsicGas(clauses).wei); + + const totalGas = Math.ceil(intrinsicGas + (totalSimulatedGas !== 0 ? totalSimulatedGas + 15000 : 0)); + + return new BigNumber(totalGas); + } catch (error) { + throw new Error(`Failed to estimate gas: ${error.message}`); + } + } + + /** + * Builds a recovery transaction for the given address. + * @param {Object} buildParams - The parameters for building the recovery transaction. + * @param {string} buildParams.baseAddress - The address to recover funds from. + * @param {RecoverOptions} buildParams.params - The recovery options. + * @returns {Promise} A promise that resolves to the built recovery transaction. + * @throws {Error} If there's no VET balance to recover or if there's an error building the transaction. + */ + private async buildRecoveryTransaction(buildParams: { + baseAddress: string; + params: RecoverOptions; + }): Promise { + const { baseAddress, params } = buildParams; + const balance = await this.getBalance(baseAddress); + + if (balance.isLessThanOrEqualTo(0)) { + throw new Error(`no VET balance to recover for address ${baseAddress}`); + } + + const recipients = [ + { + address: params.recoveryDestination, + amount: balance.toString(), + }, + ]; + + const blockRef = await this.getBlockRef(); + + const txBuilder = this.getTxBuilderFactory().getTransferBuilder(); + + txBuilder.chainTag(this.bitgo.getEnv() === 'prod' ? 0x4a : 0x27); + txBuilder.recipients(recipients); + txBuilder.sender(baseAddress); + txBuilder.addFeePayerAddress(baseAddress); + txBuilder.gas(Number(AVG_GAS_UNITS)); + txBuilder.blockRef(blockRef); + txBuilder.expiration(EXPIRATION); + txBuilder.gasPriceCoef(Number(GAS_PRICE_COEF)); + txBuilder.nonce(this.getRandomNonce()); + txBuilder.isRecovery(true); + + let tx = (await txBuilder.build()) as Transaction; + + const clauses = tx.clauses; + + const actualGasUnits = await this.estimateGas(clauses, baseAddress); + + await this.ensureVthoBalanceForFee(baseAddress, actualGasUnits); + + txBuilder.gas(actualGasUnits.toNumber()); + + tx = (await txBuilder.build()) as Transaction; + + return tx; + } } diff --git a/modules/sdk-core/src/bitgo/environments.ts b/modules/sdk-core/src/bitgo/environments.ts index a65da52013..6bc697d032 100644 --- a/modules/sdk-core/src/bitgo/environments.ts +++ b/modules/sdk-core/src/bitgo/environments.ts @@ -273,7 +273,7 @@ const mainnetBase: EnvironmentTemplate = { soneiumExplorerBaseUrl: 'https://soneium.blockscout.com', monExplorerBaseUrl: 'https://api.etherscan.io/v2', stxNodeUrl: 'https://api.hiro.so', - vetNodeUrl: 'https://rpc-mainnet.vechain.energy', + vetNodeUrl: 'https://sync-mainnet.vechain.org', xtzExplorerBaseUrl: 'https://api.tzkt.io', xtzRpcUrl: 'https://rpc.tzkt.io/mainnet', }; @@ -402,7 +402,7 @@ const testnetBase: EnvironmentTemplate = { }, }, stxNodeUrl: 'https://api.testnet.hiro.so', - vetNodeUrl: 'https://rpc-testnet.vechain.energy', + vetNodeUrl: 'https://sync-testnet.vechain.org', xtzExplorerBaseUrl: 'https://api.ghostnet.tzkt.io', xtzRpcUrl: 'https://rpc.tzkt.io/ghostnet', };