From 19af7d67bd19d7efc1fbc360ff48102be4d70e0d Mon Sep 17 00:00:00 2001 From: prithvishet2503 Date: Thu, 10 Apr 2025 13:39:08 +0530 Subject: [PATCH] feat: unsigned sweep for flare Ticket: WIN-4884 TICKET: WIN-4884 --- modules/sdk-coin-flr/src/flr.ts | 12 ++- modules/sdk-coin-flr/src/lib/resources.ts | 28 ------- .../src/lib/transactionBuilder.ts | 6 ++ modules/sdk-coin-flr/test/unit/flr.ts | 62 +++++++++++++++- modules/sdk-coin-flr/test/unit/resources.ts | 73 +++++++++++++++++++ 5 files changed, 151 insertions(+), 30 deletions(-) delete mode 100644 modules/sdk-coin-flr/src/lib/resources.ts create mode 100644 modules/sdk-coin-flr/test/unit/resources.ts diff --git a/modules/sdk-coin-flr/src/flr.ts b/modules/sdk-coin-flr/src/flr.ts index 628b3235ee..09a2a272d9 100644 --- a/modules/sdk-coin-flr/src/flr.ts +++ b/modules/sdk-coin-flr/src/flr.ts @@ -9,7 +9,13 @@ import { BaseCoin, BitGoBase, common, MPCAlgorithm, MultisigType, multisigTypes } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; -import { AbstractEthLikeNewCoins, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth'; +import { + AbstractEthLikeNewCoins, + recoveryBlockchainExplorerQuery, + UnsignedSweepTxMPCv2, + RecoverOptions, + OfflineVaultTxInfo, +} from '@bitgo/abstract-eth'; import { TransactionBuilder } from './lib'; export class Flr extends AbstractEthLikeNewCoins { @@ -40,6 +46,10 @@ export class Flr extends AbstractEthLikeNewCoins { return 'ecdsa'; } + protected async buildUnsignedSweepTxnTSS(params: RecoverOptions): Promise { + return this.buildUnsignedSweepTxnMPCv2(params); + } + async recoveryBlockchainExplorerQuery(query: Record): Promise> { const apiToken = common.Environments[this.bitgo.getEnv()].flrExplorerApiToken; const explorerUrl = common.Environments[this.bitgo.getEnv()].flrExplorerBaseUrl; diff --git a/modules/sdk-coin-flr/src/lib/resources.ts b/modules/sdk-coin-flr/src/lib/resources.ts deleted file mode 100644 index 0777a2fa80..0000000000 --- a/modules/sdk-coin-flr/src/lib/resources.ts +++ /dev/null @@ -1,28 +0,0 @@ -import EthereumCommon from '@ethereumjs/common'; -import { coins, EthereumNetwork } from '@bitgo/statics'; - -export const testnetCommon = EthereumCommon.custom( - { - name: 'flr testnet', - networkId: (coins.get('tflr').network as EthereumNetwork).chainId, - chainId: (coins.get('tflr').network as EthereumNetwork).chainId, - }, - { - baseChain: 'sepolia', - hardfork: 'london', - eips: [1559], - } -); - -export const mainnetCommon = EthereumCommon.custom( - { - name: 'flr mainnet', - networkId: (coins.get('flr').network as EthereumNetwork).chainId, - chainId: (coins.get('flr').network as EthereumNetwork).chainId, - }, - { - baseChain: 'mainnet', - hardfork: 'london', - eips: [1559], - } -); diff --git a/modules/sdk-coin-flr/src/lib/transactionBuilder.ts b/modules/sdk-coin-flr/src/lib/transactionBuilder.ts index 1232e4f26b..2e6d6ea8d2 100644 --- a/modules/sdk-coin-flr/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-flr/src/lib/transactionBuilder.ts @@ -6,6 +6,7 @@ import { TransferBuilder } from './transferBuilder'; export class TransactionBuilder extends AbstractTransactionBuilder { protected _transfer: TransferBuilder; + private _signatures: any; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -24,6 +25,11 @@ export class TransactionBuilder extends AbstractTransactionBuilder { return this._transfer; } + addSignature(publicKey, signature) { + this._signatures = []; + this._signatures.push({ publicKey, signature }); + } + protected getContractData(addresses: string[]): string { throw new Error('Method not implemented.'); } diff --git a/modules/sdk-coin-flr/test/unit/flr.ts b/modules/sdk-coin-flr/test/unit/flr.ts index 7cc69441eb..7b7722bdd7 100644 --- a/modules/sdk-coin-flr/test/unit/flr.ts +++ b/modules/sdk-coin-flr/test/unit/flr.ts @@ -1,9 +1,13 @@ -import 'should'; +import * as should from 'should'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { BitGoAPI } from '@bitgo/sdk-api'; import { Flr, Tflr } from '../../src/index'; +import { UnsignedSweepTxMPCv2 } from '@bitgo/abstract-eth'; +import { mockDataUnsignedSweep } from './resources'; +import nock from 'nock'; +import { common } from '@bitgo/sdk-core'; const bitgo: TestBitGoAPI = TestBitGo.decorate(BitGoAPI, { env: 'test' }); @@ -40,3 +44,59 @@ describe('flr', function () { }); }); }); + +describe('Build Unsigned Sweep for Self-Custody Cold Wallets - (MPCv2)', function () { + const bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' }); + const explorerUrl = common.Environments[bitgo.getEnv()].flrExplorerBaseUrl as string; + const maxFeePerGasvalue = 20000000000; + const maxPriorityFeePerGasValue = 10000000000; + const chain_id = 114; + const gasLimitvalue = 200000; + + it('should generate an unsigned sweep without derivation path', async () => { + nock(explorerUrl) + .get('/api') + .twice() + .query(mockDataUnsignedSweep.getTxListRequest) + .reply(200, mockDataUnsignedSweep.getTxListResponse); + nock(explorerUrl) + .get('/api') + .query(mockDataUnsignedSweep.getBalanceRequest) + .reply(200, mockDataUnsignedSweep.getBalanceResponse); + + const baseCoin: any = bitgo.coin('tflr'); + const transaction = (await baseCoin.recover({ + userKey: mockDataUnsignedSweep.userKey, + backupKey: mockDataUnsignedSweep.backupKey, + walletContractAddress: mockDataUnsignedSweep.walletBaseAddress, + recoveryDestination: mockDataUnsignedSweep.recoveryDestination, + isTss: true, + eip1559: { maxFeePerGas: maxPriorityFeePerGasValue, maxPriorityFeePerGas: maxFeePerGasvalue }, + gasLimit: gasLimitvalue, + replayProtectionOptions: { + chain: chain_id, + hardfork: 'london', + }, + })) as UnsignedSweepTxMPCv2; + should.exist(transaction); + transaction.should.have.property('txRequests'); + transaction.txRequests.length.should.equal(1); + const txRequest = transaction.txRequests[0]; + txRequest.should.have.property('walletCoin'); + txRequest.walletCoin.should.equal('tflr'); + txRequest.should.have.property('transactions'); + txRequest.transactions.length.should.equal(1); + const tx = txRequest.transactions[0]; + tx.should.have.property('nonce'); + tx.should.have.property('unsignedTx'); + tx.unsignedTx.should.have.property('serializedTxHex'); + tx.unsignedTx.should.have.property('signableHex'); + tx.unsignedTx.should.have.property('derivationPath'); + tx.unsignedTx.should.have.property('feeInfo'); + tx.unsignedTx.feeInfo?.should.have.property('fee'); + tx.unsignedTx.feeInfo?.should.have.property('feeString'); + tx.unsignedTx.should.have.property('parsedTx'); + tx.unsignedTx.parsedTx?.should.have.property('spendAmount'); + tx.unsignedTx.parsedTx?.should.have.property('outputs'); + }); +}); diff --git a/modules/sdk-coin-flr/test/unit/resources.ts b/modules/sdk-coin-flr/test/unit/resources.ts new file mode 100644 index 0000000000..645aaef3e7 --- /dev/null +++ b/modules/sdk-coin-flr/test/unit/resources.ts @@ -0,0 +1,73 @@ +import EthereumCommon from '@ethereumjs/common'; +import { coins, EthereumNetwork } from '@bitgo/statics'; + +export const testnetCommon = EthereumCommon.custom( + { + name: 'flr testnet', + networkId: (coins.get('tflr').network as EthereumNetwork).chainId, + chainId: (coins.get('tflr').network as EthereumNetwork).chainId, + }, + { + baseChain: 'sepolia', + hardfork: 'london', + eips: [1559], + } +); + +export const mainnetCommon = EthereumCommon.custom( + { + name: 'flr mainnet', + networkId: (coins.get('flr').network as EthereumNetwork).chainId, + chainId: (coins.get('flr').network as EthereumNetwork).chainId, + }, + { + baseChain: 'mainnet', + hardfork: 'london', + eips: [1559], + } +); + +const getTxListRequestUnsignedSweep: Record = { + module: 'account', + action: 'txlist', + address: '0x1469e6e519ff8bf398b76b4be0b50701b999f14c', +}; + +const getTxListResponseUnsignedSweep: Record = { + status: '1', + result: [ + { + hash: '0xede855d43d70ea1bb75db63d4f75113dae0845f0d4bdb0b2d8bda55249c70812', + nonce: '23', + from: '0x1469e6e519ff8bf398b76b4be0b50701b999f14c', + }, + ], + message: 'OK', +}; + +const getBalanceRequestUnsignedSweep: Record = { + module: 'account', + action: 'balance', + address: '0x1469e6e519ff8bf398b76b4be0b50701b999f14c', +}; + +const getBalanceResponseUnsignedSweep: Record = { + status: '1', + result: '100000000000000000', + message: 'OK', +}; + +export const mockDataUnsignedSweep = { + userKey: + '038412b0e79372ca618978f2bc9fc944c504e828050a55a19fdfeca93cff5ec6562ae94f204a3f99e87334f812be8a54927ff24572bc666c5436887d2e42c0997d', + backupKey: + '038412b0e79372ca618978f2bc9fc944c504e828050a55a19fdfeca93cff5ec6562ae94f204a3f99e87334f812be8a54927ff24572bc666c5436887d2e42c0997d', + derivationPath: 'm/0', + derivationSeed: '', + walletBaseAddress: '0x1469e6e519ff8bf398b76b4be0b50701b999f14c', + recoveryDestination: '0x07efb1aa5e41b70b21facd3d287548ebf632a165', + getTxListRequest: getTxListRequestUnsignedSweep, + getTxListResponse: getTxListResponseUnsignedSweep, + getBalanceRequest: getBalanceRequestUnsignedSweep, + getBalanceResponse: getBalanceResponseUnsignedSweep, +};