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

+ Kakarot Indexer +

+ +## 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); +}