Skip to content

Commit

Permalink
feat: add support for evm-like contracts and transactions (#337)
Browse files Browse the repository at this point in the history
# What ❔

Add badge that shows "EVM" for evm-like contracts and transactions.

## Checklist

<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->

- [ ] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated.

---------

Co-authored-by: petarTxFusion <130662325+petarTxFusion@users.noreply.github.com>
Co-authored-by: Roman Petriv <petriv.roma@gmail.com>
  • Loading branch information
3 people authored Dec 19, 2024
1 parent 38a05a3 commit 83560f0
Show file tree
Hide file tree
Showing 54 changed files with 336 additions and 103 deletions.
2 changes: 2 additions & 0 deletions packages/api/src/address/address.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ describe("AddressController", () => {
blockNumber: addressBalances.blockNumber,
balances: addressBalances.balances,
totalTransactions: totalTxCount,
isEvmLike: addressRecord.isEvmLike,
});
});

Expand All @@ -163,6 +164,7 @@ describe("AddressController", () => {
blockNumber: addressRecord.createdInBlockNumber,
balances: defaultBalancesResponse.balances,
totalTransactions: totalTxCount,
isEvmLike: addressRecord.isEvmLike,
});
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/address/address.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class AddressController {
creatorTxHash: addressRecord.creatorTxHash,
totalTransactions,
creatorAddress: addressRecord.creatorAddress,
isEvmLike: addressRecord.isEvmLike,
};
}

Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/address/address.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ export class Address extends BaseEntity {

@Column({ type: "bytea", nullable: true, transformer: normalizeAddressTransformer })
public readonly creatorAddress?: string;

@Column({ type: "boolean", default: false })
public readonly isEvmLike: boolean;
}
7 changes: 7 additions & 0 deletions packages/api/src/address/dtos/contract.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ export class ContractDto extends BaseAddressDto {
example: "0xd754Ff5e8a6f257E162F72578A4bB0493c0681d8",
})
public readonly creatorAddress: string;

@ApiProperty({
type: Boolean,
description: "Is the contract EVM-like",
example: true,
})
public readonly isEvmLike: boolean;
}
4 changes: 3 additions & 1 deletion packages/api/src/api/dtos/account/accountTransaction.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ export class AccountTransactionDto {
type: String,
description: "The to address of this transaction",
example: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
examples: ["0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", null],
nullable: true,
})
public readonly to: string;
public readonly to?: string;

@ApiProperty({
type: String,
Expand Down
13 changes: 6 additions & 7 deletions packages/api/src/api/transaction/transaction.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { mock } from "jest-mock-extended";
import { Logger } from "@nestjs/common";
import { TransactionService } from "../../transaction/transaction.service";
import { TransactionReceiptService } from "../../transaction/transactionReceipt.service";
import { TransactionStatus } from "../../transaction/entities/transaction.entity";
import { TransactionDetails } from "../../transaction/entities/transactionDetails.entity";
import { TransactionStatus, Transaction } from "../../transaction/entities/transaction.entity";
import { TransactionReceipt } from "../../transaction/entities/transactionReceipt.entity";
import { ResponseStatus, ResponseMessage } from "../dtos/common/responseBase.dto";
import { TransactionController } from "./transaction.controller";
Expand Down Expand Up @@ -57,7 +56,7 @@ describe("TransactionController", () => {
it("returns isError as 0 when transaction is successful", async () => {
jest
.spyOn(transactionServiceMock, "findOne")
.mockResolvedValue({ status: TransactionStatus.Included } as TransactionDetails);
.mockResolvedValue({ status: TransactionStatus.Included } as Transaction);

const response = await controller.getTransactionStatus(transactionHash);
expect(response).toEqual({
Expand All @@ -73,7 +72,7 @@ describe("TransactionController", () => {
it("returns isError as 1 when transaction is failed", async () => {
jest
.spyOn(transactionServiceMock, "findOne")
.mockResolvedValue({ status: TransactionStatus.Failed } as TransactionDetails);
.mockResolvedValue({ status: TransactionStatus.Failed } as Transaction);

const response = await controller.getTransactionStatus(transactionHash);
expect(response).toEqual({
Expand All @@ -91,7 +90,7 @@ describe("TransactionController", () => {
status: TransactionStatus.Failed,
error: "Error",
revertReason: "Reverted",
} as TransactionDetails);
} as Transaction);

const response = await controller.getTransactionStatus(transactionHash);
expect(response).toEqual({
Expand All @@ -107,7 +106,7 @@ describe("TransactionController", () => {
it("returns transaction revert reason in errDescription when transaction is failed and transaction revert reason is present", async () => {
jest
.spyOn(transactionServiceMock, "findOne")
.mockResolvedValue({ status: TransactionStatus.Failed, revertReason: "Reverted" } as TransactionDetails);
.mockResolvedValue({ status: TransactionStatus.Failed, revertReason: "Reverted" } as Transaction);

const response = await controller.getTransactionStatus(transactionHash);
expect(response).toEqual({
Expand All @@ -123,7 +122,7 @@ describe("TransactionController", () => {
it("returns empty errDescription when transaction is failed and transaction error and revert reason are not present", async () => {
jest
.spyOn(transactionServiceMock, "findOne")
.mockResolvedValue({ status: TransactionStatus.Failed } as TransactionDetails);
.mockResolvedValue({ status: TransactionStatus.Failed } as Transaction);

const response = await controller.getTransactionStatus(transactionHash);
expect(response).toEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const normalizeAddressTransformer: ValueTransformer = {
if (!hex) {
return null;
}

return getAddress(hexTransformer.from(hex));
},
};
20 changes: 19 additions & 1 deletion packages/api/src/transaction/dtos/transaction.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ export class TransactionDto {
type: String,
description: "The address this transaction is to",
example: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
examples: ["0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", null],
nullable: true,
})
public readonly to: string;
public readonly to?: string;

@ApiProperty({
type: String,
Expand Down Expand Up @@ -204,4 +206,20 @@ export class TransactionDto {
nullable: true,
})
public readonly revertReason?: string;

@ApiProperty({
type: String,
description: "Gas used by the transaction",
example: "50000000",
})
public readonly gasUsed: string;

@ApiProperty({
type: String,
description: "Address of the deployed contract",
example: "50000000",
examples: ["0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C", null],
nullable: true,
})
public readonly contractAddress?: string;
}
11 changes: 0 additions & 11 deletions packages/api/src/transaction/dtos/transactionDetails.dto.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export class AddressTransaction extends BaseEntity {
@Column({ type: "bytea", transformer: hexTransformer })
public readonly transactionHash: string;

@Column({ type: "bytea", transformer: normalizeAddressTransformer })
public readonly address: string;
@Column({ type: "bytea", transformer: normalizeAddressTransformer, nullable: true })
public readonly address?: string;

@Index()
@Column({ type: "bigint", transformer: bigIntNumberTransformer })
Expand Down
16 changes: 13 additions & 3 deletions packages/api/src/transaction/entities/transaction.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export class Transaction extends BaseEntity {
@Column({ generated: true, type: "bigint" })
public number: number;

@Column({ type: "bytea", transformer: normalizeAddressTransformer })
public readonly to: string;
@Column({ type: "bytea", transformer: normalizeAddressTransformer, nullable: true })
public readonly to?: string;

@Index()
@Column({ type: "bytea", transformer: normalizeAddressTransformer })
Expand Down Expand Up @@ -141,16 +141,26 @@ export class Transaction extends BaseEntity {
return !!this.batch;
}

public get gasUsed(): string {
return this.transactionReceipt ? this.transactionReceipt.gasUsed : null;
}

public get contractAddress(): string {
return this.transactionReceipt ? this.transactionReceipt.contractAddress : null;
}

toJSON(): any {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { number, receiptStatus, batch, ...restFields } = this;
const { number, receiptStatus, batch, transactionReceipt, ...restFields } = this;
return {
...restFields,
status: this.status,
commitTxHash: this.commitTxHash,
executeTxHash: this.executeTxHash,
proveTxHash: this.proveTxHash,
isL1BatchSealed: this.isL1BatchSealed,
gasUsed: this.gasUsed,
contractAddress: this.contractAddress,
};
}
}
18 changes: 0 additions & 18 deletions packages/api/src/transaction/entities/transactionDetails.entity.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export class TransactionReceipt extends BaseEntity {
@Column({ type: "bytea", transformer: normalizeAddressTransformer })
public readonly from: string;

@Column({ type: "bytea", transformer: hexTransformer, nullable: true })
public readonly to?: string;

@Index()
@Column({ type: "bytea", nullable: true, transformer: normalizeAddressTransformer })
public readonly contractAddress?: string;
Expand Down
3 changes: 1 addition & 2 deletions packages/api/src/transaction/transaction.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { buildDateFilter } from "../common/utils";
import { FilterTransactionsOptionsDto } from "./dtos/filterTransactionsOptions.dto";
import { TransferDto } from "../transfer/transfer.dto";
import { TransactionDto } from "./dtos/transaction.dto";
import { TransactionDetailsDto } from "./dtos/transactionDetails.dto";
import { TransferService } from "../transfer/transfer.service";
import { LogDto } from "../log/log.dto";
import { LogService } from "../log/log.service";
Expand Down Expand Up @@ -74,7 +73,7 @@ export class TransactionController {
@ApiNotFoundResponse({ description: "Transaction with the specified hash does not exist" })
public async getTransaction(
@Param("transactionHash", new ParseTransactionHashPipe()) transactionHash: string
): Promise<TransactionDetailsDto> {
): Promise<TransactionDto> {
const transactionDetail = await this.transactionService.findOne(transactionHash);
if (!transactionDetail) {
throw new NotFoundException();
Expand Down
3 changes: 1 addition & 2 deletions packages/api/src/transaction/transaction.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { TransactionController } from "./transaction.controller";
import { TransactionService } from "./transaction.service";
import { TransactionReceiptService } from "./transactionReceipt.service";
import { Transaction } from "./entities/transaction.entity";
import { TransactionDetails } from "./entities/transactionDetails.entity";
import { AddressTransaction } from "./entities/addressTransaction.entity";
import { TransactionReceipt } from "./entities/transactionReceipt.entity";
import { Batch } from "../batch/batch.entity";
Expand All @@ -14,7 +13,7 @@ import { LogModule } from "../log/log.module";

@Module({
imports: [
TypeOrmModule.forFeature([Transaction, TransactionDetails, AddressTransaction, TransactionReceipt, Batch]),
TypeOrmModule.forFeature([Transaction, AddressTransaction, TransactionReceipt, Batch]),
TransferModule,
LogModule,
CounterModule,
Expand Down
45 changes: 35 additions & 10 deletions packages/api/src/transaction/transaction.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { SortingOrder } from "../common/types";
import { CounterService } from "../counter/counter.service";
import { TransactionService, FilterTransactionsOptions } from "./transaction.service";
import { Transaction } from "./entities/transaction.entity";
import { TransactionDetails } from "./entities/transactionDetails.entity";
import { AddressTransaction } from "./entities/addressTransaction.entity";
import { Batch } from "../batch/batch.entity";

Expand All @@ -18,7 +17,6 @@ describe("TransactionService", () => {
let transaction;
let service: TransactionService;
let repositoryMock: typeorm.Repository<Transaction>;
let repositoryDetailMock: typeorm.Repository<TransactionDetails>;
let addressTransactionRepositoryMock: typeorm.Repository<AddressTransaction>;
let batchRepositoryMock: typeorm.Repository<Batch>;
let counterServiceMock: CounterService;
Expand All @@ -27,7 +25,6 @@ describe("TransactionService", () => {
beforeEach(async () => {
counterServiceMock = mock<CounterService>();
repositoryMock = mock<typeorm.Repository<Transaction>>();
repositoryDetailMock = mock<typeorm.Repository<TransactionDetails>>();
addressTransactionRepositoryMock = mock<typeorm.Repository<AddressTransaction>>();
batchRepositoryMock = mock<typeorm.Repository<Batch>>();
transaction = {
Expand All @@ -41,10 +38,6 @@ describe("TransactionService", () => {
provide: getRepositoryToken(Transaction),
useValue: repositoryMock,
},
{
provide: getRepositoryToken(TransactionDetails),
useValue: repositoryDetailMock,
},
{
provide: getRepositoryToken(AddressTransaction),
useValue: addressTransactionRepositoryMock,
Expand Down Expand Up @@ -73,13 +66,13 @@ describe("TransactionService", () => {

beforeEach(() => {
queryBuilderMock = mock<typeorm.SelectQueryBuilder<Transaction>>();
(repositoryDetailMock.createQueryBuilder as jest.Mock).mockReturnValue(queryBuilderMock);
(repositoryMock.createQueryBuilder as jest.Mock).mockReturnValue(queryBuilderMock);
(queryBuilderMock.getOne as jest.Mock).mockResolvedValue(null);
});

it("creates query builder with proper params", async () => {
await service.findOne(hash);
expect(repositoryDetailMock.createQueryBuilder).toHaveBeenCalledWith("transaction");
expect(repositoryMock.createQueryBuilder).toHaveBeenCalledWith("transaction");
});

it("filters transactions by the specified hash", async () => {
Expand All @@ -99,7 +92,10 @@ describe("TransactionService", () => {

it("selects only needed transactionReceipt fields", async () => {
await service.findOne(hash);
expect(queryBuilderMock.addSelect).toHaveBeenCalledWith(["transactionReceipt.gasUsed"]);
expect(queryBuilderMock.addSelect).toHaveBeenCalledWith([
"transactionReceipt.gasUsed",
"transactionReceipt.contractAddress",
]);
});

it("returns paginated result", async () => {
Expand Down Expand Up @@ -172,6 +168,19 @@ describe("TransactionService", () => {
expect(queryBuilderMock.where).toHaveBeenCalledWith(filterTransactionsOptions);
});

it("joins transactionReceipt record to get receipt specific fields", async () => {
await service.findAll(filterTransactionsOptions, pagingOptions);
expect(queryBuilderMock.leftJoin).toHaveBeenCalledWith("transaction.transactionReceipt", "transactionReceipt");
});

it("selects only needed transactionReceipt fields", async () => {
await service.findAll(filterTransactionsOptions, pagingOptions);
expect(queryBuilderMock.addSelect).toHaveBeenCalledWith([
"transactionReceipt.gasUsed",
"transactionReceipt.contractAddress",
]);
});

it("joins batch record to get batch specific fields", async () => {
await service.findAll(filterTransactionsOptions, pagingOptions);
expect(queryBuilderMock.leftJoin).toHaveBeenCalledWith("transaction.batch", "batch");
Expand Down Expand Up @@ -243,6 +252,22 @@ describe("TransactionService", () => {
);
});

it("joins transactionReceipt record to get receipt specific fields", async () => {
await service.findAll(filterTransactionsOptions, pagingOptions);
expect(addressTransactionsQueryBuilderMock.leftJoin).toHaveBeenCalledWith(
"transaction.transactionReceipt",
"transactionReceipt"
);
});

it("selects only needed transactionReceipt fields", async () => {
await service.findAll(filterTransactionsOptions, pagingOptions);
expect(addressTransactionsQueryBuilderMock.addSelect).toHaveBeenCalledWith([
"transactionReceipt.gasUsed",
"transactionReceipt.contractAddress",
]);
});

it("joins batch records", async () => {
await service.findAll(filterTransactionsOptions, pagingOptions);
expect(addressTransactionsQueryBuilderMock.leftJoinAndSelect).toBeCalledTimes(1);
Expand Down
Loading

0 comments on commit 83560f0

Please sign in to comment.