diff --git a/.vscode/settings.json b/.vscode/settings.json
index 54ad078ac..8ee75c18e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,5 +4,12 @@
"**/.git": false
},
"rust-analyzer.check.command": "clippy",
- "rust-analyzer.cargo.features": ["testing", "hive"]
+ "rust-analyzer.cargo.features": ["testing", "hive"],
+ "deno.enable": true,
+ "deno.suggest.imports.autoDiscover": true,
+ "[typescript]": {
+ "editor.defaultFormatter": "trunk.io"
+ },
+ "deno.lint": true,
+ "deno.unstable": true
}
diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml
index 3ea54e81b..9a74459ec 100644
--- a/docker-compose.prod.yaml
+++ b/docker-compose.prod.yaml
@@ -80,6 +80,9 @@ services:
- UNINITIALIZED_ACCOUNT_CLASS_HASH=0x1d8b8047e26b484d3f6262d1967217d980d0f2dfc69afa5661492bd5bfe2954
- ACCOUNT_CONTRACT_CLASS_HASH=0x56d311021950bf65ee500426e007b9e3ced0db97f9c1e0d29a9e03d79a9bf6c
restart: on-failure
+ volumes:
+ # Mount the indexer code
+ - indexer_code:/usr/src/indexer
depends_on:
starknet:
condition: service_started
@@ -110,16 +113,11 @@ services:
MONGO_INITDB_ROOT_USERNAME: mongo
MONGO_INITDB_ROOT_PASSWORD: mongo
- clone-repo:
- extends:
- file: docker-compose.yaml
- service: clone-repo
-
indexer:
image: quay.io/apibara/sink-mongo:0.7.0
command:
- run
- - /code/kakarot-indexer/src/main.ts
+ - /indexer/src/main.ts
environment:
# Whitelist environment variables
- ALLOW_ENV_FROM_ENV=DEBUG,APIBARA_AUTH_TOKEN,STARTING_BLOCK,STREAM_URL,SINK_TYPE,MONGO_CONNECTION_STRING,MONGO_DATABASE_NAME,STARKNET_NETWORK,KAKAROT_ADDRESS,ALLOW_NET,MONGO_REPLACE_DATA_INSIDE_TRANSACTION
@@ -136,12 +134,10 @@ services:
- KAKAROT_ADDRESS=0x612fb5de32723a19b073b3aba348e48a3d9f51448426d8931694e51db5795e6
restart: on-failure
volumes:
- - indexer_code:/code
+ - indexer_code:/indexer
networks:
- internal
depends_on:
- clone-repo:
- condition: service_completed_successfully
starknet:
condition: service_started
diff --git a/docker-compose.staging.yaml b/docker-compose.staging.yaml
index effb5634c..a93d1da4a 100644
--- a/docker-compose.staging.yaml
+++ b/docker-compose.staging.yaml
@@ -74,6 +74,9 @@ services:
- UNINITIALIZED_ACCOUNT_CLASS_HASH=0x1d8b8047e26b484d3f6262d1967217d980d0f2dfc69afa5661492bd5bfe2954
- ACCOUNT_CONTRACT_CLASS_HASH=0x56d311021950bf65ee500426e007b9e3ced0db97f9c1e0d29a9e03d79a9bf6c
restart: on-failure
+ volumes:
+ # Mount the indexer code
+ - indexer_code:/usr/src/indexer
depends_on:
starknet:
condition: service_started
@@ -104,16 +107,11 @@ services:
MONGO_INITDB_ROOT_USERNAME: mongo
MONGO_INITDB_ROOT_PASSWORD: mongo
- clone-repo:
- extends:
- file: docker-compose.yaml
- service: clone-repo
-
indexer:
image: quay.io/apibara/sink-mongo:0.7.0
command:
- run
- - /code/kakarot-indexer/src/main.ts
+ - /indexer/src/main.ts
environment:
# Whitelist environment variables
- ALLOW_ENV_FROM_ENV=DEBUG,APIBARA_AUTH_TOKEN,STARTING_BLOCK,STREAM_URL,SINK_TYPE,MONGO_CONNECTION_STRING,MONGO_DATABASE_NAME,STARKNET_NETWORK,KAKAROT_ADDRESS,ALLOW_NET,MONGO_REPLACE_DATA_INSIDE_TRANSACTION
@@ -130,12 +128,10 @@ services:
- KAKAROT_ADDRESS=0x464f7e37179d2f93ea208795bdb2d0912e8257f6fb5f67ae2559251523aee19
restart: on-failure
volumes:
- - indexer_code:/code
+ - indexer_code:/indexer
networks:
- internal
depends_on:
- clone-repo:
- condition: service_completed_successfully
starknet:
condition: service_started
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 96d2cea50..67dc9196b 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -79,6 +79,8 @@ services:
volumes:
# Mount the volume on workdir and use .env stored in root of the volume
- deployments:/usr/src/app
+ # Mount the indexer code
+ - indexer_code:/usr/src/indexer
depends_on:
deployments-parser:
condition: service_completed_successfully
@@ -115,22 +117,11 @@ services:
MONGO_INITDB_ROOT_USERNAME: mongo
MONGO_INITDB_ROOT_PASSWORD: mongo
- clone-repo:
- image: docker.io/alpine/git:latest
- entrypoint: /bin/sh
- command:
- - -c
- # clone the repository in `/code`, removing any old copy.
- - cd /code && rm -rf kakarot-indexer && git clone -v https://github.com/kkrt-labs/kakarot-indexer.git
- volumes:
- - indexer_code:/code
- restart: on-failure
-
indexer:
image: quay.io/apibara/sink-mongo:0.7.0
command:
- run
- - /code/kakarot-indexer/src/main.ts
+ - /indexer/src/main.ts
environment:
# Whitelist environment variables
- ALLOW_ENV_FROM_ENV=DEBUG,APIBARA_AUTH_TOKEN,STARTING_BLOCK,STREAM_URL,SINK_TYPE,MONGO_CONNECTION_STRING,MONGO_DATABASE_NAME,STARKNET_NETWORK,KAKAROT_ADDRESS,ALLOW_NET,MONGO_REPLACE_DATA_INSIDE_TRANSACTION
@@ -147,13 +138,11 @@ services:
- ALLOW_ENV=/deployments/.env
restart: on-failure
volumes:
- - indexer_code:/code
+ - indexer_code:/indexer
- deployments:/deployments
networks:
- internal
depends_on:
- clone-repo:
- condition: service_completed_successfully
starknet:
condition: service_started
deployments-parser:
diff --git a/docker/hive/Dockerfile b/docker/hive/Dockerfile
index 1f42e93ad..85a59e939 100644
--- a/docker/hive/Dockerfile
+++ b/docker/hive/Dockerfile
@@ -42,12 +42,6 @@ RUN case $BUILDPLATFORM in \
;; \
esac
-### Indexer transform plugin
-#### First, clone the indexer repository
-FROM docker.io/alpine/git:latest as indexer-cloner
-WORKDIR /code
-RUN git clone "https://github.com/kkrt-labs/kakarot-indexer.git"
-
#### MongoDB
FROM mongo:6.0.8 as mongo
@@ -165,7 +159,7 @@ COPY --from=katana /usr/local/bin/katana /usr/local/bin
COPY --from=apibara-build /usr/local/bin/starknet /usr/local/bin/starknet
#### We need the indexer typescript code and the binary that knows how to run it
-COPY --from=indexer-cloner /code /usr/src/app/code
+COPY ./indexer /usr/src/app/code/indexer
COPY --from=apibara-build /usr/local/bin/sink-mongo /usr/local/bin/sink-mongo
#### We need the mongo binary
diff --git a/docker/hive/start.sh b/docker/hive/start.sh
index 8f16f5a39..c9113a84e 100644
--- a/docker/hive/start.sh
+++ b/docker/hive/start.sh
@@ -50,7 +50,7 @@ echo "Launching DNA..."
starknet start --rpc=http://localhost:5050 --wait-for-rpc --head-refresh-interval-ms=500 --data=/data &
# ## Indexer
echo "Launching indexer..."
-sink-mongo run /usr/src/app/code/kakarot-indexer/src/main.ts &
+sink-mongo run /usr/src/app/code/indexer/src/main.ts &
### 3.5. Await the Indexer to be healthy
echo "Waiting for the indexer to start..."
diff --git a/docker/katana/Dockerfile b/docker/katana/Dockerfile
deleted file mode 100644
index 87ed9c06b..000000000
--- a/docker/katana/Dockerfile
+++ /dev/null
@@ -1,8 +0,0 @@
-FROM ghcr.io/dojoengine/dojo:v0.4.4 as dojo
-
-RUN apt-get update && apt-get install -y curl
-
-HEALTHCHECK --interval=10s --timeout=15s --start-period=1s --retries=5 \
- CMD curl --request POST \
- --header "Content-Type: application/json" \
- --data '{"jsonrpc": "2.0", "method": "starknet_chainId", "id": 1}' http://0.0.0.0:5050 || exit 1
diff --git a/docker/rpc/Dockerfile b/docker/rpc/Dockerfile
index 44d19e699..aa5d29abb 100644
--- a/docker/rpc/Dockerfile
+++ b/docker/rpc/Dockerfile
@@ -41,6 +41,9 @@ RUN apt-get update && \
apt-get install -y libssl-dev ca-certificates tini curl && \
rm -rf /var/lib/apt/lists/*
+# Copy the indexer code into the RPC
+COPY ./indexer /usr/src/indexer
+
# Seen in https://github.com/eqlabs/pathfinder/blob/4ab915a830953ed6f02af907937b46cb447d9a92/Dockerfile#L120 -
# Allows for passing args down to the underlying binary easily
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/kakarot-rpc"]
diff --git a/docker/rpc/Dockerfile.cross b/docker/rpc/Dockerfile.cross
index be8ba5e00..b93148b66 100644
--- a/docker/rpc/Dockerfile.cross
+++ b/docker/rpc/Dockerfile.cross
@@ -32,6 +32,9 @@ RUN apt-get update && \
apt-get install -y libssl-dev ca-certificates tini curl && \
rm -rf /var/lib/apt/lists/*
+# Copy the indexer code into the RPC
+COPY ./indexer /usr/src/indexer
+
# Seen in https://github.com/eqlabs/pathfinder/blob/4ab915a830953ed6f02af907937b46cb447d9a92/Dockerfile#L120 -
# Allows for passing args down to the underlying binary easily
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/kakarot-rpc"]
diff --git a/indexer/.env.example b/indexer/.env.example
new file mode 100644
index 000000000..f8ae67019
--- /dev/null
+++ b/indexer/.env.example
@@ -0,0 +1,16 @@
+APIBARA_AUTH_TOKEN=
+DEBUG=
+
+# e.g. http://localhost:7171 or https://goerli.starknet.a5a.ch
+STREAM_URL=http://localhost:7171
+STARTING_BLOCK=0
+
+# one of: console, mongo
+SINK_TYPE=console
+
+# if SINK_TYPE=mongo, then this is required, e.g. mongodb://mongo:mongo@mongo:27017
+MONGO_CONNECTION_STRING=
+MONGO_DATABASE_NAME=
+STARKNET_NETWORK=
+KAKAROT_ADDRESS=
+ALLOW_NET=
diff --git a/indexer/README.md b/indexer/README.md
new file mode 100644
index 000000000..a0ffe7797
--- /dev/null
+++ b/indexer/README.md
@@ -0,0 +1,69 @@
+# Kakarot Indexer
+
+Kakarot Indexer fits in the three-part architecture of the Kakarot zkEVM rollup
+(Kakarot EVM Cairo Programs, Kakarot RPC, Kakarot Indexer). It indexes the
+underlying CairoVM network on which Kakarot core is deployed in an Ethereum
+compatible format.
+
+
+
+
+
+## Table of Contents
+
+- [Motivation and FAQ](#motivation-and-faq)
+- [Installation](#installation)
+- [Usage](#usage)
+- [License](#license)
+
+## Motivation and FAQ
+
+_Why is an indexer needed?_
+
+- Because CairoVM chains (also called StarknetOS chains) have different data
+ types, it is necessary to reformat the data in an Ethereum compatible way. We
+ decided to perform this reformatting outside of the CairoVM chain, resulting
+ in duplication of data, but more modularity. Note that this reformatting
+ could've been done at runtime, at a high computational cost. Lastly, a third
+ option would consist in forking a CairoVM client (e.g. Starknet) and change
+ the data types directly within the storage of the full node.
+
+_Why not directly modify a CairoVM client?_
+
+- CairoVM chains are very novel and evolve quite fast. We are thus waiting for
+ more stability to directly fork/modify a client. New engines, storage
+ technologies and other innovations are being developed by talented teams (e.g.
+ [Starknet](https://starkware.co/starknet/),
+ [Katana](https://book.dojoengine.org/toolchain/katana/overview.html),
+ [Madara](https://madara-docs.vercel.app/)).
+
+
+For reference, click to see how the above mentioned monolithic architecture
+would look like (note that it is not the current architecture of Kakarot zkEVM):
+
+
+
+
+
+
+
+
+## Installation
+
+The Kakarot Indexer is based on the [Apibara](https://www.apibara.com/docs)
+third-party service, an indexing product for StarknetOS chains. Apibara relies
+on [Deno](https://deno.com/), a specific Javascript runtime. Follow the Deno
+installation requirements to be able to interact with the codebase.
+
+## Usage
+
+Although this codebase is open-source, it relies on third-party software, the
+[DNA protocol](https://www.apibara.com/docs/advanced/streaming-protocol). One
+may read the code, run it locally, or self-host the indexer using Apibara's
+documentation and API keys.
+
+## License
+
+[MIT License](./docs/LICENSE)
diff --git a/indexer/deno.json b/indexer/deno.json
new file mode 100644
index 000000000..3c5130f1d
--- /dev/null
+++ b/indexer/deno.json
@@ -0,0 +1,5 @@
+{
+ "tasks": {
+ "dev": "deno run --watch main.ts"
+ }
+}
diff --git a/indexer/deno.lock b/indexer/deno.lock
new file mode 100644
index 000000000..1c729a1d4
--- /dev/null
+++ b/indexer/deno.lock
@@ -0,0 +1,177 @@
+{
+ "version": "3",
+ "packages": {
+ "specifiers": {
+ "npm:@types/node": "npm:@types/node@18.16.19"
+ },
+ "npm": {
+ "@types/node@18.16.19": {
+ "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
+ "dependencies": {}
+ }
+ }
+ },
+ "remote": {
+ "https://deno.land/std@0.150.0/media_types/_util.ts": "ce9b4fc4ba1c447dafab619055e20fd88236ca6bdd7834a21f98bd193c3fbfa1",
+ "https://deno.land/std@0.150.0/media_types/mod.ts": "2d4b6f32a087029272dc59e0a55ae3cc4d1b27b794ccf528e94b1925795b3118",
+ "https://deno.land/std@0.150.0/media_types/vendor/mime-db.v1.52.0.ts": "724cee25fa40f1a52d3937d6b4fbbfdd7791ff55e1b7ac08d9319d5632c7f5af",
+ "https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
+ "https://deno.land/std@0.213.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
+ "https://deno.land/std@0.213.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
+ "https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
+ "https://deno.land/std@0.213.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
+ "https://deno.land/std@0.213.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
+ "https://deno.land/std@0.213.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
+ "https://deno.land/std@0.213.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
+ "https://deno.land/std@0.213.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
+ "https://deno.land/std@0.213.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
+ "https://deno.land/std@0.213.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
+ "https://deno.land/std@0.213.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
+ "https://deno.land/std@0.213.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
+ "https://deno.land/std@0.213.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
+ "https://deno.land/std@0.213.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
+ "https://deno.land/std@0.213.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
+ "https://deno.land/std@0.213.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c",
+ "https://deno.land/std@0.213.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
+ "https://deno.land/std@0.213.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
+ "https://deno.land/std@0.213.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
+ "https://deno.land/std@0.213.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
+ "https://deno.land/std@0.213.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54",
+ "https://deno.land/std@0.213.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
+ "https://deno.land/std@0.213.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
+ "https://deno.land/std@0.213.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7",
+ "https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
+ "https://deno.land/std@0.213.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
+ "https://deno.land/std@0.213.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
+ "https://deno.land/std@0.213.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b",
+ "https://deno.land/std@0.213.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
+ "https://deno.land/std@0.213.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145",
+ "https://deno.land/std@0.213.0/fmt/colors.ts": "aeaee795471b56fc62a3cb2e174ed33e91551b535f44677f6320336aabb54fbb",
+ "https://deno.land/x/xhr@0.3.0/mod.ts": "094aacd627fd9635cd942053bf8032b5223b909858fa9dc8ffa583752ff63b20",
+ "https://esm.sh/@apibara/indexer@0.2.2/starknet": "f67c50a85f58eaa85292c0b3fdb0af5d0a9b2cb91dc27f1a7d289fd9b240ef5a",
+ "https://esm.sh/@ethereumjs/block@5.0.1": "c0d3d2b056c05c84a41b532395bb9ff00d97266bcd9aa0077cd280dad81fc9f7",
+ "https://esm.sh/@ethereumjs/evm@2.1.0": "f9ef9071e1f4dc9aa56d8409d14f733498ba254d20f34914978c4fa45605dfff",
+ "https://esm.sh/@ethereumjs/rlp@5.0.1": "7b9dd4514d91155bac856e0ba4b82303667491dad35c3f6f9544a5c8b6253ca6",
+ "https://esm.sh/@ethereumjs/trie@6.0.1": "2ea95b4d545ec2971bde5b938442b1c8885c6651b54201a4aebcdb32d619fd46",
+ "https://esm.sh/@ethereumjs/tx@5.1.0": "db89697de73a85aec54490818cd5e604b20dcb5d90bcdc7f19d4ca96fbb30286",
+ "https://esm.sh/@ethereumjs/util@9.0.1": "9d8c888c45407154e055b5d546a44f534c7478c21cc1f85d67cc43c1b5d4bec8",
+ "https://esm.sh/@ethereumjs/vm@7.1.0": "7624a3e9f45c79ea6758285f1e9ff3fba7b9fee0c88e3899dd6fed5ba1a2a391",
+ "https://esm.sh/starknet@5.24.3": "d566d139b1b25566d70584b163184697df6e3ad769a137a9c4fbd7111e16e7a4",
+ "https://esm.sh/v135/@adraffy/ens-normalize@1.10.0/denonext/ens-normalize.mjs": "62517a294467b1122a56461b2a5961b47250b10eb163892bd6404639377bcb50",
+ "https://esm.sh/v135/@apibara/indexer@0.2.2/denonext/starknet.js": "3d4e9a238bbe354005b186d3297ad17e020517c1fcc35c9cdda9dc54def96c29",
+ "https://esm.sh/v135/@ethereumjs/block@5.0.1/denonext/block.mjs": "20a28bfa0504bfed4db609014df71a7370fab4042c8d7588e24e52799e0f5377",
+ "https://esm.sh/v135/@ethereumjs/blockchain@7.0.1/denonext/blockchain.mjs": "40d1839341e06c624a337def06784fec5ec14a2b7f3499e3697cb0dbbb7e6eaf",
+ "https://esm.sh/v135/@ethereumjs/common@4.1.0/denonext/common.mjs": "1262c0794b45bb3338b0a1f689bfad7e3025c19a7d7b25dc3343c6f05de57f2b",
+ "https://esm.sh/v135/@ethereumjs/ethash@3.0.1/denonext/ethash.mjs": "270e354c9f7428321908b15ee2ed2059d809252301975ed8319642252c4c8c45",
+ "https://esm.sh/v135/@ethereumjs/evm@2.1.0/denonext/evm.mjs": "354c7af6010e607889342a26db50d2ce6f19c0da6a7f57596b2787d953730b5d",
+ "https://esm.sh/v135/@ethereumjs/rlp@5.0.1/denonext/rlp.mjs": "e7053dd0273991e806a6a7a9ced77044fa81064deb6fbd38f72939fc371fab49",
+ "https://esm.sh/v135/@ethereumjs/statemanager@2.1.0/denonext/statemanager.mjs": "004f1d957428db256cff0c2be1df939d1e631cf1d59a70e64ca59c63005d640b",
+ "https://esm.sh/v135/@ethereumjs/trie@6.0.1/denonext/trie.mjs": "4530fab65263d29256c46a40cc86d8fe213d2b946f9707f63710a40d96838c43",
+ "https://esm.sh/v135/@ethereumjs/tx@5.1.0/denonext/tx.mjs": "598717f77b3f07f343c860b7204ca78a9e07cc4a34834f10151a7e8cd6a707bd",
+ "https://esm.sh/v135/@ethereumjs/util@9.0.1/denonext/util.mjs": "d88f5e19c3354780d1381d1396c0fdcdde3da06b9dc5f49f35c707bf2319a718",
+ "https://esm.sh/v135/@ethereumjs/vm@7.1.0/denonext/vm.mjs": "0231103fb52517f7cff4fff9a025b0f3ff400bf3789095df4973e0157c348cd9",
+ "https://esm.sh/v135/@noble/curves@1.1.0/denonext/_shortw_utils.js": "00cb8e706dc9a27d2b2ec805553cee799f10e479df771114b488f2c9c9a0dcec",
+ "https://esm.sh/v135/@noble/curves@1.1.0/denonext/abstract/curve.js": "afcf481de38fff74955d1dd734cdc15e34a900ba71ed8656ad5c8810b0e66e5a",
+ "https://esm.sh/v135/@noble/curves@1.1.0/denonext/abstract/hash-to-curve.js": "706cf8d43f436ba22f5c65f71e84403ec74124316e24e84d05cfdb297f80c0d2",
+ "https://esm.sh/v135/@noble/curves@1.1.0/denonext/abstract/modular.js": "f16d57a1fd338e39a228d197f55b1e2d8bfcc33b3be392c148863d0895779f2f",
+ "https://esm.sh/v135/@noble/curves@1.1.0/denonext/abstract/utils.js": "110d717e2e2291ab081a0af7355141153834ee8f1a605c219299d055b2267d8a",
+ "https://esm.sh/v135/@noble/curves@1.1.0/denonext/abstract/weierstrass.js": "1e6d32f1e679a3800f2a17bbf6872ec7aebb459b22c582d508a8a7b27810e86d",
+ "https://esm.sh/v135/@noble/curves@1.1.0/denonext/secp256k1.js": "2ac87abf6fecbde4c467c2500b130c3053c5b2e859f817c99817d086210e7997",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/_shortw_utils.js": "47a889dd52d7b29e8bc0d641597239d0ae0b1f10a9ee3b114c3dbd273172e9af",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/abstract/curve.js": "d4ac165ea7e6b5e401112e5c91d38a258fc6fe1dc3afa1c391cc2e74141d2228",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/abstract/hash-to-curve.js": "eb943276f11698b72a1da4dc51458edac705a3d827b0add5038a15c7aa6ab770",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/abstract/modular.js": "6d0fba13e486d0929f4ab81f7774d75b2254b934e0be7dd3fd96b959c0dd7600",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/abstract/poseidon.js": "ab1b97653828117af52678e8a62ef976b529fdc56359172639c1c8e9c1d5d895",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/abstract/utils.js": "33d6521fbb6756d058dff59616da1e55a55972b4848976c27e81c4b4bc36c0e4",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/abstract/weierstrass.js": "5cb85ed9ad826f7e623f0052a55a865ad957eab06c30697804157844e82416a9",
+ "https://esm.sh/v135/@noble/curves@1.2.0/denonext/secp256k1.js": "da024e491d38a1407d3bdf54f669bbefb879b710dfae945d4ca58a676fcfd37d",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/_assert.js": "d22dfffef44f75a84f61b9cedac204d6d39d6ccb14fc489e81bb9ab68de5f1c7",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/_sha2.js": "79869533e91a0a6ed446fb6d203c5ae9c458dc7ce990760446f4dbc0698eddab",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/crypto.js": "a66049789449b1cb447eb89a7f3f7adcd1ec4063beeacd3e0b6d84138ef38690",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/hmac.js": "04377300ce8017281732eada2e456fcbafd0dfe8fa45627d08456d82bf0e021a",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/ripemd160.js": "c5c1a6ba85f2a0c0475d5b2cb3fb00a4f796f25e887cf531a45b3df3dc47fad6",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/sha256.js": "6ea97edce4ae3dd2dd35aed79b9fef4f246d8b9112fe9f17c96c8b01d975473f",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/sha3.js": "b656b255db2dc4f555978c8a5b279d67c2bc72979c0ca4cfa2cdb7e7963ee3dc",
+ "https://esm.sh/v135/@noble/hashes@1.3.1/denonext/utils.js": "6da366e41886df220cc235aa6152c34abbcd688bee899e2eabbb885a2c5ae6ce",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/_assert.js": "8b3fcc3a8d18f25fded5ecb46250f2009f11e0586b0e1e33757d12b6d1b05591",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/_sha2.js": "9571edb1dbcf00041aaa5b4d9bafb109bb9944bf47452fae653206a0242c2a0a",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/crypto.js": "3d09b6d143b1cb8ec95375586a7d9a30a02b60cb1de984ba374c779f8e6c877a",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/hmac.js": "23f7c9bebfbb777754e776b3197005b866742ce752945bfca17bc3c3a59e0e21",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/pbkdf2.js": "bd2869e43867b1da6cf5ddab1bda5ca52fdcb37d176ff4cf937646b37df6476f",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/ripemd160.js": "d7232e571460ab5d017b0e21ad633f11a8daa5627462092b8e3e59995a4ef9bd",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/scrypt.js": "2ca88ca699a096ba5ea4db584b4473c022f8934eda10b124c5cb067b9c0c3c1d",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/sha256.js": "0c34ea09faae3f2b64691a608ffbe5b3a332f433138039c3bef2e7a53ee7f6d7",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/sha3.js": "e44ab8a4e0efac5ce9b89da919c4cb4a382170e329adbb845527fc0fb02e914d",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/sha512.js": "057089c3e7d4743a8f19ef42a0dcc3a6c9d144e0cec59040e7c17501d9677b8f",
+ "https://esm.sh/v135/@noble/hashes@1.3.2/denonext/utils.js": "bbfe700df9ae51f477688035ec3dd96739f77fd2f50439a4c84bcbc1aa7c7d4a",
+ "https://esm.sh/v135/@noble/hashes@1.3.3/denonext/_assert.js": "f8882bd96e2a6d1834a445c5af97f927b1ba028f34963c8570568e33385c4419",
+ "https://esm.sh/v135/@noble/hashes@1.3.3/denonext/_sha2.js": "7b27807ccd3cf7c3b90ce23b17bc9c5d791a72e41dd2e01a4debd9727990bca9",
+ "https://esm.sh/v135/@noble/hashes@1.3.3/denonext/crypto.js": "cf6efbafcbb35e03bcb3a36cccd3d6d1f9bc4ba23f44a79551929a28c83e7901",
+ "https://esm.sh/v135/@noble/hashes@1.3.3/denonext/sha256.js": "762e0b0cbde1990fc905eb816d30cdc0cf7dd4c23d123408c6963294f124f97d",
+ "https://esm.sh/v135/@noble/hashes@1.3.3/denonext/sha3.js": "3765211a8eec7f75e4ad8f265c023ed372b16327f214133ce4fc64c3a1423404",
+ "https://esm.sh/v135/@noble/hashes@1.3.3/denonext/utils.js": "701831e12a7e656df467b62f929ac9536ababef1b9b7445c7f87512366ae3933",
+ "https://esm.sh/v135/@scure/base@1.1.5/denonext/base.mjs": "442a66c701330f27adff8b2fa6067308ff9a82b8e178c8482882ff4aa7dfee82",
+ "https://esm.sh/v135/@scure/starknet@0.3.0/denonext/starknet.mjs": "d50471d53849ef5de4018ca196ef69fb8f487016b83c36b4a9509291672abd6c",
+ "https://esm.sh/v135/aes-js@4.0.0-beta.5/denonext/aes-js.mjs": "b1b896f1484bbfc1178a000fd60d00c9f73ef9d30f5b8dcdf9796e684b231f86",
+ "https://esm.sh/v135/bigint-crypto-utils@3.3.0/denonext/bigint-crypto-utils.mjs": "b578d1c4acc591b72e9e78ff896e705e542f91de789cd8490d1445e66acc21bd",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc1.js": "eaa4ed99321cd8b52c59ca0cb630e65a4b859244dffb4a272a36101e8c196cfc",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc16.js": "0ada74b2958a7bd3ecddb16a272b41dd18e002026569f1bfcb5a68d441db4485",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc16ccitt.js": "10fe63c105fb7f8156ce23adaf40e8c2711c7e3dad6ef77a709738cbe0d5212e",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc16kermit.js": "eee6fd14e48ac01b9f6e690038c747acd4be3f17bfb28c99f768fb831132768c",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc16modbus.js": "cd1490bfedb6f70cbb4812c5419c16318e11c7f8a006da4faa943d9705bce6c7",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc16xmodem.js": "0f1d6cd9c24ab0a93e9f1512cca6b1a5da3cb1b1c40d61cf7b1a045501fc0301",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc24.js": "21bf478c3e4793427c603a7ee0f3f14802a40f2adfd26facee0a0833b234afd5",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc32.js": "c916783306d04677a4bc72b86f34dcf6923e8c8a64e6c24480ed5e048a4e2527",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc32mpeg2.js": "c7c604fa5205fb8ba8ff65bc6cd809c497f68e608f484d80407dfc5281e1ce72",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc8.js": "42db2d1234c7cd8908bc7150bb5adf80f0e06a802292890f038c48cd967a8152",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc81wire.js": "a057513002257678c34f9960659138aaace38b1a0015d4f750afb910a77c56e8",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crc8dvbs2.js": "86324953a96dc5337f96428c3fba86dab340bf3b64667b99a34548497a1992e5",
+ "https://esm.sh/v135/crc@4.3.2/denonext/calculators/crcjam.js": "9e0668baaef5da9995a71966ad252e7feeb7f354f230979860b6674779ec316e",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc.mjs": "aebae37e3003c69cf89549c49f856ba2358ee90bd4e784c82266567020af0f19",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc1.js": "9d9f765dbea4658e92acb481622d540f8c285fa2ef4b4a34c83822119f0a37bb",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc16.js": "c1786b507befe6a2853b0cc5002cbdf9aa22e82759046877a950360545fbfef7",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc16ccitt.js": "1923e36f88a43aef4e679f5d47ef36611ed572a0363b8084309621c4b5c43608",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc16kermit.js": "5ed3db793e7f1a00f10ec32170d7847d7190a2e1574626c7bb51931771e462da",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc16modbus.js": "86c7765ea7d0508e75e8305838809da0211b822b835900c5f1d3cb4071918dc5",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc16xmodem.js": "9f45aaf4903653ecdd5cea701aa49980811be45ce771a0773ee3443208e4ef07",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc24.js": "5b72dc66c8cec68f234530538835f42e76215d3b0a7b0704508c366861fbf6dc",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc32.js": "3cf87e8817f4a233eb6266ab93aaa07889882ca20f64cd0d9722dfc78838c00e",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc32mpeg2.js": "58456f03b9496fd52318b0636ddcdd2f71e78c45d9cd68247a1adb876760fac1",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc8.js": "a2df591da512ca13d4b85abf2b1ee99b383deadbad4bcdd09787061b2db6f7e7",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc81wire.js": "1dd654ca7cf2d3f09a9465e6793984ccaef4125ef84de4fb27e982e502e6dcd1",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crc8dvbs2.js": "7c3bca5f086fc2f1be40de30c415d42bef62535389e25dc175c094dedbefaef4",
+ "https://esm.sh/v135/crc@4.3.2/denonext/crcjam.js": "98767a32beac739f17ea64a56efde41f8180572adb9f51f2286d4eaa29d96737",
+ "https://esm.sh/v135/debug@4.3.4/denonext/debug.mjs": "d2ebf776ea77aa7df1b4460eb2a4aab245a9d5b19f11fa1db25f756b350bae9d",
+ "https://esm.sh/v135/ethereum-cryptography@2.1.2/denonext/keccak.js": "56352f00aea30bac6b0b0bf6c26370e2452e47e90f6cf0b450f3062df9c39fbe",
+ "https://esm.sh/v135/ethereum-cryptography@2.1.2/denonext/random.js": "a81cc8d3c6f74555dc0d040018c588d06003ae0c686b6cf025434ff2c5caccff",
+ "https://esm.sh/v135/ethereum-cryptography@2.1.2/denonext/ripemd160.js": "35325ce7b789239bf0351ff2b2967c8ad5ad4f246217697b73ee59d4ddd0b4c3",
+ "https://esm.sh/v135/ethereum-cryptography@2.1.2/denonext/secp256k1.js": "5d3152902365507958e16c57af02485d4486bc3a6f3df2d0b0ba0e59ba21bcc7",
+ "https://esm.sh/v135/ethereum-cryptography@2.1.2/denonext/sha256.js": "961c9a682c3eebde948a48a5e71dc297f7c0927dd54e2d526ed71a23c798ec55",
+ "https://esm.sh/v135/ethereum-cryptography@2.1.2/denonext/utils.js": "5e7d94766e9c1f6fe6a1233e63d84aaa3475d8660920e327de33503b645ceed4",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/abi.js": "a4d92e6fe8b5028cf44ae4e57c3ae7cc434737e8b8207a1eeccf29a0c4cdf3b4",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/address.js": "3de1718c6d8416081667f7282ff144a3b7d6438f89d9494ee38941cea85217d8",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/constants.js": "41c55737b34d90465e50c1f8dbd9be80496645e56a32b4399f84ea082890cdbd",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/contract.js": "8b9a93c511f4beca3b25d20362f4fc206911aec0a3e43b5005ac8ac91ce0fd7a",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/crypto.js": "d562938ef89db30b54537c72ef3b33aefdc46e6a6671fcf476d4a14c81a6d078",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/ethers.mjs": "7c99e20245b2b759df541b77da9562f4ce71aeb3f4a28b4ddeca8c6749325c65",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/hash.js": "597bcb30d6cb31abe8ff716745ecedba438fc7e2834de4b6e6f8567617215af1",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/providers.js": "672b62a84a3a3ade08d87765d7ae2ef54a09c7cb3c2ba0d3a32b1a255886bfdb",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/transaction.js": "605d8b434f906a19a9dfb2121d71a87775e91727da0ab97e8c842b0192825a52",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/utils.js": "7ad81e371896457181f31fc75db95c689b5fb2ad8a994a82450d82dd88873c3c",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/wallet.js": "39e9fb0effe9900d98b5b20537d79f50b79a83fd3260ae7369c1e71177f661a5",
+ "https://esm.sh/v135/ethers@6.10.0/denonext/wordlists.js": "7f8c990943bdf8ca99d62b125bbaf6547e25afc6174f995fbb4b62fd52038e87",
+ "https://esm.sh/v135/inherits@2.0.4/denonext/inherits.mjs": "8095f3d6aea060c904fb24ae50f2882779c0acbe5d56814514c8b5153f3b4b3b",
+ "https://esm.sh/v135/isomorphic-fetch@3.0.0/denonext/isomorphic-fetch.mjs": "a791533fb7269baadd38e8e4a34a46a6cb70f5c89ea4e8f4b3e1e5ddd533d61f",
+ "https://esm.sh/v135/js-sdsl@4.4.2/denonext/js-sdsl.mjs": "9eec3540be7381f795ea76ddc1674aacdb190e4bcb84ed4d9a7e5020977a9e75",
+ "https://esm.sh/v135/lossless-json@2.0.11/denonext/lossless-json.mjs": "cb67043586fcaab9a862a403c35abc3c8485788ec59fd403565c7f550a932085",
+ "https://esm.sh/v135/lru-cache@10.1.0/denonext/lru-cache.mjs": "8eccca15513d8d1e660969697e34dd104c57dfbe3f20f0e88e04650a3e1bd93a",
+ "https://esm.sh/v135/ms@2.1.2/denonext/ms.mjs": "aa4dc45ba72554c5011168f8910cc646c37af53cfff1a15a4decced838b8eb14",
+ "https://esm.sh/v135/pako@2.1.0/denonext/pako.mjs": "a96661a4528965146d092709c0566bbb9a6fbc04e21587adfae26d48b2b3d763",
+ "https://esm.sh/v135/readable-stream@3.6.2/denonext/readable-stream.mjs": "35fc04c3200b796d676d0e83607fcfc150048bdcc5bdb7d5d836b5921049788f",
+ "https://esm.sh/v135/rustbn-wasm@0.2.0/denonext/rustbn-wasm.mjs": "4666ae759d0ac999bb8ee2ddcd43a7f38eaff82ebb744ef219937f94c0233330",
+ "https://esm.sh/v135/starknet@5.24.3/denonext/starknet.mjs": "2728a4d3cefb2ae5e0292d590509eca79906286b8abf1523368224221fe38f2d",
+ "https://esm.sh/v135/url-join@4.0.1/denonext/url-join.mjs": "1d2b840f03b6a3aaaaaa56380ea7879740d376a1dd83511b5bcf8af6c4617e1e",
+ "https://esm.sh/v135/util-deprecate@1.0.2/denonext/util-deprecate.mjs": "f69f67cf655c38428b0934e0f7c865c055834a87cc3866b629d6b2beb21005e9",
+ "https://esm.sh/v135/whatwg-fetch@3.6.19/denonext/whatwg-fetch.mjs": "65c15a9d84dcbd90308975b351e180e43ed2849f0be2312e543202d4fbb8e335"
+ }
+}
diff --git a/indexer/docs/LICENSE b/indexer/docs/LICENSE
new file mode 100644
index 000000000..b24b9b105
--- /dev/null
+++ b/indexer/docs/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Kakarot Labs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/indexer/docs/Monolith_Kakarot_zkEVM_not_live.png b/indexer/docs/Monolith_Kakarot_zkEVM_not_live.png
new file mode 100644
index 000000000..fbb5f62f4
Binary files /dev/null and b/indexer/docs/Monolith_Kakarot_zkEVM_not_live.png differ
diff --git a/indexer/docs/kakarot_indexer.png b/indexer/docs/kakarot_indexer.png
new file mode 100644
index 000000000..d882d2cee
Binary files /dev/null and b/indexer/docs/kakarot_indexer.png differ
diff --git a/indexer/src/deps.ts b/indexer/src/deps.ts
new file mode 100644
index 000000000..f5a56806a
--- /dev/null
+++ b/indexer/src/deps.ts
@@ -0,0 +1,53 @@
+// Starknet
+export { hash, RpcProvider, uint256, Contract} from "https://esm.sh/starknet@5.24.3";
+export type {
+ BlockHeader,
+ Event,
+ EventWithTransaction,
+ Transaction,
+ TransactionReceipt,
+} from "https://esm.sh/@apibara/indexer@0.2.2/starknet";
+
+// Ethereum
+export {
+ AccessListEIP2930Transaction,
+ Capability,
+ FeeMarketEIP1559Transaction,
+ isAccessListEIP2930Tx,
+ isFeeMarketEIP1559TxData,
+ isLegacyTx,
+ LegacyTransaction,
+ TransactionFactory,
+ TransactionType,
+} from "https://esm.sh/@ethereumjs/tx@5.1.0";
+
+export type {
+ JsonRpcTx,
+ TxValuesArray,
+ TypedTransaction,
+ TypedTxData,
+} from "https://esm.sh/@ethereumjs/tx@5.1.0";
+
+export {
+ bigIntToBytes,
+ bigIntToHex,
+ bytesToBigInt,
+ bytesToHex,
+ concatBytes,
+ generateAddress,
+ hexToBytes,
+ intToHex,
+ stripHexPrefix,
+} from "https://esm.sh/@ethereumjs/util@9.0.1";
+export type { PrefixedHexString } from "https://esm.sh/@ethereumjs/util@9.0.1";
+
+export { Bloom, encodeReceipt } from "https://esm.sh/@ethereumjs/vm@7.1.0";
+export type { TxReceipt } from "https://esm.sh/@ethereumjs/vm@7.1.0";
+
+export type { JsonRpcBlock } from "https://esm.sh/@ethereumjs/block@5.0.1";
+
+export { Trie } from "https://esm.sh/@ethereumjs/trie@6.0.1";
+
+export type { Log } from "https://esm.sh/@ethereumjs/evm@2.1.0";
+
+export { RLP } from "https://esm.sh/@ethereumjs/rlp@5.0.1";
diff --git a/indexer/src/main.ts b/indexer/src/main.ts
new file mode 100644
index 000000000..dc5feb25a
--- /dev/null
+++ b/indexer/src/main.ts
@@ -0,0 +1,227 @@
+// Utils
+import { padString, toHexString } from "./utils/hex.ts";
+
+// Types
+import { toEthTx, toTypedEthTx } from "./types/transaction.ts";
+import { toEthHeader } from "./types/header.ts";
+import { fromJsonRpcReceipt, toEthReceipt } from "./types/receipt.ts";
+import { JsonRpcLog, toEthLog } from "./types/log.ts";
+import { StoreItem } from "./types/storeItem.ts";
+// Starknet
+import {
+ BlockHeader,
+ EventWithTransaction,
+ hash,
+ Transaction,
+} from "./deps.ts";
+// Eth
+import { Bloom, encodeReceipt, hexToBytes, RLP, Trie } from "./deps.ts";
+
+const AUTH_TOKEN = Deno.env.get("APIBARA_AUTH_TOKEN") ?? "";
+const TRANSACTION_EXECUTED = hash.getSelectorFromName("transaction_executed");
+
+const STREAM_URL = Deno.env.get("STREAM_URL") ?? "http://localhost:7171";
+const STARTING_BLOCK = Number(Deno.env.get("STARTING_BLOCK")) ?? 0;
+if (!Number.isSafeInteger(STARTING_BLOCK) || STARTING_BLOCK < 0) {
+ throw new Error("Invalid STARTING_BLOCK");
+}
+const SINK_TYPE = Deno.env.get("SINK_TYPE") ?? "console";
+if (SINK_TYPE !== "console" && SINK_TYPE !== "mongo") {
+ throw new Error("Invalid SINK_TYPE");
+}
+
+const KAKAROT_ADDRESS = Deno.env.get("KAKAROT_ADDRESS");
+if (KAKAROT_ADDRESS === undefined) {
+ throw new Error("ENV: KAKAROT_ADDRESS is not set");
+}
+
+const sinkOptions =
+ SINK_TYPE === "mongo"
+ ? {
+ connectionString:
+ Deno.env.get("MONGO_CONNECTION_STRING") ??
+ "mongodb://mongo:mongo@mongo:27017",
+ database: Deno.env.get("MONGO_DATABASE_NAME") ?? "kakarot-test-db",
+ collectionNames: ["headers", "transactions", "receipts", "logs"],
+ }
+ : {};
+
+export const config = {
+ streamUrl: STREAM_URL,
+ authToken: AUTH_TOKEN,
+ startingBlock: STARTING_BLOCK,
+ network: "starknet",
+ finality: "DATA_STATUS_PENDING",
+ filter: {
+ header: { weak: false },
+ // Filters are unions
+ events: [
+ {
+ keys: [TRANSACTION_EXECUTED],
+ },
+ ],
+ },
+ sinkType: SINK_TYPE,
+ sinkOptions: sinkOptions,
+};
+
+const isKakarotTransaction = (transaction: Transaction) => {
+ // Filter out transactions that are not related to Kakarot.
+ // callArrayLen <- calldata[0]
+ // to <- calldata[1]
+ // selector <- calldata[2];
+ // dataOffset <- calldata[3]
+ // dataLength <- calldata[4]
+ // calldataLen <- calldata[5]
+ const calldata = transaction.invokeV1?.calldata;
+ if (calldata === undefined) {
+ console.error("No calldata in transaction");
+ console.error(JSON.stringify(transaction, null, 2));
+ return false;
+ }
+ const to = calldata[1];
+ if (to === undefined) {
+ console.error("No `to` field in calldata of transaction");
+ console.error(JSON.stringify(transaction, null, 2));
+ return false;
+ }
+ // TODO(Greged93): replace this with a more robust check.
+ // ⚠️ The existence of `to` field in invoke calldata in RPC is not enforced by protocol.
+ // Forks or modifications of the kkrt-labs/kakarot-rpc codebase could break this check.
+ if (BigInt(to) !== BigInt(KAKAROT_ADDRESS)) {
+ console.log("✅ Skipping transaction that is not related to Kakarot");
+ return false;
+ }
+ return true;
+};
+
+const NULL_BLOCK_HASH = padString("0x", 32);
+
+export default async function transform({
+ header,
+ events,
+}: {
+ header: BlockHeader;
+ events: EventWithTransaction[];
+}) {
+ // Accumulate the gas used in the block in order to calculate the cumulative gas used.
+ // We increment it by the gas used in each transaction in the flatMap iteration.
+ let cumulativeGasUsed = 0n;
+ const blockNumber = padString(toHexString(header.blockNumber), 8);
+ const isPendingBlock = padString(header.blockHash, 32) === NULL_BLOCK_HASH;
+ const blockHash = padString(header.blockHash, 32);
+ const blockLogsBloom = new Bloom();
+ const transactionTrie = new Trie();
+ const receiptTrie = new Trie();
+
+ const store: Array = [];
+
+ await Promise.all(
+ (events ?? []).map(async ({ transaction, receipt, event }) => {
+ // Can be false if the transaction is not related to a specific instance of the Kakarot contract.
+ // This is typically the case if there are multiple Kakarot contracts on the same chain.
+ const isKakarotTx = isKakarotTransaction(transaction);
+ if (!isKakarotTx) {
+ return null;
+ }
+ const typedEthTx = toTypedEthTx({ transaction });
+ // Can be null if:
+ // 1. The transaction is missing calldata.
+ // 2. The transaction is a multi-call.
+ // 3. The length of the signature array is different from 5.
+ // 4. The chain id is not encoded in the v param of the signature for a
+ // Legacy transaction.
+ // 5. The deserialization of the transaction fails.
+ if (typedEthTx === null) {
+ return null;
+ }
+ const ethTx = toEthTx({
+ transaction: typedEthTx,
+ receipt,
+ blockNumber,
+ blockHash,
+ isPendingBlock,
+ });
+ // Can be null if:
+ // 1. The typed transaction if missing a signature param (v, r, s).
+ if (ethTx === null) {
+ return null;
+ }
+
+ // Can be null if:
+ // 1. The event is part of the defined ignored events (see IGNORED_KEYS).
+ // 2. The event has an invalid number of keys.
+ const ethLogs = receipt.events
+ .map((e) => {
+ return toEthLog({
+ transaction: ethTx,
+ event: e,
+ blockNumber,
+ blockHash,
+ isPendingBlock,
+ });
+ })
+ .filter((e) => e !== null) as JsonRpcLog[];
+ const ethLogsIndexed = ethLogs.map((log, index) => {
+ log.logIndex = index.toString();
+ return log;
+ });
+
+ const ethReceipt = toEthReceipt({
+ transaction: ethTx,
+ logs: ethLogsIndexed,
+ event,
+ cumulativeGasUsed,
+ blockNumber,
+ blockHash,
+ isPendingBlock,
+ });
+
+ // Trie code is based off:
+ // - https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/block/src/block.ts#L85
+ // - https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/vm/src/buildBlock.ts#L153
+ // Add the transaction to the transaction trie.
+ await transactionTrie.put(
+ RLP.encode(Number(ethTx.transactionIndex)),
+ typedEthTx.serialize(),
+ );
+ // Add the receipt to the receipt trie.
+ const encodedReceipt = encodeReceipt(
+ fromJsonRpcReceipt(ethReceipt),
+ typedEthTx.type,
+ );
+ await receiptTrie.put(
+ RLP.encode(Number(ethTx.transactionIndex)),
+ encodedReceipt,
+ );
+ // Add the logs bloom of the receipt to the block logs bloom.
+ const receiptBloom = new Bloom(hexToBytes(ethReceipt.logsBloom));
+ blockLogsBloom.or(receiptBloom);
+ cumulativeGasUsed += BigInt(ethReceipt.gasUsed);
+
+ // Add all the eth data to the store.
+ store.push({ collection: "transactions", data: { tx: ethTx } });
+ store.push({ collection: "receipts", data: { receipt: ethReceipt } });
+ ethLogs.forEach((ethLog) => {
+ store.push({ collection: "logs", data: { log: ethLog } });
+ });
+ }),
+ );
+
+ const ethHeader = await toEthHeader({
+ header: header,
+ gasUsed: cumulativeGasUsed,
+ logsBloom: blockLogsBloom,
+ receiptRoot: receiptTrie.root(),
+ transactionRoot: transactionTrie.root(),
+ blockNumber,
+ blockHash,
+ isPendingBlock,
+ });
+ store.push({
+ collection: "headers",
+ data: { header: ethHeader },
+ });
+
+ return store;
+}
diff --git a/indexer/src/provider.ts b/indexer/src/provider.ts
new file mode 100644
index 000000000..f6d030775
--- /dev/null
+++ b/indexer/src/provider.ts
@@ -0,0 +1,515 @@
+import { Contract, RpcProvider } from "./deps.ts";
+
+const RPC_URL = Deno.env.get("STARKNET_NETWORK");
+if (RPC_URL === undefined) {
+ throw new Error("ENV: STARKNET_NETWORK is not set");
+}
+
+const KAKAROT_ADDRESS = Deno.env.get("KAKAROT_ADDRESS");
+if (KAKAROT_ADDRESS === undefined) {
+ throw new Error("ENV: KAKAROT_ADDRESS is not set");
+}
+
+export const PROVIDER = new RpcProvider({
+ nodeUrl: RPC_URL,
+});
+
+const abi = [
+ {
+ members: [
+ {
+ name: "low",
+ offset: 0,
+ type: "felt",
+ },
+ {
+ name: "high",
+ offset: 1,
+ type: "felt",
+ },
+ ],
+ name: "Uint256",
+ size: 2,
+ type: "struct",
+ },
+ {
+ members: [
+ {
+ name: "is_some",
+ offset: 0,
+ type: "felt",
+ },
+ {
+ name: "value",
+ offset: 1,
+ type: "felt",
+ },
+ ],
+ name: "Option",
+ size: 2,
+ type: "struct",
+ },
+ {
+ data: [
+ {
+ name: "previousOwner",
+ type: "felt",
+ },
+ {
+ name: "newOwner",
+ type: "felt",
+ },
+ ],
+ keys: [],
+ name: "OwnershipTransferred",
+ type: "event",
+ },
+ {
+ data: [
+ {
+ name: "evm_contract_address",
+ type: "felt",
+ },
+ {
+ name: "starknet_contract_address",
+ type: "felt",
+ },
+ ],
+ keys: [],
+ name: "evm_contract_deployed",
+ type: "event",
+ },
+ {
+ data: [
+ {
+ name: "new_class_hash",
+ type: "felt",
+ },
+ ],
+ keys: [],
+ name: "kakarot_upgraded",
+ type: "event",
+ },
+ {
+ inputs: [
+ {
+ name: "owner",
+ type: "felt",
+ },
+ {
+ name: "native_token_address",
+ type: "felt",
+ },
+ {
+ name: "account_contract_class_hash",
+ type: "felt",
+ },
+ {
+ name: "uninitialized_account_class_hash",
+ type: "felt",
+ },
+ {
+ name: "precompiles_class_hash",
+ type: "felt",
+ },
+ {
+ name: "coinbase",
+ type: "felt",
+ },
+ {
+ name: "block_gas_limit",
+ type: "felt",
+ },
+ ],
+ name: "constructor",
+ outputs: [],
+ type: "constructor",
+ },
+ {
+ inputs: [
+ {
+ name: "new_class_hash",
+ type: "felt",
+ },
+ ],
+ name: "upgrade",
+ outputs: [],
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "native_token_address",
+ type: "felt",
+ },
+ ],
+ name: "set_native_token",
+ outputs: [],
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "get_native_token",
+ outputs: [
+ {
+ name: "native_token_address",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "base_fee",
+ type: "felt",
+ },
+ ],
+ name: "set_base_fee",
+ outputs: [],
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "get_base_fee",
+ outputs: [
+ {
+ name: "base_fee",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "coinbase",
+ type: "felt",
+ },
+ ],
+ name: "set_coinbase",
+ outputs: [],
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "get_coinbase",
+ outputs: [
+ {
+ name: "coinbase",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "prev_randao",
+ type: "Uint256",
+ },
+ ],
+ name: "set_prev_randao",
+ outputs: [],
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "get_prev_randao",
+ outputs: [
+ {
+ name: "prev_randao",
+ type: "Uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "gas_limit_",
+ type: "felt",
+ },
+ ],
+ name: "set_block_gas_limit",
+ outputs: [],
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "get_block_gas_limit",
+ outputs: [
+ {
+ name: "block_gas_limit",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "evm_address",
+ type: "felt",
+ },
+ ],
+ name: "compute_starknet_address",
+ outputs: [
+ {
+ name: "contract_address",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "get_account_contract_class_hash",
+ outputs: [
+ {
+ name: "account_contract_class_hash",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "evm_address",
+ type: "felt",
+ },
+ ],
+ name: "get_starknet_address",
+ outputs: [
+ {
+ name: "starknet_address",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "evm_address",
+ type: "felt",
+ },
+ ],
+ name: "deploy_externally_owned_account",
+ outputs: [
+ {
+ name: "starknet_contract_address",
+ type: "felt",
+ },
+ ],
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "evm_address",
+ type: "felt",
+ },
+ ],
+ name: "register_account",
+ outputs: [],
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "nonce",
+ type: "felt",
+ },
+ {
+ name: "origin",
+ type: "felt",
+ },
+ {
+ name: "to",
+ type: "Option",
+ },
+ {
+ name: "gas_limit",
+ type: "felt",
+ },
+ {
+ name: "gas_price",
+ type: "felt",
+ },
+ {
+ name: "value",
+ type: "Uint256",
+ },
+ {
+ name: "data_len",
+ type: "felt",
+ },
+ {
+ name: "data",
+ type: "felt*",
+ },
+ {
+ name: "access_list_len",
+ type: "felt",
+ },
+ {
+ name: "access_list",
+ type: "felt*",
+ },
+ ],
+ name: "eth_call",
+ outputs: [
+ {
+ name: "return_data_len",
+ type: "felt",
+ },
+ {
+ name: "return_data",
+ type: "felt*",
+ },
+ {
+ name: "success",
+ type: "felt",
+ },
+ {
+ name: "gas_used",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "nonce",
+ type: "felt",
+ },
+ {
+ name: "origin",
+ type: "felt",
+ },
+ {
+ name: "to",
+ type: "Option",
+ },
+ {
+ name: "gas_limit",
+ type: "felt",
+ },
+ {
+ name: "gas_price",
+ type: "felt",
+ },
+ {
+ name: "value",
+ type: "Uint256",
+ },
+ {
+ name: "data_len",
+ type: "felt",
+ },
+ {
+ name: "data",
+ type: "felt*",
+ },
+ {
+ name: "access_list_len",
+ type: "felt",
+ },
+ {
+ name: "access_list",
+ type: "felt*",
+ },
+ ],
+ name: "eth_estimate_gas",
+ outputs: [
+ {
+ name: "return_data_len",
+ type: "felt",
+ },
+ {
+ name: "return_data",
+ type: "felt*",
+ },
+ {
+ name: "success",
+ type: "felt",
+ },
+ {
+ name: "required_gas",
+ type: "felt",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ name: "to",
+ type: "Option",
+ },
+ {
+ name: "gas_limit",
+ type: "felt",
+ },
+ {
+ name: "gas_price",
+ type: "felt",
+ },
+ {
+ name: "value",
+ type: "Uint256",
+ },
+ {
+ name: "data_len",
+ type: "felt",
+ },
+ {
+ name: "data",
+ type: "felt*",
+ },
+ {
+ name: "access_list_len",
+ type: "felt",
+ },
+ {
+ name: "access_list",
+ type: "felt*",
+ },
+ ],
+ name: "eth_send_transaction",
+ outputs: [
+ {
+ name: "return_data_len",
+ type: "felt",
+ },
+ {
+ name: "return_data",
+ type: "felt*",
+ },
+ {
+ name: "success",
+ type: "felt",
+ },
+ {
+ name: "gas_used",
+ type: "felt",
+ },
+ ],
+ type: "function",
+ },
+];
+
+export const KAKAROT = new Contract(abi, KAKAROT_ADDRESS, PROVIDER);
diff --git a/indexer/src/types/header.ts b/indexer/src/types/header.ts
new file mode 100644
index 000000000..fb1a6e3eb
--- /dev/null
+++ b/indexer/src/types/header.ts
@@ -0,0 +1,148 @@
+// Utils
+import { padString } from "../utils/hex.ts";
+
+// Starknet
+import { BlockHeader } from "../deps.ts";
+
+// Eth
+import {
+ bigIntToHex,
+ Bloom,
+ bytesToHex,
+ JsonRpcBlock as Block,
+ PrefixedHexString,
+} from "../deps.ts";
+import { KAKAROT } from "../provider.ts";
+
+/**
+ * @param header - A Starknet block header.
+ * @param blockNumber - The block number of the transaction in hex.
+ * @param blockHash - The block hash of the transaction in hex.
+ * @param gasUsed - The total gas used in the block.
+ * @param logsBloom - The logs bloom of the block.
+ * @param receiptRoot - The transaction receipt trie root of the block.
+ * @param transactionRoot - The transaction trie root of the block.
+ * @param isPendingBlock - Whether the block is pending.
+ * @returns The Ethereum block header in the json RPC format.
+ *
+ * Note: We return a JsonRpcBlock instead of a JsonHeader, since the
+ * JsonHeader from the ethereumjs-mono repo does not follow the
+ * Ethereum json RPC format for certain fields and is used as an
+ * internal type.
+ */
+export async function toEthHeader({
+ header,
+ blockNumber,
+ blockHash,
+ gasUsed,
+ logsBloom,
+ receiptRoot,
+ transactionRoot,
+ isPendingBlock,
+}: {
+ header: BlockHeader;
+ blockNumber: PrefixedHexString;
+ blockHash: PrefixedHexString;
+ gasUsed: bigint;
+ logsBloom: Bloom;
+ receiptRoot: Uint8Array;
+ transactionRoot: Uint8Array;
+ isPendingBlock: boolean;
+}): Promise {
+ const maybeTs = Date.parse(header.timestamp);
+ const ts = isNaN(maybeTs) ? 0 : Math.floor(maybeTs / 1000);
+
+ if (header.timestamp === undefined || isNaN(maybeTs)) {
+ console.error(
+ `⚠️ Block timestamp is ${header.timestamp}, Date.parse of this is invalid - Block timestamp will be set to 0.`,
+ );
+ }
+
+ let coinbase;
+ let baseFee;
+ let blockGasLimit;
+
+ try {
+ const response = (await KAKAROT.call("get_coinbase", [], {
+ // ⚠️ StarknetJS: blockIdentifier is a block hash if value is BigInt or HexString, otherwise it's a block number.
+ blockIdentifier: BigInt(blockNumber).toString(),
+ })) as {
+ coinbase: bigint;
+ };
+ coinbase = response.coinbase;
+ } catch (error) {
+ console.warn(
+ `⚠️ Failed to get coinbase for block ${blockNumber} - Error: ${error.message}`,
+ );
+ coinbase = BigInt(0);
+ }
+
+ try {
+ const response = (await KAKAROT.call("get_base_fee", [], {
+ // ⚠️ StarknetJS: blockIdentifier is a block hash if value is BigInt or HexString, otherwise it's a block number.
+ blockIdentifier: BigInt(blockNumber).toString(),
+ })) as {
+ base_fee: bigint;
+ };
+ baseFee = response.base_fee;
+ } catch (error) {
+ console.warn(
+ `⚠️ Failed to get base fee for block ${blockNumber} - Error: ${error.message}`,
+ );
+ baseFee = BigInt(0);
+ }
+
+ try {
+ const response = (await KAKAROT.call(
+ "get_block_gas_limit",
+ [],
+ {
+ // ⚠️ StarknetJS: blockIdentifier is a block hash if value is BigInt or HexString, otherwise it's a block number.
+ blockIdentifier: BigInt(blockNumber).toString(),
+ },
+ )) as {
+ block_gas_limit: bigint;
+ };
+ blockGasLimit = response.block_gas_limit;
+ } catch (error) {
+ console.warn(
+ `⚠️ Failed to get block gas limit for block ${blockNumber} - Error: ${error.message}`,
+ );
+ blockGasLimit = BigInt(0);
+ }
+
+ return {
+ number: blockNumber,
+ hash: isPendingBlock ? null : blockHash,
+ parentHash: padString(header.parentBlockHash, 32),
+ mixHash: padString("0x", 32),
+ nonce: padString("0x", 8),
+ // Empty list of uncles -> RLP encoded to 0xC0 -> Keccak(0xc0) == 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
+ sha3Uncles:
+ "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+ logsBloom: bytesToHex(logsBloom.bitvector),
+ transactionsRoot: bytesToHex(transactionRoot),
+ stateRoot: header.newRoot ?? padString("0x", 32),
+ receiptsRoot: bytesToHex(receiptRoot),
+ miner: padString(bigIntToHex(coinbase), 20),
+ difficulty: "0x00",
+ totalDifficulty: "0x00",
+ extraData: "0x",
+ size: "0x00",
+ gasLimit: padString(bigIntToHex(blockGasLimit), 32),
+ gasUsed: bigIntToHex(gasUsed),
+ timestamp: bigIntToHex(BigInt(ts)),
+ transactions: [], // we are using this structure to represent a Kakarot block header, so we don't need to include transactions
+ uncles: [],
+ withdrawals: [],
+ // Root hash of an empty trie.
+ //
+ withdrawalsRoot:
+ "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
+ baseFeePerGas: padString(bigIntToHex(baseFee), 32),
+ };
+}
+
+export type JsonRpcBlock = Omit & {
+ hash: string | null;
+};
diff --git a/indexer/src/types/log.ts b/indexer/src/types/log.ts
new file mode 100644
index 000000000..b833c2c71
--- /dev/null
+++ b/indexer/src/types/log.ts
@@ -0,0 +1,121 @@
+// Utils
+import { padBigint } from "../utils/hex.ts";
+
+// Starknet
+import { Event, hash } from "../deps.ts";
+
+// Eth
+import {
+ bigIntToHex,
+ hexToBytes,
+ JsonRpcTx,
+ Log,
+ PrefixedHexString,
+} from "../deps.ts";
+
+// Events containing these keys are not
+// ETH logs and should be ignored.
+const IGNORED_KEYS = [
+ BigInt(hash.getSelectorFromName("transaction_executed")),
+ BigInt(hash.getSelectorFromName("evm_contract_deployed")),
+ BigInt(hash.getSelectorFromName("Transfer")),
+ BigInt(hash.getSelectorFromName("Approval")),
+ BigInt(hash.getSelectorFromName("OwnershipTransferred")),
+];
+
+/**
+ * @param transaction - A Ethereum transaction.
+ * @param event - A Starknet event.
+ * @param blockNumber - The block number of the transaction in hex.
+ * @param blockHash - The block hash of the transaction in hex.
+ * @param isPendingBlock - Whether the block is pending.
+ * @returns - The log in the Ethereum format, or null if the log is invalid.
+ */
+export function toEthLog({
+ transaction,
+ event,
+ blockNumber,
+ blockHash,
+ isPendingBlock,
+}: {
+ transaction: JsonRpcTx;
+ event: Event;
+ blockNumber: PrefixedHexString;
+ blockHash: PrefixedHexString;
+ isPendingBlock: boolean;
+}): JsonRpcLog | null {
+ const keys = event.keys ?? [];
+
+ // The event must have at least one key (since the first key is the address)
+ // and an odd number of keys (since each topic is split into two keys).
+ //
+ if (keys.length < 1 || keys.length % 2 !== 1) {
+ console.error(`Invalid event ${event}`);
+ return null;
+ }
+
+ // Filter out ignored events which aren't ETH logs.
+ if (IGNORED_KEYS.includes(BigInt(keys[0]))) {
+ return null;
+ }
+
+ // The address is the first key of the event.
+ const address = padBigint(BigInt(keys[0]), 20);
+ // data field is FieldElement[] where each FieldElement represents a byte of data.
+ // We convert it to a hex string and add leading zeros to make it a valid hex byte string.
+ // Example: [1, 2, 3] -> "010203"
+ const data = event.data ?? [];
+ const paddedData = data
+ .map((d) => BigInt(d).toString(16).padStart(2, "0"))
+ .join("");
+ const topics: string[] = [];
+ for (let i = 1; i < keys.length; i += 2) {
+ // EVM Topics are u256, therefore are split into two felt keys, of at most
+ // 128 bits (remember felt are 252 bits < 256 bits).
+ topics[Math.floor(i / 2)] = padBigint(
+ (BigInt(keys[i + 1]) << 128n) + BigInt(keys[i]),
+ 32,
+ );
+ }
+
+ return {
+ removed: false,
+ logIndex: null,
+ transactionIndex: bigIntToHex(BigInt(transaction.transactionIndex ?? 0)),
+ transactionHash: transaction.hash,
+ blockHash: isPendingBlock ? null : blockHash,
+ blockNumber,
+ address,
+ data: `0x${paddedData}`,
+ topics,
+ };
+}
+
+/**
+ * @param log - JSON RPC formatted Ethereum json rpc log.
+ * @returns - A Ethereum log.
+ */
+export function fromJsonRpcLog(log: JsonRpcLog): Log {
+ return [
+ hexToBytes(log.address),
+ log.topics.map(hexToBytes),
+ hexToBytes(log.data),
+ ];
+}
+
+/**
+ * Acknowledgement: Code taken from
+ */
+export type JsonRpcLog = {
+ removed: boolean; // TAG - true when the log was removed, due to a chain reorganization. false if it's a valid log.
+ logIndex: string | null; // QUANTITY - integer of the log index position in the block. null when it's pending.
+ transactionIndex: string | null; // QUANTITY - integer of the transactions index position log was created from. null when it's pending.
+ transactionHash: string | null; // DATA, 32 Bytes - hash of the transactions this log was created from. null when it's pending.
+ blockHash: string | null; // DATA, 32 Bytes - hash of the block where this log was in. null when it's pending.
+ blockNumber: string | null; // QUANTITY - the block number where this log was in. null when it's pending.
+ address: string; // DATA, 20 Bytes - address from which this log originated.
+ data: string; // DATA - contains one or more 32 Bytes non-indexed arguments of the log.
+ topics: string[]; // Array of DATA - Array of 0 to 4 32 Bytes DATA of indexed log arguments.
+ // (In solidity: The first topic is the hash of the signature of the event
+ // (e.g. Deposit(address,bytes32,uint256)), except you declared the event with the anonymous specifier.)
+};
diff --git a/indexer/src/types/receipt.ts b/indexer/src/types/receipt.ts
new file mode 100644
index 000000000..36cda4fd5
--- /dev/null
+++ b/indexer/src/types/receipt.ts
@@ -0,0 +1,144 @@
+// Utils
+import { padBytes } from "../utils/hex.ts";
+
+// Types
+import { fromJsonRpcLog, JsonRpcLog } from "./log.ts";
+
+// Starknet
+import { Event } from "../deps.ts";
+
+// Eth
+import {
+ bigIntToHex,
+ Bloom,
+ bytesToHex,
+ generateAddress,
+ hexToBytes,
+ JsonRpcTx,
+ Log,
+ PrefixedHexString,
+ TxReceipt,
+} from "../deps.ts";
+
+/**
+ * @param transaction - A Ethereum transaction.
+ * @param logs - A array of Ethereum logs.
+ * @param event - The "transaction_executed" event.
+ * @param blockNumber - The block number of the transaction in hex.
+ * @param blockHash - The block hash of the transaction in hex.
+ * @param cumulativeGasUsed - The cumulative gas used up to this transaction.
+ * @param isPendingBlock - Whether the block is pending.
+ * @returns - The Ethereum receipt.
+ */
+export function toEthReceipt({
+ transaction,
+ logs,
+ event,
+ blockNumber,
+ blockHash,
+ cumulativeGasUsed,
+ isPendingBlock,
+}: {
+ transaction: JsonRpcTx;
+ logs: JsonRpcLog[];
+ event: Event;
+ blockNumber: PrefixedHexString;
+ blockHash: PrefixedHexString;
+ cumulativeGasUsed: bigint;
+ isPendingBlock?: boolean;
+}): JsonRpcReceipt {
+ // Gas used is the last piece of data in the transaction_executed event.
+ // https://github.com/kkrt-labs/kakarot/blob/main/src/kakarot/accounts/eoa/library.cairo
+ const gasUsed = BigInt(event.data[event.data.length - 1]);
+ // Status is the second to last piece of data in the transaction_executed event.
+ // https://github.com/kkrt-labs/kakarot/blob/main/src/kakarot/accounts/eoa/library.cairo
+ const status = bigIntToHex(BigInt(event.data[event.data.length - 2]));
+ // If there is no destination, calculate the deployed contract address.
+ const contractAddress =
+ transaction.to === null
+ ? padBytes(
+ generateAddress(
+ hexToBytes(transaction.from),
+ hexToBytes(transaction.nonce),
+ ),
+ 20,
+ )
+ : null;
+
+ return {
+ transactionHash: transaction.hash,
+ transactionIndex: bigIntToHex(BigInt(transaction.transactionIndex ?? 0)),
+ blockHash: isPendingBlock ? null : blockHash,
+ blockNumber,
+ from: transaction.from,
+ to: transaction.to,
+ cumulativeGasUsed: bigIntToHex(cumulativeGasUsed + gasUsed),
+ gasUsed: bigIntToHex(gasUsed),
+ // Incorrect, should be as in EIP1559
+ // min(transaction.max_priority_fee_per_gas, transaction.max_fee_per_gas - block.base_fee_per_gas)
+ // effective_gas_price = priority_fee_per_gas + block.base_fee_per_gas
+ // Issue is that for now we don't have access to the block base fee per gas.
+ effectiveGasPrice: transaction.gasPrice,
+ contractAddress: contractAddress,
+ logs,
+ logsBloom: logsBloom(logs.map(fromJsonRpcLog)),
+ status,
+ type: transaction.type,
+ };
+}
+
+/**
+ * @param logs - A array of Ethereum logs.
+ * @returns - The corresponding logs bloom.
+ *
+ * Acknowledgement: Code taken from
+ */
+function logsBloom(logs: Log[]): string {
+ const bloom = new Bloom();
+ for (let i = 0; i < logs.length; i++) {
+ const log = logs[i];
+ // add the address
+ bloom.add(log[0]);
+ // add the topics
+ const topics = log[1];
+ for (let q = 0; q < topics.length; q++) {
+ bloom.add(topics[q]);
+ }
+ }
+ return bytesToHex(bloom.bitvector);
+}
+
+export function fromJsonRpcReceipt(receipt: JsonRpcReceipt): TxReceipt {
+ const status = BigInt(receipt.status ?? "0");
+ return {
+ cumulativeBlockGasUsed: BigInt(receipt.cumulativeGasUsed),
+ bitvector: hexToBytes(receipt.logsBloom),
+ logs: receipt.logs.map(fromJsonRpcLog),
+ status: status === 0n ? 0 : 1,
+ };
+}
+
+/**
+ * Acknowledgement: Code taken from
+ */
+export type JsonRpcReceipt = {
+ transactionHash: string; // DATA, 32 Bytes - hash of the transaction.
+ transactionIndex: string | null; // QUANTITY - integer of the transactions index position in the block.
+ blockHash: string | null; // DATA, 32 Bytes - hash of the block where this transaction was in.
+ blockNumber: string | null; // QUANTITY - block number where this transaction was in.
+ from: string; // DATA, 20 Bytes - address of the sender.
+ to: string | null; // DATA, 20 Bytes - address of the receiver. null when it's a contract creation transaction.
+ cumulativeGasUsed: string; // QUANTITY - cumulativeGasUsed is the sum of gasUsed by this specific transaction plus the gasUsed
+ // in all preceding transactions in the same block.
+ effectiveGasPrice: string; // QUANTITY - The final gas price per gas paid by the sender in wei.
+ gasUsed: string; // QUANTITY - The amount of gas used by this specific transaction alone.
+ contractAddress: string | null; // DATA, 20 Bytes - The contract address created, if the transaction was a contract creation, otherwise null.
+ logs: JsonRpcLog[]; // Array - Array of log objects, which this transaction generated.
+ logsBloom: string; // DATA, 256 Bytes - Bloom filter for light clients to quickly retrieve related logs.
+ // It also returns either:
+ type: string; // QUANTITY - integer of the transaction's type
+ root?: string; // DATA, 32 bytes of post-transaction stateroot (pre Byzantium)
+ status?: string; // QUANTITY, either 1 (success) or 0 (failure)
+ blobGasUsed?: string; // QUANTITY, blob gas consumed by transaction (if blob transaction)
+ blobGasPrice?: string; // QUAntity, blob gas price for block including this transaction (if blob transaction)
+};
diff --git a/indexer/src/types/storeItem.ts b/indexer/src/types/storeItem.ts
new file mode 100644
index 000000000..9f6e6e6d5
--- /dev/null
+++ b/indexer/src/types/storeItem.ts
@@ -0,0 +1,21 @@
+// Types
+import { JsonRpcLog } from "./log.ts";
+import { JsonRpcReceipt } from "./receipt.ts";
+
+// Eth
+import { JsonRpcTx } from "../deps.ts";
+import { JsonRpcBlock } from "./header.ts";
+
+type Collection =
+ | "transactions"
+ | "logs"
+ | "receipts"
+ | "headers";
+
+export type StoreItem = {
+ collection: C;
+ data: C extends "transactions" ? { tx: JsonRpcTx }
+ : C extends "logs" ? { log: JsonRpcLog }
+ : C extends "receipts" ? { receipt: JsonRpcReceipt }
+ : { header: JsonRpcBlock };
+};
diff --git a/indexer/src/types/transaction.test.ts b/indexer/src/types/transaction.test.ts
new file mode 100644
index 000000000..0ee9826bd
--- /dev/null
+++ b/indexer/src/types/transaction.test.ts
@@ -0,0 +1,187 @@
+import { assertExists } from "https://deno.land/std@0.213.0/assert/mod.ts";
+import {
+ AccessListEIP2930Transaction,
+ FeeMarketEIP1559Transaction,
+ LegacyTransaction,
+ RLP,
+ Transaction,
+} from "../deps.ts";
+import { toTypedEthTx } from "./transaction.ts";
+import { assertEquals } from "https://deno.land/std@0.213.0/assert/assert_equals.ts";
+import { Common } from "https://esm.sh/v135/@ethereumjs/common@4.1.0/denonext/common.mjs";
+
+Deno.test("toTypedEthTx Legacy Transaction", () => {
+ // Given
+ const common = new Common({ chain: "mainnet", hardfork: "shanghai" });
+ const tx = new LegacyTransaction({
+ nonce: 1n,
+ gasPrice: 2n,
+ gasLimit: 3n,
+ to: "0x0000000000000000000000000000000000000001",
+ value: 4n,
+ data: new Uint8Array([0x12, 0x34]),
+ }, { common });
+ const raw = RLP.encode(tx.getMessageToSign());
+
+ const serializedTx: `0x${string}`[] = [];
+ raw.forEach((x) => serializedTx.push(`0x${x.toString(16)}`));
+ const starknetTxCalldata: `0x${string}`[] = [
+ "0x1",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ ...serializedTx,
+ ];
+
+ const starknetTx: Transaction = {
+ invokeV1: {
+ senderAddress: "0x01",
+ calldata: starknetTxCalldata,
+ },
+ meta: {
+ hash: "0x01",
+ maxFee: "0x01",
+ nonce: "0x01",
+ signature: ["0x1", "0x2", "0x3", "0x4", "0x32"],
+ version: "1",
+ },
+ };
+
+ // When
+ const ethTx = toTypedEthTx({ transaction: starknetTx }) as LegacyTransaction;
+
+ // Then
+ assertExists(ethTx);
+ assertEquals(ethTx.nonce, 1n);
+ assertEquals(ethTx.gasPrice, 2n);
+ assertEquals(ethTx.gasLimit, 3n);
+ assertEquals(ethTx.value, 4n);
+ assertEquals(ethTx.type, 0);
+ assertEquals(ethTx.data, tx.data);
+});
+
+Deno.test("toTypedEthTx EIP1559 Transaction", () => {
+ // Given
+ const common = new Common({ chain: "mainnet", hardfork: "shanghai" });
+ const tx = new FeeMarketEIP1559Transaction({
+ nonce: 1n,
+ maxFeePerGas: 4n,
+ maxPriorityFeePerGas: 3n,
+ gasLimit: 4n,
+ to: "0x0000000000000000000000000000000000000001",
+ value: 5n,
+ data: new Uint8Array([0x12, 0x34]),
+ accessList: [{
+ address: "0x0000000000000000000000000000000000000002",
+ storageKeys: [
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
+ ],
+ }],
+ }, { common });
+
+ const raw = tx.getMessageToSign();
+ const serializedTx: `0x${string}`[] = [];
+ raw.forEach((x) => serializedTx.push(`0x${x.toString(16)}`));
+ const starknetTxCalldata: `0x${string}`[] = [
+ "0x1",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ ...serializedTx,
+ ];
+
+ const starknetTx: Transaction = {
+ invokeV1: {
+ senderAddress: "0x01",
+ calldata: starknetTxCalldata,
+ },
+ meta: {
+ hash: "0x01",
+ maxFee: "0x01",
+ nonce: "0x01",
+ signature: ["0x1", "0x2", "0x3", "0x4", "0x1"],
+ version: "1",
+ },
+ };
+
+ // When
+ const ethTx = toTypedEthTx({
+ transaction: starknetTx,
+ }) as FeeMarketEIP1559Transaction;
+
+ // Then
+ assertExists(ethTx);
+ assertEquals(ethTx.nonce, 1n);
+ assertEquals(ethTx.maxFeePerGas, 4n);
+ assertEquals(ethTx.maxPriorityFeePerGas, 3n);
+ assertEquals(ethTx.gasLimit, 4n);
+ assertEquals(ethTx.value, 5n);
+ assertEquals(ethTx.type, 2);
+ assertEquals(ethTx.data, new Uint8Array([0x12, 0x34]));
+});
+
+Deno.test("toTypedEthTx EIP2930 Transaction", () => {
+ // Given
+ const common = new Common({ chain: "mainnet", hardfork: "shanghai" });
+ const tx = new AccessListEIP2930Transaction({
+ nonce: 1n,
+ gasPrice: 2n,
+ gasLimit: 3n,
+ to: "0x0000000000000000000000000000000000000001",
+ value: 4n,
+ data: new Uint8Array([0x12, 0x34]),
+ accessList: [{
+ address: "0x0000000000000000000000000000000000000002",
+ storageKeys: [
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
+ ],
+ }],
+ }, { common });
+
+ const raw = tx.getMessageToSign();
+ const serializedTx: `0x${string}`[] = [];
+ raw.forEach((x) => serializedTx.push(`0x${x.toString(16)}`));
+ const starknetTxCalldata: `0x${string}`[] = [
+ "0x1",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ "0x0",
+ ...serializedTx,
+ ];
+
+ const starknetTx: Transaction = {
+ invokeV1: {
+ senderAddress: "0x01",
+ calldata: starknetTxCalldata,
+ },
+ meta: {
+ hash: "0x01",
+ maxFee: "0x01",
+ nonce: "0x01",
+ signature: ["0x1", "0x2", "0x3", "0x4", "0x1"],
+ version: "1",
+ },
+ };
+
+ // When
+ const ethTx = toTypedEthTx({
+ transaction: starknetTx,
+ }) as AccessListEIP2930Transaction;
+
+ // Then
+ // Then
+ assertExists(ethTx);
+ assertEquals(ethTx.nonce, 1n);
+ assertEquals(ethTx.gasPrice, 2n);
+ assertEquals(ethTx.gasLimit, 3n);
+ assertEquals(ethTx.value, 4n);
+ assertEquals(ethTx.type, 1);
+ assertEquals(ethTx.data, tx.data);
+ assertEquals(ethTx.accessList, tx.accessList);
+});
diff --git a/indexer/src/types/transaction.ts b/indexer/src/types/transaction.ts
new file mode 100644
index 000000000..a7aca4cc5
--- /dev/null
+++ b/indexer/src/types/transaction.ts
@@ -0,0 +1,274 @@
+// Utils
+import { padBigint, padBytes } from "../utils/hex.ts";
+
+// Starknet
+import { Transaction, TransactionReceipt, uint256 } from "../deps.ts";
+
+// Eth
+import {
+ AccessListEIP2930Transaction,
+ bigIntToBytes,
+ bigIntToHex,
+ Capability,
+ concatBytes,
+ FeeMarketEIP1559Transaction,
+ intToHex,
+ isAccessListEIP2930Tx,
+ isFeeMarketEIP1559TxData,
+ isLegacyTx,
+ JsonRpcTx,
+ LegacyTransaction,
+ PrefixedHexString,
+ RLP,
+ TransactionFactory,
+ TransactionType,
+ TxValuesArray,
+ TypedTransaction,
+ TypedTxData,
+} from "../deps.ts";
+
+/**
+ * @param transaction - Typed transaction to be converted.
+ * @param header - The block header of the block containing the transaction.
+ * @param receipt The transaction receipt of the transaction.
+ * @param blockNumber - The block number of the transaction in hex.
+ * @param blockHash - The block hash of the transaction in hex.
+ * @param isPendingBlock - Whether the block is pending.
+ * @returns - The transaction in the Ethereum format, or null if the transaction is invalid.
+ *
+ * Acknowledgement: Code taken from
+ */
+export function toEthTx({
+ transaction,
+ receipt,
+ blockNumber,
+ blockHash,
+ isPendingBlock,
+}: {
+ transaction: TypedTransaction;
+ receipt: TransactionReceipt;
+ blockNumber: PrefixedHexString;
+ blockHash: PrefixedHexString;
+ isPendingBlock: boolean;
+}): (JsonRpcTx & { yParity?: string }) | null {
+ const index = receipt.transactionIndex;
+
+ if (index === undefined) {
+ console.error(
+ "Known bug (apibara): ⚠️ Transaction index is undefined - Transaction index will be set to 0.",
+ );
+ }
+
+ const txJSON = transaction.toJSON();
+ if (
+ txJSON.r === undefined ||
+ txJSON.s === undefined ||
+ txJSON.v === undefined
+ ) {
+ console.error(
+ `Transaction is not signed: {r: ${txJSON.r}, s: ${txJSON.s}, v: ${txJSON.v}}`,
+ );
+ // TODO: Ping alert webhooks
+ return null;
+ }
+ // If the transaction is a legacy, we can calculate it from the v value.
+ // v = 35 + 2 * chainId + yParity -> chainId = (v - 35) / 2
+ const chainId = isLegacyTx(transaction) &&
+ transaction.supports(Capability.EIP155ReplayProtection)
+ ? bigIntToHex((BigInt(txJSON.v) - 35n) / 2n)
+ : txJSON.chainId;
+
+ const result: JsonRpcTx & { yParity?: string } = {
+ blockHash: isPendingBlock ? null : blockHash,
+ blockNumber,
+ from: transaction.getSenderAddress().toString(),
+ gas: txJSON.gasLimit!,
+ gasPrice: txJSON.gasPrice ?? txJSON.maxFeePerGas!,
+ maxFeePerGas: txJSON.maxFeePerGas,
+ maxPriorityFeePerGas: txJSON.maxPriorityFeePerGas,
+ type: intToHex(transaction.type),
+ accessList: txJSON.accessList,
+ chainId,
+ hash: padBytes(transaction.hash(), 32),
+ input: txJSON.data!,
+ nonce: txJSON.nonce!,
+ to: transaction.to?.toString() ?? null,
+ transactionIndex: isPendingBlock ? null : padBigint(BigInt(index ?? 0), 32),
+ value: txJSON.value!,
+ v: txJSON.v,
+ r: txJSON.r,
+ s: txJSON.s,
+ maxFeePerBlobGas: txJSON.maxFeePerBlobGas,
+ blobVersionedHashes: txJSON.blobVersionedHashes,
+ };
+ // Adding yParity for EIP-1559 and EIP-2930 transactions
+ // To fit the Ethereum format, we need to add the yParity field.
+ if (
+ isFeeMarketEIP1559TxData(transaction) ||
+ isAccessListEIP2930Tx(transaction)
+ ) {
+ result.yParity = txJSON.v;
+ }
+ return result;
+}
+
+/**
+ * @param transaction - A Kakarot transaction.
+ * @returns - The Typed transaction in the Ethereum format
+ */
+export function toTypedEthTx({
+ transaction,
+}: {
+ transaction: Transaction;
+}): TypedTransaction | null {
+ const calldata = transaction.invokeV1?.calldata;
+ if (!calldata) {
+ console.error("No calldata");
+ console.error(JSON.stringify(transaction, null, 2));
+ return null;
+ }
+ const callArrayLen = BigInt(calldata[0]);
+ // Multi-calls are not supported for now.
+ if (callArrayLen !== 1n) {
+ console.error(`Invalid call array length ${callArrayLen}`);
+ console.error(JSON.stringify(transaction, null, 2));
+ return null;
+ }
+
+ // callArrayLen <- calldata[0]
+ // to <- calldata[1]
+ // selector <- calldata[2];
+ // dataOffset <- calldata[3]
+ // dataLength <- calldata[4]
+ // calldataLen <- calldata[5]
+ const bytes = concatBytes(
+ ...calldata.slice(6).map((x) => bigIntToBytes(BigInt(x))),
+ );
+
+ const signature = transaction.meta.signature;
+ if (signature.length !== 5) {
+ console.error(`Invalid signature length ${signature.length}`);
+ console.error(JSON.stringify(transaction, null, 2));
+ return null;
+ }
+ const r = uint256.uint256ToBN({ low: signature[0], high: signature[1] });
+ const s = uint256.uint256ToBN({ low: signature[2], high: signature[3] });
+ const v = BigInt(signature[4]);
+
+ try {
+ const ethTxUnsigned = fromSerializedData(bytes);
+ return addSignature(ethTxUnsigned, r, s, v);
+ } catch (e) {
+ if (e instanceof Error) {
+ console.error(`Invalid transaction: ${e.message}`);
+ } else {
+ console.error(`Unknown throw ${e}`);
+ throw e;
+ }
+ // TODO: Ping alert webhooks
+ return null;
+ }
+}
+
+/**
+ * @param bytes - The bytes of the rlp encoded transaction without signature.
+ * For Legacy = rlp([nonce, gasprice, startgas, to, value, data, chainid, 0, 0])
+ * For EIP1559 = [0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list])]
+ * For EIP2930 = [0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList])]
+ * @returns - Decoded unsigned transaction.
+ * @throws - Error if the transaction is a BlobEIP4844Tx or the rlp encoding is not an array.
+ */
+function fromSerializedData(bytes: Uint8Array): TypedTransaction {
+ const txType = bytes[0];
+ if (txType <= 0x7f) {
+ switch (txType) {
+ case TransactionType.AccessListEIP2930:
+ return AccessListEIP2930Transaction.fromSerializedTx(bytes);
+ case TransactionType.FeeMarketEIP1559:
+ return FeeMarketEIP1559Transaction.fromSerializedTx(bytes);
+ default:
+ throw new Error(`Invalid tx type: ${txType}`);
+ }
+ } else {
+ const values = RLP.decode(bytes);
+ if (!Array.isArray(values)) {
+ throw new Error("Invalid serialized tx input: must be array");
+ }
+ // In the case of a Legacy, we need to update the chain id to be a value >= 37.
+ // This is due to the fact that LegacyTransaction's constructor (used by fromValuesArray)
+ // will check if v >= 37. Since we pass it [v, r, s] = [chain_id, 0, 0], we need to force
+ // the chain id to be >= 37. This value will be updated during the call to addSignature.
+ values[6] = bigIntToBytes(37n);
+ return LegacyTransaction.fromValuesArray(
+ values as TxValuesArray[TransactionType.Legacy],
+ );
+ }
+}
+
+/**
+ * @param tx - Typed transaction to be signed.
+ * @param r - Signature r value.
+ * @param s - Signature s value.
+ * @param v - Signature v value. In case of EIP155ReplayProtection, must include the chain ID.
+ * @returns - Passed transaction with the signature added.
+ * @throws - Error if the transaction is a BlobEIP4844Tx or if v param is < 35 for a
+ * LegacyTx.
+ */
+function addSignature(
+ tx: TypedTransaction,
+ r: bigint,
+ s: bigint,
+ v: bigint,
+): TypedTransaction {
+ const TypedTxData = ((): TypedTxData => {
+ if (isLegacyTx(tx)) {
+ if (v < 35) {
+ throw new Error(`Invalid v value: ${v}`);
+ }
+ return LegacyTransaction.fromTxData({
+ nonce: tx.nonce,
+ gasPrice: tx.gasPrice,
+ gasLimit: tx.gasLimit,
+ to: tx.to,
+ value: tx.value,
+ data: tx.data,
+ v,
+ r,
+ s,
+ });
+ } else if (isAccessListEIP2930Tx(tx)) {
+ return AccessListEIP2930Transaction.fromTxData({
+ chainId: tx.chainId,
+ nonce: tx.nonce,
+ gasPrice: tx.gasPrice,
+ gasLimit: tx.gasLimit,
+ to: tx.to,
+ value: tx.value,
+ data: tx.data,
+ accessList: tx.accessList,
+ v,
+ r,
+ s,
+ });
+ } else if (isFeeMarketEIP1559TxData(tx)) {
+ return FeeMarketEIP1559Transaction.fromTxData({
+ chainId: tx.chainId,
+ nonce: tx.nonce,
+ maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
+ maxFeePerGas: tx.maxFeePerGas,
+ gasLimit: tx.gasLimit,
+ to: tx.to,
+ value: tx.value,
+ data: tx.data,
+ accessList: tx.accessList,
+ v,
+ r,
+ s,
+ });
+ } else {
+ throw new Error(`Invalid transaction type: ${tx}`);
+ }
+ })();
+
+ return TransactionFactory.fromTxData(TypedTxData);
+}
diff --git a/indexer/src/utils/hex.test.ts b/indexer/src/utils/hex.test.ts
new file mode 100644
index 000000000..a37c02c51
--- /dev/null
+++ b/indexer/src/utils/hex.test.ts
@@ -0,0 +1,65 @@
+import { assertEquals } from "https://deno.land/std@0.213.0/assert/mod.ts";
+import { padBigint, padBytes, padString, toHexString } from "./hex.ts";
+
+Deno.test("toHexString #1", () => {
+ const x = "1234";
+ const expected = "0x4d2";
+ const paddedX = toHexString(x);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("toHexString #2", () => {
+ const x = undefined;
+ const expected = "0x";
+ const paddedX = toHexString(x);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("padString #1", () => {
+ const x = "0x010203";
+ const expected = "0x0000000000010203";
+ const paddedX = padString(x, 8);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("padString #2", () => {
+ const x = undefined;
+ const expected = "0x0000000000000000";
+ const paddedX = padString(x, 8);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("padBigint #1", () => {
+ const x = BigInt("0x010203");
+ const expected = "0x0000000000010203";
+ const paddedX = padBigint(x, 8);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("padBigint #2", () => {
+ const x = undefined;
+ const expected = "0x0000000000000000";
+ const paddedX = padBigint(x, 8);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("padBytes #1", () => {
+ const x = new Uint8Array([1, 2, 3]);
+ const expected = "0x0000000000010203";
+ const paddedX = padBytes(x, 8);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("padBytes #2", () => {
+ const x = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ const expected = "0x010203040506070809";
+ const paddedX = padBytes(x, 8);
+ assertEquals(paddedX, expected);
+});
+
+Deno.test("padBytes #3", () => {
+ const x = undefined;
+ const expected = "0x0000000000000000";
+ const paddedX = padBytes(x, 8);
+ assertEquals(paddedX, expected);
+});
diff --git a/indexer/src/utils/hex.ts b/indexer/src/utils/hex.ts
new file mode 100644
index 000000000..aae9be94f
--- /dev/null
+++ b/indexer/src/utils/hex.ts
@@ -0,0 +1,54 @@
+// Eth
+import { bigIntToHex, bytesToHex } from "../deps.ts";
+import { PrefixedHexString, stripHexPrefix } from "../deps.ts";
+
+/**
+ * @param hex - A decimal string.
+ */
+export function toHexString(decimal: string | undefined): PrefixedHexString {
+ if (decimal === undefined) {
+ return "0x";
+ }
+ return bigIntToHex(BigInt(decimal));
+}
+
+/**
+ * @param hex - A hex string.
+ * @param length - The final length in bytes of the hex string.
+ */
+export function padString(
+ hex: PrefixedHexString | undefined,
+ length: number,
+): PrefixedHexString {
+ return "0x" + (stripHexPrefix(hex ?? "0x").padStart(2 * length, "0"));
+}
+
+/**
+ * @param b - A bigint.
+ * @param length - The final length in bytes of the hex string.
+ */
+export function padBigint(
+ b: bigint | undefined,
+ length: number,
+): PrefixedHexString {
+ return "0x" +
+ (stripHexPrefix(bigIntToHex(b ?? 0n)).padStart(2 * length, "0"));
+}
+
+/**
+ * @param bytes - A Uint8Array.
+ * @param length - The final length in bytes of the array. If
+ * the array is longer than the length, it is returned as is.
+ */
+export function padBytes(
+ maybeBytes: Uint8Array | undefined,
+ length: number,
+): PrefixedHexString {
+ const bytes = maybeBytes ?? new Uint8Array();
+ if (bytes.length > length) {
+ return bytesToHex(bytes);
+ }
+ const result = new Uint8Array(length);
+ result.set(bytes, length - bytes.length);
+ return bytesToHex(result);
+}