├── .env.example ├── .github └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md └── src ├── abi └── SP1Vector.json ├── main.rs ├── models.rs └── schema.rs /.env.example: -------------------------------------------------------------------------------- 1 | AVAIL_CLIENT_URL= 2 | SUCCINCT_URL= 3 | AVAIL_CHAIN_NAME= 4 | CONTRACT_CHAIN_ID= 5 | VECTORX_CONTRACT_ADDRESS= 6 | BRIDGE_CONTRACT_ADDRESS= 7 | ETHEREUM_CLIENT_URL= 8 | CHAIN__RPC_URL= 9 | CHAIN__CONTRACT_ADDRESS= 10 | BEACONCHAIN_URL= 11 | ETH_HEAD_CACHE_MAXAGE= 12 | HEAD_CACHE_MAXAGE= 13 | AVL_HEAD_CACHE_MAXAGE= 14 | AVL_PROOF_CACHE_MAXAGE= 15 | ETH_PROOF_CACHE_MAXAGE= 16 | PROOF_CACHE_MAXAGE= 17 | SLOT_MAPPING_CACHE_MAXAGE= 18 | TRANSACTIONS_CACHE_MAXAGE= 19 | HOST= 20 | PORT= 21 | 22 | POSTGRES_URL= 23 | PG_USERNAME= 24 | PG_PASSWORD= 25 | POSTGRES_DB= 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches-ignore: 5 | - '**' 6 | tags-ignore: 7 | - 'v*.*.*' 8 | - 'v*.*.*-*' 9 | release: 10 | types: 11 | - released 12 | - prereleased 13 | jobs: 14 | binary_linux_amd64: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install Cargo Deps And Build Bridge API 19 | shell: bash 20 | run: | 21 | curl https://sh.rustup.rs -sSf | sh -s -- -y 22 | source "$HOME/.cargo/env" 23 | sudo apt-get update && sudo apt-get install -y protobuf-compiler pkg-config 24 | cargo build --profile maxperf -p bridge-api 25 | mv target/maxperf/bridge-api target/maxperf/bridge-api-linux-amd64 26 | pushd target/maxperf/ 27 | tar czf bridge-api-linux-amd64.tar.gz bridge-api-linux-amd64 28 | popd 29 | - uses: actions/upload-artifact@v4 30 | with: 31 | name: bridge-api-linux-amd64-binary 32 | path: target/maxperf/bridge-api-linux-amd64.tar.gz 33 | 34 | binary_publish: 35 | needs: [binary_linux_amd64] 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/download-artifact@v4 39 | with: 40 | name: bridge-api-linux-amd64-binary 41 | - name: Export Tag Var 42 | id: prepare 43 | run: | 44 | TAG=${GITHUB_REF#refs/tags/} 45 | echo ::set-output name=tag_name::${TAG} 46 | - name: Publish Binaries 47 | uses: svenstaro/upload-release-action@v2 48 | with: 49 | repo_token: ${{ secrets.PAT_TOKEN }} 50 | file: /home/runner/work/bridge-api/bridge-api/bridge-api* 51 | release_name: ${{ steps.prepare.outputs.tag_name }} 52 | tag: ${{ steps.prepare.outputs.tag_name }} 53 | overwrite: true 54 | file_glob: true 55 | 56 | docker_build_push: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v4 61 | - name: Set up QEMU 62 | uses: docker/setup-qemu-action@v3 63 | - name: Set up Docker Buildx 64 | id: buildx 65 | uses: docker/setup-buildx-action@v3 66 | - name: Export Tag Var 67 | id: prepare 68 | run: | 69 | TAG=${GITHUB_REF#refs/tags/} 70 | echo ::set-output name=tag_name::${TAG} 71 | - name: Login to Dockerhub 72 | uses: docker/login-action@v3 73 | with: 74 | username: ${{ secrets.DOCKERHUB_USERNAME }} 75 | password: ${{ secrets.DOCKERHUB_TOKEN }} 76 | - name: Build and push images 77 | uses: docker/build-push-action@v5 78 | with: 79 | builder: ${{ steps.buildx.outputs.name }} 80 | context: . 81 | file: ./Dockerfile 82 | platforms: linux/amd64 83 | push: true 84 | tags: availj/bridge-api:${{ steps.prepare.outputs.tag_name }} 85 | build-args: | 86 | BUILD_PROFILE=maxperf 87 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: dtolnay/rust-toolchain@stable 20 | with: 21 | toolchain: stable 22 | components: rustfmt 23 | - uses: Swatinem/rust-cache@v2 24 | with: 25 | cache-on-failure: true 26 | - run: cargo build --all-features -v 27 | - run: cargo test --all-features -v 28 | - run: cargo fmt --all -- --check 29 | # TODO enable security audit once the lib deps are updated. 30 | # - name: Security audit 31 | # uses: actions-rs/audit-check@v1 32 | # with: 33 | # token: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | .env.local 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bridge-api" 3 | version = "0.1.2" 4 | edition = "2024" 5 | authors = ["Avail Team"] 6 | default-run = "bridge-api" 7 | repository = "https://github.com/availproject/bridge-api.git" 8 | publish = false 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | alloy = { version = "1", features = ["serde", "json"] } 14 | axum = { version = "0.8", features = ["http2", "macros", "tracing"] } 15 | dotenvy = "0.15" 16 | jsonrpsee = { version = "0.25", features = ["http-client", "macros", "async-client"] } 17 | reqwest = { version = "0.12", features = ["json", "brotli"] } 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = { version = "1.0", features = ["arbitrary_precision"] } 20 | sha3 = "0.10" 21 | tokio = { version = "1.35", features = ["macros", "rt-multi-thread", "parking_lot"] } 22 | tower-http = { version = "0.5", features = ["trace", "compression-br", "cors"] } 23 | tracing = "0.1" 24 | tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } 25 | sp-io = "34.0" 26 | sp-core = "34.0" 27 | avail-core = { git = "https://github.com/availproject/avail-core", tag = "core-node-3"} 28 | http = "1.1" 29 | backon = { version = "0.5", features = ["tokio-sleep"] } 30 | anyhow = "1" 31 | lazy_static = "1.5" 32 | diesel = { version = "2.2", features = ["postgres", "chrono", "r2d2"] } 33 | diesel-enum = "0.2.1" 34 | serde_with = "3.11.0" 35 | chrono = { version = "0.4", features = ["serde"] } 36 | 37 | [target.'cfg(not(target_env = "msvc"))'.dependencies] 38 | tikv-jemallocator = "0.6" 39 | 40 | [profile.debug-fast] 41 | inherits = "release" 42 | debug = true 43 | 44 | [profile.release] 45 | panic = "abort" 46 | 47 | [profile.maxperf] 48 | inherits = "release" 49 | lto = "fat" 50 | codegen-units = 1 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.87-slim AS builder 2 | WORKDIR /build 3 | COPY . . 4 | ARG BUILD_PROFILE=release 5 | 6 | RUN apt update && apt install -y make libssl-dev pkg-config libpq-dev \ 7 | && cargo build --profile $BUILD_PROFILE --locked \ 8 | && cp /build/target/$BUILD_PROFILE/bridge-api /build/bridge-api 9 | 10 | FROM ubuntu:24.04 as run 11 | WORKDIR /app 12 | 13 | COPY --from=builder /build/bridge-api /usr/local/bin 14 | 15 | RUN adduser --disabled-password --gecos "" --no-create-home --uid 1000 bridge \ 16 | && apt-get update && apt-get install -y ca-certificates libpq-dev \ 17 | && apt clean \ 18 | && chown -R bridge:bridge /usr/local/bin/bridge-api 19 | 20 | USER bridge 21 | 22 | EXPOSE 8080 23 | 24 | ENTRYPOINT ["/usr/local/bin/bridge-api"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Avail Project 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bridge API 2 | 3 | The bridge API is a REST API for fetching proofs from Avail's Kate RPC and Succinct API to submit on Ethereum or 4 | any off-chain proof verification. 5 | 6 | ## Deploying the bridge API 7 | 8 | * Create an `.env` file according to the `.env.example` 9 | * To build the service: 10 | 11 | ```bash 12 | # for developing, make a debug build 13 | cargo build 14 | # and run it! 15 | cargo run 16 | ``` 17 | 18 | * Or instead, make release builds for production: 19 | 20 | ```bash 21 | cargo run --release 22 | # you can use maxperf to optimize for runtime performance: 23 | cargo run --profile maxperf 24 | # you can use RUSTFLAGS to use CPU-native optimizations: 25 | RUSTFLAGS="-C target-cpu=native" cargo run --profile maxperf 26 | ``` 27 | 28 | ## Usage 29 | 30 | * The bridge API operates on the 8080 port by default (can be configured). 31 | 32 | * Supported API versions on the server: 33 | 34 | * Request 35 | 36 | `GET /versions` 37 | 38 | ```bash 39 | # curl /versions 40 | curl http://localhost:8080/versions 41 | ``` 42 | 43 | * Response 44 | 45 | ```json 46 | ["v1"] 47 | ``` 48 | 49 | ### Liveness of the server 50 | 51 | * To verify that the API is live, you can query the root like: 52 | 53 | * Request 54 | 55 | `GET /` 56 | 57 | ```bash 58 | # curl 59 | curl http://localhost:8080 60 | ``` 61 | 62 | * Response 63 | 64 | ```json 65 | {"name":"Avail Bridge API"} 66 | ``` 67 | 68 | * To get information of the bridge details: 69 | 70 | * Request 71 | 72 | `GET /v1/info` 73 | 74 | ```bash 75 | # curl 76 | curl http://localhost:8080/v1/info 77 | ``` 78 | 79 | * Response 80 | 81 | ```json 82 | { 83 | "availChainName": "hex", 84 | "bridgeContractAddress": "0x1369A4C9391cF90D393b40fAeAD521b0F7019dc5", 85 | "vectorXChainId": "11155111", 86 | "vectorXContractAddress": "0x570f6a1936386a4e060C2Daebbd0b6f5C091e13f" 87 | } 88 | ``` 89 | 90 | 91 | 92 | ### Get current Ethereum head 93 | 94 | * To get the latest Ethereum block number, query: 95 | * Request 96 | `GET /v1/eth/head` 97 | 98 | ```bash 99 | # curl /v1/eth/head 100 | curl http://localhost:8080/v1/eth/head 101 | ``` 102 | * Response 103 | 104 | ```json 105 | { 106 | "slot":4454752, 107 | "timestamp":1709191840, 108 | "timestampDiff":1716 109 | } 110 | ``` 111 | 112 | ### Get current Avail head 113 | 114 | * To get the latest Avail block number, query: 115 | * Request 116 | `GET /v1/avl/head` 117 | 118 | ```bash 119 | # curl /v1/avl/head 120 | curl http://localhost:8080/v1/avl/head 121 | ``` 122 | * Response 123 | 124 | ```json 125 | { 126 | "data":{ 127 | "end":512738, 128 | "start":488581 129 | } 130 | } 131 | ``` 132 | 133 | ### Get SP1Vector head 134 | * To get the latest Avail block stored on an SP1Vector contract instance, query: 135 | * Request 136 | `GET /v1/head/{chain_id}` 137 | 138 | ```bash 139 | # curl /v1/head/{chain_id} 140 | curl http://localhost:8080/v1/head/{chain_id} 141 | ``` 142 | * Response 143 | 144 | ```json 145 | { 146 | "head": 123456 147 | } 148 | ``` 149 | 150 | ### Generate Merkle Proof 151 | 152 | * To generate a proof, simply query the `eth/proof` endpoint with the block hash and extrinsic index like (both are 153 | required): 154 | 155 | * Request 156 | 157 | `GET /v1/eth/proof/:blockhash?index=` 158 | 159 | ```bash 160 | # curl "/v1/eth/proof/?index=" 161 | curl "http://localhost:8080/v1/eth/proof/0x5bc7bd3a4793132007d6d0d9c55dc2ded2fe721a49bd771c1d290e6a3c6ec237?index=5" 162 | ``` 163 | 164 | * Response 165 | 166 | ```json 167 | { 168 | "blobRoot": "0x511030804f9768c9d5c4826cdc7eba25ba0fd8e73ea32467e5fad547397620f8", 169 | "blockHash": "0x5bc7bd3a4793132007d6d0d9c55dc2ded2fe721a49bd771c1d290e6a3c6ec237", 170 | "bridgeRoot": "0xf6c807bc73a637957a61d620bd5e4ef8c7dd234e5fc96dfb6d6041bbe2947782", 171 | "dataRoot": "0x2179e18ee112b080794b40f2239d77041c715ad7392d9fce054b7c10eacd4ebc", 172 | "dataRootCommitment": "0x41cfe14b2e229cc5b4ee0cb7c3c909e1f78ae9e32f986e7496bfd4e007e06519", 173 | "dataRootIndex": 48, 174 | "dataRootProof": [ 175 | "0x0395f21560a9ccc1f2aa972601250256fbdb20fd936e1723397ff8d5e4f07b5d", 176 | "0x1e91eb5ce2802373a583ce83898e8b4c1bb648e3c76bad87820a197b73b6d23b", 177 | "0xd49b33b5754aa6c9549e9677e4c646bd4e7d500a2ab9761cffff5363f4608ac7", 178 | "0x575858cb3bb948af2d8c4582310f951eb798281f71e913e044c6c415031f58a3", 179 | "0x353fe475ab9b0e00c3bfae8598fef61ac2921a7928b21ad45b6594c023611156", 180 | "0x4cb574d05c6606d2509ec6849e0cb53d04c5eead1cdbed4704018da938df5460", 181 | "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", 182 | "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c" 183 | ], 184 | "leaf": "0xe17de7631392427460102691ba8a22adf5fb410548e50d6c636bf1f96840c3c3", 185 | "leafIndex": 0, 186 | "leafProof": [ 187 | "0x00017cadd87ec12039f98d646afaa33ed843056ad12f5e971cc81be15d00c26f", 188 | "0xd046caabde74922f9d69e9fd33de6d3b9ee0f5c536183c4f4259f078afda538a" 189 | ], 190 | "message": { 191 | "destinationDomain": 2, 192 | "from": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", 193 | "id": 256491151949829, 194 | "message": { 195 | "fungibleToken": { 196 | "amount": 5000000000000000, 197 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 198 | } 199 | }, 200 | "originDomain": 1, 201 | "to": "0x0000000000000000000000000000000000000000000000000000000000000000" 202 | }, 203 | "rangeHash": "0x21c402a3ccf8df26cb720c6d2fb409f04c809adef7a9a852e463cca83588f4fb" 204 | } 205 | ``` 206 | 207 | ### Generate Merkle Proof 208 | 209 | * To generate a proof, simply query the `v1/proof` endpoint with the mandatory query params block hash, extrinsic index and chain id: 210 | 211 | * Request 212 | 213 | `GET /v1/proof/?block_hash=&index=` 214 | 215 | ```bash 216 | # curl "/v1/proof/?block_hash=&index=" 217 | curl "http://localhost:8080/v1/proof/11155111?block_hash=0x5bc7bd3a4793132007d6d0d9c55dc2ded2fe721a49bd771c1d290e6a3c6ec237&index=5" 218 | ``` 219 | 220 | * Response 221 | 222 | ```json 223 | { 224 | "blobRoot": "0x511030804f9768c9d5c4826cdc7eba25ba0fd8e73ea32467e5fad547397620f8", 225 | "blockHash": "0x5bc7bd3a4793132007d6d0d9c55dc2ded2fe721a49bd771c1d290e6a3c6ec237", 226 | "bridgeRoot": "0xf6c807bc73a637957a61d620bd5e4ef8c7dd234e5fc96dfb6d6041bbe2947782", 227 | "dataRoot": "0x2179e18ee112b080794b40f2239d77041c715ad7392d9fce054b7c10eacd4ebc", 228 | "dataRootCommitment": "0x41cfe14b2e229cc5b4ee0cb7c3c909e1f78ae9e32f986e7496bfd4e007e06519", 229 | "dataRootIndex": 48, 230 | "dataRootProof": [ 231 | "0x0395f21560a9ccc1f2aa972601250256fbdb20fd936e1723397ff8d5e4f07b5d", 232 | "0x1e91eb5ce2802373a583ce83898e8b4c1bb648e3c76bad87820a197b73b6d23b", 233 | "0xd49b33b5754aa6c9549e9677e4c646bd4e7d500a2ab9761cffff5363f4608ac7", 234 | "0x575858cb3bb948af2d8c4582310f951eb798281f71e913e044c6c415031f58a3", 235 | "0x353fe475ab9b0e00c3bfae8598fef61ac2921a7928b21ad45b6594c023611156", 236 | "0x4cb574d05c6606d2509ec6849e0cb53d04c5eead1cdbed4704018da938df5460", 237 | "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", 238 | "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c" 239 | ], 240 | "leaf": "0xe17de7631392427460102691ba8a22adf5fb410548e50d6c636bf1f96840c3c3", 241 | "leafIndex": 0, 242 | "leafProof": [ 243 | "0x00017cadd87ec12039f98d646afaa33ed843056ad12f5e971cc81be15d00c26f", 244 | "0xd046caabde74922f9d69e9fd33de6d3b9ee0f5c536183c4f4259f078afda538a" 245 | ], 246 | "message": { 247 | "destinationDomain": 2, 248 | "from": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", 249 | "id": 256491151949829, 250 | "message": { 251 | "fungibleToken": { 252 | "amount": 5000000000000000, 253 | "asset_id": "0x0000000000000000000000000000000000000000000000000000000000000000" 254 | } 255 | }, 256 | "originDomain": 1, 257 | "to": "0x0000000000000000000000000000000000000000000000000000000000000000" 258 | }, 259 | "rangeHash": "0x21c402a3ccf8df26cb720c6d2fb409f04c809adef7a9a852e463cca83588f4fb" 260 | } 261 | ``` 262 | 263 | ### Get Account/Storage proofs 264 | 265 | * To get a proof, simply query the `/v1/avl/proof/:message_id` endpoint with the message id: 266 | 267 | * Request 268 | 269 | `GET /v1/avl/proof/:block_hash/:messageId` 270 | 271 | ```bash 272 | # curl "/v1/avl/proof//" 273 | curl "http://localhost:8080/v1/avl/proof/0x7963d8403d137cb5560e2436df07c233d18030b5f3f0c61b85083e2a8f2b5e55/1" 274 | ``` 275 | 276 | * Response 277 | 278 | ```json 279 | { 280 | "accountProof": [ 281 | "0xf90211a04ea3386c3564d92c70c842f4fe40a382ab0c0915bd52f1cfdf515e7df40f6365a05fd188dd610941144f5367487b343528f30b6fe1713e14c489a925d31b76de8ea081a4596748d2119583d96af1f6459bfc4d4cf48e5cf9f4171bb0f15b54bfc705a0934bf30f9e4643c1e8cabd31b65867d5dce6702ff922d5d5f88edd77c16eddf8a020c236a887760702595f069baa5c20a5d7f7ff56e99e4f2291e6c383ec2f1376a0a4c5d6569cf6acb8b7b744bdb847ee84c7a250a05858f21c4b71e892e0f6368ca0431a25d2f4d04d92b7f663f91b65029bd2a443fdcd71d6a5d1cd2bad5f937da5a09d5da24c3c97ebd7ed4897206e30464414bdaf91fb633ef6e5aa61ec4573b829a0e1850f0a51e6f8fb4e7b7d0a592a478e4fd4f55132faad2e05c774f2c5bf3722a0e437d19bee3cdd31e6a54da59dd1730d20ad59b1e816ba304b0f8cf89fa62697a07c9058190b5603de7af44f45d2654832a5e5186db18b28514101f5df81287a27a08637a03272d24e7d380f3d68a38525d0348cce5fe90ae32f2b51149badd96f75a0d45f94f513a8918bafb074ac20f951e6ddd59cfa414550ea7af155ea7530e386a03dc457251a135a4002731a17ee72e722897a64509dd108eeb729bb1d9a84bf3ca0e1834ab71a27a983540fee14db56a40c26e3936db6b76d950a117acd5d2a03fea053388d24ffe072f62c6ed5dc8d8e286e167648a5bed4f9a1445df2b2581a589880", 282 | "0xf90211a05f1dfe6f285811ddc2b5c1b2e0b4c9715586274627a60885abf66fe60f58f39ea098709a8cac54a765fa8b09b12849171bc2aadece08d4d21e2631efb92e422a72a09456842fb2f41eeaad70f98549983ec28ac5cc97b2bacd325a341bf8fecbd3fda012f6579824d8706f219fab4e6209270b3c5a7f9b9b10434422173bee004ebf87a0cad660f5066719c1e57fa895b385aedb678d89843f93fa9d284137cce585d912a08aee33972b41bd2c81bb805877f91b5c720c518551071c8c7b7b5035fc855dcaa0fe78af4efca09613a5b166012ad59ce6450d8f612d872010b8cb2cfce5171e01a0ed34274b556d95bc83b368a313d81e2418c84435e094626f568294de2c6adda3a0c4a8516496de7bd739485f7f0ea764e5f1e64f18ea24d5c2662df9321ad476b0a0c3ac05b241ad4ce2e59c05d280a56cdcaa38b8d974d0086a517ea0e71f47dbbba0c786b94bc71965e950e2cddbc6dc3d20b9527f14ac6720dffd93531f58cbb2caa0db4936424a52afb5f811937e21c36dbfc115f36167f57ea0f0770ba5b2b01509a08bfc560bf436ebfd1b9b2a16b38c37fdef7b27b5c5facf3d8487034100e04da3a0d19c58734f31adc98c08131a3f441d4945c63a569d381265517fb270d149fcd9a0a49a515f1d226b9a55079dd09128e8d068ce4473fc2afc7ce83ca73b4a4aeca8a039f4a70c5da7a6f9a75ccfbfa4e4660f367d83ed133633a09d8d85f3b81619b080", 283 | "0xf90211a013013c8d88c90029dae1eb17e9fde9d31aa12a845f977d5b05edfbb836490ed1a08313217b9700aa20e3f2bca060da9c6986794e32e39ee8378c5fc090ea3900c4a0bb6f74f7517cc6d53e1ac28cda4b7492460eac8ad25dae2e6f5dd83054975f5aa06591f5961ce401397d1b7ca8ace8c514f0711029a4804afc5dff746535f4b310a0acb9ff577f6e255b1739bdca46b3fca567678334cef7a7c3459110d398117ca0a0ca52f676d720f2148ea17a8949d8456043eca88e500c0455de24bd5f97c4574ba043f8dbbd691453a0601664d5a35b89b7980c679924f93427d446ccb4b8f54bfba0429ee0cf4db61c968b343ade1502260068beaa87dc31e673c3abeaad60dbe7a5a03e27446763b24c289f918b1710dd069019fb95cd822997fb6fbc1db75ac8b0eca0fdf20b9898a99a2378bb653195060908595370eb56d905306ca1d1260d5c4120a0c63d1fb8a5be22724ea48117eab3b87f9a2ff76c6dec7f3568ddc16098f31b68a0e527b82ba67b4d9299c0e11f48362894ff615bf39e36ccde2d4e13b636262833a008b21c1e3dc4c5938833ed344842aff4a75297019736e1b66820a959d3092561a02bcb48ab6bf1b0b426008ab0919010b847473a3710806632bed77f05d10b2f5ca09b61c79db573dcc92d61a101051e2cffc43389cbffb39cba671fd5a706c26cd1a030051e73e7a062b2812459e8cd4c2db94c408f2dc706fc16ed632cda8707f3bd80", 284 | "0xf90211a007df9be996660f9f91495c59b998832068088ae9034c5b04639694168b9571e6a0aa1dc52398e74cf48729ec879a6ee91c2e73cc73c076f953dda4dfe580ff05e3a0afc1d0a6d1d8723d5c16d5b25673eb3f4d05ef1466612ee9182781bbe987611ea038064f5d9c62106631b05c606352db759e3b535cdafeafb16638c9515731a7cba06800f0958bb61b89dd4bb02eb3da4bf6c14ad00b4a48a66019b0e6d15cc84ee1a08c149b7166f15e51a524ee32ea212f60d6df6a800e01d4fa22ffede5a39ee9e5a093922185d4bdbe12d5e5c62194739038e89b01d67f476acc8865817ac7ed9008a016d39ae51fad6e0f060d8653d173460ed52a823010a29db6edbc042b21e78fbca0f2c153d9c2c2b50644bc8098925dbb3ed618fa7e82ebb94e40965b2c977b6811a01f01669c56000db9ac524e334dace75e9f082db5c95ed9075caacc99165abc76a04ced0f74f50c438075f4d18fe1a0bbafa324e4a5a0d5fb7865acb35c72533e4ba0c8053b16343a4f4dd43ab76abb1cac2f8e791fdb3fa9121014cf7a42151860d3a06961b3a4f6ca879e0377f734d66f930af3258e1492be0c693d395c84a311a644a0a38ea23af3ce3d1d5b485a5c264f9cffaabe184e362466de6bf6e634062f5548a062843d42cff1c276a0e11cc1c6921151b7e84b8de41a2834c0beac97f6a07de3a0609f8c69b1fc2754e2d08946285d80a5a319647acef900dd97a62d21258a035880", 285 | "0xf90211a0aec1c4685e2c3b64c256d10b3135ef48b013524839f8373dbd0bc0eac8d7ba1fa05ab97dd3b415869a2674d9dd1744abfb45de8dffc2c302b05c1336d40c80cf14a07ad83ef3d645503b9afcf48de32fb677beef8b637aa120e6624acea518eb5b3fa030775e5e8230f409c292e2d4033a7530f83656cb67148acc599d37f782f8ef8ba013f3d308779aeb8958a612b37a07807a1783b0f99e1e5f76303fb7c3c44d622ca06e7b7e9dcfc4c24c702aef326848015731a4e20f0d8a02a401197e99c62ca1d7a0ae60461c17040fd1475fceac5e5f5528d2b972b084b4d5413f8cfb41f1ac7075a029a319fc16340bb5889952869c0184a1fc1d0427aa488dd597909671c9b41b6ca03abb30bb5daf1a503a65f2e7979f61ed288f163937406758e2a44ed7a751d216a04e9cfefe88b368b66e1da951909d6e6ce639845c19690778f6e316c9dc902d52a0c7eb14d0cf6465626d7834f200810b009aeb7b13f384c096ebe3ec503b75f4faa0ad3c0d202e74be0f188316e4e466ae7fca89b6a20994b22747b2f95febf5a449a0692d63a3756a510f8467f7da09e97da406265b4201fa4fe3577cbb887045cd6ea093b71eb86d53ddf8d13df0e7c446867dbacf096455045a2bd9690c56a443b4c3a0e712c0b14d1130db6db7edd7a93af2fc1a06d2e5793c4e54dcacca98b4cc6997a0bcc85c28e9f209b9b419e66f87bce37fbb42ba8416880935275c377d2fcebcc180", 286 | "0xf90191a0deb916373640a76bb6056aa37e9d548908c92fb5d3ea1fc69a1c99ddeb40eb24a029b4cdce0d4f7eed71cf6f6d21f4d27ef510b3e36c7c67587e84a08b8b288de380a0cd92c24f26cec2802437f2f8e56cdd35e47ed3edc4c806540740f5bf83f1c5ffa0aadf083c80930dee9f09d295708b24f31d2f41f50e1442e32ecbf03eb5b4a707a0f5335f3280d3be255686e95e46a353a1562a8008919a0051a46fb3fcce7c53caa07f9373bba9b4111e6d4ef57fecd16526497cdeaceccdb26ae09c526b834c83c3a0d000e75b89a65aedb850451d369e3ac5bf4d7ff0d71ec5e837417c0eaf074135a0a994cad72a8a641c62979d6f94ea0087a14770876869de76ff0aba4c51b7cfc480a094b7d0ec3e0de0f4135fed0b225c1a5f09fd8660e37fa7ee3d47cdf774fe4940a06e2c53738e77dd7b3c06aada2fcd6035a8435f8edbd40e0e9856439024502a2fa000e7bef9cf8c6301c9d0aba5e61136a898292f1cbde8861987d0999358f5505f80a0c232ad4a9e338ea79c8f86fc6a83952f212b4477a031a3c24d98d5aac8ef1fac8080", 287 | "0xf87180808080a038eace52a35a1cb3ba4dfc5a7dd4fd884d999c017dd48546779e9b1ceee867f3a07564257a73fdfa4e290ad21fe914294174996ac87095b5730370371f5ea133e980808080a0d51dbe737bf6d8c5b89bcf7724074067d2a4986c4180a5016a8bebfbc92f56e1808080808080", 288 | "0xf8669d38b7b6c4749ee47ec3483ea3325831ccd2fbcbbbcf7cb559ed13d35a14b846f8440280a0927826564770fb917bb1bc72e196fcf2fe2601c838c744106d1691a9da45b795a0fc50d62823735da871a4b45630e8f4a5aef99c18855869762b05d4f7fac4a859" 289 | ], 290 | "storageProof": [ 291 | "0xf90211a02b61c0a3f1012b0c3fe640d90daf8f756f30665e9763d97c2809e683f57418bca0ca985e3b14af8741416d7c735fcc856c510138e8a82edd8506e49d8cac71258ca0bfebf2d2da505708433ec9e6d7d6f5f5706b502435ad98b8429f04f497f1e79ca0481bcb4d57a33e6fdd2d824b3f0bea708c76f437b114d043da26a9b019220552a01e5139fa355fc9ab10b4901bb0a1aee4b97b21441f3a6c2ecccadd2009b6e34fa05806452c1672d430087e3aac3e49901fe6788e8d5e2db17140ed49d3e4a6e262a00bc15c11195738ae6967054b12acc80948f99d852b3a05b2572407c67e98dfaca028fa67ddfd82c094d593d0001f099cff4f5bdad51cf0d77f4e5d15243c23701fa06ddefc9d483203b04420fc19c1de59664c839a726edb7cd6d519f63444293567a0fa38292ed34577ba490dfb1335d9ee2871bf64a498d8af13eb665516bc122fc8a03b6f3c05d6c9151aa3f552d3088d12e80ab70d78ef046ddf4d311dc35fd89699a02bbf18d484d22ce32d8efb9ad5236ca770f34ca46093a062c9fd0c00fce179b1a093e4702951798f5331327b8a6a613307e12edc789dc1ddb21ba4ef185122cedda091b0434eedf92e47d68213d40c2ead10fb94f7ce34845d9b85ddc255a5586f98a0b6a8a018fe45d5a78c237915cdc4db38a151c2078e9fd72ed53481072e816366a0c4a7126e02988de7caae8abdd299cf6fbde0eb2adc9be75ded86e8ea8388dead80", 292 | "0xf90151a0401fa10b5959532f16179be0bd2506e7f849495298c2de8fd5b8c4d63b1f2c1ca01dcc18ed03ed3183fe26600b23afedbebdd947d6b5019cc010ea359e0234afe0a050030a57a0878bb51bbd0c62d3ba0c2b782ebc5d6ec012a7a1bd0f20917558b6a0db69566205faf59c73db049a04d81f63db7d2dbafbe66ed91557e511ed0938d780a0e6114e00cb6c2aa9cf632cb93e5ccad1b4c9af79c769950743a647fe829c602ca0bcb35466c5b4c32c509da47141cb7b9c3dddff0ebcd0ea050bad4d398e2fb25f808080a054876fae9e71b3585d4e83ee48c65b366f6f13a3c1b9e2749918145eabd1c46da004531fc338c340e676badc1a99cad6ece2da3d08945f806cb7063bf3343d35f48080a0c68fcd91600b226b6c847f28d2f6ca3f60c55b1009b26e59f4d1087bb5d6fcc0a06c5aec8aa331b1f04b26a06cb64c5a6d63616d717f236d31106751bbea0d666880", 293 | "0xf871a08eccb5e838d7d0699e06d85c472bb097d8012c44d790e5d15c5b8465c7abb88180a02581c4c4535083ecd9ea1a314216bbe948f27bccb2e997c7796a9eec8f4c3df0a0c453ceda114a9775f135a7a2687f75e753c6f814789528fb73bb8cb5dec7eac680808080808080808080808080", 294 | "0xf8429f31265685397ec9fa17535b5603e86e2b01a583b71373e1b2cbfac2a5bff58fa1a0eb70a047920b4aa1f3a418b52e455694d4e1a2362fd7fbcf16fe53d798311beb" 295 | ] 296 | } 297 | ``` 298 | 299 | ### Get bridge transactions 300 | 301 | * Get bridge transactions: 302 | 303 | * Request 304 | 305 | `GET /v1/transactions?availAddress=0x1a985fdff5f6eee4afce1dc0f367ab925cdca57e7e8585329830fc3ce6ef4e7aðAddress=0x48e7e157cf873c15a5a6734ea37c000e1cb2383d` 306 | 307 | ```bash 308 | # curl /v1/transactions?ethAddress=ðAddress= 309 | curl localhost:8080/v1/transactions?availAddress=0x1a985fdff5f6eee4afce1dc0f367ab925cdca57e7e8585329830fc3ce6ef4e7aðAddress=0x48e7e157cf873c15a5a6734ea37c000e1cb2383d 310 | ``` 311 | 312 | * Response 313 | 314 | ```json 315 | { 316 | "availSend": [ 317 | { 318 | "amount": "10000000000000000", 319 | "depositorAddress": "0x1a985fdff5f6eee4afce1dc0f367ab925cdca57e7e8585329830fc3ce6ef4e7a", 320 | "destinationBlockHash": null, 321 | "destinationBlockNumber": null, 322 | "destinationTimestamp": null, 323 | "messageId": 106695577567233, 324 | "receiverAddress": "0x8d31529525f23b14767d4dde78567ca083d3d56f", 325 | "sourceBlockHash": "0x9cd91b1bd5497d96a98cf20e1563df29d85b8444d906a64a40980629e1288d70", 326 | "sourceBlockNumber": 24842, 327 | "sourceTimestamp": "2024-04-23T08:15:40", 328 | "sourceTransactionHash": "0x9cd91b1bd5497d96a98cf20e1563df29d85b8444d906a64a40980629e1288d70", 329 | "sourceTransactionIndex": 1, 330 | "status": "claimPending", 331 | "tokenId": "0x0000000000000000000000000000000000000000000000000000000000000000" 332 | } 333 | ], 334 | "ethSend": [ 335 | { 336 | "amount": "10000000000000000", 337 | "depositorAddress": "0x48e7e157cf873c15a5a6734ea37c000e1cb2383d", 338 | "destinationBlockHash": "0xb52bdaf649c84fdccdf2e18aac13e18b0a67ceca11e23bfb94e6476cab23eb38", 339 | "destinationBlockNumber": 22650, 340 | "destinationTimestamp": "2024-04-22T20:03:40", 341 | "destinationTransactionIndex": 1, 342 | "messageId": 311, 343 | "receiverAddress": "0x1a985fdff5f6eee4afce1dc0f367ab925cdca57e7e8585329830fc3ce6ef4e7a", 344 | "sourceBlockHash": "0x463b0853f65f7a80c4c8ca188c37eb9589fb42cfe327bc1736ce02245bc045e8", 345 | "sourceBlockNumber": 5755158, 346 | "sourceTimestamp": "2024-04-22T19:40:24", 347 | "sourceTransactionHash": "0xcf7945f68d7544a81eb8c32a794e97e229da3535aebd5488a4404f0a1093f29c", 348 | "status": "bridged", 349 | "tokenId": "0x0000000000000000000000000000000000000000000000000000000000000000" 350 | } 351 | ] 352 | } 353 | ``` 354 | 355 | ### Map slot to Ethereum block number (Deprecated) 356 | 357 | * To map Ethereum slot to a block number: 358 | 359 | * Request 360 | 361 | `GET /beacon/slot/:slot_number` 362 | 363 | ```bash 364 | # curl /beacon/slot/ 365 | curl http://localhost:8080/beacon/slot/4448512 366 | ``` 367 | 368 | * Response 369 | 370 | ```json 371 | { 372 | "blockHash":"0x5282299b298fe1d7238f1a48aa0f5e7cc19ccbcdeeba020b610db78abeb0d52b", 373 | "blockNumber":5380093 374 | } 375 | ``` 376 | 377 | ### Examples of using bridge api 378 | 379 | * We have prepared a set of examples written in Rust and Typescript to help you understand how to use bridge api. You can explore these examples by visiting our [code examples repository](https://github.com/availproject/avail-bridge-examples). 380 | -------------------------------------------------------------------------------- /src/abi/SP1Vector.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "function", 4 | "name": "DEFAULT_ADMIN_ROLE", 5 | "inputs": [], 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "bytes32", 10 | "internalType": "bytes32" 11 | } 12 | ], 13 | "stateMutability": "view" 14 | }, 15 | { 16 | "type": "function", 17 | "name": "GUARDIAN_ROLE", 18 | "inputs": [], 19 | "outputs": [ 20 | { 21 | "name": "", 22 | "type": "bytes32", 23 | "internalType": "bytes32" 24 | } 25 | ], 26 | "stateMutability": "view" 27 | }, 28 | { 29 | "type": "function", 30 | "name": "TIMELOCK_ROLE", 31 | "inputs": [], 32 | "outputs": [ 33 | { 34 | "name": "", 35 | "type": "bytes32", 36 | "internalType": "bytes32" 37 | } 38 | ], 39 | "stateMutability": "view" 40 | }, 41 | { 42 | "type": "function", 43 | "name": "VERSION", 44 | "inputs": [], 45 | "outputs": [ 46 | { 47 | "name": "", 48 | "type": "string", 49 | "internalType": "string" 50 | } 51 | ], 52 | "stateMutability": "pure" 53 | }, 54 | { 55 | "type": "function", 56 | "name": "approvedRelayers", 57 | "inputs": [ 58 | { 59 | "name": "", 60 | "type": "address", 61 | "internalType": "address" 62 | } 63 | ], 64 | "outputs": [ 65 | { 66 | "name": "", 67 | "type": "bool", 68 | "internalType": "bool" 69 | } 70 | ], 71 | "stateMutability": "view" 72 | }, 73 | { 74 | "type": "function", 75 | "name": "authoritySetIdToHash", 76 | "inputs": [ 77 | { 78 | "name": "", 79 | "type": "uint64", 80 | "internalType": "uint64" 81 | } 82 | ], 83 | "outputs": [ 84 | { 85 | "name": "", 86 | "type": "bytes32", 87 | "internalType": "bytes32" 88 | } 89 | ], 90 | "stateMutability": "view" 91 | }, 92 | { 93 | "type": "function", 94 | "name": "blockHeightToHeaderHash", 95 | "inputs": [ 96 | { 97 | "name": "", 98 | "type": "uint32", 99 | "internalType": "uint32" 100 | } 101 | ], 102 | "outputs": [ 103 | { 104 | "name": "", 105 | "type": "bytes32", 106 | "internalType": "bytes32" 107 | } 108 | ], 109 | "stateMutability": "view" 110 | }, 111 | { 112 | "type": "function", 113 | "name": "checkRelayer", 114 | "inputs": [], 115 | "outputs": [ 116 | { 117 | "name": "", 118 | "type": "bool", 119 | "internalType": "bool" 120 | } 121 | ], 122 | "stateMutability": "view" 123 | }, 124 | { 125 | "type": "function", 126 | "name": "commitHeaderRange", 127 | "inputs": [ 128 | { 129 | "name": "proof", 130 | "type": "bytes", 131 | "internalType": "bytes" 132 | }, 133 | { 134 | "name": "publicValues", 135 | "type": "bytes", 136 | "internalType": "bytes" 137 | } 138 | ], 139 | "outputs": [], 140 | "stateMutability": "nonpayable" 141 | }, 142 | { 143 | "type": "function", 144 | "name": "dataRootCommitments", 145 | "inputs": [ 146 | { 147 | "name": "", 148 | "type": "bytes32", 149 | "internalType": "bytes32" 150 | } 151 | ], 152 | "outputs": [ 153 | { 154 | "name": "", 155 | "type": "bytes32", 156 | "internalType": "bytes32" 157 | } 158 | ], 159 | "stateMutability": "view" 160 | }, 161 | { 162 | "type": "function", 163 | "name": "frozen", 164 | "inputs": [], 165 | "outputs": [ 166 | { 167 | "name": "", 168 | "type": "bool", 169 | "internalType": "bool" 170 | } 171 | ], 172 | "stateMutability": "view" 173 | }, 174 | { 175 | "type": "function", 176 | "name": "gateway_deprecated", 177 | "inputs": [], 178 | "outputs": [ 179 | { 180 | "name": "", 181 | "type": "address", 182 | "internalType": "address" 183 | } 184 | ], 185 | "stateMutability": "view" 186 | }, 187 | { 188 | "type": "function", 189 | "name": "getRoleAdmin", 190 | "inputs": [ 191 | { 192 | "name": "role", 193 | "type": "bytes32", 194 | "internalType": "bytes32" 195 | } 196 | ], 197 | "outputs": [ 198 | { 199 | "name": "", 200 | "type": "bytes32", 201 | "internalType": "bytes32" 202 | } 203 | ], 204 | "stateMutability": "view" 205 | }, 206 | { 207 | "type": "function", 208 | "name": "grantRole", 209 | "inputs": [ 210 | { 211 | "name": "role", 212 | "type": "bytes32", 213 | "internalType": "bytes32" 214 | }, 215 | { 216 | "name": "account", 217 | "type": "address", 218 | "internalType": "address" 219 | } 220 | ], 221 | "outputs": [], 222 | "stateMutability": "nonpayable" 223 | }, 224 | { 225 | "type": "function", 226 | "name": "hasRole", 227 | "inputs": [ 228 | { 229 | "name": "role", 230 | "type": "bytes32", 231 | "internalType": "bytes32" 232 | }, 233 | { 234 | "name": "account", 235 | "type": "address", 236 | "internalType": "address" 237 | } 238 | ], 239 | "outputs": [ 240 | { 241 | "name": "", 242 | "type": "bool", 243 | "internalType": "bool" 244 | } 245 | ], 246 | "stateMutability": "view" 247 | }, 248 | { 249 | "type": "function", 250 | "name": "headerRangeCommitmentTreeSize", 251 | "inputs": [], 252 | "outputs": [ 253 | { 254 | "name": "", 255 | "type": "uint32", 256 | "internalType": "uint32" 257 | } 258 | ], 259 | "stateMutability": "view" 260 | }, 261 | { 262 | "type": "function", 263 | "name": "headerRangeFunctionId_deprecated", 264 | "inputs": [], 265 | "outputs": [ 266 | { 267 | "name": "", 268 | "type": "bytes32", 269 | "internalType": "bytes32" 270 | } 271 | ], 272 | "stateMutability": "view" 273 | }, 274 | { 275 | "type": "function", 276 | "name": "initialize", 277 | "inputs": [ 278 | { 279 | "name": "_params", 280 | "type": "tuple", 281 | "internalType": "struct SP1Vector.InitParameters", 282 | "components": [ 283 | { 284 | "name": "guardian", 285 | "type": "address", 286 | "internalType": "address" 287 | }, 288 | { 289 | "name": "height", 290 | "type": "uint32", 291 | "internalType": "uint32" 292 | }, 293 | { 294 | "name": "header", 295 | "type": "bytes32", 296 | "internalType": "bytes32" 297 | }, 298 | { 299 | "name": "authoritySetId", 300 | "type": "uint64", 301 | "internalType": "uint64" 302 | }, 303 | { 304 | "name": "authoritySetHash", 305 | "type": "bytes32", 306 | "internalType": "bytes32" 307 | }, 308 | { 309 | "name": "headerRangeCommitmentTreeSize", 310 | "type": "uint32", 311 | "internalType": "uint32" 312 | }, 313 | { 314 | "name": "vectorProgramVkey", 315 | "type": "bytes32", 316 | "internalType": "bytes32" 317 | }, 318 | { 319 | "name": "verifier", 320 | "type": "address", 321 | "internalType": "address" 322 | } 323 | ] 324 | } 325 | ], 326 | "outputs": [], 327 | "stateMutability": "nonpayable" 328 | }, 329 | { 330 | "type": "function", 331 | "name": "latestAuthoritySetId", 332 | "inputs": [], 333 | "outputs": [ 334 | { 335 | "name": "", 336 | "type": "uint64", 337 | "internalType": "uint64" 338 | } 339 | ], 340 | "stateMutability": "view" 341 | }, 342 | { 343 | "type": "function", 344 | "name": "latestBlock", 345 | "inputs": [], 346 | "outputs": [ 347 | { 348 | "name": "", 349 | "type": "uint32", 350 | "internalType": "uint32" 351 | } 352 | ], 353 | "stateMutability": "view" 354 | }, 355 | { 356 | "type": "function", 357 | "name": "proxiableUUID", 358 | "inputs": [], 359 | "outputs": [ 360 | { 361 | "name": "", 362 | "type": "bytes32", 363 | "internalType": "bytes32" 364 | } 365 | ], 366 | "stateMutability": "view" 367 | }, 368 | { 369 | "type": "function", 370 | "name": "rangeStartBlocks", 371 | "inputs": [ 372 | { 373 | "name": "", 374 | "type": "bytes32", 375 | "internalType": "bytes32" 376 | } 377 | ], 378 | "outputs": [ 379 | { 380 | "name": "", 381 | "type": "uint32", 382 | "internalType": "uint32" 383 | } 384 | ], 385 | "stateMutability": "view" 386 | }, 387 | { 388 | "type": "function", 389 | "name": "renounceRole", 390 | "inputs": [ 391 | { 392 | "name": "role", 393 | "type": "bytes32", 394 | "internalType": "bytes32" 395 | }, 396 | { 397 | "name": "account", 398 | "type": "address", 399 | "internalType": "address" 400 | } 401 | ], 402 | "outputs": [], 403 | "stateMutability": "nonpayable" 404 | }, 405 | { 406 | "type": "function", 407 | "name": "revokeRole", 408 | "inputs": [ 409 | { 410 | "name": "role", 411 | "type": "bytes32", 412 | "internalType": "bytes32" 413 | }, 414 | { 415 | "name": "account", 416 | "type": "address", 417 | "internalType": "address" 418 | } 419 | ], 420 | "outputs": [], 421 | "stateMutability": "nonpayable" 422 | }, 423 | { 424 | "type": "function", 425 | "name": "rotate", 426 | "inputs": [ 427 | { 428 | "name": "proof", 429 | "type": "bytes", 430 | "internalType": "bytes" 431 | }, 432 | { 433 | "name": "publicValues", 434 | "type": "bytes", 435 | "internalType": "bytes" 436 | } 437 | ], 438 | "outputs": [], 439 | "stateMutability": "nonpayable" 440 | }, 441 | { 442 | "type": "function", 443 | "name": "rotateFunctionId_deprecated", 444 | "inputs": [], 445 | "outputs": [ 446 | { 447 | "name": "", 448 | "type": "bytes32", 449 | "internalType": "bytes32" 450 | } 451 | ], 452 | "stateMutability": "view" 453 | }, 454 | { 455 | "type": "function", 456 | "name": "setCheckRelayer", 457 | "inputs": [ 458 | { 459 | "name": "_checkRelayer", 460 | "type": "bool", 461 | "internalType": "bool" 462 | } 463 | ], 464 | "outputs": [], 465 | "stateMutability": "nonpayable" 466 | }, 467 | { 468 | "type": "function", 469 | "name": "setRelayerApproval", 470 | "inputs": [ 471 | { 472 | "name": "_relayer", 473 | "type": "address", 474 | "internalType": "address" 475 | }, 476 | { 477 | "name": "_approved", 478 | "type": "bool", 479 | "internalType": "bool" 480 | } 481 | ], 482 | "outputs": [], 483 | "stateMutability": "nonpayable" 484 | }, 485 | { 486 | "type": "function", 487 | "name": "stateRootCommitments", 488 | "inputs": [ 489 | { 490 | "name": "", 491 | "type": "bytes32", 492 | "internalType": "bytes32" 493 | } 494 | ], 495 | "outputs": [ 496 | { 497 | "name": "", 498 | "type": "bytes32", 499 | "internalType": "bytes32" 500 | } 501 | ], 502 | "stateMutability": "view" 503 | }, 504 | { 505 | "type": "function", 506 | "name": "supportsInterface", 507 | "inputs": [ 508 | { 509 | "name": "interfaceId", 510 | "type": "bytes4", 511 | "internalType": "bytes4" 512 | } 513 | ], 514 | "outputs": [ 515 | { 516 | "name": "", 517 | "type": "bool", 518 | "internalType": "bool" 519 | } 520 | ], 521 | "stateMutability": "view" 522 | }, 523 | { 524 | "type": "function", 525 | "name": "updateBlockRangeData", 526 | "inputs": [ 527 | { 528 | "name": "_startBlocks", 529 | "type": "uint32[]", 530 | "internalType": "uint32[]" 531 | }, 532 | { 533 | "name": "_endBlocks", 534 | "type": "uint32[]", 535 | "internalType": "uint32[]" 536 | }, 537 | { 538 | "name": "_headerHashes", 539 | "type": "bytes32[]", 540 | "internalType": "bytes32[]" 541 | }, 542 | { 543 | "name": "_dataRootCommitments", 544 | "type": "bytes32[]", 545 | "internalType": "bytes32[]" 546 | }, 547 | { 548 | "name": "_stateRootCommitments", 549 | "type": "bytes32[]", 550 | "internalType": "bytes32[]" 551 | }, 552 | { 553 | "name": "_endAuthoritySetId", 554 | "type": "uint64", 555 | "internalType": "uint64" 556 | }, 557 | { 558 | "name": "_endAuthoritySetHash", 559 | "type": "bytes32", 560 | "internalType": "bytes32" 561 | } 562 | ], 563 | "outputs": [], 564 | "stateMutability": "nonpayable" 565 | }, 566 | { 567 | "type": "function", 568 | "name": "updateCommitmentTreeSize", 569 | "inputs": [ 570 | { 571 | "name": "_headerRangeCommitmentTreeSize", 572 | "type": "uint32", 573 | "internalType": "uint32" 574 | } 575 | ], 576 | "outputs": [], 577 | "stateMutability": "nonpayable" 578 | }, 579 | { 580 | "type": "function", 581 | "name": "updateFreeze", 582 | "inputs": [ 583 | { 584 | "name": "_freeze", 585 | "type": "bool", 586 | "internalType": "bool" 587 | } 588 | ], 589 | "outputs": [], 590 | "stateMutability": "nonpayable" 591 | }, 592 | { 593 | "type": "function", 594 | "name": "updateGenesisState", 595 | "inputs": [ 596 | { 597 | "name": "_height", 598 | "type": "uint32", 599 | "internalType": "uint32" 600 | }, 601 | { 602 | "name": "_header", 603 | "type": "bytes32", 604 | "internalType": "bytes32" 605 | }, 606 | { 607 | "name": "_authoritySetId", 608 | "type": "uint64", 609 | "internalType": "uint64" 610 | }, 611 | { 612 | "name": "_authoritySetHash", 613 | "type": "bytes32", 614 | "internalType": "bytes32" 615 | } 616 | ], 617 | "outputs": [], 618 | "stateMutability": "nonpayable" 619 | }, 620 | { 621 | "type": "function", 622 | "name": "updateVectorXProgramVkey", 623 | "inputs": [ 624 | { 625 | "name": "_vectorXProgramVkey", 626 | "type": "bytes32", 627 | "internalType": "bytes32" 628 | } 629 | ], 630 | "outputs": [], 631 | "stateMutability": "nonpayable" 632 | }, 633 | { 634 | "type": "function", 635 | "name": "updateVerifier", 636 | "inputs": [ 637 | { 638 | "name": "_verifier", 639 | "type": "address", 640 | "internalType": "address" 641 | } 642 | ], 643 | "outputs": [], 644 | "stateMutability": "nonpayable" 645 | }, 646 | { 647 | "type": "function", 648 | "name": "upgradeTo", 649 | "inputs": [ 650 | { 651 | "name": "newImplementation", 652 | "type": "address", 653 | "internalType": "address" 654 | } 655 | ], 656 | "outputs": [], 657 | "stateMutability": "nonpayable" 658 | }, 659 | { 660 | "type": "function", 661 | "name": "upgradeToAndCall", 662 | "inputs": [ 663 | { 664 | "name": "newImplementation", 665 | "type": "address", 666 | "internalType": "address" 667 | }, 668 | { 669 | "name": "data", 670 | "type": "bytes", 671 | "internalType": "bytes" 672 | } 673 | ], 674 | "outputs": [], 675 | "stateMutability": "payable" 676 | }, 677 | { 678 | "type": "function", 679 | "name": "vectorXProgramVkey", 680 | "inputs": [], 681 | "outputs": [ 682 | { 683 | "name": "", 684 | "type": "bytes32", 685 | "internalType": "bytes32" 686 | } 687 | ], 688 | "stateMutability": "view" 689 | }, 690 | { 691 | "type": "function", 692 | "name": "verifier", 693 | "inputs": [], 694 | "outputs": [ 695 | { 696 | "name": "", 697 | "type": "address", 698 | "internalType": "contract ISP1Verifier" 699 | } 700 | ], 701 | "stateMutability": "view" 702 | }, 703 | { 704 | "type": "event", 705 | "name": "AdminChanged", 706 | "inputs": [ 707 | { 708 | "name": "previousAdmin", 709 | "type": "address", 710 | "indexed": false, 711 | "internalType": "address" 712 | }, 713 | { 714 | "name": "newAdmin", 715 | "type": "address", 716 | "indexed": false, 717 | "internalType": "address" 718 | } 719 | ], 720 | "anonymous": false 721 | }, 722 | { 723 | "type": "event", 724 | "name": "AuthoritySetStored", 725 | "inputs": [ 726 | { 727 | "name": "authoritySetId", 728 | "type": "uint64", 729 | "indexed": false, 730 | "internalType": "uint64" 731 | }, 732 | { 733 | "name": "authoritySetHash", 734 | "type": "bytes32", 735 | "indexed": false, 736 | "internalType": "bytes32" 737 | } 738 | ], 739 | "anonymous": false 740 | }, 741 | { 742 | "type": "event", 743 | "name": "BeaconUpgraded", 744 | "inputs": [ 745 | { 746 | "name": "beacon", 747 | "type": "address", 748 | "indexed": true, 749 | "internalType": "address" 750 | } 751 | ], 752 | "anonymous": false 753 | }, 754 | { 755 | "type": "event", 756 | "name": "HeadUpdate", 757 | "inputs": [ 758 | { 759 | "name": "blockNumber", 760 | "type": "uint32", 761 | "indexed": false, 762 | "internalType": "uint32" 763 | }, 764 | { 765 | "name": "headerHash", 766 | "type": "bytes32", 767 | "indexed": false, 768 | "internalType": "bytes32" 769 | } 770 | ], 771 | "anonymous": false 772 | }, 773 | { 774 | "type": "event", 775 | "name": "HeaderRangeCommitmentStored", 776 | "inputs": [ 777 | { 778 | "name": "startBlock", 779 | "type": "uint32", 780 | "indexed": false, 781 | "internalType": "uint32" 782 | }, 783 | { 784 | "name": "endBlock", 785 | "type": "uint32", 786 | "indexed": false, 787 | "internalType": "uint32" 788 | }, 789 | { 790 | "name": "dataCommitment", 791 | "type": "bytes32", 792 | "indexed": false, 793 | "internalType": "bytes32" 794 | }, 795 | { 796 | "name": "stateCommitment", 797 | "type": "bytes32", 798 | "indexed": false, 799 | "internalType": "bytes32" 800 | }, 801 | { 802 | "name": "headerRangeCommitmentTreeSize", 803 | "type": "uint32", 804 | "indexed": false, 805 | "internalType": "uint32" 806 | } 807 | ], 808 | "anonymous": false 809 | }, 810 | { 811 | "type": "event", 812 | "name": "HeaderRangeRequested", 813 | "inputs": [ 814 | { 815 | "name": "trustedBlock", 816 | "type": "uint32", 817 | "indexed": false, 818 | "internalType": "uint32" 819 | }, 820 | { 821 | "name": "trustedHeader", 822 | "type": "bytes32", 823 | "indexed": false, 824 | "internalType": "bytes32" 825 | }, 826 | { 827 | "name": "authoritySetId", 828 | "type": "uint64", 829 | "indexed": false, 830 | "internalType": "uint64" 831 | }, 832 | { 833 | "name": "authoritySetHash", 834 | "type": "bytes32", 835 | "indexed": false, 836 | "internalType": "bytes32" 837 | }, 838 | { 839 | "name": "targetBlock", 840 | "type": "uint32", 841 | "indexed": false, 842 | "internalType": "uint32" 843 | } 844 | ], 845 | "anonymous": false 846 | }, 847 | { 848 | "type": "event", 849 | "name": "Initialized", 850 | "inputs": [ 851 | { 852 | "name": "version", 853 | "type": "uint8", 854 | "indexed": false, 855 | "internalType": "uint8" 856 | } 857 | ], 858 | "anonymous": false 859 | }, 860 | { 861 | "type": "event", 862 | "name": "RoleAdminChanged", 863 | "inputs": [ 864 | { 865 | "name": "role", 866 | "type": "bytes32", 867 | "indexed": true, 868 | "internalType": "bytes32" 869 | }, 870 | { 871 | "name": "previousAdminRole", 872 | "type": "bytes32", 873 | "indexed": true, 874 | "internalType": "bytes32" 875 | }, 876 | { 877 | "name": "newAdminRole", 878 | "type": "bytes32", 879 | "indexed": true, 880 | "internalType": "bytes32" 881 | } 882 | ], 883 | "anonymous": false 884 | }, 885 | { 886 | "type": "event", 887 | "name": "RoleGranted", 888 | "inputs": [ 889 | { 890 | "name": "role", 891 | "type": "bytes32", 892 | "indexed": true, 893 | "internalType": "bytes32" 894 | }, 895 | { 896 | "name": "account", 897 | "type": "address", 898 | "indexed": true, 899 | "internalType": "address" 900 | }, 901 | { 902 | "name": "sender", 903 | "type": "address", 904 | "indexed": true, 905 | "internalType": "address" 906 | } 907 | ], 908 | "anonymous": false 909 | }, 910 | { 911 | "type": "event", 912 | "name": "RoleRevoked", 913 | "inputs": [ 914 | { 915 | "name": "role", 916 | "type": "bytes32", 917 | "indexed": true, 918 | "internalType": "bytes32" 919 | }, 920 | { 921 | "name": "account", 922 | "type": "address", 923 | "indexed": true, 924 | "internalType": "address" 925 | }, 926 | { 927 | "name": "sender", 928 | "type": "address", 929 | "indexed": true, 930 | "internalType": "address" 931 | } 932 | ], 933 | "anonymous": false 934 | }, 935 | { 936 | "type": "event", 937 | "name": "RotateRequested", 938 | "inputs": [ 939 | { 940 | "name": "currentAuthoritySetId", 941 | "type": "uint64", 942 | "indexed": false, 943 | "internalType": "uint64" 944 | }, 945 | { 946 | "name": "currentAuthoritySetHash", 947 | "type": "bytes32", 948 | "indexed": false, 949 | "internalType": "bytes32" 950 | } 951 | ], 952 | "anonymous": false 953 | }, 954 | { 955 | "type": "event", 956 | "name": "Upgraded", 957 | "inputs": [ 958 | { 959 | "name": "implementation", 960 | "type": "address", 961 | "indexed": true, 962 | "internalType": "address" 963 | } 964 | ], 965 | "anonymous": false 966 | }, 967 | { 968 | "type": "error", 969 | "name": "AuthoritySetMismatch", 970 | "inputs": [] 971 | }, 972 | { 973 | "type": "error", 974 | "name": "AuthoritySetNotFound", 975 | "inputs": [] 976 | }, 977 | { 978 | "type": "error", 979 | "name": "BlockHeightMismatch", 980 | "inputs": [] 981 | }, 982 | { 983 | "type": "error", 984 | "name": "ContractFrozen", 985 | "inputs": [] 986 | }, 987 | { 988 | "type": "error", 989 | "name": "InvalidMerkleTreeSize", 990 | "inputs": [] 991 | }, 992 | { 993 | "type": "error", 994 | "name": "InvalidProofType", 995 | "inputs": [] 996 | }, 997 | { 998 | "type": "error", 999 | "name": "InvalidTargetBlock", 1000 | "inputs": [] 1001 | }, 1002 | { 1003 | "type": "error", 1004 | "name": "NextAuthoritySetExists", 1005 | "inputs": [] 1006 | }, 1007 | { 1008 | "type": "error", 1009 | "name": "OldAuthoritySetId", 1010 | "inputs": [] 1011 | }, 1012 | { 1013 | "type": "error", 1014 | "name": "OnlyGuardian", 1015 | "inputs": [ 1016 | { 1017 | "name": "sender", 1018 | "type": "address", 1019 | "internalType": "address" 1020 | } 1021 | ] 1022 | }, 1023 | { 1024 | "type": "error", 1025 | "name": "OnlyTimelock", 1026 | "inputs": [ 1027 | { 1028 | "name": "sender", 1029 | "type": "address", 1030 | "internalType": "address" 1031 | } 1032 | ] 1033 | }, 1034 | { 1035 | "type": "error", 1036 | "name": "RelayerNotApproved", 1037 | "inputs": [] 1038 | }, 1039 | { 1040 | "type": "error", 1041 | "name": "TrustedHeaderMismatch", 1042 | "inputs": [] 1043 | }, 1044 | { 1045 | "type": "error", 1046 | "name": "TrustedHeaderNotFound", 1047 | "inputs": [] 1048 | } 1049 | ] 1050 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod models; 2 | mod schema; 3 | use crate::models::{AvailSend, EthereumSend, StatusEnum}; 4 | use crate::schema::avail_sends::dsl::avail_sends; 5 | use crate::schema::ethereum_sends::dsl::ethereum_sends; 6 | use alloy::primitives::{Address, B256, U256, hex}; 7 | use alloy::providers::ProviderBuilder; 8 | use alloy::sol; 9 | use anyhow::{Context, Result}; 10 | use avail_core::data_proof::AddressedMessage; 11 | use axum::body::{Body, to_bytes}; 12 | use axum::response::Response; 13 | use axum::{ 14 | Router, 15 | extract::{Json, Path, Query, State}, 16 | http::StatusCode, 17 | response::IntoResponse, 18 | routing::get, 19 | }; 20 | use backon::ExponentialBuilder; 21 | use backon::Retryable; 22 | use chrono::{NaiveDateTime, Utc}; 23 | use diesel::{ 24 | ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, SelectableHelper, r2d2, 25 | r2d2::ConnectionManager, 26 | }; 27 | use http::Method; 28 | use jsonrpsee::{ 29 | core::ClientError, 30 | core::client::ClientT, 31 | http_client::{HttpClient, HttpClientBuilder}, 32 | rpc_params, 33 | }; 34 | use lazy_static::lazy_static; 35 | use reqwest::Client; 36 | use serde::{Deserialize, Deserializer, Serialize}; 37 | use serde_json::{Value, json}; 38 | use serde_with::serde_as; 39 | use sha3::{Digest, Keccak256}; 40 | use sp_core::{Decode, H160, H256}; 41 | use sp_io::hashing::twox_128; 42 | use std::collections::HashMap; 43 | use std::sync::Arc; 44 | use std::{env, process, time::Duration}; 45 | #[cfg(not(target_env = "msvc"))] 46 | use tikv_jemallocator::Jemalloc; 47 | use tokio::task::JoinHandle; 48 | use tokio::{join, sync::RwLock}; 49 | use tower_http::{ 50 | compression::CompressionLayer, 51 | cors::{Any, CorsLayer}, 52 | trace::TraceLayer, 53 | }; 54 | use tracing_subscriber::prelude::*; 55 | 56 | #[cfg(not(target_env = "msvc"))] 57 | #[global_allocator] 58 | static GLOBAL: Jemalloc = Jemalloc; 59 | 60 | sol!( 61 | #[allow(missing_docs)] 62 | #[sol(rpc)] 63 | SP1Vector, 64 | "src/abi/SP1Vector.json" 65 | ); 66 | 67 | #[derive(Debug, Deserialize)] 68 | struct Root { 69 | data: Data, 70 | } 71 | 72 | #[derive(Debug, Deserialize)] 73 | struct Data { 74 | message: Message, 75 | } 76 | 77 | #[derive(Debug, Deserialize)] 78 | struct Message { 79 | slot: String, 80 | body: MessageBody, 81 | } 82 | 83 | #[derive(Debug, Deserialize)] 84 | struct MessageBody { 85 | execution_payload: ExecutionPayload, 86 | } 87 | 88 | #[derive(Debug, Deserialize)] 89 | struct ExecutionPayload { 90 | block_number: String, 91 | block_hash: String, 92 | } 93 | 94 | #[derive(Debug, serde::Serialize)] 95 | struct BlockSummary { 96 | slot: String, 97 | block_number: String, 98 | block_hash: String, 99 | } 100 | 101 | #[derive(Debug)] 102 | struct AppState { 103 | avail_client: HttpClient, 104 | ethereum_client: HttpClient, 105 | request_client: Client, 106 | succinct_base_url: String, 107 | beaconchain_base_url: String, 108 | avail_chain_name: String, 109 | contract_chain_id: String, 110 | contract_address: String, 111 | bridge_contract_address: String, 112 | eth_head_cache_maxage: u16, 113 | avl_head_cache_maxage: u16, 114 | head_cache_maxage: u16, 115 | avl_proof_cache_maxage: u32, 116 | eth_proof_cache_maxage: u32, 117 | proof_cache_maxage: u32, 118 | eth_proof_failure_cache_maxage: u32, 119 | slot_mapping_cache_maxage: u32, 120 | transactions_cache_maxage: u32, 121 | connection_pool: r2d2::Pool>, 122 | chains: HashMap, 123 | } 124 | 125 | #[derive(Debug)] 126 | struct Chain { 127 | rpc_url: String, 128 | contract_address: Address, 129 | } 130 | 131 | #[derive(Deserialize)] 132 | struct IndexStruct { 133 | index: u32, 134 | } 135 | 136 | #[derive(Deserialize)] 137 | struct ProofQueryStruct { 138 | index: u32, 139 | block_hash: B256, 140 | } 141 | 142 | #[derive(Deserialize, Serialize)] 143 | #[serde(rename_all = "camelCase")] 144 | struct KateQueryDataProofResponse { 145 | data_proof: DataProof, 146 | #[serde(skip_serializing_if = "Option::is_none")] 147 | message: Option, 148 | } 149 | 150 | #[derive(Deserialize, Serialize)] 151 | #[serde(rename_all = "camelCase")] 152 | struct DataProof { 153 | roots: Roots, 154 | proof: Vec, 155 | leaf_index: u32, 156 | leaf: B256, 157 | } 158 | 159 | #[derive(Deserialize, Serialize)] 160 | #[serde(rename_all = "camelCase")] 161 | struct Roots { 162 | data_root: B256, 163 | blob_root: B256, 164 | bridge_root: B256, 165 | } 166 | 167 | #[derive(Deserialize)] 168 | #[serde(rename_all = "camelCase")] 169 | struct AccountStorageProofResponse { 170 | account_proof: Vec, 171 | storage_proof: Vec, 172 | } 173 | 174 | #[derive(Deserialize)] 175 | struct StorageProof { 176 | proof: Vec, 177 | } 178 | 179 | #[derive(Deserialize)] 180 | struct SuccinctAPIResponse { 181 | data: Option, 182 | error: Option, 183 | success: Option, 184 | } 185 | 186 | #[derive(Serialize)] 187 | #[serde(rename_all = "camelCase")] 188 | struct SlotMappingResponse { 189 | block_hash: String, 190 | block_number: String, 191 | } 192 | 193 | #[derive(Deserialize)] 194 | #[serde(rename_all = "camelCase")] 195 | struct SuccinctAPIData { 196 | range_hash: B256, 197 | data_commitment: B256, 198 | merkle_branch: Vec, 199 | index: u16, 200 | } 201 | 202 | #[derive(Serialize)] 203 | #[serde(rename_all = "camelCase")] 204 | struct AggregatedResponse { 205 | data_root_proof: Vec, 206 | leaf_proof: Vec, 207 | range_hash: B256, 208 | data_root_index: u16, 209 | leaf: B256, 210 | leaf_index: u32, 211 | data_root: B256, 212 | blob_root: B256, 213 | bridge_root: B256, 214 | data_root_commitment: B256, 215 | block_hash: B256, 216 | message: Option, 217 | } 218 | 219 | impl AggregatedResponse { 220 | pub fn new( 221 | range_data: SuccinctAPIData, 222 | data_proof_res: KateQueryDataProofResponse, 223 | hash: B256, 224 | ) -> Self { 225 | AggregatedResponse { 226 | data_root_proof: range_data.merkle_branch, 227 | leaf_proof: data_proof_res.data_proof.proof, 228 | range_hash: range_data.range_hash, 229 | data_root_index: range_data.index, 230 | leaf: data_proof_res.data_proof.leaf, 231 | leaf_index: data_proof_res.data_proof.leaf_index, 232 | data_root: data_proof_res.data_proof.roots.data_root, 233 | blob_root: data_proof_res.data_proof.roots.blob_root, 234 | bridge_root: data_proof_res.data_proof.roots.bridge_root, 235 | data_root_commitment: range_data.data_commitment, 236 | block_hash: hash, 237 | message: data_proof_res.message, 238 | } 239 | } 240 | } 241 | 242 | #[derive(Serialize)] 243 | #[serde(rename_all = "camelCase")] 244 | struct EthProofResponse { 245 | account_proof: Vec, 246 | storage_proof: Vec, 247 | } 248 | 249 | #[derive(Serialize)] 250 | #[serde(rename_all = "camelCase")] 251 | struct HeadResponseV2 { 252 | pub slot: u64, 253 | pub block_number: u64, 254 | pub block_hash: B256, 255 | pub timestamp: u64, 256 | pub timestamp_diff: u64, 257 | } 258 | 259 | #[derive(Serialize)] 260 | #[serde(rename_all = "camelCase")] 261 | struct HeadResponseLegacy { 262 | pub slot: u64, 263 | pub timestamp: u64, 264 | pub timestamp_diff: u64, 265 | } 266 | 267 | #[derive(Serialize, Deserialize)] 268 | struct ChainHeadResponse { 269 | pub head: u32, 270 | } 271 | 272 | #[derive(Serialize, Deserialize)] 273 | #[serde(rename_all = "camelCase")] 274 | struct RangeBlocks { 275 | start: u32, 276 | end: u32, 277 | } 278 | 279 | #[derive(Serialize, Deserialize)] 280 | #[serde(rename_all = "camelCase")] 281 | struct RangeBlocksAPIResponse { 282 | data: RangeBlocks, 283 | } 284 | 285 | #[derive(Debug, Deserialize)] 286 | pub struct HeaderBlockNumber { 287 | #[serde(deserialize_with = "hex_to_u32")] 288 | pub number: u32, 289 | } 290 | 291 | fn hex_to_u32<'de, D>(deserializer: D) -> Result 292 | where 293 | D: Deserializer<'de>, 294 | { 295 | let s: &str = Deserialize::deserialize(deserializer)?; 296 | u32::from_str_radix(s.trim_start_matches("0x"), 16).map_err(serde::de::Error::custom) 297 | } 298 | 299 | async fn alive() -> Result, StatusCode> { 300 | Ok(Json(json!({ "name": "Avail Bridge API" }))) 301 | } 302 | 303 | #[inline(always)] 304 | async fn info(State(state): State>) -> Result, StatusCode> { 305 | Ok(Json(json!({ 306 | "vectorXContractAddress": state.contract_address, 307 | "vectorXChainId": state.contract_chain_id, 308 | "bridgeContractAddress" : state.bridge_contract_address, 309 | "availChainName": state.avail_chain_name, 310 | }))) 311 | } 312 | 313 | #[inline(always)] 314 | async fn versions(State(_state): State>) -> Result, StatusCode> { 315 | Ok(Json(json!(["v1"]))) 316 | } 317 | 318 | #[derive(Debug, Deserialize)] 319 | #[serde(rename_all = "camelCase")] 320 | struct TransactionQueryParams { 321 | eth_address: Option, 322 | avail_address: Option, 323 | } 324 | 325 | #[derive(Debug, Serialize, Deserialize)] 326 | #[serde_as] 327 | #[serde(rename_all = "camelCase")] 328 | pub struct TransactionData { 329 | pub message_id: i64, 330 | pub status: StatusEnum, 331 | pub source_transaction_hash: String, 332 | pub source_block_number: i64, 333 | pub source_block_hash: String, 334 | #[serde(skip_serializing_if = "Option::is_none")] 335 | pub source_transaction_index: Option, 336 | #[serde_as(as = "TimestampSeconds")] 337 | pub source_timestamp: NaiveDateTime, 338 | pub token_id: String, 339 | pub destination_block_number: Option, 340 | pub destination_block_hash: Option, 341 | #[serde(skip_serializing_if = "Option::is_none")] 342 | pub destination_transaction_index: Option, 343 | #[serde_as(as = "Option")] 344 | pub destination_timestamp: Option, 345 | pub depositor_address: String, 346 | pub receiver_address: String, 347 | pub amount: String, 348 | } 349 | 350 | #[derive(Debug, Default, Serialize, Deserialize)] 351 | #[serde(rename_all = "camelCase")] 352 | pub struct TransactionResult { 353 | pub avail_send: Vec, 354 | pub eth_send: Vec, 355 | } 356 | 357 | fn map_ethereum_send_to_transaction_result(send: EthereumSend) -> TransactionData { 358 | TransactionData { 359 | message_id: send.message_id, 360 | status: send.status, 361 | source_transaction_hash: send.source_transaction_hash, 362 | source_block_number: send.source_block_number, 363 | source_block_hash: send.source_block_hash, 364 | source_transaction_index: None, 365 | source_timestamp: send.source_timestamp, 366 | token_id: send.token_id, 367 | destination_block_number: send.destination_block_number, 368 | destination_block_hash: send.destination_block_hash, 369 | destination_transaction_index: send.destination_transaction_index, 370 | destination_timestamp: send.destination_timestamp, 371 | depositor_address: send.depositor_address, 372 | receiver_address: send.receiver_address, 373 | amount: send.amount, 374 | } 375 | } 376 | 377 | // Function to map AvailSend to TransactionResult 378 | fn map_avail_send_to_transaction_result(send: AvailSend) -> TransactionData { 379 | TransactionData { 380 | message_id: send.message_id, 381 | status: send.status, 382 | source_transaction_hash: send.source_transaction_hash, 383 | source_block_number: send.source_block_number, 384 | source_block_hash: send.source_block_hash, 385 | source_transaction_index: Some(send.source_transaction_index), 386 | source_timestamp: send.source_timestamp, 387 | token_id: send.token_id, 388 | destination_block_number: send.destination_block_number, 389 | destination_block_hash: send.destination_block_hash, 390 | destination_transaction_index: None, 391 | destination_timestamp: send.destination_timestamp, 392 | depositor_address: send.depositor_address, 393 | receiver_address: send.receiver_address, 394 | amount: send.amount, 395 | } 396 | } 397 | 398 | #[inline(always)] 399 | async fn transactions( 400 | Query(address_query): Query, 401 | State(state): State>, 402 | ) -> impl IntoResponse { 403 | if address_query.eth_address.is_none() && address_query.avail_address.is_none() { 404 | tracing::error!("Query params not provided."); 405 | return ( 406 | StatusCode::BAD_REQUEST, 407 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 408 | Json(json!({ "error": "Invalid request: Query params not provided"})), 409 | ); 410 | } 411 | 412 | let cloned_state = state.clone(); 413 | let mut conn = cloned_state 414 | .connection_pool 415 | .get_timeout(Duration::from_secs(1)) 416 | .expect("Get connection pool"); 417 | 418 | // Initialize the result variables 419 | let mut transaction_results: TransactionResult = TransactionResult::default(); 420 | 421 | // Return the combined results 422 | if let Some(eth_address) = address_query.eth_address { 423 | let ethereum_sends_results = ethereum_sends 424 | .select(EthereumSend::as_select()) 425 | .filter(schema::ethereum_sends::depositor_address.eq(format!("{:?}", eth_address))) 426 | .order_by(schema::ethereum_sends::source_timestamp.desc()) 427 | .limit(500) 428 | .load::(&mut conn); 429 | 430 | match ethereum_sends_results { 431 | Ok(transaction) => { 432 | transaction_results.eth_send = transaction 433 | .into_iter() 434 | .map(map_ethereum_send_to_transaction_result) 435 | .collect() 436 | } 437 | Err(e) => { 438 | tracing::error!("Cannot get ethereum send transactions: {:?}", e); 439 | return ( 440 | StatusCode::INTERNAL_SERVER_ERROR, 441 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 442 | Json(json!({ "error": e.to_string()})), 443 | ); 444 | } 445 | } 446 | } 447 | if let Some(avail_address) = address_query.avail_address { 448 | let avail_sends_results = avail_sends 449 | .select(AvailSend::as_select()) 450 | .filter(schema::avail_sends::depositor_address.eq(format!("{:?}", avail_address))) 451 | .order_by(schema::avail_sends::source_timestamp.desc()) 452 | .limit(500) 453 | .load::(&mut conn); 454 | 455 | match avail_sends_results { 456 | Ok(transaction) => { 457 | transaction_results.avail_send = transaction 458 | .into_iter() 459 | .map(map_avail_send_to_transaction_result) 460 | .collect() 461 | } 462 | Err(e) => { 463 | tracing::error!("Cannot get avail send transactions: {:?}", e); 464 | return ( 465 | StatusCode::INTERNAL_SERVER_ERROR, 466 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 467 | Json(json!({ "error": e.to_string()})), 468 | ); 469 | } 470 | } 471 | } 472 | 473 | ( 474 | StatusCode::OK, 475 | [( 476 | "Cache-Control", 477 | format!( 478 | "public, max-age={}, immutable", 479 | state.transactions_cache_maxage 480 | ), 481 | )], 482 | Json(json!(transaction_results)), 483 | ) 484 | } 485 | 486 | #[inline(always)] 487 | async fn get_eth_proof( 488 | Path(block_hash): Path, 489 | Query(index_struct): Query, 490 | State(state): State>, 491 | ) -> impl IntoResponse { 492 | let cloned_state = state.clone(); 493 | let data_proof_response_fut = tokio::spawn(async move { 494 | cloned_state 495 | .avail_client 496 | .request( 497 | "kate_queryDataProof", 498 | rpc_params![index_struct.index, &block_hash], 499 | ) 500 | .await 501 | }); 502 | 503 | let eth_proof_cache_maxage = state.eth_proof_cache_maxage; 504 | let eth_proof_failure_cache_maxage = state.eth_proof_failure_cache_maxage; 505 | let url = format!( 506 | "{}?chainName={}&contractChainId={}&contractAddress={}&blockHash={}", 507 | state.succinct_base_url, 508 | state.avail_chain_name, 509 | state.contract_chain_id, 510 | state.contract_address, 511 | block_hash 512 | ); 513 | 514 | let succinct_response_fut = tokio::spawn(async move { 515 | let succinct_response = state.request_client.get(url).send().await; 516 | match succinct_response { 517 | Ok(resp) => resp.json::().await, 518 | Err(err) => Err(err), 519 | } 520 | }); 521 | let (data_proof, succinct_response) = join!(data_proof_response_fut, succinct_response_fut); 522 | let data_proof_res: KateQueryDataProofResponse = match data_proof { 523 | Ok(resp) => match resp { 524 | Ok(data) => data, 525 | Err(err) => { 526 | let err_str = err.to_string(); 527 | let is_warn = err_str.contains("Missing block") 528 | || err_str.contains("Cannot fetch tx data") 529 | || err_str.contains("is not finalized"); 530 | 531 | if is_warn { 532 | tracing::warn!("❌ Cannot get kate data proof response: {:?}", err); 533 | } else { 534 | tracing::error!("❌ Cannot get kate data proof response: {:?}", err); 535 | } 536 | 537 | return ( 538 | StatusCode::BAD_REQUEST, 539 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 540 | Json(json!({ "error": err.to_string()})), 541 | ); 542 | } 543 | }, 544 | Err(err) => { 545 | tracing::error!("❌ {:?}", err); 546 | return ( 547 | StatusCode::INTERNAL_SERVER_ERROR, 548 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 549 | Json(json!({ "error": err.to_string()})), 550 | ); 551 | } 552 | }; 553 | let succinct_data = match succinct_response { 554 | Ok(data) => match data { 555 | Ok(SuccinctAPIResponse { 556 | data: Some(data), .. 557 | }) => data, 558 | Ok(SuccinctAPIResponse { 559 | success: Some(false), 560 | error: Some(data), 561 | .. 562 | }) => { 563 | if data.contains("not in the range of blocks") { 564 | tracing::warn!( 565 | "⏳ Succinct VectorX contract not updated yet! Response: {}", 566 | data 567 | ); 568 | } else { 569 | tracing::error!( 570 | "❌ Succinct API returned unsuccessfully. Response: {}", 571 | data 572 | ); 573 | } 574 | return ( 575 | StatusCode::NOT_FOUND, 576 | [( 577 | "Cache-Control", 578 | format!( 579 | "public, max-age={}, immutable", 580 | eth_proof_failure_cache_maxage 581 | ), 582 | )], 583 | Json(json!({ "error": data })), 584 | ); 585 | } 586 | Err(err) => { 587 | tracing::error!("❌ {:?}", err); 588 | return ( 589 | StatusCode::INTERNAL_SERVER_ERROR, 590 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 591 | Json(json!({ "error": err.to_string()})), 592 | ); 593 | } 594 | _ => { 595 | tracing::error!("❌ Succinct API returned no data"); 596 | return ( 597 | StatusCode::INTERNAL_SERVER_ERROR, 598 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 599 | Json(json!({ "error": "Succinct API returned no data"})), 600 | ); 601 | } 602 | }, 603 | Err(err) => { 604 | tracing::error!("❌ {:?}", err); 605 | return ( 606 | StatusCode::INTERNAL_SERVER_ERROR, 607 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 608 | Json(json!({ "error": err.to_string()})), 609 | ); 610 | } 611 | }; 612 | 613 | ( 614 | StatusCode::OK, 615 | [( 616 | "Cache-Control", 617 | format!("public, max-age={}, immutable", eth_proof_cache_maxage), 618 | )], 619 | Json(json!(AggregatedResponse { 620 | data_root_proof: succinct_data.merkle_branch, 621 | leaf_proof: data_proof_res.data_proof.proof, 622 | range_hash: succinct_data.range_hash, 623 | data_root_index: succinct_data.index, 624 | leaf: data_proof_res.data_proof.leaf, 625 | leaf_index: data_proof_res.data_proof.leaf_index, 626 | data_root: data_proof_res.data_proof.roots.data_root, 627 | blob_root: data_proof_res.data_proof.roots.blob_root, 628 | bridge_root: data_proof_res.data_proof.roots.bridge_root, 629 | data_root_commitment: succinct_data.data_commitment, 630 | block_hash, 631 | message: data_proof_res.message 632 | })), 633 | ) 634 | } 635 | 636 | #[inline(always)] 637 | async fn get_avl_proof( 638 | Path((block_hash, message_id_query)): Path<(B256, U256)>, 639 | State(state): State>, 640 | ) -> impl IntoResponse { 641 | let mut hasher = Keccak256::new(); 642 | hasher.update( 643 | [ 644 | message_id_query.to_be_bytes_vec(), 645 | U256::from(1).to_be_bytes_vec(), 646 | ] 647 | .concat(), 648 | ); 649 | let result = hasher.finalize(); 650 | let proof: Result = state 651 | .ethereum_client 652 | .request( 653 | "eth_getProof", 654 | rpc_params![ 655 | state.bridge_contract_address.as_str(), 656 | [B256::from_slice(&result[..]).to_string()], 657 | block_hash 658 | ], 659 | ) 660 | .await; 661 | 662 | match proof { 663 | Ok(mut resp) => ( 664 | StatusCode::OK, 665 | [( 666 | "Cache-Control", 667 | format!( 668 | "public, max-age={}, immutable", 669 | state.avl_proof_cache_maxage 670 | ), 671 | )], 672 | Json(json!(EthProofResponse { 673 | account_proof: resp.account_proof, 674 | storage_proof: resp.storage_proof.swap_remove(0).proof, 675 | })), 676 | ), 677 | Err(err) => { 678 | tracing::error!("❌ Cannot get account and storage proofs: {:?}", err); 679 | if err.to_string().ends_with("status code: 429") { 680 | ( 681 | StatusCode::TOO_MANY_REQUESTS, 682 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 683 | Json(json!({ "error": err.to_string()})), 684 | ) 685 | } else { 686 | ( 687 | StatusCode::INTERNAL_SERVER_ERROR, 688 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 689 | Json(json!({ "error": err.to_string()})), 690 | ) 691 | } 692 | } 693 | } 694 | } 695 | 696 | /// Creates a request to the beaconcha service for mapping slot to the block number. 697 | #[inline(always)] 698 | async fn get_beacon_slot( 699 | Path(slot): Path, 700 | State(state): State>, 701 | ) -> impl IntoResponse { 702 | let resp = state 703 | .request_client 704 | .get(format!( 705 | "{}/eth/v2/beacon/blocks/{}", 706 | state.beaconchain_base_url, slot 707 | )) 708 | .send() 709 | .await; 710 | 711 | match resp { 712 | Ok(ok) => { 713 | let response_data = ok.json::().await; 714 | match response_data { 715 | Ok(rsp_data) => ( 716 | StatusCode::OK, 717 | [( 718 | "Cache-Control", 719 | format!( 720 | "public, max-age={}, immutable", 721 | state.slot_mapping_cache_maxage 722 | ), 723 | )], 724 | Json(json!(SlotMappingResponse { 725 | block_number: rsp_data.data.message.body.execution_payload.block_number, 726 | block_hash: rsp_data.data.message.body.execution_payload.block_hash 727 | })), 728 | ), 729 | Err(err) => { 730 | tracing::error!("❌ Cannot get beacon API response data: {:?}", err); 731 | ( 732 | StatusCode::INTERNAL_SERVER_ERROR, 733 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 734 | Json(json!({ "error": err.to_string()})), 735 | ) 736 | } 737 | } 738 | } 739 | Err(err) => { 740 | tracing::error!("❌ Cannot get beacon API data: {:?}", err); 741 | ( 742 | StatusCode::INTERNAL_SERVER_ERROR, 743 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 744 | Json(json!({ "error": err.to_string()})), 745 | ) 746 | } 747 | } 748 | } 749 | 750 | /// get_eth_head returns Ethereum head with the latest slot/block that is stored and a time. 751 | #[inline(always)] 752 | async fn get_eth_head(State(state): State>) -> impl IntoResponse { 753 | let slot_block_head = SLOT_BLOCK_HEAD.read().await; 754 | if let Some((slot, block, hash, timestamp)) = slot_block_head.as_ref() { 755 | let now = Utc::now().timestamp() as u64; 756 | ( 757 | StatusCode::OK, 758 | [( 759 | "Cache-Control", 760 | format!( 761 | "public, max-age={}, must-revalidate", 762 | state.eth_head_cache_maxage 763 | ), 764 | )], 765 | Json(json!(HeadResponseV2 { 766 | slot: *slot, 767 | block_number: *block, 768 | block_hash: *hash, 769 | timestamp: *timestamp, 770 | timestamp_diff: now - *timestamp 771 | })), 772 | ) 773 | } else { 774 | ( 775 | StatusCode::INTERNAL_SERVER_ERROR, 776 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 777 | Json(json!({ "error": "Not found"})), 778 | ) 779 | } 780 | } 781 | 782 | /// get_eth_head returns Ethereum head with the latest slot/block that is stored and a time. 783 | #[inline(always)] 784 | async fn get_eth_head_legacy(State(state): State>) -> impl IntoResponse { 785 | let slot_block_head = SLOT_BLOCK_HEAD.read().await; 786 | if let Some((slot, _block, _hash, timestamp)) = slot_block_head.as_ref() { 787 | let now = Utc::now().timestamp() as u64; 788 | ( 789 | StatusCode::OK, 790 | [( 791 | "Cache-Control", 792 | format!( 793 | "public, max-age={}, must-revalidate", 794 | state.eth_head_cache_maxage 795 | ), 796 | )], 797 | Json(json!(HeadResponseLegacy { 798 | slot: *slot, 799 | timestamp: *timestamp, 800 | timestamp_diff: now - *timestamp 801 | })), 802 | ) 803 | } else { 804 | ( 805 | StatusCode::INTERNAL_SERVER_ERROR, 806 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 807 | Json(json!({ "error": "Not found"})), 808 | ) 809 | } 810 | } 811 | 812 | /// get_avl_head returns start and end blocks which the contract has commitments 813 | #[inline(always)] 814 | async fn get_avl_head(State(state): State>) -> impl IntoResponse { 815 | let url = format!( 816 | "{}/{}/?contractChainId={}&contractAddress={}", 817 | state.succinct_base_url, "range", state.contract_chain_id, state.contract_address 818 | ); 819 | let response = state.request_client.get(url).send().await; 820 | match response { 821 | Ok(ok) => { 822 | let range_response = ok.json::().await; 823 | match range_response { 824 | Ok(range_blocks) => ( 825 | StatusCode::OK, 826 | [( 827 | "Cache-Control", 828 | format!( 829 | "public, max-age={}, must-revalidate", 830 | state.avl_head_cache_maxage 831 | ), 832 | )], 833 | Json(json!(range_blocks)), 834 | ), 835 | Err(err) => { 836 | tracing::error!("❌ Cannot parse range blocks: {:?}", err.to_string()); 837 | ( 838 | StatusCode::INTERNAL_SERVER_ERROR, 839 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 840 | Json(json!({ "error": err.to_string()})), 841 | ) 842 | } 843 | } 844 | } 845 | Err(err) => { 846 | tracing::error!("❌ Cannot get avl head: {:?}", err.to_string()); 847 | ( 848 | StatusCode::INTERNAL_SERVER_ERROR, 849 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 850 | Json(json!({ "error": err.to_string()})), 851 | ) 852 | } 853 | } 854 | } 855 | 856 | #[inline(always)] 857 | async fn get_head( 858 | Path(chain_id): Path, 859 | State(state): State>, 860 | ) -> impl IntoResponse { 861 | match state.chains.get(&chain_id) { 862 | Some(chain) => { 863 | let provider = match ProviderBuilder::new().connect(&chain.rpc_url).await { 864 | Ok(provider) => provider, 865 | Err(err) => { 866 | tracing::error!("❌ Cannot connect to provider: {:?}", err); 867 | return ( 868 | StatusCode::INTERNAL_SERVER_ERROR, 869 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 870 | Json(json!({ "error": err.to_string()})), 871 | ); 872 | } 873 | }; 874 | let contract = SP1Vector::new(chain.contract_address, provider); 875 | match contract.latestBlock().call().await { 876 | Ok(head) => { 877 | tracing::debug!("✅ Latest block on chain {}: {}", chain_id, head); 878 | ( 879 | StatusCode::OK, 880 | [( 881 | "Cache-Control", 882 | format!( 883 | "public, max-age={}, must-revalidate", 884 | state.head_cache_maxage 885 | ), 886 | )], 887 | Json(json!(ChainHeadResponse { head })), 888 | ) 889 | } 890 | Err(err) => { 891 | tracing::error!("❌ Cannot get latest block from contract: {:?}", err); 892 | ( 893 | StatusCode::INTERNAL_SERVER_ERROR, 894 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 895 | Json(json!({ "error": err.to_string()})), 896 | ) 897 | } 898 | } 899 | } 900 | None => ( 901 | StatusCode::BAD_REQUEST, 902 | [( 903 | "Cache-Control", 904 | "public, max-age=3600, must-revalidate".to_string(), 905 | )], 906 | Json(json!({ "error": "Unsupported chain ID"})), 907 | ), 908 | } 909 | } 910 | 911 | /// get_proof returns a proof from Avail for the provided chain id 912 | #[inline(always)] 913 | async fn get_proof( 914 | Path(chain_id): Path, 915 | Query(query_proof): Query, 916 | State(state): State>, 917 | ) -> impl IntoResponse { 918 | let block_hash = query_proof.block_hash; 919 | let index = query_proof.index; 920 | 921 | let header_response: Result = state 922 | .avail_client 923 | .request("chain_getHeader", rpc_params![block_hash]) 924 | .await; 925 | if let Err(err) = header_response { 926 | tracing::error!("Error fetching Avail block. {:?}", err); 927 | return ( 928 | StatusCode::NOT_FOUND, 929 | [( 930 | "Cache-Control", 931 | "public, max-age=60, must-revalidate".to_string(), 932 | )], 933 | Json( 934 | json!({ "error": format!("Cannot fetch {:?} provided Avail block.", block_hash) }), 935 | ), 936 | ); 937 | } else if let Ok(requested_block) = header_response { 938 | let chain_head_rsp = fetch_chain_head(state.clone(), chain_id).await; 939 | if let Ok(head) = chain_head_rsp { 940 | if requested_block.number > head { 941 | tracing::warn!( 942 | "Contract not yet synced for the provided block hash {}, the last synced block number {}", 943 | block_hash, 944 | head 945 | ); 946 | return ( 947 | StatusCode::NOT_FOUND, 948 | [( 949 | "Cache-Control", 950 | "public, max-age=60, must-revalidate".to_string(), 951 | )], 952 | Json( 953 | json!({ "error": format!("Provided block hash {:?} is not yet in the range.", block_hash) }), 954 | ), 955 | ); 956 | } 957 | } else if let Err(err) = chain_head_rsp { 958 | tracing::warn!("Cannot get chain head: {:?}", err); 959 | return ( 960 | StatusCode::NOT_FOUND, 961 | [( 962 | "Cache-Control", 963 | "public, max-age=360, must-revalidate".to_string(), 964 | )], 965 | Json(json!({ "error": format!("Provided chain id {:?} not found.", chain_id) })), 966 | ); 967 | } 968 | } 969 | 970 | let data_proof_response_fut = spawn_kate_proof(state.clone(), index, block_hash); 971 | let merkle_proof_range_fut = spawn_merkle_proof_range_fetch(state.clone(), block_hash); 972 | let (data_proof, range_response) = join!(data_proof_response_fut, merkle_proof_range_fut); 973 | let data_proof_res: KateQueryDataProofResponse = match data_proof { 974 | Ok(resp) => match resp { 975 | Ok(data) => data, 976 | Err(err) => { 977 | let err_str = err.to_string(); 978 | let is_warn = err_str.contains("Missing block") 979 | || err_str.contains("Cannot fetch tx data") 980 | || err_str.contains("is not finalized"); 981 | if is_warn { 982 | tracing::warn!("Cannot get kate data proof response: {:?}", err); 983 | } else { 984 | tracing::error!("Cannot get kate data proof response: {:?}", err); 985 | } 986 | 987 | return ( 988 | StatusCode::BAD_REQUEST, 989 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 990 | Json(json!({ "error": err.to_string()})), 991 | ); 992 | } 993 | }, 994 | Err(err) => { 995 | tracing::error!("Cannot fetch kate query data proof response {:?}", err); 996 | return ( 997 | StatusCode::INTERNAL_SERVER_ERROR, 998 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 999 | Json(json!({ "error": err.to_string()})), 1000 | ); 1001 | } 1002 | }; 1003 | let range_data = match range_response { 1004 | Ok(data) => match data { 1005 | Ok(SuccinctAPIResponse { 1006 | data: Some(data), .. 1007 | }) => data, 1008 | Ok(SuccinctAPIResponse { 1009 | success: Some(false), 1010 | error: Some(data), 1011 | .. 1012 | }) => { 1013 | if data.contains("not in the range of blocks") { 1014 | tracing::warn!( 1015 | "Succinct VectorX contract not updated yet! Response: {}", 1016 | data 1017 | ); 1018 | } else { 1019 | tracing::error!("Succinct API returned unsuccessfully. Response: {}", data); 1020 | } 1021 | return ( 1022 | StatusCode::NOT_FOUND, 1023 | [( 1024 | "Cache-Control", 1025 | "public, max-age=60, must-revalidate".to_string(), 1026 | )], 1027 | Json(json!({ "error": data })), 1028 | ); 1029 | } 1030 | Err(err) => { 1031 | tracing::error!("Cannot get succinct api response {:?}", err); 1032 | return ( 1033 | StatusCode::INTERNAL_SERVER_ERROR, 1034 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 1035 | Json(json!({ "error": err.to_string()})), 1036 | ); 1037 | } 1038 | _ => { 1039 | tracing::error!("Succinct API returned no data"); 1040 | return ( 1041 | StatusCode::INTERNAL_SERVER_ERROR, 1042 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 1043 | Json(json!({ "error": "Succinct API returned no data"})), 1044 | ); 1045 | } 1046 | }, 1047 | Err(err) => { 1048 | tracing::error!("Cannot get merkle proof response {:?}", err); 1049 | return ( 1050 | StatusCode::INTERNAL_SERVER_ERROR, 1051 | [("Cache-Control", "max-age=60, must-revalidate".to_string())], 1052 | Json(json!({ "error": err.to_string()})), 1053 | ); 1054 | } 1055 | }; 1056 | 1057 | ( 1058 | StatusCode::OK, 1059 | [( 1060 | "Cache-Control", 1061 | format!("public, max-age={}, immutable", state.proof_cache_maxage), 1062 | )], 1063 | Json(json!(AggregatedResponse::new( 1064 | range_data, 1065 | data_proof_res, 1066 | block_hash 1067 | ))), 1068 | ) 1069 | } 1070 | 1071 | // spawn_kate_proof fetch queryDataProof from Avail chain 1072 | fn spawn_kate_proof( 1073 | state: Arc, 1074 | index: u32, 1075 | block_hash: B256, 1076 | ) -> JoinHandle> { 1077 | tokio::spawn(async move { 1078 | state 1079 | .avail_client 1080 | .request("kate_queryDataProof", rpc_params![index, &block_hash]) 1081 | .await 1082 | }) 1083 | } 1084 | 1085 | // spawn_merkle_proof_range_fetch fetches merkle proof for a block range 1086 | fn spawn_merkle_proof_range_fetch( 1087 | state: Arc, 1088 | block_hash: B256, 1089 | ) -> JoinHandle> { 1090 | let url = format!( 1091 | "{}?chainName={}&contractChainId={}&contractAddress={}&blockHash={}", 1092 | state.succinct_base_url, 1093 | state.avail_chain_name, 1094 | state.contract_chain_id, 1095 | state.contract_address, 1096 | block_hash 1097 | ); 1098 | tokio::spawn(async move { 1099 | let res = state.request_client.get(url).send().await; 1100 | match res { 1101 | Ok(resp) => resp.json::().await, 1102 | Err(e) => Err(e), 1103 | } 1104 | }) 1105 | } 1106 | 1107 | // fetch_chain_head returns current state of the chain head on the chain with provided chain id 1108 | async fn fetch_chain_head(state: Arc, chain_id: u64) -> Result { 1109 | let response: Response = get_head(Path(chain_id), State(state)).await.into_response(); 1110 | if response.status() != StatusCode::OK { 1111 | Err(anyhow::anyhow!( 1112 | "Cannot fetch chain head for chain {}", 1113 | chain_id 1114 | )) 1115 | } else { 1116 | let body = response.into_body(); 1117 | let b = to_bytes(body, 2048).await?; 1118 | let body: ChainHeadResponse = serde_json::from_slice(b.to_vec().as_slice())?; 1119 | Ok(body.head) 1120 | } 1121 | } 1122 | 1123 | #[tokio::main] 1124 | async fn main() { 1125 | dotenvy::dotenv().ok(); 1126 | tracing_subscriber::registry() 1127 | .with(tracing_subscriber::fmt::layer().json()) 1128 | .with( 1129 | tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { 1130 | "bridge_api=debug,tower_http=debug,axum::rejection=trace".into() 1131 | }), 1132 | ) 1133 | .init(); 1134 | 1135 | let max_concurrent_request: usize = env::var("MAX_CONCURRENT_REQUEST") 1136 | .ok() 1137 | .and_then(|max_request| max_request.parse::().ok()) 1138 | .unwrap_or(1024); 1139 | 1140 | // Connection pool 1141 | let connections_string = format!( 1142 | "postgresql://{}:{}@{}/{}", 1143 | env::var("PG_USERNAME").unwrap_or("myuser".to_owned()), 1144 | env::var("PG_PASSWORD").unwrap_or("mypassword".to_owned()), 1145 | env::var("POSTGRES_URL").unwrap_or("localhost:5432".to_owned()), 1146 | env::var("POSTGRES_DB").unwrap_or("bridge-ui-indexer".to_owned()), 1147 | ); 1148 | let connection_pool = r2d2::Pool::builder() 1149 | .build(ConnectionManager::::new(connections_string)) 1150 | .expect("Failed to create pool."); 1151 | const SUPPORTED_CHAIN_IDS: [u64; 7] = [1, 123, 32657, 84532, 11155111, 17000, 421614]; 1152 | // loop through expected_chain_ids and store the chain information, if value is missing, skip chain_id 1153 | let chains = SUPPORTED_CHAIN_IDS 1154 | .iter() 1155 | .filter_map(|&chain_id| { 1156 | let rpc_url = env::var(format!("CHAIN_{}_RPC_URL", chain_id)).ok()?; 1157 | let contract_address = env::var(format!("CHAIN_{}_CONTRACT_ADDRESS", chain_id)) 1158 | .ok() 1159 | .and_then(|addr| addr.parse::
().ok())?; 1160 | Some(( 1161 | chain_id, 1162 | Chain { 1163 | rpc_url, 1164 | contract_address, 1165 | }, 1166 | )) 1167 | }) 1168 | .collect::>(); 1169 | 1170 | let shared_state = Arc::new(AppState { 1171 | avail_client: HttpClientBuilder::default() 1172 | .max_concurrent_requests(max_concurrent_request) 1173 | .build( 1174 | env::var("AVAIL_CLIENT_URL") 1175 | .unwrap_or("https://avail-turing.public.blastapi.io/api".to_owned()), 1176 | ) 1177 | .unwrap(), 1178 | ethereum_client: HttpClientBuilder::default() 1179 | .max_concurrent_requests(max_concurrent_request) 1180 | .build( 1181 | env::var("ETHEREUM_CLIENT_URL") 1182 | .unwrap_or("https://ethereum-sepolia.publicnode.com".to_owned()), 1183 | ) 1184 | .unwrap(), 1185 | request_client: Client::builder().brotli(true).build().unwrap(), 1186 | succinct_base_url: env::var("SUCCINCT_URL") 1187 | .unwrap_or("https://beaconapi.succinct.xyz/api/integrations/vectorx".to_owned()), 1188 | beaconchain_base_url: env::var("BEACONCHAIN_URL") 1189 | .unwrap_or("https://sepolia.beaconcha.in/api/v1/slot".to_owned()), 1190 | contract_address: env::var("VECTORX_CONTRACT_ADDRESS") 1191 | .unwrap_or("0xe542dB219a7e2b29C7AEaEAce242c9a2Cd528F96".to_owned()), 1192 | contract_chain_id: env::var("CONTRACT_CHAIN_ID").unwrap_or("11155111".to_owned()), 1193 | avail_chain_name: env::var("AVAIL_CHAIN_NAME").unwrap_or("turing".to_owned()), 1194 | bridge_contract_address: env::var("BRIDGE_CONTRACT_ADDRESS") 1195 | .unwrap_or("0x967F7DdC4ec508462231849AE81eeaa68Ad01389".to_owned()), 1196 | eth_head_cache_maxage: env::var("ETH_HEAD_CACHE_MAXAGE") 1197 | .ok() 1198 | .and_then(|max_request| max_request.parse::().ok()) 1199 | .unwrap_or(240), 1200 | avl_head_cache_maxage: env::var("AVL_HEAD_CACHE_MAXAGE") 1201 | .ok() 1202 | .and_then(|max_request| max_request.parse::().ok()) 1203 | .unwrap_or(600), 1204 | head_cache_maxage: env::var("HEAD_CACHE_MAXAGE") 1205 | .ok() 1206 | .and_then(|max_request| max_request.parse::().ok()) 1207 | .unwrap_or(60), 1208 | eth_proof_cache_maxage: env::var("ETH_PROOF_CACHE_MAXAGE") 1209 | .ok() 1210 | .and_then(|proof_response| proof_response.parse::().ok()) 1211 | .unwrap_or(172800), 1212 | proof_cache_maxage: env::var("PROOF_CACHE_MAXAGE") 1213 | .ok() 1214 | .and_then(|proof_response| proof_response.parse::().ok()) 1215 | .unwrap_or(172800), 1216 | eth_proof_failure_cache_maxage: env::var("ETH_PROOF_FAILURE_CACHE_MAXAGE") 1217 | .ok() 1218 | .and_then(|proof_response| proof_response.parse::().ok()) 1219 | .unwrap_or(5400), 1220 | avl_proof_cache_maxage: env::var("AVL_PROOF_CACHE_MAXAGE") 1221 | .ok() 1222 | .and_then(|proof_response| proof_response.parse::().ok()) 1223 | .unwrap_or(172800), 1224 | slot_mapping_cache_maxage: env::var("SLOT_MAPPING_CACHE_MAXAGE") 1225 | .ok() 1226 | .and_then(|slot_mapping_response| slot_mapping_response.parse::().ok()) 1227 | .unwrap_or(172800), 1228 | transactions_cache_maxage: env::var("TRANSACTIONS_CACHE_MAXAGE") 1229 | .ok() 1230 | .and_then(|transactions_mapping_response| { 1231 | transactions_mapping_response.parse::().ok() 1232 | }) 1233 | .unwrap_or(60), 1234 | connection_pool, 1235 | chains, 1236 | }); 1237 | 1238 | let app = Router::new() 1239 | .route("/", get(alive)) 1240 | .route("/versions", get(versions)) 1241 | .route("/v1/info", get(info)) 1242 | .route("/info", get(info)) 1243 | .route("/v1/eth/proof/{block_hash}", get(get_eth_proof)) 1244 | .route("/eth/proof/{block_hash}", get(get_eth_proof)) 1245 | .route("/v1/eth/head", get(get_eth_head)) 1246 | .route("/eth/head", get(get_eth_head_legacy)) 1247 | .route("/v1/avl/head", get(get_avl_head)) 1248 | .route("/avl/head", get(get_avl_head)) 1249 | .route( 1250 | "/v1/avl/proof/{block_hash}/{message_id}", 1251 | get(get_avl_proof), 1252 | ) 1253 | .route("/v1/transactions", get(transactions)) 1254 | .route("/transactions", get(transactions)) 1255 | .route("/avl/proof/{block_hash}/{message_id}", get(get_avl_proof)) 1256 | .route("/beacon/slot/{slot_number}", get(get_beacon_slot)) 1257 | .route("/v1/head/{chain_id}", get(get_head)) 1258 | .route("/v1/proof/{chain_id}", get(get_proof)) 1259 | .layer(TraceLayer::new_for_http()) 1260 | .layer(CompressionLayer::new()) 1261 | .layer( 1262 | CorsLayer::new() 1263 | .allow_methods(vec![Method::GET]) 1264 | .allow_origin(Any), 1265 | ) 1266 | .with_state(shared_state.clone()); 1267 | 1268 | let host = env::var("HOST").unwrap_or("0.0.0.0".to_owned()); 1269 | let port = env::var("PORT").unwrap_or("8080".to_owned()); 1270 | let listener = tokio::net::TcpListener::bind(format!("{}:{}", host, port)) 1271 | .await 1272 | .unwrap(); 1273 | 1274 | tokio::spawn(async move { 1275 | tracing::info!("Starting head tracking task"); 1276 | if let Err(e) = track_slot_avail_task(shared_state.clone()).await { 1277 | tracing::error!("Error occurred, cannot continue: {e:#}"); 1278 | process::exit(1); 1279 | } 1280 | }); 1281 | tracing::info!("🚀 Started server on host {} with port {}", host, port); 1282 | axum::serve(listener, app).await.unwrap(); 1283 | } 1284 | 1285 | lazy_static! { 1286 | static ref SLOT_BLOCK_HEAD: RwLock> = RwLock::new(None); 1287 | } 1288 | 1289 | async fn track_slot_avail_task(state: Arc) -> Result<()> { 1290 | let pallet = "Vector"; 1291 | let head = "Head"; 1292 | let timestamp = "Timestamps"; 1293 | 1294 | let do_work = || { 1295 | let state = state.clone(); 1296 | async move { 1297 | loop { 1298 | let finalized_block_hash_str: String = state 1299 | .avail_client 1300 | .request("chain_getFinalizedHead", rpc_params![]) 1301 | .await 1302 | .context("finalized head")?; 1303 | 1304 | let head_key = format!( 1305 | "0x{}{}", 1306 | hex::encode(twox_128(pallet.as_bytes())), 1307 | hex::encode(twox_128(head.as_bytes())) 1308 | ); 1309 | let head_str: String = state 1310 | .avail_client 1311 | .request( 1312 | "state_getStorage", 1313 | rpc_params![head_key, finalized_block_hash_str.clone()], 1314 | ) 1315 | .await 1316 | .context("head key")?; 1317 | 1318 | let slot_from_hex = 1319 | sp_core::bytes::from_hex(head_str.as_str()).context("decode slot")?; 1320 | let slot: u64 = 1321 | Decode::decode(&mut slot_from_hex.as_slice()).context("slot decode 2")?; 1322 | 1323 | let timestamp_key = format!( 1324 | "0x{}{}{}", 1325 | hex::encode(twox_128(pallet.as_bytes())), 1326 | hex::encode(twox_128(timestamp.as_bytes())), 1327 | &head_str[2..].to_string() 1328 | ); 1329 | 1330 | let timestamp_str: String = state 1331 | .avail_client 1332 | .request( 1333 | "state_getStorage", 1334 | rpc_params![timestamp_key, finalized_block_hash_str], 1335 | ) 1336 | .await 1337 | .context("timestamp key")?; 1338 | 1339 | let timestamp_from_hex = 1340 | sp_core::bytes::from_hex(timestamp_str.as_str()).context("timestamp decode")?; 1341 | 1342 | let timestamp: u64 = Decode::decode(&mut timestamp_from_hex.as_slice()) 1343 | .context("timestamp decode 2")?; 1344 | 1345 | let slot_block_head = SLOT_BLOCK_HEAD.read().await; 1346 | if let Some((old_slot, _old_block, _old_hash, _old_timestamp)) = 1347 | slot_block_head.as_ref() 1348 | { 1349 | if old_slot == &slot { 1350 | tokio::time::sleep(Duration::from_secs(60 * 10)).await; 1351 | continue; 1352 | } 1353 | } 1354 | 1355 | drop(slot_block_head); 1356 | 1357 | let response = reqwest::get(format!( 1358 | "{}/eth/v2/beacon/blocks/{}", 1359 | state.beaconchain_base_url, slot 1360 | )) 1361 | .await 1362 | .context("Cannot get beacon block")?; 1363 | let root = response 1364 | .json::() 1365 | .await 1366 | .context("Cannot decode beacon response")?; 1367 | let bl = root 1368 | .data 1369 | .message 1370 | .body 1371 | .execution_payload 1372 | .block_number 1373 | .parse()?; 1374 | let hash = root 1375 | .data 1376 | .message 1377 | .body 1378 | .execution_payload 1379 | .block_hash 1380 | .parse()?; 1381 | let mut slot_block_head = SLOT_BLOCK_HEAD.write().await; 1382 | tracing::info!("Beacon mapping: {slot}:{bl}"); 1383 | *slot_block_head = Some((slot, bl, hash, timestamp)); 1384 | drop(slot_block_head); 1385 | 1386 | tokio::time::sleep(Duration::from_secs(60 * 5)).await; 1387 | } 1388 | #[allow(unreachable_code)] // forces for type inference later on 1389 | Ok::<(), anyhow::Error>(()) 1390 | } 1391 | }; 1392 | 1393 | let retry_settings = ExponentialBuilder::default() 1394 | .with_min_delay(Duration::from_secs(5)) 1395 | .with_max_delay(Duration::from_secs(60)) 1396 | .with_max_times(10) 1397 | .with_factor(2.0); 1398 | do_work.retry(&retry_settings).await?; 1399 | 1400 | Ok(()) 1401 | } 1402 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::sql_types::Status; 2 | use chrono::NaiveDateTime; 3 | use diesel::pg::{Pg, PgValue}; 4 | use diesel::serialize::{IsNull, Output}; 5 | use diesel::{ 6 | deserialize::{self, FromSql}, 7 | expression::AsExpression, 8 | serialize::ToSql, 9 | *, 10 | }; 11 | use jsonrpsee::core::Serialize; 12 | use serde::Deserialize; 13 | use serde_with::serde_as; 14 | use std::io::Write; 15 | 16 | #[derive(Debug, Clone, PartialEq, FromSqlRow, AsExpression, Eq)] 17 | #[diesel(sql_type = Status)] 18 | #[derive(Serialize, Deserialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub enum StatusEnum { 21 | InProgress, 22 | ClaimPending, 23 | Bridged, 24 | } 25 | impl ToSql for StatusEnum { 26 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { 27 | match *self { 28 | StatusEnum::InProgress => out.write_all(b"IN_PROGRESS")?, 29 | StatusEnum::ClaimPending => out.write_all(b"CLAIM_PENDING")?, 30 | StatusEnum::Bridged => out.write_all(b"BRIDGED")?, 31 | } 32 | Ok(IsNull::No) 33 | } 34 | } 35 | 36 | impl FromSql for StatusEnum { 37 | fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { 38 | match bytes.as_bytes() { 39 | b"IN_PROGRESS" => Ok(StatusEnum::InProgress), 40 | b"CLAIM_PENDING" => Ok(StatusEnum::ClaimPending), 41 | b"BRIDGED" => Ok(StatusEnum::Bridged), 42 | _ => Err(format!( 43 | "Unrecognized enum variant {}", 44 | std::str::from_utf8(bytes.as_bytes()).unwrap() 45 | ) 46 | .as_str() 47 | .into()), 48 | } 49 | } 50 | } 51 | 52 | #[derive(Queryable, Selectable, Insertable, Identifiable, Serialize)] 53 | #[serde(rename_all = "camelCase")] 54 | #[diesel(table_name = crate::schema::avail_sends)] 55 | #[diesel(primary_key(message_id))] 56 | #[diesel(check_for_backend(diesel::pg::Pg))] 57 | #[derive(Clone, Debug)] 58 | #[serde_as] 59 | pub struct AvailSend { 60 | pub message_id: i64, 61 | pub status: StatusEnum, 62 | pub source_transaction_hash: String, 63 | pub source_block_number: i64, 64 | pub source_block_hash: String, 65 | pub source_transaction_index: i64, 66 | #[serde_as(as = "TimestampSeconds")] 67 | pub source_timestamp: NaiveDateTime, 68 | pub token_id: String, 69 | pub destination_block_number: Option, 70 | pub destination_block_hash: Option, 71 | #[serde_as(as = "Option")] 72 | pub destination_timestamp: Option, 73 | pub depositor_address: String, 74 | pub receiver_address: String, 75 | pub amount: String, 76 | } 77 | 78 | #[derive(Queryable, Selectable, Insertable, Identifiable, Serialize)] 79 | #[serde(rename_all = "camelCase")] 80 | #[diesel(table_name = crate::schema::ethereum_sends)] 81 | #[diesel(primary_key(message_id))] 82 | #[diesel(check_for_backend(diesel::pg::Pg))] 83 | #[derive(Clone, Debug)] 84 | #[serde_as] 85 | pub struct EthereumSend { 86 | pub message_id: i64, 87 | pub status: StatusEnum, 88 | pub source_transaction_hash: String, 89 | pub source_block_number: i64, 90 | pub source_block_hash: String, 91 | #[serde_as(as = "TimestampSeconds")] 92 | pub source_timestamp: NaiveDateTime, 93 | pub token_id: String, 94 | pub destination_block_number: Option, 95 | pub destination_block_hash: Option, 96 | pub destination_transaction_index: Option, 97 | #[serde_as(as = "Option")] 98 | pub destination_timestamp: Option, 99 | pub depositor_address: String, 100 | pub receiver_address: String, 101 | pub amount: String, 102 | } 103 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | // @generated automatically by Diesel CLI. 2 | 3 | pub mod sql_types { 4 | #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)] 5 | #[diesel(postgres_type(name = "claim_type"))] 6 | pub struct ClaimType; 7 | 8 | #[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)] 9 | #[diesel(postgres_type(name = "status"))] 10 | pub struct Status; 11 | } 12 | 13 | diesel::table! { 14 | use diesel::sql_types::*; 15 | use super::sql_types::Status; 16 | use super::sql_types::ClaimType; 17 | 18 | avail_sends (message_id) { 19 | message_id -> Int8, 20 | status -> Status, 21 | #[max_length = 66] 22 | source_transaction_hash -> Varchar, 23 | source_block_number -> Int8, 24 | #[max_length = 66] 25 | source_block_hash -> Varchar, 26 | source_transaction_index -> Int8, 27 | source_timestamp -> Timestamp, 28 | #[max_length = 66] 29 | token_id -> Varchar, 30 | #[max_length = 66] 31 | destination_transaction_hash -> Nullable, 32 | destination_block_number -> Nullable, 33 | #[max_length = 66] 34 | destination_block_hash -> Nullable, 35 | destination_timestamp -> Nullable, 36 | #[max_length = 66] 37 | depositor_address -> Varchar, 38 | #[max_length = 22] 39 | receiver_address -> Varchar, 40 | #[max_length = 255] 41 | amount -> Varchar, 42 | claim_type -> ClaimType, 43 | } 44 | } 45 | 46 | diesel::table! { 47 | use diesel::sql_types::*; 48 | use super::sql_types::Status; 49 | use super::sql_types::ClaimType; 50 | 51 | ethereum_sends (message_id) { 52 | message_id -> Int8, 53 | status -> Status, 54 | #[max_length = 66] 55 | source_transaction_hash -> Varchar, 56 | source_block_number -> Int8, 57 | #[max_length = 66] 58 | source_block_hash -> Varchar, 59 | source_timestamp -> Timestamp, 60 | #[max_length = 66] 61 | token_id -> Varchar, 62 | #[max_length = 66] 63 | destination_transaction_hash -> Nullable, 64 | destination_block_number -> Nullable, 65 | #[max_length = 66] 66 | destination_block_hash -> Nullable, 67 | destination_transaction_index-> Nullable, 68 | destination_timestamp -> Nullable, 69 | #[max_length = 66] 70 | depositor_address -> Varchar, 71 | #[max_length = 66] 72 | receiver_address -> Varchar, 73 | #[max_length = 255] 74 | amount -> Varchar, 75 | claim_type -> ClaimType, 76 | } 77 | } 78 | 79 | diesel::allow_tables_to_appear_in_same_query!(avail_sends, ethereum_sends,); 80 | --------------------------------------------------------------------------------