diff --git a/src/decoders/evmAdvance.ts b/src/decoders/evmAdvance.ts index 523913c..40ed8f1 100644 --- a/src/decoders/evmAdvance.ts +++ b/src/decoders/evmAdvance.ts @@ -1,6 +1,6 @@ import { Address, decodeFunctionData, Hex, parseAbi } from 'viem'; -interface Data { +export interface Data { chainId: bigint; appContract: Address; msgSender: Address; @@ -11,7 +11,7 @@ interface Data { payload: Hex; } -const evmAdvanceAbi = parseAbi([ +export const evmAdvanceAbi = parseAbi([ `function EvmAdvance( uint256 chainId, address appContract,address msgSender, uint256 blockNumber, uint256 blockTimestamp, uint256 prevRandao, uint256 index, bytes calldata payload) external`, ]); diff --git a/tests/handlers/v2/ApplicationCreated.test.ts b/tests/handlers/v2/ApplicationCreated.test.ts new file mode 100644 index 0000000..9635e6f --- /dev/null +++ b/tests/handlers/v2/ApplicationCreated.test.ts @@ -0,0 +1,160 @@ +import { sepolia } from 'viem/chains'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import ApplicationCreated from '../../../src/handlers/v2/ApplicationCreated'; +import { + Application, + ApplicationFactory, + Chain, + RollupVersion, +} from '../../../src/model'; +import { generateIDFrom } from '../../../src/utils'; +import { block, ctx, logApplicationCreatedV2, logs } from '../../stubs/params'; +import { mockModelImplementation } from '../../stubs/utils'; + +vi.mock('../../../src/model/', async (importOriginal) => { + const actualMods = await importOriginal; + const Application = vi.fn(); + const ApplicationFactory = vi.fn(); + const Chain = vi.fn(); + const RollupVersion = { v1: 'v1', v2: 'v2' }; + + return { + ...actualMods!, + Application, + ApplicationFactory, + Chain, + RollupVersion, + }; +}); + +describe('ApplicationCreated v2', () => { + let applicationCreated: ApplicationCreated; + const mockFactoryStorage = new Map(); + const mockApplicationStorage = new Map(); + const mockChainStorage = new Map(); + + beforeEach(() => { + applicationCreated = new ApplicationCreated( + mockFactoryStorage, + mockApplicationStorage, + mockChainStorage, + ); + + // Mock models to return simple Object when new Model() is called. + mockModelImplementation(Application); + mockModelImplementation(Chain); + mockModelImplementation(ApplicationFactory); + + mockFactoryStorage.clear(); + mockApplicationStorage.clear(); + mockChainStorage.clear(); + vi.clearAllMocks(); + }); + + describe('handle', async () => { + test('should ignore events that are not of type application-created', async () => { + await applicationCreated.handle(logs[0], block, ctx); + expect(mockFactoryStorage.size).toBe(0); + expect(mockApplicationStorage.size).toBe(0); + }); + + test('should create an chain object after handling application-created event', async () => { + await applicationCreated.handle( + logApplicationCreatedV2, + block, + ctx, + ); + + expect(mockChainStorage.size).toEqual(1); + const [chain] = Array.from(mockChainStorage.values()); + expect(chain).toEqual({ id: sepolia.id.toString() }); + }); + + test('should throw error when chain-id information is not available in the Log ', async () => { + try { + const clonedLog = structuredClone(logApplicationCreatedV2); + delete clonedLog.transaction?.chainId; + await applicationCreated.handle(clonedLog, block, ctx); + expect(true).toEqual('Should not reach that expectation.'); + } catch (error) { + expect(error.message).toEqual( + 'Chain id is required to save ApplicationCreated events and related data!', + ); + } + }); + + test('should create the entities after handling the application-created event', async () => { + await applicationCreated.handle( + logApplicationCreatedV2, + block, + ctx, + ); + + const factoryId = generateIDFrom([ + sepolia.id, + logApplicationCreatedV2.address, + ]); + + const applicationId = generateIDFrom([ + sepolia.id, + '0xfb92024ec789bb2fbbc5cd1390386843c5fb7694', + RollupVersion.v2, + ]); + + expect(mockFactoryStorage.size).toBe(1); + expect(mockApplicationStorage.size).toBe(1); + expect(mockFactoryStorage.has(factoryId)).toBe(true); + expect(mockApplicationStorage.has(applicationId)).toBe(true); + + const [factory] = Array.from(mockFactoryStorage.values()); + const [application] = Array.from(mockApplicationStorage.values()); + + expect(factory).toEqual({ + id: factoryId, + address: logApplicationCreatedV2.address, + chain: { id: sepolia.id.toString() }, + }); + expect(application).toEqual({ + id: applicationId, + address: '0xfb92024ec789bb2fbbc5cd1390386843c5fb7694', + factory: factory, + owner: '0x590f92fea8df163fff2d7df266364de7ce8f9e16', + timestamp: 1728693996n, + chain: { id: sepolia.id.toString() }, + rollupVersion: 'v2', + }); + }); + + test('should set the timestamp in seconds from the block timestamp', async () => { + await applicationCreated.handle( + logApplicationCreatedV2, + block, + ctx, + ); + const applicationId = generateIDFrom([ + sepolia.id, + '0xfb92024ec789bb2fbbc5cd1390386843c5fb7694', + RollupVersion.v2, + ]); + + const timestampInSeconds = + BigInt(logApplicationCreatedV2.block.timestamp) / 1000n; + + const [application] = Array.from(mockApplicationStorage.values()); + + expect(application).toEqual({ + factory: { + id: '11155111-0x1d4cfbd2622d802a07ceb4c3401bbb455c9dbdc3', + address: logApplicationCreatedV2.address, + chain: { id: sepolia.id.toString() }, + }, + id: applicationId, + owner: '0x590f92fea8df163fff2d7df266364de7ce8f9e16', + timestamp: timestampInSeconds, + address: '0xfb92024ec789bb2fbbc5cd1390386843c5fb7694', + chain: { id: sepolia.id.toString() }, + rollupVersion: 'v2', + }); + }); + }); +}); diff --git a/tests/handlers/v2/InputAdded.test.ts b/tests/handlers/v2/InputAdded.test.ts new file mode 100644 index 0000000..0d91c59 --- /dev/null +++ b/tests/handlers/v2/InputAdded.test.ts @@ -0,0 +1,414 @@ +import { afterEach } from 'node:test'; +import { sepolia } from 'viem/chains'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { Contract as ERC20 } from '../../../src/abi/ERC20'; +import { Contract as ERC721 } from '../../../src/abi/ERC721'; +import InputAdded from '../../../src/handlers/v2/InputAdded'; +import { + Application, + Chain, + Erc1155Deposit, + Erc1155Transfer, + Erc20Deposit, + Erc721Deposit, + Input, + MultiToken, + NFT, + Token, +} from '../../../src/model'; +import { + block, + ctx, + logErc1155BatchTransferV2, + logErc1155SingleTransferV2, + logErc20TransferV2, + logErc721Transfer, + logErc721TransferV2, + logInputAddedV2, + logs, +} from '../../stubs/params'; +import { mockModelImplementation } from '../../stubs/utils'; + +vi.mock('../../../src/abi/ERC20'); + +vi.mock('../../../src/abi/ERC721'); + +vi.mock('../../../src/model/', async () => { + const Token = vi.fn(); + const Erc20Deposit = vi.fn(); + const Application = vi.fn(); + const Input = vi.fn(); + const Erc721Deposit = vi.fn(); + const NFT = vi.fn(); + const Erc1155Deposit = vi.fn(); + const MultiToken = vi.fn(); + const Erc1155Transfer = vi.fn(); + const Chain = vi.fn(); + const RollupVersion = { v1: 'v1', v2: 'v2' }; + + return { + Application, + Token, + Erc20Deposit, + Erc721Deposit, + Input, + NFT, + MultiToken, + Erc1155Deposit, + Erc1155Transfer, + Chain, + RollupVersion, + }; +}); + +const ApplicationMock = mockModelImplementation(Application); +const InputMock = mockModelImplementation(Input); +const NFTStub = mockModelImplementation(NFT); +const ERC721Mock = vi.mocked(ERC721, true); +const ERC721DepositStub = mockModelImplementation(Erc721Deposit); +const ERC20Mock = vi.mocked(ERC20, true); +const ERC20DepositStub = mockModelImplementation(Erc20Deposit); +const TokenStub = mockModelImplementation(Token); +const MultiTokenStub = mockModelImplementation(MultiToken); +const ERC1155DepositStub = mockModelImplementation(Erc1155Deposit); +const ERC1155TransferStub = mockModelImplementation(Erc1155Transfer); +const ChainStub = mockModelImplementation(Chain); + +describe('InputAdded', () => { + let inputAdded: InputAdded; + const mockTokenStorage = new Map(); + const mockDepositStorage = new Map(); + const mockInputStorage = new Map(); + const mockApplicationStorage = new Map(); + const mockNftStorage = new Map(); + const mockErc721DepositStorage = new Map(); + const mockMultiTokenStorage = new Map(); + const mockErc1155DepositStorage = new Map(); + const mockChainStorage = new Map(); + const expectedChain = { id: sepolia.id.toString() }; + + beforeEach(() => { + inputAdded = new InputAdded( + mockTokenStorage, + mockDepositStorage, + mockApplicationStorage, + mockInputStorage, + mockNftStorage, + mockErc721DepositStorage, + mockMultiTokenStorage, + mockErc1155DepositStorage, + mockChainStorage, + ); + + mockTokenStorage.clear(); + mockDepositStorage.clear(); + mockApplicationStorage.clear(); + mockInputStorage.clear(); + mockNftStorage.clear(); + mockErc721DepositStorage.clear(); + mockMultiTokenStorage.clear(); + mockErc1155DepositStorage.clear(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('handle', async () => { + test('should ignore events other than InputAdded', async () => { + await inputAdded.handle(logs[1], block, ctx); + expect(mockInputStorage.size).toBe(0); + expect(mockApplicationStorage.size).toBe(0); + expect(mockDepositStorage.size).toBe(0); + }); + + test('should handle event type InputAdded', async () => { + await inputAdded.handle(logInputAddedV2, block, ctx); + expect(mockApplicationStorage.size).toBe(1); + expect(mockInputStorage.size).toBe(1); + }); + + test('when creating a non-existing app it should also set the timestamp in seconds', async () => { + await inputAdded.handle(logInputAddedV2, block, ctx); + + const timestamp = BigInt(logInputAddedV2.block.timestamp) / 1000n; + + const [application] = mockApplicationStorage.values(); + expect(application).toEqual({ + id: `${sepolia.id}-0xfb92024ec789bb2fbbc5cd1390386843c5fb7694-v2`, + address: '0xfb92024ec789bb2fbbc5cd1390386843c5fb7694', + timestamp, + chain: expectedChain, + rollupVersion: 'v2', + }); + }); + + test('should throw error when chain-id information is not available in the Log ', async () => { + try { + const clonedLog = structuredClone(logInputAddedV2); + delete clonedLog.transaction?.chainId; + await inputAdded.handle(clonedLog, block, ctx); + expect(true).toEqual('Should not reach that expectation.'); + } catch (error) { + expect(error.message).toEqual( + 'Chain id is required to save InputAdded events and related data!', + ); + } + }); + + describe('ERC-20 deposit', () => { + const name = 'Wrapped Ether'; + const symbol = 'WETH'; + const decimals = 18; + + beforeEach(() => { + // some default returns for the ERC20 contract calls + ERC20Mock.prototype.name.mockResolvedValue(name); + ERC20Mock.prototype.symbol.mockResolvedValue(symbol); + ERC20Mock.prototype.decimals.mockResolvedValue(decimals); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test('Should store the token information', async () => { + await inputAdded.handle(logErc20TransferV2, block, ctx); + expect(mockTokenStorage.size).toBe(1); + const [token] = mockTokenStorage.values(); + + expect(token).toEqual({ + chain: expectedChain, + decimals: decimals, + id: `${sepolia.id}-0x813ae0539daf858599a1b2a7083380542a7b1bb5`, + address: '0x813ae0539daf858599a1b2a7083380542a7b1bb5', + name: name, + symbol: symbol, + }); + }); + + test('should store the deposit information', async () => { + await inputAdded.handle(logErc20TransferV2, block, ctx); + + expect(mockDepositStorage.size).toBe(1); + const [deposit] = mockDepositStorage.values(); + + expect(deposit).toEqual({ + chain: expectedChain, + id: `${sepolia.id}-0x4ca2f6935200b9a782a78f408f640f17b29809d8-v2-10`, + amount: 111000000000000000n, + from: '0xf9e958241c1ca380cfcd50170ec43974bded0bff', + token: { + id: `${sepolia.id.toString()}-0x813ae0539daf858599a1b2a7083380542a7b1bb5`, + address: '0x813ae0539daf858599a1b2a7083380542a7b1bb5', + decimals: 18, + name: 'Wrapped Ether', + symbol: 'WETH', + chain: expectedChain, + }, + }); + }); + + test('should assign the erc20 deposit information correctly into the input', async () => { + await inputAdded.handle(logErc20TransferV2, block, ctx); + + expect(mockInputStorage.size).toEqual(1); + const [input] = + mockInputStorage.values() as IterableIterator; + + expect(input.erc20Deposit).toEqual({ + chain: expectedChain, + amount: 111000000000000000n, + from: '0xf9e958241c1ca380cfcd50170ec43974bded0bff', + id: `${sepolia.id}-0x4ca2f6935200b9a782a78f408f640f17b29809d8-v2-10`, + token: { + decimals: 18, + id: `${sepolia.id}-0x813ae0539daf858599a1b2a7083380542a7b1bb5`, + address: '0x813ae0539daf858599a1b2a7083380542a7b1bb5', + name: 'Wrapped Ether', + symbol: 'WETH', + chain: expectedChain, + }, + }); + + expect(input.erc721Deposit).toBeUndefined; + }); + }); + + describe('ERC-721 deposits', () => { + const name = 'BrotherNFT'; + const symbol = 'BRUH'; + + beforeEach(() => { + ERC721Mock.prototype.name.mockResolvedValue(name); + ERC721Mock.prototype.symbol.mockResolvedValue(symbol); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test('should store the token information', async () => { + await inputAdded.handle(logErc721TransferV2, block, ctx); + + expect(mockNftStorage.size).toBe(1); + const [token] = + mockNftStorage.values() as IterableIterator; + + expect(token).toEqual({ + chain: expectedChain, + id: `${sepolia.id}-0x7a3cc9c0408887a030a0354330c36a9cd681aa7e`, + address: '0x7a3cc9c0408887a030a0354330c36a9cd681aa7e', + name, + symbol, + }); + }); + + test('should store the deposit information', async () => { + await inputAdded.handle(logErc721TransferV2, block, ctx); + + expect(mockErc721DepositStorage.size).toBe(1); + const [deposit] = + mockErc721DepositStorage.values() as IterableIterator; + + expect(deposit).toEqual({ + chain: expectedChain, + id: `${sepolia.id}-0x4ca2f6935200b9a782a78f408f640f17b29809d8-v2-1`, + from: logErc721Transfer.transaction?.from, + token: { + chain: expectedChain, + id: `${sepolia.id}-0x7a3cc9c0408887a030a0354330c36a9cd681aa7e`, + address: '0x7a3cc9c0408887a030a0354330c36a9cd681aa7e', + name, + symbol, + }, + tokenIndex: 1n, + }); + }); + + test('should assign the erc721 deposit information correctly into the input', async () => { + await inputAdded.handle(logErc721TransferV2, block, ctx); + + expect(mockInputStorage.size).toBe(1); + const [input] = + mockInputStorage.values() as IterableIterator; + + expect(input.erc721Deposit).toEqual({ + chain: expectedChain, + from: '0xa074683b5be015f053b5dceb064c41fc9d11b6e5', + id: `${sepolia.id}-0x4ca2f6935200b9a782a78f408f640f17b29809d8-v2-1`, + token: { + chain: expectedChain, + id: `${sepolia.id}-0x7a3cc9c0408887a030a0354330c36a9cd681aa7e`, + address: '0x7a3cc9c0408887a030a0354330c36a9cd681aa7e', + name, + symbol, + }, + tokenIndex: 1n, + }); + }); + + test('should handle the absence of name and symbol methods in the ERC-721 contract', async () => { + ERC721Mock.prototype.name.mockRejectedValue( + new Error('No name method implemented on contract'), + ); + ERC721Mock.prototype.symbol.mockRejectedValue( + new Error('No symbol method implemented on contract'), + ); + + await inputAdded.handle(logErc721TransferV2, block, ctx); + + expect(mockInputStorage.size).toBe(1); + const [input] = + mockInputStorage.values() as IterableIterator; + expect(input.erc721Deposit?.token).toEqual({ + id: `${sepolia.id}-0x7a3cc9c0408887a030a0354330c36a9cd681aa7e`, + address: '0x7a3cc9c0408887a030a0354330c36a9cd681aa7e', + name: null, + symbol: null, + chain: expectedChain, + }); + }); + }); + + describe('ERC-1155 deposits', () => { + const tokenAddress = `${sepolia.id}-0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e`; + + afterEach(() => { + vi.clearAllMocks(); + }); + + test('should store the token information', async () => { + expect(mockMultiTokenStorage.size).toBe(0); + + await inputAdded.handle(logErc1155SingleTransferV2, block, ctx); + + expect(mockMultiTokenStorage.size).toBe(1); + const token = mockMultiTokenStorage.get(tokenAddress); + expect(token?.id).toEqual(tokenAddress); + expect(token?.address).toEqual( + '0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e', + ); + expect(token?.chain).toEqual({ id: '11155111' }); + }); + + test('should store the deposit information for single transfer', async () => { + const inputId = `${sepolia.id}-0x4ca2f6935200b9a782a78f408f640f17b29809d8-v2-2`; + expect(mockErc1155DepositStorage.size).toBe(0); + await inputAdded.handle(logErc1155SingleTransferV2, block, ctx); + + expect(mockErc1155DepositStorage.size).toBe(1); + + const deposit = mockErc1155DepositStorage.get(inputId); + + expect(deposit).toEqual({ + from: '0xa074683b5be015f053b5dceb064c41fc9d11b6e5', + id: inputId, + chain: expectedChain, + token: { + id: `${sepolia.id}-0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e`, + address: '0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e', + chain: expectedChain, + }, + transfers: [ + { + amount: 100n, + tokenIndex: 0n, + }, + ], + }); + }); + + test('should store the deposit information for batch transfer', async () => { + const inputId = `${sepolia.id}-0x4ca2f6935200b9a782a78f408f640f17b29809d8-v2-2`; + expect(mockErc1155DepositStorage.size).toBe(0); + await inputAdded.handle(logErc1155BatchTransferV2, block, ctx); + + expect(mockErc1155DepositStorage.size).toBe(1); + + const deposit = mockErc1155DepositStorage.get(inputId); + + expect(deposit).toEqual({ + from: '0xa074683b5be015f053b5dceb064c41fc9d11b6e5', + id: inputId, + chain: expectedChain, + token: { + id: `${sepolia.id}-0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e`, + address: '0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e', + chain: expectedChain, + }, + transfers: [ + { + amount: 100n, + tokenIndex: 1n, + }, + { + amount: 200n, + tokenIndex: 2n, + }, + ], + }); + }); + }); + }); +}); diff --git a/tests/stubs/params.ts b/tests/stubs/params.ts index 502cdfa..5e26b1c 100644 --- a/tests/stubs/params.ts +++ b/tests/stubs/params.ts @@ -8,10 +8,18 @@ import { CartesiDAppFactoryAddress, ERC20PortalAddress, InputBoxAddress, + RollupsAddressBook, } from '../../src/config'; -import { Input } from '../../src/model'; +import { Input, RollupVersion } from '../../src/model'; import { BlockData, Log } from '../../src/processor'; import { generateIDFrom } from '../../src/utils'; +import { + buildInputAddedLogData, + encodeErc1155BatchInput, + encodeErc1155SingleInput, + encodeErc20PortalInput, + encodeErc721PortalInput, +} from './utils'; vi.mock('@subsquid/logger', async (importOriginal) => { const actualMods = await importOriginal; @@ -53,6 +61,8 @@ export const input = { chain: { id: sepolia.id.toString(), }, + address: '0x60a7048c3136293071605a4eaffef49923e981cc', + rollupVersion: RollupVersion.v1, }, index: 1, msgSender: ERC20PortalAddress, @@ -68,6 +78,156 @@ export const input = { '0x6a3d76983453c0f74188bd89e01576c35f9d9b02daecdd49f7171aeb2bd3dc78', } satisfies Input; +export const logApplicationCreatedV2: Log = { + id: '0006859373-c8732-000014', + logIndex: 14, + transactionIndex: 10, + address: '0x1d4cfbd2622d802a07ceb4c3401bbb455c9dbdc3', + data: '0x000000000000000000000000590f92fea8df163fff2d7df266364de7ce8f9e169f24c52e0fcd1ac696d00405c3bd5adc558c48936919ac5ab3718fcb7d70f93f000000000000000000000000fb92024ec789bb2fbbc5cd1390386843c5fb7694', + topics: [ + '0xe73165c2d277daf8713fd08b40845cb6bb7a20b2b543f3d35324a475660fcebd', + '0x0000000000000000000000004821e772f7e84abd6cfd63cdb3ca098807d8ee0a', + ], + // @ts-ignore + block: { + id: '0006859373-c8732', + height: 6859373, + hash: '0xc8732c84ececbcf7a96881cd7ec30e3409e6ba66404b2ebd89e271517c399d8d', + parentHash: + '0x04aab6d11d9cb2c00b33b37612a34137bb9e1d3c26d8ef1fd0f9dddd6cd2b471', + timestamp: 1728693996000, + }, + // @ts-ignore + transaction: { + id: '0006859373-c8732-000010', + transactionIndex: 10, + hash: '0x3fa0363695c48298965949aab43b49299559254977908ce696506c00c1f6a75c', + from: '0x590f92fea8df163fff2d7df266364de7ce8f9e16', + to: '0x4c11c7f82d6d56a726f9b53dd99af031afd86bb6', + value: 0n, + chainId: 11155111, + }, +}; + +export const logInputAddedV2: Log = { + id: '0006859416-fd6ca-000028', + logIndex: 28, + transactionIndex: 15, + address: '0x593e5bcf894d6829dd26d0810da7f064406aebb6', + data: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000144415bf3630000000000000000000000000000000000000000000000000000000000aa36a7000000000000000000000000fb92024ec789bb2fbbc5cd1390386843c5fb7694000000000000000000000000590f92fea8df163fff2d7df266364de7ce8f9e16000000000000000000000000000000000000000000000000000000000068aa98000000000000000000000000000000000000000000000000000000006709c980094a6affe3aa787280fbdf0a19cdb161b26afdd58fd2672c4615c07ced7f351d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002bb1100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + topics: [ + '0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98', + '0x000000000000000000000000fb92024ec789bb2fbbc5cd1390386843c5fb7694', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + //@ts-ignore + block: { + id: '0006859416-fd6ca', + height: 6859416, + hash: '0xfd6ca8df8f19efebb7fea47c9697c37504c2da623039ef928d48a2f547613bd9', + parentHash: + '0x1642b6306abb197f45b916aeee19659ed2feb132b2c4d9a0bda89c5e7ff6629a', + timestamp: 1728694656000, + }, + // @ts-ignore + transaction: { + id: '0006859416-fd6ca-000015', + transactionIndex: 15, + hash: '0x0449bc3dcc0f0cdcbd5674823d0102eefd54c1803f0ae6c1812e73bd26d2a4c9', + from: '0x590f92fea8df163fff2d7df266364de7ce8f9e16', + to: '0x593e5bcf894d6829dd26d0810da7f064406aebb6', + value: 0n, + chainId: 11155111, + }, +}; + +const fromAddress = '0xf9e958241c1ca380cfcd50170ec43974bded0bff'; + +export const logErc20TransferV2: Log = { + ...logInputAddedV2, + topics: [ + logInputAddedV2.topics[0], + encodeAbiParameters([{ type: 'address' }], [dappAddress]), + encodeAbiParameters([{ type: 'uint256' }], [10n]), + ], + data: buildInputAddedLogData({ + appContract: dappAddress, + index: 10n, + msgSender: RollupsAddressBook.v2.ERC20Portal, + payload: encodeErc20PortalInput({ + token: '0x813ae0539daf858599a1b2a7083380542a7b1bb5', + sender: fromAddress, + amount: 111000000000000000n, + execLayerData: '0x', + }), + }), +}; + +export const logErc721TransferV2: Log = { + ...logInputAddedV2, + topics: [ + logInputAddedV2.topics[0], + encodeAbiParameters([{ type: 'address' }], [dappAddress]), + encodeAbiParameters([{ type: 'uint256' }], [10n]), + ], + data: buildInputAddedLogData({ + appContract: dappAddress, + index: 1n, + msgSender: RollupsAddressBook.v2.ERC721Portal, + payload: encodeErc721PortalInput({ + token: '0x7a3cc9c0408887a030a0354330c36a9cd681aa7e', + tokenId: 1n, + sender: '0xa074683b5be015f053b5dceb064c41fc9d11b6e5', + baseLayerData: '0x', + execLayerData: '0x', + }), + }), +}; + +export const logErc1155SingleTransferV2: Log = { + ...logInputAddedV2, + topics: [ + logInputAddedV2.topics[0], + encodeAbiParameters([{ type: 'address' }], [dappAddress]), + encodeAbiParameters([{ type: 'uint256' }], [2n]), + ], + data: buildInputAddedLogData({ + appContract: dappAddress, + index: 2n, + msgSender: RollupsAddressBook.v2.ERC1155SinglePortal, + payload: encodeErc1155SingleInput({ + token: '0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e', + sender: '0xa074683b5be015f053b5dceb064c41fc9d11b6e5', + tokenId: 0n, + value: 100n, + baseLayerData: '0x', + execLayerData: '0x', + }), + }), +}; + +export const logErc1155BatchTransferV2: Log = { + ...logInputAddedV2, + topics: [ + logInputAddedV2.topics[0], + encodeAbiParameters([{ type: 'address' }], [dappAddress]), + encodeAbiParameters([{ type: 'uint256' }], [2n]), + ], + data: buildInputAddedLogData({ + appContract: dappAddress, + index: 2n, + msgSender: RollupsAddressBook.v2.ERC1155BatchPortal, + payload: encodeErc1155BatchInput({ + token: '0x2960f4db2b0993ae5b59bc4a0f5ec7a1767e905e', + sender: '0xa074683b5be015f053b5dceb064c41fc9d11b6e5', + tokenIds: [1n, 2n], + values: [100n, 200n], + baseLayerData: '0x', + execLayerData: '0x', + }), + }), +}; + export const logErc721Transfer: Log = { id: '0004867730-000035-2c78f', address: InputBoxAddress, diff --git a/tests/stubs/utils.ts b/tests/stubs/utils.ts index 59d4386..6b13654 100644 --- a/tests/stubs/utils.ts +++ b/tests/stubs/utils.ts @@ -1,4 +1,14 @@ +import { abi as InputBoxV2Abi } from '@cartesi/rollups-v2/export/artifacts/contracts/inputs/InputBox.sol/InputBox.json'; +import { + Address, + encodeAbiParameters, + encodeFunctionData, + encodePacked, + Hex, + parseAbiParameters, +} from 'viem'; import { MockedObject, vi } from 'vitest'; +import { evmAdvanceAbi } from '../../src/decoders/evmAdvance'; export const mockModelImplementation = (object: T) => { const Mock = vi.mocked(object) as MockedObject; @@ -6,3 +16,174 @@ export const mockModelImplementation = (object: T) => { Mock.mockImplementation((args) => ({ ...args } as object)); return Mock; }; + +type ERC721PortalInput = { + token: Address; + sender: Address; + tokenId: bigint; + execLayerData: Hex; + baseLayerData: Hex; +}; + +type ERC1155BatchPortalInput = { + token: Address; + sender: Address; + tokenIds: bigint[]; + values: bigint[]; + execLayerData: Hex; + baseLayerData: Hex; +}; + +type ERC1155SinglePortalInput = { + token: Address; + sender: Address; + tokenId: bigint; + value: bigint; + execLayerData: Hex; + baseLayerData: Hex; +}; + +type ERC20PortalInput = { + token: Address; + sender: Address; + amount: bigint; + execLayerData: Hex; +}; + +type EvmAdvanceInput = { + chainId: bigint; + appContract: Address; + msgSender: Address; + blockNumber: bigint; + blockTimestamp: bigint; + prevRandao: bigint; + index: bigint; + payload: Hex; +}; + +const baseExecLayerAbiParameters = parseAbiParameters( + 'bytes baseLayer, bytes execLayer', +); + +export const encodeErc20PortalInput = ({ + token, + sender, + amount, + execLayerData, +}: ERC20PortalInput) => + encodePacked( + ['address', 'address', 'uint256', 'bytes'], + [token, sender, amount, execLayerData], + ); + +export const encodeErc721PortalInput = ({ + baseLayerData, + execLayerData, + sender, + token, + tokenId, +}: ERC721PortalInput) => { + const data = encodeAbiParameters(baseExecLayerAbiParameters, [ + baseLayerData, + execLayerData, + ]); + + return encodePacked( + ['address', 'address', 'uint256', 'bytes'], + [token, sender, tokenId, data], + ); +}; + +export const encodeErc1155SingleInput = ({ + baseLayerData, + execLayerData, + sender, + token, + tokenId, + value, +}: ERC1155SinglePortalInput) => { + const data = encodeAbiParameters(baseExecLayerAbiParameters, [ + baseLayerData, + execLayerData, + ]); + + return encodePacked( + ['address', 'address', 'uint256', 'uint256', 'bytes'], + [token, sender, tokenId, value, data], + ); +}; + +export const encodeErc1155BatchInput = ({ + baseLayerData, + execLayerData, + sender, + token, + tokenIds, + values, +}: ERC1155BatchPortalInput) => { + const data = encodeAbiParameters( + parseAbiParameters( + 'uint[] ids, uint[] values, bytes baseLayer, bytes execLayer', + ), + [tokenIds, values, baseLayerData, execLayerData], + ); + + return encodePacked(['address', 'address', 'bytes'], [token, sender, data]); +}; + +const buildInputFor = ( + appContract: Address, + payload: Hex, + msgSender: Address, + index: bigint, +): EvmAdvanceInput => { + return { + chainId: 11155111n, + appContract, + blockTimestamp: 1691384268n, + blockNumber: 4040941n, + index, + payload, + msgSender, + prevRandao: 2n, + }; +}; + +const encodeWithEvmAdvance = (input: EvmAdvanceInput) => + encodeFunctionData({ + abi: evmAdvanceAbi, + functionName: 'EvmAdvance', + args: [ + input.chainId, + input.appContract, + input.msgSender, + input.blockNumber, + input.blockTimestamp, + input.prevRandao, + input.index, + input.payload, + ], + }); + +const inputAddedEvt = InputBoxV2Abi.find( + (item) => item.type === 'event' && item.name === 'InputAdded', +); + +interface BuildInputAddedDataParams { + appContract: Address; + payload: Hex; + index: bigint; + msgSender: Address; +} + +export const buildInputAddedLogData = ({ + appContract, + index, + msgSender, + payload, +}: BuildInputAddedDataParams) => { + const input = buildInputFor(appContract, payload, msgSender, index); + const encodedInput = encodeWithEvmAdvance(input); + + return encodeAbiParameters([{ type: 'bytes' }], [encodedInput]); +};