diff --git a/packages/api/src/common/transformers/normalizeAddress.transformer.ts b/packages/api/src/common/transformers/normalizeAddress.transformer.ts index ec08defd4..2d4421830 100644 --- a/packages/api/src/common/transformers/normalizeAddress.transformer.ts +++ b/packages/api/src/common/transformers/normalizeAddress.transformer.ts @@ -10,6 +10,9 @@ export const normalizeAddressTransformer: ValueTransformer = { if (!hex) { return null; } + if (hexTransformer.from(hex) === "0x") { + return ""; + } return getAddress(hexTransformer.from(hex)); }, }; diff --git a/packages/api/src/transaction/dtos/transaction.dto.ts b/packages/api/src/transaction/dtos/transaction.dto.ts index 4d8020c99..5244b9b95 100644 --- a/packages/api/src/transaction/dtos/transaction.dto.ts +++ b/packages/api/src/transaction/dtos/transaction.dto.ts @@ -212,4 +212,12 @@ export class TransactionDto { nullable: true, }) public readonly isEvmLike?: boolean; + + @ApiProperty({ + type: String, + description: "Address of the first deployed EVM contract", + example: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", + nullable: true, + }) + public readonly contractAddress?: string; } diff --git a/packages/api/src/transaction/entities/transaction.entity.ts b/packages/api/src/transaction/entities/transaction.entity.ts index 86930f2af..ecf27e7b7 100644 --- a/packages/api/src/transaction/entities/transaction.entity.ts +++ b/packages/api/src/transaction/entities/transaction.entity.ts @@ -110,6 +110,9 @@ export class Transaction extends BaseEntity { @Column({ type: "boolean", nullable: true }) public readonly isEvmLike?: boolean; + @Column({ type: "bytea", transformer: hexTransformer, nullable: true }) + public readonly contractAddress?: string; + public get status(): TransactionStatus { if (this.receiptStatus === 0) { return TransactionStatus.Failed; diff --git a/packages/api/test/transaction.e2e-spec.ts b/packages/api/test/transaction.e2e-spec.ts index bb3ceb75a..6b7f22563 100644 --- a/packages/api/test/transaction.e2e-spec.ts +++ b/packages/api/test/transaction.e2e-spec.ts @@ -269,6 +269,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4009", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e19", isEvmLike: null, + contractAddress: null, isL1BatchSealed: false, isL1Originated: true, l1BatchNumber: 9, @@ -298,6 +299,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4008", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e18", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 8, @@ -327,6 +329,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4007", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e17", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 7, @@ -356,6 +359,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4006", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e16", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 6, @@ -385,6 +389,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4005", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e15", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 5, @@ -414,6 +419,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4004", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e14", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 4, @@ -443,6 +449,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4003", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e13", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 3, @@ -472,6 +479,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4002", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e12", isEvmLike: null, + contractAddress: null, isL1BatchSealed: false, isL1Originated: true, l1BatchNumber: 1, @@ -501,6 +509,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4001", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e11", isEvmLike: null, + contractAddress: null, isL1BatchSealed: false, isL1Originated: true, l1BatchNumber: 1, @@ -530,6 +539,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4000", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e10", isEvmLike: null, + contractAddress: null, isL1BatchSealed: false, isL1Originated: true, l1BatchNumber: 1, @@ -569,6 +579,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4008", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e18", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 8, @@ -598,6 +609,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4007", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e17", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 7, @@ -627,6 +639,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4006", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e16", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 6, @@ -712,6 +725,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4001", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e11", isEvmLike: null, + contractAddress: null, isL1BatchSealed: false, isL1Originated: true, l1BatchNumber: 1, @@ -766,6 +780,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4001", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e11", isEvmLike: null, + contractAddress: null, isL1BatchSealed: false, isL1Originated: true, l1BatchNumber: 1, @@ -820,6 +835,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4007", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e17", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 7, @@ -849,6 +865,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4006", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e16", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 6, @@ -940,6 +957,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4008", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e18", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 8, @@ -978,6 +996,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4005", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e15", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 5, @@ -1016,6 +1035,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4003", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e13", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 3, @@ -1054,6 +1074,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4000", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e10", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 1, @@ -1092,6 +1113,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4009", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e19", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 9, @@ -1130,6 +1152,7 @@ describe("TransactionController (e2e)", () => { maxPriorityFeePerGas: "4000", hash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e10", isEvmLike: null, + contractAddress: null, isL1BatchSealed: true, isL1Originated: true, l1BatchNumber: 1, diff --git a/packages/app/src/composables/common/Api.d.ts b/packages/app/src/composables/common/Api.d.ts index fefa74957..29ec7b300 100644 --- a/packages/app/src/composables/common/Api.d.ts +++ b/packages/app/src/composables/common/Api.d.ts @@ -95,6 +95,7 @@ declare namespace Api { error: string | null; revertReason: string | null; isEvmLike: boolean; + contractAddress: string | null; }; type Transfer = { diff --git a/packages/app/src/composables/useTransaction.ts b/packages/app/src/composables/useTransaction.ts index aef58db49..f57e93fb5 100644 --- a/packages/app/src/composables/useTransaction.ts +++ b/packages/app/src/composables/useTransaction.ts @@ -223,7 +223,7 @@ export function mapTransaction( }, value: transaction.value, from: transaction.from, - to: transaction.to, + to: transaction.isEvmLike && transaction.contractAddress ? transaction.contractAddress : transaction.to, ethCommitTxHash: transaction.commitTxHash ?? null, ethExecuteTxHash: transaction.executeTxHash ?? null, ethProveTxHash: transaction.proveTxHash ?? null, diff --git a/packages/app/tests/components/transactions/Table.spec.ts b/packages/app/tests/components/transactions/Table.spec.ts index 0fc964295..a0b7cad3d 100644 --- a/packages/app/tests/components/transactions/Table.spec.ts +++ b/packages/app/tests/components/transactions/Table.spec.ts @@ -72,6 +72,7 @@ const transaction: TransactionListItem = { maxFeePerGas: "7000", maxPriorityFeePerGas: "8000", isEvmLike: true, + contractAddress: null, error: null, revertReason: null, }; diff --git a/packages/app/tests/composables/useTransactions.spec.ts b/packages/app/tests/composables/useTransactions.spec.ts index 2ffa3df7e..0f9dad410 100644 --- a/packages/app/tests/composables/useTransactions.spec.ts +++ b/packages/app/tests/composables/useTransactions.spec.ts @@ -34,6 +34,7 @@ const transaction: TransactionListItem = { maxFeePerGas: "7000", maxPriorityFeePerGas: "8000", isEvmLike: true, + contractAddress: null, error: null, revertReason: null, }; diff --git a/packages/data-fetcher/src/transaction/transaction.service.spec.ts b/packages/data-fetcher/src/transaction/transaction.service.spec.ts index 7ce857a30..46f291c9b 100644 --- a/packages/data-fetcher/src/transaction/transaction.service.spec.ts +++ b/packages/data-fetcher/src/transaction/transaction.service.spec.ts @@ -185,6 +185,7 @@ describe("TransactionService", () => { revertReason: traceTransactionResult.revertReason, isEvmLike: false, to: undefined, + contractAddress: null, }); }); }); @@ -200,6 +201,7 @@ describe("TransactionService", () => { receiptStatus: 0, isEvmLike: false, to: undefined, + contractAddress: null, }); }); }); diff --git a/packages/data-fetcher/src/transaction/transaction.service.ts b/packages/data-fetcher/src/transaction/transaction.service.ts index 33d70063d..2dcf6efcd 100644 --- a/packages/data-fetcher/src/transaction/transaction.service.ts +++ b/packages/data-fetcher/src/transaction/transaction.service.ts @@ -14,6 +14,7 @@ export interface TransactionInfo extends types.TransactionResponse { error?: string; revertReason?: string; isEvmLike?: boolean; + contractAddress?: string; } export interface TransactionData extends LogsData { @@ -81,13 +82,15 @@ export class TransactionService { ); const isEvmLike = transactionInfo.to === null; - const toAddress = - isEvmLike && logsData.contractAddresses?.length > 0 ? logsData.contractAddresses[0].address : transactionInfo.to; + const toAddress = isEvmLike ? "0x" : transactionInfo.to; + const contractAddress = + isEvmLike && logsData.contractAddresses?.length > 0 ? logsData.contractAddresses[0].address : ""; const updatedTransactionInfo = { ...transactionInfo, isEvmLike, to: toAddress, + contractAddress, } as unknown as TransactionInfo; stopTransactionProcessingMeasuring(); diff --git a/packages/worker/src/dataFetcher/types.ts b/packages/worker/src/dataFetcher/types.ts index 6ccf2c2cc..030e21a99 100644 --- a/packages/worker/src/dataFetcher/types.ts +++ b/packages/worker/src/dataFetcher/types.ts @@ -79,6 +79,7 @@ export interface TransactionInfo error?: string; revertReason?: string; isEvmLike?: boolean; + contractAddress?: string; } export type TransactionReceipt = Modify< diff --git a/packages/worker/src/entities/transaction.entity.ts b/packages/worker/src/entities/transaction.entity.ts index b78db74d2..207e0cf57 100644 --- a/packages/worker/src/entities/transaction.entity.ts +++ b/packages/worker/src/entities/transaction.entity.ts @@ -98,4 +98,7 @@ export class Transaction extends CountableEntity { @Column({ type: "boolean", nullable: true }) public readonly isEvmLike?: boolean; + + @Column({ type: "bytea", transformer: hexTransformer, nullable: true }) + public readonly contractAddress?: string; } diff --git a/packages/worker/src/migrations/1732834001043-MakeTransactionsToFieldOptional.ts b/packages/worker/src/migrations/1732834001043-MakeTransactionsToFieldOptional.ts deleted file mode 100644 index f803f44e1..000000000 --- a/packages/worker/src/migrations/1732834001043-MakeTransactionsToFieldOptional.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class MakeTransactionsToFieldOptional1732834001043 implements MigrationInterface { - name = "MakeTransactionsToFieldOptional1732834001043"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "transactions" ALTER COLUMN "to" DROP NOT NULL`); - await queryRunner.query(`DROP INDEX "public"."IDX_f8b841e005e13e12008d9778ed"`); - await queryRunner.query(`ALTER TABLE "addressTransactions" ALTER COLUMN "address" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "transactionReceipts" ALTER COLUMN "to" DROP NOT NULL`); - await queryRunner.query( - `CREATE INDEX "IDX_f8b841e005e13e12008d9778ed" ON "addressTransactions" ("address", "blockNumber", "receivedAt", "transactionIndex") ` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_f8b841e005e13e12008d9778ed"`); - await queryRunner.query(`ALTER TABLE "transactionReceipts" ALTER COLUMN "to" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "addressTransactions" ALTER COLUMN "address" SET NOT NULL`); - await queryRunner.query( - `CREATE INDEX "IDX_f8b841e005e13e12008d9778ed" ON "addressTransactions" ("address", "blockNumber", "receivedAt", "transactionIndex") ` - ); - await queryRunner.query(`ALTER TABLE "transactions" ALTER COLUMN "to" SET NOT NULL`); - } -} diff --git a/packages/worker/src/migrations/1733778459691-AddContractAddressFieldToTransaction.ts b/packages/worker/src/migrations/1733778459691-AddContractAddressFieldToTransaction.ts new file mode 100644 index 000000000..fe9d18c7b --- /dev/null +++ b/packages/worker/src/migrations/1733778459691-AddContractAddressFieldToTransaction.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddContractAddressFieldToTransaction1733778459691 implements MigrationInterface { + name = "AddContractAddressFieldToTransaction1733778459691"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "transactions" ADD "contractAddress" bytea`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "transactions" DROP COLUMN "contractAddress"`); + } +} diff --git a/packages/worker/src/transaction/transaction.processor.spec.ts b/packages/worker/src/transaction/transaction.processor.spec.ts index cb08c4325..0049f1f42 100644 --- a/packages/worker/src/transaction/transaction.processor.spec.ts +++ b/packages/worker/src/transaction/transaction.processor.spec.ts @@ -84,6 +84,7 @@ describe("TransactionProcessor", () => { receivedAt: "2023-12-29T06:52:51.438Z", type: 3, isEvmLike: false, + contractAddress: null, }, transactionReceipt: { hash: "transactionHash", @@ -137,6 +138,7 @@ describe("TransactionProcessor", () => { effectiveGasPrice: transactionData.transactionReceipt.gasPrice, type: transactionData.transaction.type, isEvmLike: transactionData.transaction.isEvmLike, + contractAddress: transactionData.transaction.contractAddress, }); }); diff --git a/packages/worker/src/transaction/transaction.processor.ts b/packages/worker/src/transaction/transaction.processor.ts index 88bbef89f..39469bcbc 100644 --- a/packages/worker/src/transaction/transaction.processor.ts +++ b/packages/worker/src/transaction/transaction.processor.ts @@ -42,6 +42,7 @@ export class TransactionProcessor { ...transactionData.transaction, transactionIndex: transactionData.transaction.index, isEvmLike: transactionData.transaction.isEvmLike, + contractAddress: transactionData.transaction.contractAddress, }); this.logger.debug({