From e049f3d813edd75a1c86119c378acc46debb517c Mon Sep 17 00:00:00 2001 From: HFazelinia Date: Tue, 23 Jul 2024 14:35:12 +0100 Subject: [PATCH 1/2] consider decimals drop in EvmChain --- .changeset/light-fans-call.md | 5 + packages/chains/evm/lib/EvmChain.ts | 59 ++++-- packages/chains/evm/tests/EvmChain.spec.ts | 203 +++++++++++++++++++++ packages/chains/evm/tests/TestUtils.ts | 13 ++ packages/chains/evm/tests/testData.ts | 63 +++++++ 5 files changed, 332 insertions(+), 11 deletions(-) create mode 100644 .changeset/light-fans-call.md diff --git a/.changeset/light-fans-call.md b/.changeset/light-fans-call.md new file mode 100644 index 0000000..3ffd8e2 --- /dev/null +++ b/.changeset/light-fans-call.md @@ -0,0 +1,5 @@ +--- +'@rosen-chains/evm': major +--- + +consider decimals drop diff --git a/packages/chains/evm/lib/EvmChain.ts b/packages/chains/evm/lib/EvmChain.ts index a1d2f87..ded8f3f 100644 --- a/packages/chains/evm/lib/EvmChain.ts +++ b/packages/chains/evm/lib/EvmChain.ts @@ -127,6 +127,11 @@ abstract class EvmChain extends AbstractChain { for (const singleOrder of orders) { let trx; if (singleOrder.assets.nativeToken !== 0n) { + const value = this.tokenMap.unwrapAmount( + this.NATIVE_TOKEN_ID, + singleOrder.assets.nativeToken, + this.CHAIN + ).amount; trx = Transaction.from({ type: 2, to: singleOrder.address, @@ -134,15 +139,20 @@ abstract class EvmChain extends AbstractChain { maxPriorityFeePerGas: maxPriorityFeePerGas, maxFeePerGas: gasPrice, data: '0x' + eventId, - value: singleOrder.assets.nativeToken, + value: value, chainId: this.CHAIN_ID, }); } else { const token = singleOrder.assets.tokens[0]; + const tokenValue = this.tokenMap.unwrapAmount( + token.id, + token.value, + this.CHAIN + ).amount; const data = EvmUtils.encodeTransferCallData( token.id, singleOrder.address, - token.value + tokenValue ); trx = Transaction.from({ @@ -176,7 +186,14 @@ abstract class EvmChain extends AbstractChain { // check the balance in the lock address const requiredAssets: AssetBalance = orders.reduce( (sum: AssetBalance, order: SinglePayment) => { - return ChainUtils.sumAssetBalance(sum, order.assets); + const wrappedBalance = ChainUtils.sumAssetBalance(sum, order.assets); + // TODO: is it okay to do this? other chains use wrapped value in requiredAssets + return ChainUtils.unwrapAssetBalance( + wrappedBalance, + this.tokenMap, + this.NATIVE_TOKEN_ID, + this.CHAIN + ); }, { nativeToken: totalGas * gasPrice, @@ -246,10 +263,16 @@ abstract class EvmChain extends AbstractChain { }); } + const wrappedAssets = ChainUtils.wrapAssetBalance( + assets, + this.tokenMap, + this.NATIVE_TOKEN_ID, + this.CHAIN + ); // no need to calculate outputAssets separately, they are always equal in account-based return { - inputAssets: assets, - outputAssets: structuredClone(assets), + inputAssets: wrappedAssets, + outputAssets: structuredClone(wrappedAssets), }; }; @@ -306,7 +329,15 @@ abstract class EvmChain extends AbstractChain { } } } - return payment; + return payment.map((singleOrder) => { + singleOrder.assets = ChainUtils.wrapAssetBalance( + singleOrder.assets, + this.tokenMap, + this.NATIVE_TOKEN_ID, + this.CHAIN + ); + return singleOrder; + }); }; /** @@ -480,7 +511,7 @@ abstract class EvmChain extends AbstractChain { * checks the following conditions before: * - transaction must of of type 2 * - fees are set appropriately according to the current network's condition - * - lock address stil have enough funds + * - lock address still have enough funds * @param transaction the transaction */ submitTransaction = async ( @@ -723,10 +754,16 @@ abstract class EvmChain extends AbstractChain { }; }) ); - return { - nativeToken: nativeTokenBalance, - tokens: tokens, - }; + const wrappedAssets = ChainUtils.wrapAssetBalance( + { + nativeToken: nativeTokenBalance, + tokens: tokens, + }, + this.tokenMap, + this.NATIVE_TOKEN_ID, + this.CHAIN + ); + return wrappedAssets; }; } diff --git a/packages/chains/evm/tests/EvmChain.spec.ts b/packages/chains/evm/tests/EvmChain.spec.ts index 564b25b..47bb174 100644 --- a/packages/chains/evm/tests/EvmChain.spec.ts +++ b/packages/chains/evm/tests/EvmChain.spec.ts @@ -334,6 +334,79 @@ describe('EvmChain', () => { ); }).not.rejects; }); + + /** + * @target EvmChain.generateMultipleTransactions should generate payment + * transaction successfully for wrapped order + * @dependencies + * @scenario + * - mock hasLockAddressEnoughAssets, getMaxFeePerGas + * - mock getGasRequired, getAddressNextNonce + * - mock getMaxPriorityFeePerGas + * - call the function + * - check returned value + * @expected + * - PaymentTransaction txType, eventId and network should be as + * expected + * - extracted order of generated transaction should be the same as input + * order + * - eventId should be properly in the transaction data + * - no extra data should be found in the transaction data + * - transaction must be of type 2 and has no blobs + * - nonce must be the same as the next available nonce + */ + it('should generate payment transaction successfully for wrapped order', async () => { + const evmChain = + testUtils.generateChainObjectWithMultiDecimalTokenMap(network); + + const order = TestData.nativePaymentWrappedOrder; + const eventId = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const txType = TransactionType.payment; + const nonce = 54; + + // mock hasLockAddressEnoughAssets, getMaxFeePerGas, + // getGasRequired, getAddressNextNonce, getMaxPriorityFeePerGas + testUtils.mockHasLockAddressEnoughAssets(evmChain, true); + testUtils.mockGetMaxFeePerGas(network, 10n); + testUtils.mockGetGasRequired(network, 21000n); + testUtils.mockGetAddressNextAvailableNonce(network, nonce); + testUtils.mockGetMaxPriorityFeePerGas(network, 10n); + + // run test + const evmTx = await evmChain.generateMultipleTransactions( + eventId, + txType, + order, + [], + [] + ); + + // check returned value + expect(evmTx[0].txType).toEqual(txType); + expect(evmTx[0].eventId).toEqual(eventId); + expect(evmTx[0].network).toEqual(evmChain.CHAIN); + + // extracted order of generated transaction should be the same as input order + const extractedOrder = evmChain.extractTransactionOrder(evmTx[0]); + expect(extractedOrder).toEqual(order); + + const tx = Serializer.deserialize(evmTx[0].txBytes); + + // check eventId encoded at the end of the data + expect(tx.data.substring(2, 34)).toEqual(eventId); + + // check there is no more data + expect(tx.data.length).toEqual(34); + + // check transaction type + expect(tx.type).toEqual(2); + + // check blobs zero + expect(tx.maxFeePerBlobGas).toEqual(null); + + // check nonce + expect(tx.nonce).toEqual(nonce); + }); }); describe('rawTxToPaymentTransaction', () => { @@ -513,6 +586,45 @@ describe('EvmChain', () => { await evmChain.getTransactionAssets(paymentTx); }).rejects.toThrowError(TransactionFormatError); }); + + /** + * @target EvmChain.getTransactionAssets should wrap transaction assets + * successfully + * @dependencies + * @scenario + * - mock PaymentTransaction + * - call the function + * - check returned value + * @expected + * - it should return mocked transaction assets (both input and output assets) + */ + it('should wrap transaction assets successfully', async () => { + const evmChain = + testUtils.generateChainObjectWithMultiDecimalTokenMap(network); + + const eventId = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const txType = TransactionType.payment; + // mock PaymentTransaction + const trx = { ...TestData.transaction1Json }; + trx.data = '0x'; + const tx = Transaction.from(trx); + const assets = { ...TestData.transaction1WrappedAssets }; + assets.tokens = []; + const paymentTx = new PaymentTransaction( + evmChain.CHAIN, + tx.unsignedHash, + eventId, + Serializer.serialize(tx), + txType + ); + + // check returned value + const result = await evmChain.getTransactionAssets(paymentTx); + + // check returned value + expect(result.inputAssets).toEqual(assets); + expect(result.outputAssets).toEqual(assets); + }); }); describe('verifyTransactionFee', () => { @@ -1102,6 +1214,52 @@ describe('EvmChain', () => { evmChain.extractTransactionOrder(paymentTx) ).rejects.toThrowError(TransactionFormatError); }); + + /** + * @target EvmChain.extractTransactionOrder should wrap transaction + * order successfully + * @dependencies + * @scenario + * - mock PaymentTransaction + * - run test + * - check returned value + * @expected + * - it should return mocked transaction order + */ + it('should wrap transaction order successfully', () => { + const evmChain = + testUtils.generateChainObjectWithMultiDecimalTokenMap(network); + + // mock PaymentTransaction + const eventId = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const txType = TransactionType.payment; + const tx = Transaction.from({ + ...(TestData.erc20transaction as TransactionLike), + }); + tx.data = '0x'; + tx.value = 100n; + const paymentTx = new PaymentTransaction( + evmChain.CHAIN, + tx.unsignedHash, + eventId, + Serializer.serialize(tx), + txType + ); + + // run test + const result = evmChain.extractTransactionOrder(paymentTx); + + // check returned value + expect(result).toEqual([ + { + address: '0xedee4752e5a2f595151c94762fb38e5730357785', + assets: { + nativeToken: 1n, + tokens: [], + }, + }, + ]); + }); }); describe('submitTransaction', () => { @@ -1886,5 +2044,50 @@ describe('EvmChain', () => { // check returned value expect(result).toEqual({ nativeToken: 0n, tokens: [] }); }); + + /** + * @target EvmChain.getAddressAssets should wrap address assets successfully + * @dependencies + * @scenario + * - mock getAddressBalanceForNativeToken + * - mock getAddressBalanceForERC20Asset for each supported tokens + * - call the function + * - check returned value + * @expected + * - it should return mocked address assets (both input and output assets) + */ + it('should wrap address assets successfully', async () => { + const evmChain = + testUtils.generateChainObjectWithMultiDecimalTokenMap(network); + + mockGetAddressBalanceForNativeToken(evmChain.network, 1000n); + vi.spyOn(network, 'getAddressBalanceForERC20Asset').mockImplementation( + async (address, tokenId) => { + if (tokenId === '0xedee4752e5a2f595151c94762fb38e5730357785') + return 0n; + else if (tokenId === '0x12345752e5a2f595151c94762fb38e5730357785') + return 10n; + else if (tokenId === '0xedee4752e5a2f595151c94762fb38e5730357786') + return 30n; + else if (tokenId === '0xedee4752e5a2f595151c94762fb38e5730357787') + return 40n; + else return 0n; + } + ); + + // run test + const result = await evmChain.getAddressAssets(TestData.lockAddress); + + // check returned value + expect(result).toEqual({ + nativeToken: 10n, + tokens: [ + { id: '0xedee4752e5a2f595151c94762fb38e5730357785', value: 0n }, + { id: '0x12345752e5a2f595151c94762fb38e5730357785', value: 10n }, + { id: '0xedee4752e5a2f595151c94762fb38e5730357786', value: 30n }, + { id: '0xedee4752e5a2f595151c94762fb38e5730357787', value: 40n }, + ], + }); + }); }); }); diff --git a/packages/chains/evm/tests/TestUtils.ts b/packages/chains/evm/tests/TestUtils.ts index 781f6ad..ffda8f1 100644 --- a/packages/chains/evm/tests/TestUtils.ts +++ b/packages/chains/evm/tests/TestUtils.ts @@ -94,3 +94,16 @@ export const generateChainObject = ( signFn ); }; + +export const generateChainObjectWithMultiDecimalTokenMap = ( + network: TestEvmNetwork, + signFn: TssSignFunction = mockedSignFn +) => { + return new TestChain( + network, + configs, + testData.multiDecimalTokenMap, + testData.supportedTokens, + signFn + ); +}; diff --git a/packages/chains/evm/tests/testData.ts b/packages/chains/evm/tests/testData.ts index d27564e..c6d3d47 100644 --- a/packages/chains/evm/tests/testData.ts +++ b/packages/chains/evm/tests/testData.ts @@ -24,6 +24,16 @@ export const nativePaymentOrder: PaymentOrder = [ }, ]; +export const nativePaymentWrappedOrder: PaymentOrder = [ + { + address: '0x6a6a84990fe4d261c6c7c701ea2ce64c0c32b1c7', + assets: { + nativeToken: 10n, + tokens: [], + }, + }, +]; + export const testLockAddress = '0x4606d11ff65b17d29e8c5e4085f9a868a8e5e4f2'; export const transaction0PaymentTransaction = new PaymentTransaction( @@ -275,6 +285,16 @@ export const transaction1Assets: AssetBalance = { ], }; +export const transaction1WrappedAssets: AssetBalance = { + nativeToken: BigInt(210) * BigInt(48978500000), + tokens: [ + { + id: '0xedee4752e5a2f595151c94762fb38e5730357785', + value: BigInt(3810110000), + }, + ], +}; + export const paralelTransactions = [ Transaction.from({ type: 2, @@ -463,3 +483,46 @@ export const testTokenMap: RosenTokens = { idKeys: {}, tokens: [], }; + +export const multiDecimalTokenMap: RosenTokens = { + idKeys: { + ergo: 'tokenId', + cardano: 'tokenId', + test: 'tokenId', + }, + tokens: [ + { + ergo: { + tokenId: + '1c7435e608ab710c56bbe0f635e2a5e86ddf856f7d3d2d1d4dfefa62fbbfb9b4', + name: 'testETHER', + decimals: 1, + metaData: { + type: 'EIP-004', + residency: 'wrapped', + }, + }, + cardano: { + tokenId: + '6d7cc9577a04be165cc4f2cf36f580dbeaf88f68e78f790805430940.727345544845522d6c6f656e', + policyId: '6d7cc9577a04be165cc4f2cf36f580dbeaf88f68e78f790805430940', + assetName: '727345544845522d6c6f656e', + name: 'rsETHER-loen', + decimals: 1, + metaData: { + type: 'CIP26', + residency: 'wrapped', + }, + }, + test: { + tokenId: 'test-native-token', + name: 'ETHER', + decimals: 3, + metaData: { + type: 'native', + residency: 'native', + }, + }, + }, + ], +}; From a269d1a9a21418e6c9cab424789a38dc1247d670 Mon Sep 17 00:00:00 2001 From: HFazelinia Date: Wed, 24 Jul 2024 11:19:38 +0100 Subject: [PATCH 2/2] minor improvement --- packages/chains/evm/lib/EvmChain.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/chains/evm/lib/EvmChain.ts b/packages/chains/evm/lib/EvmChain.ts index ded8f3f..ee5fb75 100644 --- a/packages/chains/evm/lib/EvmChain.ts +++ b/packages/chains/evm/lib/EvmChain.ts @@ -185,18 +185,14 @@ abstract class EvmChain extends AbstractChain { // check the balance in the lock address const requiredAssets: AssetBalance = orders.reduce( - (sum: AssetBalance, order: SinglePayment) => { - const wrappedBalance = ChainUtils.sumAssetBalance(sum, order.assets); - // TODO: is it okay to do this? other chains use wrapped value in requiredAssets - return ChainUtils.unwrapAssetBalance( - wrappedBalance, - this.tokenMap, + (sum: AssetBalance, order: SinglePayment) => + ChainUtils.sumAssetBalance(sum, order.assets), + { + nativeToken: this.tokenMap.wrapAmount( this.NATIVE_TOKEN_ID, + totalGas * gasPrice, this.CHAIN - ); - }, - { - nativeToken: totalGas * gasPrice, + ).amount, tokens: [], } );