Skip to content

feat: add internalTransactions table / api#543

Open
dutterbutter wants to merge 26 commits intomainfrom
db/update-internal-tx-api-endpoints
Open

feat: add internalTransactions table / api#543
dutterbutter wants to merge 26 commits intomainfrom
db/update-internal-tx-api-endpoints

Conversation

@dutterbutter
Copy link
Copy Markdown
Contributor

@dutterbutter dutterbutter commented Dec 7, 2025

What ❔

  • Adds internal tx table
  • updates worker / data-fetcher / api for internal txs
  • updates traceTransaction to account for all internal txs
  • update internal tx endpoint to use new table

Why ❔

  • legacy implementation from era still being used
  • allows the collection / querying of all internal txs for contracts and accounts

To view:

By address:

curl "http://localhost:3020/api?module=account&action=txlistinternal&address=0x785c0219d5e23950f88452d23113b3b680006e6a"

By hash:

curl "http://localhost:3020/api?module=account&action=txlistinternal&txhash=0xfecccf9afeea8f502950fc5c697b6827161df699347604666050a8ed2a93fe68"

By block range:

curl "http://localhost:3020/api?/account/txlistinternal?startblock=1000&endblock=2000&page=1&offset=100

Checklist

  • 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.

@dutterbutter dutterbutter requested a review from Romsters December 7, 2025 01:32
@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 7, 2025

API E2E Test Results

185 tests   185 ✅  18s ⏱️
 13 suites    0 💤
  1 files      0 ❌

Results for commit bf45a17.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 7, 2025

API Prividium E2E Test Results

4 tests   4 ✅  6s ⏱️
1 suites  0 💤
1 files    0 ❌

Results for commit bf45a17.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 7, 2025

Unit Test Results

    4 files    261 suites   12m 17s ⏱️
2 067 tests 2 066 ✅ 1 💤 0 ❌
2 231 runs  2 230 ✅ 1 💤 0 ❌

Results for commit bf45a17.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 7, 2025

Visit the preview URL for this PR:
https://staging-scan-v2-zksyncos--pr-543-3muwe6x4.web.app

@dutterbutter dutterbutter changed the title feat: add internalTransactions table and integrate with api feat: add internalTransactions table / api Dec 7, 2025
Comment thread packages/worker/src/entities/internalTransaction.entity.ts Outdated
Comment thread packages/worker/src/repositories/internalTransaction.repository.ts Outdated
Comment thread packages/worker/src/transaction/transaction.processor.ts Outdated
Comment thread packages/worker/src/migrations/1760990000000-CreateInternalTransactions.ts Outdated
Comment thread packages/api/src/transaction/internalTransaction.service.ts Outdated
Comment thread packages/api/src/transaction/internalTransaction.service.ts Outdated
Comment thread packages/api/src/transaction/internalTransaction.service.ts Outdated
Comment thread packages/worker/src/repositories/internalTransaction.repository.ts Outdated
@Romsters
Copy link
Copy Markdown
Collaborator

Romsters commented Dec 11, 2025

Do you plan to add a BFF endpoint for the front-end?

@dutterbutter
Copy link
Copy Markdown
Contributor Author

Do you plan to add a BFF endpoint for the front-end?

I havent thought about it yet. If needed I will do so subsequently when I open a PR for UI internal tx tables. Unless you suggest otherwise?

@Romsters
Copy link
Copy Markdown
Collaborator

Do you plan to add a BFF endpoint for the front-end?

I havent thought about it yet. If needed I will do so subsequently when I open a PR for UI internal tx tables. Unless you suggest otherwise?

Ok, fine

public readonly number: number;

@ManyToOne(() => InternalTransaction, { onDelete: "CASCADE" })
@JoinColumn([
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Please use number from InternalTransaction, it's faster as it is PK.

@PrimaryColumn({ generated: true, type: "bigint" })
public readonly number: number;

@ManyToOne(() => InternalTransaction, { onDelete: "CASCADE" })
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use onDelete: "CASCADE only on block for consistency, we intentionally allow cascade deletion only on block as logically you can only revert an entire block, never a single transaction.

])
public readonly internalTransaction: InternalTransaction;

@Index()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Index(["transactionHash", "traceAddress"]) makes this index redundant

@Index(["blockNumber", "traceIndex"])
@Index(["transactionHash", "traceAddress"], { unique: true })
@Index(["type", "blockNumber", "traceIndex"])
@Index(["from"])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need from and to indexes

@PrimaryColumn({ generated: true, type: "bigint" })
public readonly number: number;

@ManyToOne(() => Transaction, { onDelete: "CASCADE" })
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -1,12 +1,15 @@
export * from "./base.repository";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exporting an abstract repository from here?


private async addAddressInternalTransactions(records: Partial<InternalTransaction>[]): Promise<void> {
const addressInternalTransactions = records.flatMap((record) => {
const baseRecord = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not critical, but I'd rather exclude number and include all the other fields, typeorm just ignores the fields not in the schema, but we won't need to edit this function if we decide to add some more fields.


const denormalizedRecords = [];

if (record.from) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why checking if from is not nullable?

return denormalizedRecords;
});

if (addressInternalTransactions.length === 0) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this check

value: BigInt(transactionTrace.value || "0x0").toString(),
gas: transactionTrace.gas ? BigInt(transactionTrace.gas).toString() : undefined,
gasUsed: transactionTrace.gasUsed ? BigInt(transactionTrace.gasUsed).toString() : undefined,
input: transactionTrace.input || "0x",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just leave these as undefined as in the DB these fields are nullable?

{
...pagingOptions,
limit: pagingOptions.offset,
route: "account/txlistinternal",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

route is not needed for this endpoint

from: internalTx.from,
to: internalTx.to || "",
value: internalTx.value,
gas: internalTx.gas?.toString() || "",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why .toString()? Isn't it already a string?

value: internalTx.value,
gas: internalTx.gas?.toString() || "",
input: internalTx.input || "",
type: internalTx.callType || internalTx.type.toLowerCase(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't type always present? Also why type.toLowerCase()? Let's just store it in lowercase?

public readonly timestamp: Date;

@Column({ type: "varchar", nullable: true })
public readonly callType?: string;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need callType?

input: transactionTrace.input || "0x",
output: transactionTrace.output || "0x",
type: traceType.toUpperCase(),
callType: traceType,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it supposed to be something different? Also do we really need it?

input: internalTx.input || "",
type: internalTx.callType || internalTx.type.toLowerCase(),
contractAddress:
internalTx.type.toUpperCase() === "CREATE" || internalTx.type.toUpperCase() === "CREATE2"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this check needed? internalTx.to is anyway undefined if type is CREATE or CREATE2

paginationOptions: IPaginationOptions
): Promise<Pagination<InternalTransaction>> {
if (filterOptions.address) {
const normalizedAddress = normalizeAddressTransformer.to(filterOptions.address);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for normalizing, as address is bytea

queryBuilder.where({ address: normalizedAddress });

if (filterOptions.transactionHash) {
const normalizedHash = normalizeAddressTransformer.to(filterOptions.transactionHash);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here

const isContract = addressRecord?.bytecode && addressRecord.bytecode !== "0x";

if (!isContract) {
queryBuilder.andWhere("internalTransaction.value > :zero", { zero: "0" });
Copy link
Copy Markdown
Collaborator

@Romsters Romsters Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just internalTransaction.value > 0 ?

const isContract = addressRecord?.bytecode && addressRecord.bytecode !== "0x";

if (!isContract) {
queryBuilder.andWhere("internalTransaction.value > :zero", { zero: "0" });
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why skipping 0 value if address is not a contract?

}

const queryBuilder = this.createBaseQuery();
// filter by transaction hash
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove these comments

// filter by transaction hash
// filter by transaction hash
if (filterOptions.transactionHash) {
const normalizedHash = normalizeAddressTransformer.to(filterOptions.transactionHash);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here

transactionHash: normalizedHash,
});
}
// block range filtering
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to remove these type of comments. Your code is easy to read.

return await paginate<InternalTransaction>(queryBuilder, paginationOptions);
}

/**
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as here

/**
* Find internal transactions by transaction hash
*/
public async findByTransactionHash(transactionHash: string): Promise<InternalTransaction[]> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function needed if findAll covers it, especially with hardcoded page and limit?

import { CountableEntity } from "./countable.entity";

@Entity({ name: "internalTransactions" })
@Index(["transactionHash", "traceIndex"])
Copy link
Copy Markdown
Collaborator

@Romsters Romsters Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss how each of these indexes is used

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants