├── .dockerignore
├── .env.example
├── .github
└── workflows
│ └── build-aws-image.yaml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── contracts
├── Cargo.toml
├── abi
│ ├── ERC20.json
│ ├── IWETH9.json
│ ├── LLMOracleCoordinator.json
│ └── LLMOracleRegistry.json
└── src
│ ├── addresses.rs
│ ├── balance.rs
│ ├── errors.rs
│ ├── interfaces.rs
│ └── lib.rs
├── core
├── Cargo.toml
├── src
│ ├── cli
│ │ ├── commands
│ │ │ ├── coordinator
│ │ │ │ ├── mod.rs
│ │ │ │ ├── request.rs
│ │ │ │ ├── serve.rs
│ │ │ │ └── view.rs
│ │ │ ├── mod.rs
│ │ │ ├── registry.rs
│ │ │ └── token.rs
│ │ ├── mod.rs
│ │ └── parsers.rs
│ ├── compute
│ │ ├── execute.rs
│ │ ├── generation
│ │ │ ├── execute.rs
│ │ │ ├── handler.rs
│ │ │ ├── mod.rs
│ │ │ ├── postprocess
│ │ │ │ ├── identity.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── swan.rs
│ │ │ ├── request.rs
│ │ │ └── workflow.rs
│ │ ├── handler.rs
│ │ ├── mod.rs
│ │ ├── nonce.rs
│ │ ├── utils.rs
│ │ └── validation
│ │ │ ├── execute.rs
│ │ │ ├── handler.rs
│ │ │ ├── mod.rs
│ │ │ └── workflow.rs
│ ├── configurations
│ │ └── mod.rs
│ ├── lib.rs
│ ├── main.rs
│ └── node
│ │ ├── anvil.rs
│ │ ├── coordinator.rs
│ │ ├── core.rs
│ │ ├── mod.rs
│ │ ├── registry.rs
│ │ ├── token.rs
│ │ └── types.rs
└── tests
│ ├── abi_encode_test.rs
│ ├── contracts
│ ├── TestNonce.sol
│ └── TestSolidityPacked.sol
│ ├── oracle_test.rs
│ ├── registry_test.rs
│ ├── request_test.rs
│ ├── swan_test.rs
│ └── weth_test.rs
├── misc
└── arweave.js
├── secrets
└── .gitignore
└── storage
├── Cargo.toml
└── src
├── arweave.rs
├── lib.rs
└── traits.rs
/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !src
3 | !Cargo.toml
4 | !Cargo.lock
5 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # RPC URL to connect with blockchain (required)
2 | RPC_URL=your-rpc-url
3 |
4 | # Logging level
5 | RUST_LOG=none,dria_oracle=info
6 |
7 | # Your Ethereum wallet for Oracle operations (required)
8 | # 32-byte private key, as a hexadecimal string without 0x prefix
9 | # example: ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
10 | SECRET_KEY=your-secret-key
11 |
12 | # Coordinator address (optional)
13 | COORDINATOR_ADDRESS=
14 |
15 | ## Arweave configurations
16 | # path to wallet, only required if your BYTE_LIMIT is enough that
17 | # you may do an Arweave upload to store a large value on-chain
18 | ARWEAVE_WALLET_PATH=./path/to/wallet.json
19 | # Base URIL for Arweave, may be kept as is
20 | ARWEAVE_BASE_URL=https://node1.bundlr.network
21 | # Bytesize threshold, if a value is larger than this it will be stored
22 | # on Arweave and the transaction id itself will be returned
23 | ARWEAVE_BYTE_LIMIT=1024
24 |
25 | ## Ollama (if used, optional) ##
26 | OLLAMA_HOST=http://127.0.0.1
27 | OLLAMA_PORT=11434
28 | # if "true", automatically pull models from Ollama
29 | OLLAMA_AUTO_PULL=true
30 |
31 | ## Open AI (if used, required) ##
32 | OPENAI_API_KEY=
33 |
34 | ## Gemini (if used, required) ##
35 | GEMINI_API_KEY=
36 |
37 | ## OpenRouter (if used, required) ##
38 | OPENROUTER_API_KEY=
39 |
40 | ## Additional Services (optional)
41 | SERPER_API_KEY=
42 | JINA_API_KEY=
43 |
--------------------------------------------------------------------------------
/.github/workflows/build-aws-image.yaml:
--------------------------------------------------------------------------------
1 | name: Dria Oracle Image on AWS
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 |
7 | permissions:
8 | id-token: write
9 | contents: read
10 |
11 | jobs:
12 | build-and-push:
13 | name: Build and Push GPT Dev Image
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v3
19 |
20 | - name: Set up QEMU
21 | uses: docker/setup-qemu-action@v3
22 |
23 | - name: Set up Docker Buildx
24 | uses: docker/setup-buildx-action@v3
25 |
26 | - name: Configure AWS credentials
27 | uses: aws-actions/configure-aws-credentials@v4
28 | with:
29 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
30 | aws-region: us-east-1
31 |
32 | - name: Login to ECR
33 | uses: docker/login-action@v3
34 | with:
35 | registry: 974464413599.dkr.ecr.us-east-1.amazonaws.com
36 |
37 | - name: Get Unix Time
38 | id: timestamp
39 | run: echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT
40 |
41 | - name: Get SHA
42 | id: sha
43 | run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
44 |
45 | - name: Get Branch Name
46 | id: branch
47 | run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT
48 |
49 | - name: Set Image Tag
50 | id: itag
51 | run: echo "itag=${{ steps.branch.outputs.branch }}-${{ steps.sha.outputs.sha }}-${{ steps.timestamp.outputs.timestamp }}" >> $GITHUB_OUTPUT
52 |
53 | - name: Build and push
54 | uses: docker/build-push-action@v6
55 | env:
56 | IMAGE_TAG: ${{ steps.itag.outputs.itag }}
57 | with:
58 | target: gpt
59 | platforms: linux/amd64
60 | push: true
61 | tags: |
62 | 974464413599.dkr.ecr.us-east-1.amazonaws.com/dkn-l2-oracle-gpt:dev
63 | 974464413599.dkr.ecr.us-east-1.amazonaws.com/dkn-l2-oracle-gpt:${{ env.IMAGE_TAG }}
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .env*
3 | !.env.example
4 | .vscode
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = ["core", "contracts", "storage"]
4 | default-members = ["core"]
5 |
6 | [workspace.package]
7 | edition = "2021"
8 | version = "0.2.38"
9 | license = "Apache-2.0"
10 | readme = "README.md"
11 | authors = ["erhant"]
12 |
13 | [workspace.dependencies]
14 | # core
15 | alloy = { version = "0.8.0", features = ["full"] }
16 | alloy-chains = "0.1.24"
17 | tokio = { version = "1.39.2", features = [
18 | "macros",
19 | "rt-multi-thread",
20 | "signal",
21 | ] }
22 | tokio-util = "0.7.13"
23 |
24 | # workflows
25 | dkn-workflows = { git = "https://github.com/firstbatchxyz/dkn-compute-node" }
26 |
27 | # errors & logging & env
28 | env_logger = "0.11.5"
29 | eyre = "0.6.12"
30 | log = "0.4.22"
31 | dotenvy = "0.15.7"
32 |
33 | # utils
34 | async-trait = "0.1.81"
35 | reqwest = "0.12.5"
36 |
37 | # serde
38 | serde = "1.0.204"
39 | serde_json = "1.0.122"
40 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=$BUILDPLATFORM rust:1.81.0 as builder
2 |
3 | # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
4 | #
5 | # We use distroless, which allow the following platforms:
6 | # linux/amd64
7 | # linux/arm64
8 | # linux/arm
9 | #
10 | # To build an image & push them to Docker hub for this Dockerfile:
11 | #
12 | # docker buildx build --platform=linux/amd64,linux/arm64,linux/arm . -t firstbatch/dria-compute-node:latest --builder=dria-builder --push
13 | ARG BUILDPLATFORM
14 | ARG TARGETPLATFORM
15 | RUN echo "Build platform: $BUILDPLATFORM"
16 | RUN echo "Target platform: $TARGETPLATFORM"
17 |
18 | # build release binary
19 | WORKDIR /usr/src/app
20 | COPY . .
21 | RUN cargo build --release
22 |
23 | # copy release binary to distroless
24 | FROM --platform=$BUILDPLATFORM gcr.io/distroless/cc AS gpt
25 | COPY --from=builder /usr/src/app/target/release/dria-oracle /
26 |
27 | ENTRYPOINT ["./dria-oracle"]
28 |
--------------------------------------------------------------------------------
/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 Copyright 2023 FirstBatch
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # load .env
2 | ifneq (,$(wildcard ./.env))
3 | include .env
4 | export
5 | endif
6 |
7 | ###############################################################################
8 | .PHONY: launch # | Run with INFO logging & release mode
9 | launch:
10 | cargo run --release serve
11 |
12 | .PHONY: run # | Run with INFO logging
13 | run:
14 | cargo run serve
15 |
16 | .PHONY: debug # | Run with crate-level DEBUG logging & info-level workflows
17 | debug:
18 | RUST_LOG=none,dria_oracle=debug,dkn_workflows=debug,ollama_workflows=info cargo run serve
19 |
20 | ###############################################################################
21 | .PHONY: install # | Install to path
22 | install:
23 | cargo install --path .
24 |
25 | .PHONY: build # | Build
26 | build:
27 | cargo build
28 |
29 | .PHONY: docs # | Generate & open documentation
30 | docs:
31 | cargo doc --open --no-deps --document-private-items
32 |
33 | .PHONY: lint # | Run clippy
34 | lint:
35 | cargo clippy
36 |
37 | .PHONY: format # | Run formatter
38 | format:
39 | cargo fmt -v
40 |
41 | .PHONY: version # | Print version
42 | version:
43 | @cargo pkgid | cut -d# -f2
44 |
45 | .PHONY: test # | Run tests
46 | test:
47 | RUST_LOG=none,dria_oracle=info cargo test --all-features
48 |
49 | ###############################################################################
50 | # abi source can be given from outside, and defaults as shown here
51 | ABI_SRC_PATH?=../dria-contracts/artifacts
52 | ABI_DEST_PATH=./src/contracts/abi
53 |
54 | .PHONY: abis # | Copy contract abis from a neighbor repo (ABI_SRC_PATH).
55 | abis:
56 | @echo "Copying contract ABIs from $(ABI_SRC_PATH) to $(ABI_DEST_PATH)"
57 | cp $(ABI_SRC_PATH)/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.json $(ABI_DEST_PATH)/ERC20.json
58 | cp $(ABI_SRC_PATH)/contracts/llm/LLMOracleCoordinator.sol/LLMOracleCoordinator.json $(ABI_DEST_PATH)/LLMOracleCoordinator.json
59 | cp $(ABI_SRC_PATH)/contracts/llm/LLMOracleRegistry.sol/LLMOracleRegistry.json $(ABI_DEST_PATH)/LLMOracleRegistry.json
60 |
61 | ###############################################################################
62 | # https://stackoverflow.com/a/45843594
63 | .PHONY: help # | List targets
64 | help:
65 | @grep '^.PHONY: .* #' Makefile | sed 's/\.PHONY: \(.*\) # \(.*\)/\1 \2/' | expand -t20
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Dria Oracle Node
8 |
9 |
10 | Dria Oracle Node serves LLM workflow tasks directly from smart-contracts.
11 |
12 |
13 |
14 | ## Installation
15 |
16 | Install Dria Oracle Node with:
17 |
18 | ```sh
19 | cargo install --git https://github.com/firstbatchxyz/dria-oracle-node
20 | ```
21 |
22 | ## Setup
23 |
24 | Create an `.env` file by copying `.env.example`. You have to fill the following variables:
25 |
26 | - Get an RPC URL from a provider such as [Alchemy](https://www.alchemy.com/) or [Infura](https://www.infura.io/), and set it as `RPC_URL`.
27 | - Provide an Ethereum wallet secret key to `SECRET_KEY`, make sure it has funds to pay for gas and tokens.
28 |
29 | > [!NOTE]
30 | >
31 | > The contract addresses are determined with respect to the chain connected via RPC URL, but you can override it via `COORDINATOR_ADDRESS` environment variable.
32 | > In any case, you should not need to do this.
33 |
34 | > [!TIP]
35 | >
36 | > You can have multiple environment files, and specify them explicitly with the `-e` argument, e.g.
37 | >
38 | > ```sh
39 | > # base sepolia
40 | > dria-oracle -e ./.env.base-sepolia registrations
41 | >
42 | > # base mainnet
43 | > dria-oracle -e ./.env.base-mainnet registrations
44 | > ```
45 |
46 | ### Arweave
47 |
48 | You can save gas costs using [Arweave](https://arweave.org/):
49 |
50 | - Provide an Arweave wallet via `ARWEAVE_WALLET_PATH` variable so that you can use Arweave for large results. You can create one [here](https://arweave.app/).
51 | - You can set `ARWEAVE_BYTE_LIMIT` to determine the byte length threshold, beyond which values are uploaded to Arweave. It defaults to 1024, so any data less than that many bytes will be written as-is.
52 |
53 | If you omit Arweave, it will only use the client for downloading things from Arweave, but will never upload.
54 |
55 | ### LLM Providers
56 |
57 | As for the LLM providers:
58 |
59 | - If you are using Ollama, make sure it is running and the host & port are correct.
60 | - If you are using OpenAI, provide the `OPENAI_API_KEY`.
61 | - If you are using Gemini, provide the `GEMINI_API_KEY`.
62 | - If you are using OpenRouter, provide the `OPENROUTER_API_KEY`.
63 |
64 | ## Usage
65 |
66 | After installatioon, a binary called `dria-oracle` will be created. You can see the available commands with:
67 |
68 | ```sh
69 | dria-oracle help
70 | ```
71 |
72 | The CLI provides several methods to interact with the oracle contracts.
73 |
74 | - [Registration](#registration)
75 | - [Launching the Node](#launching-the-node)
76 | - [Viewing Tasks](#viewing-tasks)
77 | - [Balance & Rewards](#balance--rewards)
78 |
79 | > [!TIP]
80 | >
81 | > You can enable debug-level logs with the `-d` option.
82 |
83 | ### Registration
84 |
85 | To serve oracle requests, you **MUST** first register as your desired oracle type, i.e. `generator` or `validator`. These are handled by the registration commands `register` and `unregister` which accepts multiple arguments to register at once. You can then see your registrations with `registrations` command.
86 |
87 | Here is an example:
88 |
89 | ```sh
90 | # 1. Register as both generator and validator
91 | dria-oracle register generator validator
92 |
93 | # 2. See that you are registered
94 | dria-oracle registrations
95 |
96 | # 3. Unregister from validator
97 | dria-oracle unregister validator
98 | ```
99 |
100 | You will need to have some tokens in your balance, which will be approved automatically if required by the register command.
101 |
102 | > [!TIP]
103 | >
104 | > Using WETH, we can do this quite easily via `cast`:
105 | >
106 | > ```sh
107 | > cast send 0x4200000000000000000000000000000000000006 "deposit()()" --rpc-url https://mainnet.base.org --private-key $SECRET_KEY --value $AMOUNT
108 | > ```
109 | >
110 | > You can also convert back to ETH using:
111 | >
112 | > ```sh
113 | > cast send 0x4200000000000000000000000000000000000006 "withdraw(uint256)()" $AMOUNT --rpc-url https://mainnet.base.org --private-key $SECRET_KEY
114 | > ```
115 |
116 | ### Launching the Node
117 |
118 | We launch our node using the `serve` command, followed by models of our choice and the oracle type that we would like to serve.
119 | If we provide no oracle types, it will default to the ones that we are registered to.
120 |
121 | ```sh
122 | # run as generator
123 | dria-oracle serve -m=gpt-4o-mini -m=llama3.1:latest generator
124 |
125 | # run as validator
126 | dria-oracle serve -m=gpt-4o validator
127 |
128 | # run as kinds that you are registered to
129 | dria-oracle serve -m=gpt-4o
130 | ```
131 |
132 | We can start handling tasks from previous blocks until now, and then continue listening for more events:
133 |
134 | ```sh
135 | # start from 100th block until now, and subscribe to new events
136 | dria-oracle serve -m=gpt-4o --from=100
137 | ```
138 |
139 | > [!TIP]
140 | >
141 | > You can terminate the application from the terminal as usual (e.g. CTRL+C) to quit the node.
142 |
143 | Or, we can handle tasks between specific blocks only, the application will exit upon finishing blocks unlike before:
144 |
145 | ```sh
146 | # handle tasks between blocks 100 and 500
147 | dria-oracle serve -m=gpt-4o --from=100 --to=500
148 | ```
149 |
150 | Finally, we can handle an existing task specifically as well (if its unhandled for some reason):
151 |
152 | ```sh
153 | # note that if task id is given, `from` and `to` will be ignored
154 | dria-oracle serve -m=gpt-4o --task-id
155 | ```
156 |
157 | > [!WARNING]
158 | >
159 | > Validators must use `gpt-4o` model.
160 |
161 | #### Using Arweave
162 |
163 | To save from gas fees, an Oracle node can upload its response to Arweave and then store the transaction id of that upload to the contract instead. This is differentiated by looking at the response, and see that it is exactly 64 hexadecimal characters. It is then decoded from hex and encoded to `base64url` format, which can then be used to access the data at `https//arweave.net/{txid-here}`. This **requires** an Arweave wallet.
164 |
165 | Following the same logic, the Oracle node can read task inputs from Arweave as well. This **does not require** an Arweave a wallet.
166 |
167 | ### Viewing Tasks
168 |
169 | You can `view` the details of a task by its task id:
170 |
171 | ```sh
172 | dria-oracle view --task-id
173 | ```
174 |
175 | You can also view the task status updates between blocks with the same command, by providing `from` and `to` blocks,
176 | which defaults from `earliest` block to `latest` block if none is provided.
177 |
178 | ```sh
179 | dria-oracle view # earliest to latest
180 | dria-oracle view --from=100 # 100 to latest
181 | dria-oracle view --to=100 # earliest to 100
182 | dria-oracle view --from=100 --to=200 # 100 to 200
183 | ```
184 |
185 | ### Balance & Rewards
186 |
187 | At any time, you can see your balance with:
188 |
189 | ```sh
190 | dria-oracle balance
191 | ```
192 |
193 | As you respond to tasks, you will have rewards available to you. You can see & claim them using your node:
194 |
195 | ```sh
196 | # print rewards
197 | dria-oracle rewards
198 |
199 | # claim all rewards
200 | dria-oracle claim
201 | ```
202 |
203 | ### Making a Request
204 |
205 | Although the oracle is only supposed to serve requests made from other parties, it is also able to make requests from the CLI. See usage with the help option:
206 |
207 | ```sh
208 | dria-oracle request -h
209 | ```
210 |
211 | It mainly takes an input argument, followed by multiple model arguments:
212 |
213 | ```sh
214 | dria-oracle request "What is 2+2?" gpt-4o-mini phi3:3.8b
215 | ```
216 |
217 | ## Development
218 |
219 | If you would like to contribute, please create an issue first! To start developing, clone the repository:
220 |
221 | ```sh
222 | git clone https://github.com/firstbatchxyz/dria-oracle-node.git
223 | ```
224 |
225 | ### Testing
226 |
227 | Run tests with:
228 |
229 | ```sh
230 | make test
231 | ```
232 |
233 | ### Documentation
234 |
235 | You can view the inline documentation with:
236 |
237 | ```sh
238 | make docs
239 | ```
240 |
241 | ### Styling
242 |
243 | Lint and format with:
244 |
245 | ```sh
246 | make lint # clippy
247 | make format # rustfmt
248 | ```
249 |
250 | ## License
251 |
252 | This project is licensed under the [Apache License 2.0](https://opensource.org/license/Apache-2.0).
253 |
--------------------------------------------------------------------------------
/contracts/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dria-oracle-contracts"
3 | description = "Dria Oracle Contract Interfaces"
4 | version.workspace = true
5 | edition.workspace = true
6 | license.workspace = true
7 | authors.workspace = true
8 |
9 | [dependencies]
10 | alloy.workspace = true
11 | alloy-chains.workspace = true
12 | eyre.workspace = true
13 |
--------------------------------------------------------------------------------
/contracts/abi/ERC20.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "inputs": [
4 | {
5 | "internalType": "address",
6 | "name": "spender",
7 | "type": "address"
8 | },
9 | {
10 | "internalType": "uint256",
11 | "name": "allowance",
12 | "type": "uint256"
13 | },
14 | {
15 | "internalType": "uint256",
16 | "name": "needed",
17 | "type": "uint256"
18 | }
19 | ],
20 | "name": "ERC20InsufficientAllowance",
21 | "type": "error"
22 | },
23 | {
24 | "inputs": [
25 | {
26 | "internalType": "address",
27 | "name": "sender",
28 | "type": "address"
29 | },
30 | {
31 | "internalType": "uint256",
32 | "name": "balance",
33 | "type": "uint256"
34 | },
35 | {
36 | "internalType": "uint256",
37 | "name": "needed",
38 | "type": "uint256"
39 | }
40 | ],
41 | "name": "ERC20InsufficientBalance",
42 | "type": "error"
43 | },
44 | {
45 | "inputs": [
46 | {
47 | "internalType": "address",
48 | "name": "approver",
49 | "type": "address"
50 | }
51 | ],
52 | "name": "ERC20InvalidApprover",
53 | "type": "error"
54 | },
55 | {
56 | "inputs": [
57 | {
58 | "internalType": "address",
59 | "name": "receiver",
60 | "type": "address"
61 | }
62 | ],
63 | "name": "ERC20InvalidReceiver",
64 | "type": "error"
65 | },
66 | {
67 | "inputs": [
68 | {
69 | "internalType": "address",
70 | "name": "sender",
71 | "type": "address"
72 | }
73 | ],
74 | "name": "ERC20InvalidSender",
75 | "type": "error"
76 | },
77 | {
78 | "inputs": [
79 | {
80 | "internalType": "address",
81 | "name": "spender",
82 | "type": "address"
83 | }
84 | ],
85 | "name": "ERC20InvalidSpender",
86 | "type": "error"
87 | },
88 | {
89 | "anonymous": false,
90 | "inputs": [
91 | {
92 | "indexed": true,
93 | "internalType": "address",
94 | "name": "owner",
95 | "type": "address"
96 | },
97 | {
98 | "indexed": true,
99 | "internalType": "address",
100 | "name": "spender",
101 | "type": "address"
102 | },
103 | {
104 | "indexed": false,
105 | "internalType": "uint256",
106 | "name": "value",
107 | "type": "uint256"
108 | }
109 | ],
110 | "name": "Approval",
111 | "type": "event"
112 | },
113 | {
114 | "anonymous": false,
115 | "inputs": [
116 | {
117 | "indexed": true,
118 | "internalType": "address",
119 | "name": "from",
120 | "type": "address"
121 | },
122 | {
123 | "indexed": true,
124 | "internalType": "address",
125 | "name": "to",
126 | "type": "address"
127 | },
128 | {
129 | "indexed": false,
130 | "internalType": "uint256",
131 | "name": "value",
132 | "type": "uint256"
133 | }
134 | ],
135 | "name": "Transfer",
136 | "type": "event"
137 | },
138 | {
139 | "inputs": [
140 | {
141 | "internalType": "address",
142 | "name": "owner",
143 | "type": "address"
144 | },
145 | {
146 | "internalType": "address",
147 | "name": "spender",
148 | "type": "address"
149 | }
150 | ],
151 | "name": "allowance",
152 | "outputs": [
153 | {
154 | "internalType": "uint256",
155 | "name": "",
156 | "type": "uint256"
157 | }
158 | ],
159 | "stateMutability": "view",
160 | "type": "function"
161 | },
162 | {
163 | "inputs": [
164 | {
165 | "internalType": "address",
166 | "name": "spender",
167 | "type": "address"
168 | },
169 | {
170 | "internalType": "uint256",
171 | "name": "value",
172 | "type": "uint256"
173 | }
174 | ],
175 | "name": "approve",
176 | "outputs": [
177 | {
178 | "internalType": "bool",
179 | "name": "",
180 | "type": "bool"
181 | }
182 | ],
183 | "stateMutability": "nonpayable",
184 | "type": "function"
185 | },
186 | {
187 | "inputs": [
188 | {
189 | "internalType": "address",
190 | "name": "account",
191 | "type": "address"
192 | }
193 | ],
194 | "name": "balanceOf",
195 | "outputs": [
196 | {
197 | "internalType": "uint256",
198 | "name": "",
199 | "type": "uint256"
200 | }
201 | ],
202 | "stateMutability": "view",
203 | "type": "function"
204 | },
205 | {
206 | "inputs": [],
207 | "name": "decimals",
208 | "outputs": [
209 | {
210 | "internalType": "uint8",
211 | "name": "",
212 | "type": "uint8"
213 | }
214 | ],
215 | "stateMutability": "view",
216 | "type": "function"
217 | },
218 | {
219 | "inputs": [],
220 | "name": "name",
221 | "outputs": [
222 | {
223 | "internalType": "string",
224 | "name": "",
225 | "type": "string"
226 | }
227 | ],
228 | "stateMutability": "view",
229 | "type": "function"
230 | },
231 | {
232 | "inputs": [],
233 | "name": "symbol",
234 | "outputs": [
235 | {
236 | "internalType": "string",
237 | "name": "",
238 | "type": "string"
239 | }
240 | ],
241 | "stateMutability": "view",
242 | "type": "function"
243 | },
244 | {
245 | "inputs": [],
246 | "name": "totalSupply",
247 | "outputs": [
248 | {
249 | "internalType": "uint256",
250 | "name": "",
251 | "type": "uint256"
252 | }
253 | ],
254 | "stateMutability": "view",
255 | "type": "function"
256 | },
257 | {
258 | "inputs": [
259 | {
260 | "internalType": "address",
261 | "name": "to",
262 | "type": "address"
263 | },
264 | {
265 | "internalType": "uint256",
266 | "name": "value",
267 | "type": "uint256"
268 | }
269 | ],
270 | "name": "transfer",
271 | "outputs": [
272 | {
273 | "internalType": "bool",
274 | "name": "",
275 | "type": "bool"
276 | }
277 | ],
278 | "stateMutability": "nonpayable",
279 | "type": "function"
280 | },
281 | {
282 | "inputs": [
283 | {
284 | "internalType": "address",
285 | "name": "from",
286 | "type": "address"
287 | },
288 | {
289 | "internalType": "address",
290 | "name": "to",
291 | "type": "address"
292 | },
293 | {
294 | "internalType": "uint256",
295 | "name": "value",
296 | "type": "uint256"
297 | }
298 | ],
299 | "name": "transferFrom",
300 | "outputs": [
301 | {
302 | "internalType": "bool",
303 | "name": "",
304 | "type": "bool"
305 | }
306 | ],
307 | "stateMutability": "nonpayable",
308 | "type": "function"
309 | }
310 | ]
311 |
--------------------------------------------------------------------------------
/contracts/abi/IWETH9.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "anonymous": false,
4 | "inputs": [
5 | {
6 | "indexed": true,
7 | "internalType": "address",
8 | "name": "src",
9 | "type": "address"
10 | },
11 | {
12 | "indexed": true,
13 | "internalType": "address",
14 | "name": "guy",
15 | "type": "address"
16 | },
17 | {
18 | "indexed": false,
19 | "internalType": "uint256",
20 | "name": "wad",
21 | "type": "uint256"
22 | }
23 | ],
24 | "name": "Approval",
25 | "type": "event"
26 | },
27 | {
28 | "anonymous": false,
29 | "inputs": [
30 | {
31 | "indexed": true,
32 | "internalType": "address",
33 | "name": "dst",
34 | "type": "address"
35 | },
36 | {
37 | "indexed": false,
38 | "internalType": "uint256",
39 | "name": "wad",
40 | "type": "uint256"
41 | }
42 | ],
43 | "name": "Deposit",
44 | "type": "event"
45 | },
46 | {
47 | "anonymous": false,
48 | "inputs": [
49 | {
50 | "indexed": true,
51 | "internalType": "address",
52 | "name": "src",
53 | "type": "address"
54 | },
55 | {
56 | "indexed": true,
57 | "internalType": "address",
58 | "name": "dst",
59 | "type": "address"
60 | },
61 | {
62 | "indexed": false,
63 | "internalType": "uint256",
64 | "name": "wad",
65 | "type": "uint256"
66 | }
67 | ],
68 | "name": "Transfer",
69 | "type": "event"
70 | },
71 | {
72 | "anonymous": false,
73 | "inputs": [
74 | {
75 | "indexed": true,
76 | "internalType": "address",
77 | "name": "src",
78 | "type": "address"
79 | },
80 | {
81 | "indexed": false,
82 | "internalType": "uint256",
83 | "name": "wad",
84 | "type": "uint256"
85 | }
86 | ],
87 | "name": "Withdrawal",
88 | "type": "event"
89 | },
90 | {
91 | "payable": true,
92 | "stateMutability": "payable",
93 | "type": "fallback"
94 | },
95 | {
96 | "constant": true,
97 | "inputs": [
98 | {
99 | "internalType": "address",
100 | "name": "",
101 | "type": "address"
102 | },
103 | {
104 | "internalType": "address",
105 | "name": "",
106 | "type": "address"
107 | }
108 | ],
109 | "name": "allowance",
110 | "outputs": [
111 | {
112 | "internalType": "uint256",
113 | "name": "",
114 | "type": "uint256"
115 | }
116 | ],
117 | "payable": false,
118 | "stateMutability": "view",
119 | "type": "function"
120 | },
121 | {
122 | "constant": false,
123 | "inputs": [
124 | {
125 | "internalType": "address",
126 | "name": "guy",
127 | "type": "address"
128 | },
129 | {
130 | "internalType": "uint256",
131 | "name": "wad",
132 | "type": "uint256"
133 | }
134 | ],
135 | "name": "approve",
136 | "outputs": [
137 | {
138 | "internalType": "bool",
139 | "name": "",
140 | "type": "bool"
141 | }
142 | ],
143 | "payable": false,
144 | "stateMutability": "nonpayable",
145 | "type": "function"
146 | },
147 | {
148 | "constant": true,
149 | "inputs": [
150 | {
151 | "internalType": "address",
152 | "name": "",
153 | "type": "address"
154 | }
155 | ],
156 | "name": "balanceOf",
157 | "outputs": [
158 | {
159 | "internalType": "uint256",
160 | "name": "",
161 | "type": "uint256"
162 | }
163 | ],
164 | "payable": false,
165 | "stateMutability": "view",
166 | "type": "function"
167 | },
168 | {
169 | "constant": true,
170 | "inputs": [],
171 | "name": "decimals",
172 | "outputs": [
173 | {
174 | "internalType": "uint8",
175 | "name": "",
176 | "type": "uint8"
177 | }
178 | ],
179 | "payable": false,
180 | "stateMutability": "view",
181 | "type": "function"
182 | },
183 | {
184 | "constant": false,
185 | "inputs": [],
186 | "name": "deposit",
187 | "outputs": [],
188 | "payable": true,
189 | "stateMutability": "payable",
190 | "type": "function"
191 | },
192 | {
193 | "constant": true,
194 | "inputs": [],
195 | "name": "name",
196 | "outputs": [
197 | {
198 | "internalType": "string",
199 | "name": "",
200 | "type": "string"
201 | }
202 | ],
203 | "payable": false,
204 | "stateMutability": "view",
205 | "type": "function"
206 | },
207 | {
208 | "constant": true,
209 | "inputs": [],
210 | "name": "symbol",
211 | "outputs": [
212 | {
213 | "internalType": "string",
214 | "name": "",
215 | "type": "string"
216 | }
217 | ],
218 | "payable": false,
219 | "stateMutability": "view",
220 | "type": "function"
221 | },
222 | {
223 | "constant": true,
224 | "inputs": [],
225 | "name": "totalSupply",
226 | "outputs": [
227 | {
228 | "internalType": "uint256",
229 | "name": "",
230 | "type": "uint256"
231 | }
232 | ],
233 | "payable": false,
234 | "stateMutability": "view",
235 | "type": "function"
236 | },
237 | {
238 | "constant": false,
239 | "inputs": [
240 | {
241 | "internalType": "address",
242 | "name": "dst",
243 | "type": "address"
244 | },
245 | {
246 | "internalType": "uint256",
247 | "name": "wad",
248 | "type": "uint256"
249 | }
250 | ],
251 | "name": "transfer",
252 | "outputs": [
253 | {
254 | "internalType": "bool",
255 | "name": "",
256 | "type": "bool"
257 | }
258 | ],
259 | "payable": false,
260 | "stateMutability": "nonpayable",
261 | "type": "function"
262 | },
263 | {
264 | "constant": false,
265 | "inputs": [
266 | {
267 | "internalType": "address",
268 | "name": "src",
269 | "type": "address"
270 | },
271 | {
272 | "internalType": "address",
273 | "name": "dst",
274 | "type": "address"
275 | },
276 | {
277 | "internalType": "uint256",
278 | "name": "wad",
279 | "type": "uint256"
280 | }
281 | ],
282 | "name": "transferFrom",
283 | "outputs": [
284 | {
285 | "internalType": "bool",
286 | "name": "",
287 | "type": "bool"
288 | }
289 | ],
290 | "payable": false,
291 | "stateMutability": "nonpayable",
292 | "type": "function"
293 | },
294 | {
295 | "constant": false,
296 | "inputs": [
297 | {
298 | "internalType": "uint256",
299 | "name": "wad",
300 | "type": "uint256"
301 | }
302 | ],
303 | "name": "withdraw",
304 | "outputs": [],
305 | "payable": false,
306 | "stateMutability": "nonpayable",
307 | "type": "function"
308 | }
309 | ]
310 |
--------------------------------------------------------------------------------
/contracts/abi/LLMOracleRegistry.json:
--------------------------------------------------------------------------------
1 | [
2 | { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" },
3 | {
4 | "type": "function",
5 | "name": "UPGRADE_INTERFACE_VERSION",
6 | "inputs": [],
7 | "outputs": [{ "name": "", "type": "string", "internalType": "string" }],
8 | "stateMutability": "view"
9 | },
10 | {
11 | "type": "function",
12 | "name": "addToWhitelist",
13 | "inputs": [
14 | { "name": "accounts", "type": "address[]", "internalType": "address[]" }
15 | ],
16 | "outputs": [],
17 | "stateMutability": "nonpayable"
18 | },
19 | {
20 | "type": "function",
21 | "name": "generatorStakeAmount",
22 | "inputs": [],
23 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }],
24 | "stateMutability": "view"
25 | },
26 | {
27 | "type": "function",
28 | "name": "getStakeAmount",
29 | "inputs": [
30 | {
31 | "name": "kind",
32 | "type": "uint8",
33 | "internalType": "enum LLMOracleKind"
34 | }
35 | ],
36 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }],
37 | "stateMutability": "view"
38 | },
39 | {
40 | "type": "function",
41 | "name": "initialize",
42 | "inputs": [
43 | {
44 | "name": "_generatorStakeAmount",
45 | "type": "uint256",
46 | "internalType": "uint256"
47 | },
48 | {
49 | "name": "_validatorStakeAmount",
50 | "type": "uint256",
51 | "internalType": "uint256"
52 | },
53 | { "name": "_token", "type": "address", "internalType": "address" },
54 | {
55 | "name": "_minRegistrationTime",
56 | "type": "uint256",
57 | "internalType": "uint256"
58 | }
59 | ],
60 | "outputs": [],
61 | "stateMutability": "nonpayable"
62 | },
63 | {
64 | "type": "function",
65 | "name": "isRegistered",
66 | "inputs": [
67 | { "name": "user", "type": "address", "internalType": "address" },
68 | {
69 | "name": "kind",
70 | "type": "uint8",
71 | "internalType": "enum LLMOracleKind"
72 | }
73 | ],
74 | "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }],
75 | "stateMutability": "view"
76 | },
77 | {
78 | "type": "function",
79 | "name": "isWhitelisted",
80 | "inputs": [{ "name": "", "type": "address", "internalType": "address" }],
81 | "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }],
82 | "stateMutability": "view"
83 | },
84 | {
85 | "type": "function",
86 | "name": "minRegistrationTime",
87 | "inputs": [],
88 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }],
89 | "stateMutability": "view"
90 | },
91 | {
92 | "type": "function",
93 | "name": "owner",
94 | "inputs": [],
95 | "outputs": [{ "name": "", "type": "address", "internalType": "address" }],
96 | "stateMutability": "view"
97 | },
98 | {
99 | "type": "function",
100 | "name": "proxiableUUID",
101 | "inputs": [],
102 | "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }],
103 | "stateMutability": "view"
104 | },
105 | {
106 | "type": "function",
107 | "name": "register",
108 | "inputs": [
109 | {
110 | "name": "kind",
111 | "type": "uint8",
112 | "internalType": "enum LLMOracleKind"
113 | }
114 | ],
115 | "outputs": [],
116 | "stateMutability": "nonpayable"
117 | },
118 | {
119 | "type": "function",
120 | "name": "registrationTimes",
121 | "inputs": [
122 | { "name": "oracle", "type": "address", "internalType": "address" },
123 | { "name": "", "type": "uint8", "internalType": "enum LLMOracleKind" }
124 | ],
125 | "outputs": [
126 | {
127 | "name": "registeredTime",
128 | "type": "uint256",
129 | "internalType": "uint256"
130 | }
131 | ],
132 | "stateMutability": "view"
133 | },
134 | {
135 | "type": "function",
136 | "name": "registrations",
137 | "inputs": [
138 | { "name": "oracle", "type": "address", "internalType": "address" },
139 | { "name": "", "type": "uint8", "internalType": "enum LLMOracleKind" }
140 | ],
141 | "outputs": [
142 | { "name": "amount", "type": "uint256", "internalType": "uint256" }
143 | ],
144 | "stateMutability": "view"
145 | },
146 | {
147 | "type": "function",
148 | "name": "removeFromWhitelist",
149 | "inputs": [
150 | { "name": "account", "type": "address", "internalType": "address" }
151 | ],
152 | "outputs": [],
153 | "stateMutability": "nonpayable"
154 | },
155 | {
156 | "type": "function",
157 | "name": "renounceOwnership",
158 | "inputs": [],
159 | "outputs": [],
160 | "stateMutability": "nonpayable"
161 | },
162 | {
163 | "type": "function",
164 | "name": "setStakeAmounts",
165 | "inputs": [
166 | {
167 | "name": "_generatorStakeAmount",
168 | "type": "uint256",
169 | "internalType": "uint256"
170 | },
171 | {
172 | "name": "_validatorStakeAmount",
173 | "type": "uint256",
174 | "internalType": "uint256"
175 | }
176 | ],
177 | "outputs": [],
178 | "stateMutability": "nonpayable"
179 | },
180 | {
181 | "type": "function",
182 | "name": "token",
183 | "inputs": [],
184 | "outputs": [
185 | { "name": "", "type": "address", "internalType": "contract ERC20" }
186 | ],
187 | "stateMutability": "view"
188 | },
189 | {
190 | "type": "function",
191 | "name": "transferOwnership",
192 | "inputs": [
193 | { "name": "newOwner", "type": "address", "internalType": "address" }
194 | ],
195 | "outputs": [],
196 | "stateMutability": "nonpayable"
197 | },
198 | {
199 | "type": "function",
200 | "name": "unregister",
201 | "inputs": [
202 | {
203 | "name": "kind",
204 | "type": "uint8",
205 | "internalType": "enum LLMOracleKind"
206 | }
207 | ],
208 | "outputs": [
209 | { "name": "amount", "type": "uint256", "internalType": "uint256" }
210 | ],
211 | "stateMutability": "nonpayable"
212 | },
213 | {
214 | "type": "function",
215 | "name": "upgradeToAndCall",
216 | "inputs": [
217 | {
218 | "name": "newImplementation",
219 | "type": "address",
220 | "internalType": "address"
221 | },
222 | { "name": "data", "type": "bytes", "internalType": "bytes" }
223 | ],
224 | "outputs": [],
225 | "stateMutability": "payable"
226 | },
227 | {
228 | "type": "function",
229 | "name": "validatorStakeAmount",
230 | "inputs": [],
231 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }],
232 | "stateMutability": "view"
233 | },
234 | {
235 | "type": "event",
236 | "name": "AddedToWhitelist",
237 | "inputs": [
238 | {
239 | "name": "account",
240 | "type": "address",
241 | "indexed": true,
242 | "internalType": "address"
243 | }
244 | ],
245 | "anonymous": false
246 | },
247 | {
248 | "type": "event",
249 | "name": "Initialized",
250 | "inputs": [
251 | {
252 | "name": "version",
253 | "type": "uint64",
254 | "indexed": false,
255 | "internalType": "uint64"
256 | }
257 | ],
258 | "anonymous": false
259 | },
260 | {
261 | "type": "event",
262 | "name": "OwnershipTransferred",
263 | "inputs": [
264 | {
265 | "name": "previousOwner",
266 | "type": "address",
267 | "indexed": true,
268 | "internalType": "address"
269 | },
270 | {
271 | "name": "newOwner",
272 | "type": "address",
273 | "indexed": true,
274 | "internalType": "address"
275 | }
276 | ],
277 | "anonymous": false
278 | },
279 | {
280 | "type": "event",
281 | "name": "Registered",
282 | "inputs": [
283 | {
284 | "name": "",
285 | "type": "address",
286 | "indexed": true,
287 | "internalType": "address"
288 | },
289 | {
290 | "name": "kind",
291 | "type": "uint8",
292 | "indexed": false,
293 | "internalType": "enum LLMOracleKind"
294 | }
295 | ],
296 | "anonymous": false
297 | },
298 | {
299 | "type": "event",
300 | "name": "RemovedFromWhitelist",
301 | "inputs": [
302 | {
303 | "name": "account",
304 | "type": "address",
305 | "indexed": true,
306 | "internalType": "address"
307 | }
308 | ],
309 | "anonymous": false
310 | },
311 | {
312 | "type": "event",
313 | "name": "Unregistered",
314 | "inputs": [
315 | {
316 | "name": "",
317 | "type": "address",
318 | "indexed": true,
319 | "internalType": "address"
320 | },
321 | {
322 | "name": "kind",
323 | "type": "uint8",
324 | "indexed": false,
325 | "internalType": "enum LLMOracleKind"
326 | }
327 | ],
328 | "anonymous": false
329 | },
330 | {
331 | "type": "event",
332 | "name": "Upgraded",
333 | "inputs": [
334 | {
335 | "name": "implementation",
336 | "type": "address",
337 | "indexed": true,
338 | "internalType": "address"
339 | }
340 | ],
341 | "anonymous": false
342 | },
343 | {
344 | "type": "error",
345 | "name": "AddressEmptyCode",
346 | "inputs": [
347 | { "name": "target", "type": "address", "internalType": "address" }
348 | ]
349 | },
350 | {
351 | "type": "error",
352 | "name": "AlreadyRegistered",
353 | "inputs": [{ "name": "", "type": "address", "internalType": "address" }]
354 | },
355 | {
356 | "type": "error",
357 | "name": "ERC1967InvalidImplementation",
358 | "inputs": [
359 | {
360 | "name": "implementation",
361 | "type": "address",
362 | "internalType": "address"
363 | }
364 | ]
365 | },
366 | { "type": "error", "name": "ERC1967NonPayable", "inputs": [] },
367 | { "type": "error", "name": "FailedCall", "inputs": [] },
368 | { "type": "error", "name": "InsufficientFunds", "inputs": [] },
369 | { "type": "error", "name": "InvalidInitialization", "inputs": [] },
370 | { "type": "error", "name": "NotInitializing", "inputs": [] },
371 | {
372 | "type": "error",
373 | "name": "NotRegistered",
374 | "inputs": [{ "name": "", "type": "address", "internalType": "address" }]
375 | },
376 | {
377 | "type": "error",
378 | "name": "NotWhitelisted",
379 | "inputs": [
380 | { "name": "validator", "type": "address", "internalType": "address" }
381 | ]
382 | },
383 | {
384 | "type": "error",
385 | "name": "OwnableInvalidOwner",
386 | "inputs": [
387 | { "name": "owner", "type": "address", "internalType": "address" }
388 | ]
389 | },
390 | {
391 | "type": "error",
392 | "name": "OwnableUnauthorizedAccount",
393 | "inputs": [
394 | { "name": "account", "type": "address", "internalType": "address" }
395 | ]
396 | },
397 | {
398 | "type": "error",
399 | "name": "TooEarlyToUnregister",
400 | "inputs": [
401 | {
402 | "name": "minTimeToWait",
403 | "type": "uint256",
404 | "internalType": "uint256"
405 | }
406 | ]
407 | },
408 | { "type": "error", "name": "UUPSUnauthorizedCallContext", "inputs": [] },
409 | {
410 | "type": "error",
411 | "name": "UUPSUnsupportedProxiableUUID",
412 | "inputs": [{ "name": "slot", "type": "bytes32", "internalType": "bytes32" }]
413 | }
414 | ]
415 |
--------------------------------------------------------------------------------
/contracts/src/addresses.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::{address, Address};
2 | use alloy_chains::NamedChain;
3 |
4 | /// Contract addresses.
5 | ///
6 | /// All contracts can be derived from the `coordinator` contract.
7 | #[derive(Debug, Clone)]
8 | pub struct ContractAddresses {
9 | /// Token used within the registry and coordinator.
10 | pub token: Address,
11 | /// Oracle registry.
12 | pub registry: Address,
13 | /// Oracle coordinator.
14 | pub coordinator: Address,
15 | }
16 |
17 | impl std::fmt::Display for ContractAddresses {
18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 | write!(
20 | f,
21 | "Contract Addresses:\n Token: {}\n Registry: {}\n Coordinator: {}",
22 | self.token, self.registry, self.coordinator
23 | )
24 | }
25 | }
26 |
27 | /// Returns the coordinator contract address for a given chain.
28 | ///
29 | /// Will return an error if the chain is not supported, i.e. a coordinator address
30 | /// is not deployed there.
31 | pub fn get_coordinator_address(chain: NamedChain) -> eyre::Result {
32 | let addresses = match chain {
33 | NamedChain::AnvilHardhat => address!("9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"),
34 | NamedChain::BaseSepolia => address!("13f977bde221b470d3ae055cde7e1f84debfe202"),
35 | NamedChain::Base => address!("17b6d1eddcd5f9ca19bb2ffed2f3deb6bd74bd20"),
36 | _ => return Err(eyre::eyre!("Chain {} is not supported", chain)),
37 | };
38 |
39 | Ok(addresses)
40 | }
41 |
--------------------------------------------------------------------------------
/contracts/src/balance.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::{utils::format_ether, Address, U256};
2 | use std::fmt::Display;
3 |
4 | /// A token balance contains amount, token symbol and the token address if its non-native token.
5 | #[derive(Debug)]
6 | pub struct TokenBalance {
7 | /// Amount of tokens as bigint.
8 | pub amount: U256,
9 | /// Token symbol, for display purposes.
10 | pub symbol: String,
11 | /// Token contract address, `None` if its ETH (native token).
12 | pub address: Option,
13 | }
14 |
15 | impl TokenBalance {
16 | /// Create a new token result.
17 | pub fn new(amount: U256, symbol: impl ToString, address: Option) -> Self {
18 | Self {
19 | amount,
20 | symbol: symbol.to_string(),
21 | address,
22 | }
23 | }
24 | }
25 |
26 | impl Display for TokenBalance {
27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 | write!(
29 | f,
30 | "{} {} {}",
31 | format_ether(self.amount),
32 | self.symbol,
33 | self.address.map(|s| s.to_string()).unwrap_or_default() // empty-string if `None`
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/contracts/src/errors.rs:
--------------------------------------------------------------------------------
1 | use alloy::contract::Error;
2 | use alloy::primitives::utils::format_ether;
3 | use eyre::{eyre, ErrReport};
4 |
5 | use super::OracleCoordinator::OracleCoordinatorErrors;
6 | use super::OracleRegistry::OracleRegistryErrors;
7 | use super::ERC20::ERC20Errors;
8 |
9 | /// Generic contract error reporter, handles custom errors for known contracts such as ERC20, LLMOracleRegistry, and LLMOracleCoordinator.
10 | ///
11 | /// The given contract error is matched against known contract errors and a custom error message is returned
12 | pub fn contract_error_report(error: Error) -> ErrReport {
13 | match error {
14 | Error::UnknownFunction(function) => {
15 | eyre!("Unknown function: function {} does not exist", function)
16 | }
17 | Error::UnknownSelector(selector) => eyre!(
18 | "Unknown function: function with selector {} does not exist",
19 | selector
20 | ),
21 | Error::PendingTransactionError(tx) => {
22 | eyre!("Transaction is pending: {:?}", tx)
23 | }
24 | Error::NotADeploymentTransaction => {
25 | eyre!("Transaction is not a deployment transaction")
26 | }
27 | Error::ContractNotDeployed => eyre!("Contract is not deployed"),
28 | Error::AbiError(e) => eyre!("An error occurred ABI encoding or decoding: {}", e),
29 | Error::TransportError(error) => {
30 | const VALIDATE: bool = false;
31 |
32 | // here we try to parse the error w.r.t provided contract interfaces
33 | // or return a default one in the end if it was not parsed successfully
34 | if let Some(payload) = error.as_error_resp() {
35 | // an ERC20 error
36 | if let Some(erc_20_error) = payload.as_decoded_error::(VALIDATE) {
37 | erc_20_error.into()
38 | } else
39 | // an OracleRegistry error
40 | if let Some(registry_error) =
41 | payload.as_decoded_error::(VALIDATE)
42 | {
43 | registry_error.into()
44 | } else
45 | // an OracleCoordinator error
46 | if let Some(coordinator_error) =
47 | payload.as_decoded_error::(VALIDATE)
48 | {
49 | coordinator_error.into()
50 | } else {
51 | eyre!("Unhandled error response: {:#?}", payload)
52 | }
53 | } else {
54 | eyre!("Unknown transport error: {:#?}", error)
55 | }
56 | }
57 | }
58 | }
59 |
60 | impl From for ErrReport {
61 | fn from(value: ERC20Errors) -> Self {
62 | match value {
63 | ERC20Errors::ERC20InsufficientAllowance(e) => eyre!(
64 | "Insufficient allowance for {} (have {}, need {})",
65 | e.spender,
66 | format_ether(e.allowance),
67 | format_ether(e.needed)
68 | ),
69 | ERC20Errors::ERC20InsufficientBalance(e) => eyre!(
70 | "Insufficient balance for {} (have {}, need {})",
71 | e.sender,
72 | format_ether(e.balance),
73 | format_ether(e.needed)
74 | ),
75 | ERC20Errors::ERC20InvalidReceiver(e) => {
76 | eyre!("Invalid receiver: {}", e.receiver)
77 | }
78 | ERC20Errors::ERC20InvalidApprover(e) => {
79 | eyre!("Invalid approver: {}", e.approver)
80 | }
81 | ERC20Errors::ERC20InvalidSender(e) => eyre!("Invalid sender: {}", e.sender),
82 | ERC20Errors::ERC20InvalidSpender(e) => eyre!("Invalid spender: {}", e.spender),
83 | }
84 | }
85 | }
86 |
87 | impl From for ErrReport {
88 | fn from(value: OracleRegistryErrors) -> Self {
89 | match value {
90 | OracleRegistryErrors::AlreadyRegistered(e) => {
91 | eyre!("Already registered: {}", e._0)
92 | }
93 | OracleRegistryErrors::InsufficientFunds(_) => eyre!("Insufficient funds."),
94 | OracleRegistryErrors::NotRegistered(e) => eyre!("Not registered: {}", e._0),
95 | OracleRegistryErrors::OwnableInvalidOwner(e) => {
96 | eyre!("Invalid owner: {}", e.owner)
97 | }
98 | OracleRegistryErrors::OwnableUnauthorizedAccount(e) => {
99 | eyre!("Unauthorized account: {}", e.account)
100 | } // _ => eyre!("Unhandled Oracle registry error"),
101 | OracleRegistryErrors::TooEarlyToUnregister(e) => {
102 | eyre!(
103 | "Too early to unregister: {} secs remaining",
104 | e.minTimeToWait
105 | )
106 | }
107 | OracleRegistryErrors::NotWhitelisted(e) => {
108 | eyre!("Validator {} is not whitelisted", e.validator)
109 | }
110 | // generic
111 | OracleRegistryErrors::FailedCall(_) => {
112 | eyre!("Failed call")
113 | }
114 | OracleRegistryErrors::ERC1967InvalidImplementation(e) => {
115 | eyre!("Invalid implementation: {}", e.implementation)
116 | }
117 | OracleRegistryErrors::UUPSUnauthorizedCallContext(_) => {
118 | eyre!("Unauthorized UUPS call context")
119 | }
120 | OracleRegistryErrors::UUPSUnsupportedProxiableUUID(e) => {
121 | eyre!("Unsupported UUPS proxiable UUID: {}", e.slot)
122 | }
123 | OracleRegistryErrors::ERC1967NonPayable(_) => {
124 | eyre!("ERC1967 Non-payable")
125 | }
126 | OracleRegistryErrors::InvalidInitialization(_) => {
127 | eyre!("Invalid initialization")
128 | }
129 | OracleRegistryErrors::AddressEmptyCode(e) => {
130 | eyre!("Address {} is empty", e.target)
131 | }
132 | OracleRegistryErrors::NotInitializing(_) => {
133 | eyre!("Not initializing")
134 | }
135 | }
136 | }
137 | }
138 |
139 | impl From for ErrReport {
140 | fn from(value: OracleCoordinatorErrors) -> Self {
141 | match value {
142 | OracleCoordinatorErrors::AlreadyResponded(e) => {
143 | eyre!("Already responded to task {}", e.taskId)
144 | }
145 | OracleCoordinatorErrors::InsufficientFees(e) => {
146 | eyre!("Insufficient fees (have: {}, want: {})", e.have, e.want)
147 | }
148 | OracleCoordinatorErrors::InvalidParameterRange(e) => {
149 | eyre!(
150 | "Invalid parameter range: {} <= {}* <= {}",
151 | e.min,
152 | e.have,
153 | e.max
154 | )
155 | }
156 | OracleCoordinatorErrors::InvalidNonce(e) => {
157 | eyre!("Invalid nonce for task: {} (nonce: {})", e.taskId, e.nonce)
158 | }
159 | OracleCoordinatorErrors::InvalidTaskStatus(e) => eyre!(
160 | "Invalid status for task: {} (have: {}, want: {})",
161 | e.taskId,
162 | e.have,
163 | e.want
164 | ),
165 | OracleCoordinatorErrors::InvalidValidation(e) => {
166 | eyre!("Invalid validation for task: {}", e.taskId)
167 | }
168 | OracleCoordinatorErrors::NotRegistered(e) => {
169 | eyre!("Not registered: {}", e.oracle)
170 | }
171 | OracleCoordinatorErrors::OwnableInvalidOwner(e) => {
172 | eyre!("Invalid owner: {}", e.owner)
173 | }
174 | OracleCoordinatorErrors::OwnableUnauthorizedAccount(e) => {
175 | eyre!("Unauthorized account: {}", e.account)
176 | }
177 | // generic
178 | OracleCoordinatorErrors::FailedInnerCall(_) => {
179 | eyre!("Failed inner call")
180 | }
181 | OracleCoordinatorErrors::ERC1967InvalidImplementation(e) => {
182 | eyre!("Invalid implementation: {}", e.implementation)
183 | }
184 | OracleCoordinatorErrors::UUPSUnauthorizedCallContext(_) => {
185 | eyre!("Unauthorized UUPS call context")
186 | }
187 | OracleCoordinatorErrors::UUPSUnsupportedProxiableUUID(e) => {
188 | eyre!("Unsupported UUPS proxiable UUID: {}", e.slot)
189 | }
190 | OracleCoordinatorErrors::ERC1967NonPayable(_) => {
191 | eyre!("ERC1967 Non-payable")
192 | }
193 | OracleCoordinatorErrors::InvalidInitialization(_) => {
194 | eyre!("Invalid initialization")
195 | }
196 | OracleCoordinatorErrors::AddressEmptyCode(e) => {
197 | eyre!("Address {} is empty", e.target)
198 | }
199 | OracleCoordinatorErrors::NotInitializing(_) => {
200 | eyre!("Not initializing")
201 | }
202 | }
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/contracts/src/interfaces.rs:
--------------------------------------------------------------------------------
1 | use alloy::{
2 | primitives::{Bytes, FixedBytes},
3 | sol,
4 | };
5 | use eyre::{Context, Result};
6 |
7 | use self::OracleCoordinator::StatusUpdate;
8 |
9 | // OpenZepeplin ERC20
10 | sol!(
11 | #[allow(missing_docs)]
12 | #[sol(rpc)]
13 | ERC20,
14 | "./abi/ERC20.json"
15 | );
16 |
17 | // Base WETH
18 | sol!(
19 | #[allow(missing_docs)]
20 | #[sol(rpc)]
21 | WETH,
22 | "./abi/IWETH9.json"
23 | );
24 |
25 | sol!(
26 | #[allow(missing_docs)]
27 | #[sol(rpc)]
28 | OracleRegistry,
29 | "./abi/LLMOracleRegistry.json"
30 | );
31 |
32 | sol!(
33 | #[allow(missing_docs)]
34 | #[sol(rpc)]
35 | OracleCoordinator,
36 | "./abi/LLMOracleCoordinator.json"
37 | );
38 |
39 | /// `OracleKind` as it appears within the registry.
40 | #[derive(Debug, Clone, Copy, PartialEq)]
41 | pub enum OracleKind {
42 | Generator,
43 | Validator,
44 | }
45 |
46 | impl From for u8 {
47 | fn from(kind: OracleKind) -> u8 {
48 | kind as u8
49 | }
50 | }
51 |
52 | impl TryFrom for OracleKind {
53 | type Error = eyre::Error;
54 |
55 | fn try_from(value: u8) -> Result {
56 | match value {
57 | 0 => Ok(OracleKind::Generator),
58 | 1 => Ok(OracleKind::Validator),
59 | _ => Err(eyre::eyre!("Invalid OracleKind: {}", value)),
60 | }
61 | }
62 | }
63 |
64 | impl std::fmt::Display for OracleKind {
65 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 | match self {
67 | OracleKind::Generator => write!(f, "Generator"),
68 | OracleKind::Validator => write!(f, "Validator"),
69 | }
70 | }
71 | }
72 |
73 | /// `TaskStatus` as it appears within the coordinator.
74 | #[derive(Debug, Clone, Copy, Default)]
75 | pub enum TaskStatus {
76 | #[default]
77 | None,
78 | PendingGeneration,
79 | PendingValidation,
80 | Completed,
81 | }
82 |
83 | impl From for u8 {
84 | fn from(status: TaskStatus) -> u8 {
85 | status as u8
86 | }
87 | }
88 |
89 | impl TryFrom for TaskStatus {
90 | type Error = eyre::Error;
91 |
92 | fn try_from(value: u8) -> Result {
93 | match value {
94 | 0 => Ok(TaskStatus::None),
95 | 1 => Ok(TaskStatus::PendingGeneration),
96 | 2 => Ok(TaskStatus::PendingValidation),
97 | 3 => Ok(TaskStatus::Completed),
98 | _ => Err(eyre::eyre!("Invalid TaskStatus: {}", value)),
99 | }
100 | }
101 | }
102 |
103 | impl std::fmt::Display for TaskStatus {
104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 | match self {
106 | Self::None => write!(f, "None"),
107 | Self::PendingGeneration => write!(f, "Pending Generation"),
108 | Self::PendingValidation => write!(f, "Pending Validation"),
109 | Self::Completed => write!(f, "Completed"),
110 | }
111 | }
112 | }
113 |
114 | impl std::fmt::Display for StatusUpdate {
115 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 | write!(
117 | f,
118 | "Task {}: {} -> {}",
119 | self.taskId, self.statusBefore, self.statusAfter
120 | )
121 | }
122 | }
123 |
124 | /// Small utility to convert bytes to string.
125 | #[inline(always)]
126 | pub fn bytes_to_string(bytes: &Bytes) -> Result {
127 | String::from_utf8(bytes.to_vec()).wrap_err("could not convert bytes to string")
128 | }
129 |
130 | /// Small utility to convert string to bytes.
131 | #[inline(always)]
132 | pub fn string_to_bytes(input: String) -> Bytes {
133 | input.into()
134 | }
135 |
136 | /// Small utility to convert bytes32 to string.
137 | /// Simply reads bytes until the first null byte.
138 | pub fn bytes32_to_string(bytes: &FixedBytes<32>) -> Result {
139 | let mut bytes = bytes.to_vec();
140 | if let Some(pos) = bytes.iter().position(|&b| b == 0) {
141 | bytes.truncate(pos);
142 | } else {
143 | return Err(eyre::eyre!("Could not find null terminator in bytes32"));
144 | }
145 | String::from_utf8(bytes).wrap_err("could not convert bytes to string")
146 | }
147 |
148 | /// Small utility to convert string to bytes32.
149 | /// Ensures that the string is at most 31 bytes long, and right-bads zeros for the null-terminator.
150 | pub fn string_to_bytes32(input: String) -> Result> {
151 | let mut bytes = input.into_bytes();
152 | if bytes.len() > 31 {
153 | return Err(eyre::eyre!("String is too long for bytes32"));
154 | }
155 |
156 | // append bytes via resizing
157 | bytes.resize(32, 0);
158 | Ok(FixedBytes::from_slice(&bytes))
159 | }
160 |
161 | #[cfg(test)]
162 | mod tests {
163 | use super::*;
164 |
165 | #[test]
166 | fn test_string_to_bytes() {
167 | let input = "hello".to_string();
168 | let bytes = string_to_bytes(input.clone());
169 | let string = bytes_to_string(&bytes).unwrap();
170 | assert_eq!(input, string);
171 | }
172 |
173 | #[test]
174 | fn test_string_to_bytes32() {
175 | let input = "hello".to_string();
176 | let bytes32 = string_to_bytes32(input.clone()).unwrap();
177 | let string = bytes32_to_string(&bytes32).unwrap();
178 | assert_eq!(input, string);
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/contracts/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod balance;
2 | pub use balance::*;
3 |
4 | mod addresses;
5 | pub use addresses::*;
6 |
7 | mod interfaces;
8 | pub use interfaces::*;
9 |
10 | mod errors;
11 | pub use errors::*;
12 |
--------------------------------------------------------------------------------
/core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dria-oracle"
3 | description = "Dria Knowledge Network Oracle Node"
4 | version.workspace = true
5 | edition.workspace = true
6 | license.workspace = true
7 | authors.workspace = true
8 |
9 | [features]
10 | anvil = ["alloy/node-bindings"]
11 |
12 | [dependencies]
13 | # core
14 | alloy.workspace = true
15 | alloy-chains.workspace = true
16 | tokio.workspace = true
17 | tokio-util.workspace = true
18 |
19 | # workflows
20 | dkn-workflows = { git = "https://github.com/firstbatchxyz/dkn-compute-node" }
21 |
22 | # errors & logging & env
23 | env_logger = "0.11.5"
24 | eyre = "0.6.12"
25 | log = "0.4.22"
26 | dotenvy = "0.15.7"
27 |
28 | # utils
29 | futures-util = "0.3.30"
30 | bytes = "1.7.1"
31 | rand = "0.8.5"
32 | reqwest.workspace = true
33 |
34 | # b64, hex, serde
35 | base64 = "0.22.1"
36 | hex = "0.4.3"
37 | hex-literal = "0.4.1"
38 | serde.workspace = true
39 | serde_json.workspace = true
40 |
41 | # cli
42 | clap = { version = "4.5.13", features = ["derive", "env"] }
43 |
44 | dria-oracle-storage = { path = "../storage" }
45 | dria-oracle-contracts = { path = "../contracts" }
46 |
--------------------------------------------------------------------------------
/core/src/cli/commands/coordinator/mod.rs:
--------------------------------------------------------------------------------
1 | use alloy::eips::BlockNumberOrTag;
2 | use eyre::{Context, Result};
3 | use futures_util::StreamExt;
4 | use std::time::Duration;
5 | use tokio_util::sync::CancellationToken;
6 |
7 | use crate::DriaOracle;
8 |
9 | mod request;
10 | mod serve;
11 | mod view;
12 |
13 | impl DriaOracle {
14 | /// Starts the oracle node.
15 | pub(in crate::cli) async fn serve(
16 | &self,
17 | from_block: Option,
18 | to_block: Option,
19 | cancellation: CancellationToken,
20 | ) -> Result<()> {
21 | log::info!(
22 | "Started oracle as {} using models: {}",
23 | self.kinds
24 | .iter()
25 | .map(|k| k.to_string())
26 | .collect::>()
27 | .join(", "),
28 | self.workflows
29 | .models
30 | .iter()
31 | .map(|(_, m)| m.to_string())
32 | .collect::>()
33 | .join(", ")
34 | );
35 |
36 | // check previous tasks if `from_block` is given
37 | if let Some(from_block) = from_block {
38 | tokio::select! {
39 | _ = cancellation.cancelled() => {
40 | log::debug!("Cancellation signal received. Stopping...");
41 | return Ok(());
42 | }
43 | result = self.process_tasks_within_range(from_block, to_block.unwrap_or(BlockNumberOrTag::Latest)) => {
44 | if let Err(e) = result {
45 | log::error!("Could not handle previous tasks: {:?}", e);
46 | log::warn!("Continuing anyways...");
47 | }
48 | }
49 | }
50 | }
51 |
52 | if to_block.is_some() {
53 | // if there was a `to_block` specified, we are done at this point
54 | return Ok(());
55 | }
56 |
57 | // otherwise, we can continue with the event loop
58 | loop {
59 | // subscribe to new tasks
60 | log::info!("Subscribing to task events");
61 | let mut event_stream = self
62 | .coordinator
63 | .StatusUpdate_filter()
64 | .watch()
65 | .await
66 | .wrap_err("could not subscribe to tasks")?
67 | .into_stream();
68 |
69 | // start the event loop
70 | log::info!("Listening for events...");
71 | loop {
72 | tokio::select! {
73 | _ = cancellation.cancelled() => {
74 | log::debug!("Cancellation signal received. Stopping...");
75 | return Ok(());
76 | }
77 | next = event_stream.next() => {
78 | match next {
79 | Some(Ok((event, log))) => {
80 | log::debug!(
81 | "Handling task {} (tx: {})",
82 | event.taskId,
83 | log.transaction_hash.unwrap_or_default()
84 | );
85 | self.process_task_by_event(event).await
86 | }
87 | Some(Err(e)) => log::error!("Could not handle event: {}", e),
88 | None => {
89 | log::warn!("Stream ended, waiting a bit before restarting.");
90 | tokio::time::sleep(Duration::from_secs(5)).await;
91 | break
92 | },
93 | }
94 | }
95 | }
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/core/src/cli/commands/coordinator/request.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::utils::format_ether;
2 | use dkn_workflows::Model;
3 | use dria_oracle_contracts::string_to_bytes;
4 | use eyre::Result;
5 |
6 | impl crate::DriaOracle {
7 | /// Requests a task with the given parameters.
8 | ///
9 | /// Oracle does not usually do this, but we still provide the capability for testing & playing around.
10 | pub async fn request_task(
11 | &self,
12 | input: &str,
13 | models: Vec,
14 | difficulty: u8,
15 | num_gens: u64,
16 | num_vals: u64,
17 | protocol: String,
18 | ) -> Result<()> {
19 | log::info!("Requesting a new task.");
20 | let input = string_to_bytes(input.to_string());
21 | let models_str = models
22 | .iter()
23 | .map(|m| m.to_string())
24 | .collect::>()
25 | .join(",");
26 | let models = string_to_bytes(models_str);
27 |
28 | // get total fee for the request
29 | log::debug!("Checking fee & allowance.");
30 | let total_fee = self
31 | .get_request_fee(difficulty, num_gens, num_vals)
32 | .await?
33 | .totalFee;
34 | // check balance
35 | let balance = self.get_token_balance(self.address()).await?.amount;
36 | if balance < total_fee {
37 | return Err(eyre::eyre!(
38 | "Insufficient balance. Please fund your wallet."
39 | ));
40 | }
41 |
42 | // check current allowance
43 | let allowance = self
44 | .allowance(self.address(), *self.coordinator.address())
45 | .await?
46 | .amount;
47 | // make sure we have enough allowance
48 | if allowance < total_fee {
49 | let approval_amount = total_fee - allowance;
50 | log::info!(
51 | "Insufficient allowance. Approving the required amount: {}.",
52 | format_ether(approval_amount)
53 | );
54 |
55 | self.approve(*self.coordinator.address(), approval_amount)
56 | .await?;
57 | log::info!("Token approval successful.");
58 | }
59 |
60 | // make the request
61 | let receipt = self
62 | .request(input, models, difficulty, num_gens, num_vals, protocol)
63 | .await?;
64 | log::info!(
65 | "Task requested successfully. tx: {}",
66 | receipt.transaction_hash
67 | );
68 |
69 | Ok(())
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/core/src/cli/commands/coordinator/serve.rs:
--------------------------------------------------------------------------------
1 | use crate::{compute::handle_request, DriaOracle};
2 | use alloy::{eips::BlockNumberOrTag, primitives::U256};
3 | use dria_oracle_contracts::{
4 | bytes32_to_string, bytes_to_string, OracleCoordinator::StatusUpdate, TaskStatus,
5 | };
6 | use eyre::Result;
7 |
8 | impl DriaOracle {
9 | pub(in crate::cli) async fn process_task_by_id(&self, task_id: U256) -> Result<()> {
10 | log::info!("Processing task {}.", task_id);
11 | let request = self.coordinator.requests(task_id).call().await?;
12 |
13 | log::info!(
14 | "Request Information:\nRequester: {}\nStatus: {}\nInput: {}\nModels: {}\nProtocol: {}",
15 | request.requester,
16 | TaskStatus::try_from(request.status)?,
17 | bytes_to_string(&request.input)?,
18 | bytes_to_string(&request.models)?,
19 | bytes32_to_string(&request.protocol)?
20 | );
21 |
22 | let status = TaskStatus::try_from(request.status)?;
23 | match handle_request(self, status, task_id, request.protocol).await {
24 | Ok(Some(_receipt)) => {}
25 | Ok(None) => {
26 | log::info!("Task {} ignored.", task_id)
27 | }
28 | // using `{:#}` here to get a single-line error message
29 | Err(e) => log::error!("Could not process task {}: {:#}", task_id, e),
30 | }
31 |
32 | Ok(())
33 | }
34 |
35 | pub(in crate::cli) async fn process_task_by_event(&self, event: StatusUpdate) {
36 | let Ok(status) = TaskStatus::try_from(event.statusAfter) else {
37 | log::error!("Could not parse task status: {}", event.statusAfter);
38 | return;
39 | };
40 |
41 | if let Err(err) = handle_request(self, status, event.taskId, event.protocol).await {
42 | log::error!("Could not process task {}: {:?}", event.taskId, err);
43 | }
44 | }
45 |
46 | pub(in crate::cli) async fn process_tasks_within_range(
47 | &self,
48 | from_block: BlockNumberOrTag,
49 | to_block: BlockNumberOrTag,
50 | ) -> Result<()> {
51 | log::info!(
52 | "Processing tasks between blocks: {} - {}",
53 | from_block
54 | .as_number()
55 | .map(|n| n.to_string())
56 | .unwrap_or(from_block.to_string()),
57 | to_block
58 | .as_number()
59 | .map(|n| n.to_string())
60 | .unwrap_or(to_block.to_string())
61 | );
62 |
63 | for (event, _) in self.get_tasks_in_range(from_block, to_block).await? {
64 | self.process_task_by_event(event).await;
65 | }
66 |
67 | Ok(())
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/core/src/cli/commands/coordinator/view.rs:
--------------------------------------------------------------------------------
1 | use alloy::{eips::BlockNumberOrTag, primitives::U256};
2 | use dria_oracle_contracts::{bytes32_to_string, bytes_to_string, TaskStatus};
3 | use eyre::Result;
4 |
5 | impl crate::DriaOracle {
6 | /// Views the task events between two blocks, logs everything on screen.
7 | pub(in crate::cli) async fn view_task_events(
8 | &self,
9 | from_block: BlockNumberOrTag,
10 | to_block: BlockNumberOrTag,
11 | ) -> Result<()> {
12 | log::info!(
13 | "Viewing task ids & statuses between blocks: {} - {}",
14 | from_block
15 | .as_number()
16 | .map(|n| n.to_string())
17 | .unwrap_or(from_block.to_string()),
18 | to_block
19 | .as_number()
20 | .map(|n| n.to_string())
21 | .unwrap_or(to_block.to_string())
22 | );
23 |
24 | let task_events = self.get_tasks_in_range(from_block, to_block).await?;
25 | for (event, log) in task_events {
26 | log::info!(
27 | "Task {} changed from {} to {} at block {}, tx: {}",
28 | event.taskId,
29 | TaskStatus::try_from(event.statusBefore).unwrap_or_default(),
30 | TaskStatus::try_from(event.statusAfter).unwrap_or_default(),
31 | log.block_number.unwrap_or_default(),
32 | log.transaction_hash.unwrap_or_default()
33 | );
34 | }
35 |
36 | Ok(())
37 | }
38 |
39 | /// Views the request, responses and validations of a single task, logs everything on screen.
40 | pub(in crate::cli) async fn view_task(&self, task_id: U256) -> Result<()> {
41 | log::info!("Viewing task {}.", task_id);
42 | let (request, responses, validations) = self.get_task(task_id).await?;
43 |
44 | log::info!(
45 | "Request Information:\nRequester: {}\nStatus: {}\nInput: {}\nModels: {}\nProtocol: {}",
46 | request.requester,
47 | TaskStatus::try_from(request.status)?,
48 | bytes_to_string(&request.input)?,
49 | bytes_to_string(&request.models)?,
50 | bytes32_to_string(&request.protocol)?
51 | );
52 |
53 | log::info!("Responses:");
54 | if responses._0.is_empty() {
55 | log::info!("There are no responses yet.");
56 | } else {
57 | for (idx, response) in responses._0.iter().enumerate() {
58 | log::info!(
59 | "Response #{}\nOutput: {}\nMetadata: {}\nGenerator: {}",
60 | idx,
61 | bytes_to_string(&response.output)?,
62 | bytes_to_string(&response.metadata)?,
63 | response.responder
64 | );
65 | }
66 | }
67 |
68 | log::info!("Validations:");
69 | if validations._0.is_empty() {
70 | log::info!("There are no validations yet.");
71 | } else {
72 | for (idx, validation) in validations._0.iter().enumerate() {
73 | log::info!(
74 | "Validation #{}\nScores: {:?}\nMetadata: {}\nValidator: {}",
75 | idx,
76 | validation.scores,
77 | bytes_to_string(&validation.metadata)?,
78 | validation.validator
79 | );
80 | }
81 | }
82 |
83 | Ok(())
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/core/src/cli/commands/mod.rs:
--------------------------------------------------------------------------------
1 | use alloy::{eips::BlockNumberOrTag, primitives::U256};
2 | use clap::Subcommand;
3 | use dkn_workflows::Model;
4 | use dria_oracle_contracts::OracleKind;
5 |
6 | use super::parsers::*;
7 |
8 | mod coordinator;
9 | mod registry;
10 | mod token;
11 |
12 | // https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes
13 | #[derive(Subcommand)]
14 | pub enum Commands {
15 | /// Register oracle as a specific oracle kind.
16 | Register {
17 | #[arg(help = "The oracle kinds to register as.", required = true, value_parser = parse_oracle_kind)]
18 | kinds: Vec,
19 | },
20 | /// Unregister oracle as a specific oracle kind.
21 | Unregister {
22 | #[arg(help = "The oracle kinds to unregister as.", required = true, value_parser = parse_oracle_kind)]
23 | kinds: Vec,
24 | },
25 | /// See all registrations.
26 | Registrations,
27 | /// See the current balance of the oracle node.
28 | Balance,
29 | /// See claimable rewards from the coordinator.
30 | Rewards,
31 | /// Claim rewards from the coordinator.
32 | Claim,
33 | /// Serve the oracle node.
34 | Serve {
35 | #[arg(help = "The oracle kinds to handle tasks as, if omitted will default to all registered kinds.", value_parser = parse_oracle_kind)]
36 | kinds: Vec,
37 | #[arg(short, long = "model", help = "The model(s) to serve.", required = true, value_parser = parse_model)]
38 | models: Vec,
39 | #[arg(
40 | long,
41 | help = "Block number to starting listening from, omit to start from latest block.",
42 | value_parser = parse_block_number_or_tag
43 | )]
44 | from: Option,
45 | #[arg(
46 | long,
47 | help = "Block number to stop listening at, omit to keep running the node indefinitely.",
48 | value_parser = parse_block_number_or_tag
49 | )]
50 | to: Option,
51 | #[arg(
52 | short,
53 | long,
54 | help = "Optional task id to serve specifically.",
55 | required = false
56 | )]
57 | task_id: Option,
58 | },
59 | /// View tasks.
60 | View {
61 | #[arg(long, help = "Starting block number, defaults to 'earliest'.", value_parser = parse_block_number_or_tag)]
62 | from: Option,
63 | #[arg(long, help = "Ending block number, defaults to 'latest'.", value_parser = parse_block_number_or_tag)]
64 | to: Option,
65 | #[arg(short, long, help = "Task id to view.")]
66 | task_id: Option,
67 | },
68 | /// Request a task.
69 | Request {
70 | #[arg(help = "The input to request a task with.", required = true)]
71 | input: String,
72 | #[arg(help = "The model(s) to accept.", required = true, value_parser=parse_model)]
73 | models: Vec,
74 | #[arg(long, help = "The difficulty of the task.", default_value_t = 2)]
75 | difficulty: u8,
76 | #[arg(long, help = "Protocol name for the request", default_value = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))]
77 | protocol: String,
78 | #[arg(
79 | long,
80 | help = "The number of generations to request.",
81 | default_value_t = 1
82 | )]
83 | num_gens: u64,
84 | #[arg(
85 | long,
86 | help = "The number of validations to request.",
87 | default_value_t = 1
88 | )]
89 | num_vals: u64,
90 | },
91 | }
92 |
--------------------------------------------------------------------------------
/core/src/cli/commands/registry.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::utils::format_ether;
2 | use dria_oracle_contracts::OracleKind;
3 | use eyre::Result;
4 |
5 | use crate::DriaOracle;
6 |
7 | impl DriaOracle {
8 | /// Registers the oracle node as an oracle for the given `kind`.
9 | ///
10 | /// - If the node is already registered, it will do nothing.
11 | /// - If the node is not registered, it will approve the required amount of tokens
12 | /// to the registry and then register the node.
13 | pub async fn register(&self, kind: OracleKind) -> Result<()> {
14 | log::info!("Registering as a {}.", kind);
15 |
16 | // check if registered already
17 | if self.is_registered(kind).await? {
18 | log::warn!("You are already registered as a {}.", kind);
19 | return Ok(());
20 | }
21 |
22 | // calculate the required approval for registration
23 | let stake = self.get_registry_stake_amount(kind).await?;
24 | let allowance = self
25 | .allowance(self.address(), *self.registry.address())
26 | .await?;
27 |
28 | // approve if necessary
29 | if allowance.amount < stake.amount {
30 | let difference = stake.amount - allowance.amount;
31 | log::info!(
32 | "Approving {} tokens for {} registration.",
33 | format_ether(difference),
34 | kind
35 | );
36 |
37 | // check balance
38 | let balance = self.get_token_balance(self.address()).await?;
39 | if balance.amount < difference {
40 | return Err(eyre::eyre!(
41 | "Not enough balance to approve. (have: {}, required: {})",
42 | balance,
43 | difference
44 | ));
45 | }
46 |
47 | // approve the difference
48 | self.approve(*self.registry.address(), difference).await?;
49 | } else {
50 | log::info!("Already approved enough tokens.");
51 | }
52 |
53 | // register
54 | log::info!("Registering.");
55 | self.register_kind(kind).await?;
56 |
57 | Ok(())
58 | }
59 |
60 | /// Unregisters the oracle node as an oracle for the given `kind`.
61 | ///
62 | /// - If the node is not registered, it will do nothing.
63 | /// - If the node is registered, it will unregister the node and transfer all allowance
64 | /// from the registry back to the oracle.
65 | pub async fn unregister(&self, kind: OracleKind) -> Result<()> {
66 | log::info!("Unregistering as {}.", kind);
67 |
68 | // check if not registered anyways
69 | if !self.is_registered(kind).await? {
70 | log::warn!("You are already not registered as a {}.", kind);
71 | return Ok(());
72 | }
73 |
74 | self.unregister_kind(kind).await?;
75 |
76 | // transfer all allowance from registry back to oracle
77 | // to get back the registrations fee
78 | let allowance = self
79 | .allowance(*self.registry.address(), self.address())
80 | .await?;
81 | log::info!(
82 | "Transferring all allowance ({}) back from registry.",
83 | allowance
84 | );
85 | self.transfer_from(*self.registry.address(), self.address(), allowance.amount)
86 | .await?;
87 |
88 | Ok(())
89 | }
90 |
91 | /// Displays the registration status of the oracle node for all oracle kinds.
92 | pub(in crate::cli) async fn display_registrations(&self) -> Result<()> {
93 | for kind in [OracleKind::Generator, OracleKind::Validator] {
94 | let is_registered = self.is_registered(kind).await?;
95 | log::info!("{}: {}", kind, is_registered);
96 | }
97 |
98 | Ok(())
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/core/src/cli/commands/token.rs:
--------------------------------------------------------------------------------
1 | use crate::DriaOracle;
2 | use eyre::Result;
3 |
4 | impl DriaOracle {
5 | /// Display token balances.
6 | pub(in crate::cli) async fn display_balance(&self) -> Result<()> {
7 | let eth_balance = self.get_native_balance(self.address()).await?;
8 | let token_balance = self.get_token_balance(self.address()).await?;
9 |
10 | log::info!(
11 | "Your balances:\n{}",
12 | [eth_balance, token_balance]
13 | .map(|b| b.to_string())
14 | .join("\n")
15 | );
16 |
17 | Ok(())
18 | }
19 |
20 | /// Show the amount of claimable rewards.
21 | pub(in crate::cli) async fn display_rewards(&self) -> Result<()> {
22 | let allowance = self
23 | .allowance(*self.coordinator.address(), self.address())
24 | .await?;
25 |
26 | log::info!("Claimable rewards:");
27 | log::info!("{} ", allowance);
28 | if allowance.amount.is_zero() {
29 | log::warn!("You have no claimable rewards!");
30 | }
31 |
32 | Ok(())
33 | }
34 |
35 | /// Claim rewards
36 | pub(in crate::cli) async fn claim_rewards(&self) -> Result<()> {
37 | // get allowance
38 | let allowance = self
39 | .allowance(*self.coordinator.address(), self.address())
40 | .await?;
41 |
42 | // check if there are rewards to claim
43 | if allowance.amount.is_zero() {
44 | log::warn!("No rewards to claim.");
45 | } else {
46 | // transfer rewards
47 | self.transfer_from(
48 | *self.coordinator.address(),
49 | self.address(),
50 | allowance.amount,
51 | )
52 | .await?;
53 | log::info!("Rewards claimed: {}.", allowance);
54 | }
55 |
56 | Ok(())
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/core/src/cli/mod.rs:
--------------------------------------------------------------------------------
1 | // use alloy::eips::BlockNumberOrTag;
2 | use alloy::eips::BlockNumberOrTag;
3 | use eyre::Result;
4 | use std::{env, path::PathBuf};
5 | use tokio_util::sync::CancellationToken;
6 |
7 | mod commands;
8 | use commands::Commands;
9 |
10 | mod parsers;
11 | use parsers::*;
12 |
13 | const DEFAULT_TX_TIMEOUT_SECS: u64 = 160;
14 |
15 | #[derive(clap::Parser)]
16 | #[command(version, about, long_about = None)]
17 | #[command(propagate_version = true)]
18 | pub struct Cli {
19 | #[command(subcommand)]
20 | pub command: Commands,
21 |
22 | /// Path to the .env file
23 | #[arg(short, long, default_value = "./.env")]
24 | pub env: PathBuf,
25 |
26 | /// Enable debug-level logs
27 | #[arg(short, long)]
28 | pub debug: bool,
29 | }
30 |
31 | impl Cli {
32 | pub fn read_secret_key() -> Result {
33 | let key = env::var("SECRET_KEY")?;
34 | parse_secret_key(&key)
35 | }
36 |
37 | pub fn read_rpc_url() -> Result {
38 | let url = env::var("RPC_URL")?;
39 | parse_url(&url)
40 | }
41 |
42 | pub fn read_tx_timeout() -> Result {
43 | let timeout = env::var("TX_TIMEOUT_SECS").unwrap_or(DEFAULT_TX_TIMEOUT_SECS.to_string());
44 | timeout.parse().map_err(Into::into)
45 | }
46 | }
47 |
48 | /// Handles a given CLI command, using the provided node.
49 | pub async fn handle_command(command: Commands, mut node: crate::DriaOracle) -> Result<()> {
50 | match command {
51 | Commands::Balance => node.display_balance().await?,
52 | Commands::Claim => node.claim_rewards().await?,
53 | Commands::Rewards => node.display_rewards().await?,
54 | Commands::Serve {
55 | task_id,
56 | kinds,
57 | models,
58 | from,
59 | to,
60 | } => {
61 | let token = CancellationToken::new();
62 | node.prepare_oracle(kinds, models).await?;
63 |
64 | if let Some(task_id) = task_id {
65 | node.process_task_by_id(task_id).await?
66 | } else {
67 | // create a signal handler
68 | let termination_token = token.clone();
69 | let termination_handle = tokio::spawn(async move {
70 | wait_for_termination(termination_token).await.unwrap();
71 | });
72 |
73 | // launch node
74 | node.serve(from, to, token).await?;
75 |
76 | // wait for handle
77 | if let Err(e) = termination_handle.await {
78 | log::error!("Error in termination handler: {}", e);
79 | }
80 | }
81 | }
82 | Commands::View { task_id, from, to } => {
83 | if let Some(task_id) = task_id {
84 | node.view_task(task_id).await?
85 | } else {
86 | node.view_task_events(
87 | from.unwrap_or(BlockNumberOrTag::Earliest),
88 | to.unwrap_or(BlockNumberOrTag::Latest),
89 | )
90 | .await?
91 | }
92 | }
93 | Commands::Register { kinds } => {
94 | for kind in kinds {
95 | node.register(kind).await?
96 | }
97 | }
98 | Commands::Unregister { kinds } => {
99 | for kind in kinds {
100 | node.unregister(kind).await?;
101 | }
102 | }
103 | Commands::Registrations => node.display_registrations().await?,
104 | Commands::Request {
105 | input,
106 | models,
107 | difficulty,
108 | num_gens,
109 | num_vals,
110 | protocol,
111 | } => {
112 | node.request_task(&input, models, difficulty, num_gens, num_vals, protocol)
113 | .await?
114 | }
115 | };
116 |
117 | Ok(())
118 | }
119 |
120 | /// Waits for various termination signals, and cancels the given token when the signal is received.
121 | async fn wait_for_termination(cancellation: CancellationToken) -> Result<()> {
122 | #[cfg(unix)]
123 | {
124 | use tokio::signal::unix::{signal, SignalKind};
125 | let mut sigterm = signal(SignalKind::terminate())?;
126 | let mut sigint = signal(SignalKind::interrupt())?;
127 | tokio::select! {
128 | _ = sigterm.recv() => log::warn!("Recieved SIGTERM"),
129 | _ = sigint.recv() => log::warn!("Recieved SIGINT"),
130 | _ = cancellation.cancelled() => {
131 | // no need to wait if cancelled anyways
132 | // although this is not likely to happen
133 | return Ok(());
134 | }
135 | };
136 |
137 | cancellation.cancel();
138 | }
139 |
140 | #[cfg(not(unix))]
141 | {
142 | log::error!("No signal handling for this platform: {}", env::consts::OS);
143 | cancellation.cancel();
144 | }
145 |
146 | log::info!("Terminating the application...");
147 |
148 | Ok(())
149 | }
150 |
--------------------------------------------------------------------------------
/core/src/cli/parsers.rs:
--------------------------------------------------------------------------------
1 | use alloy::{eips::BlockNumberOrTag, hex::FromHex, primitives::B256};
2 | use dkn_workflows::Model;
3 | use dria_oracle_contracts::OracleKind;
4 | use eyre::{eyre, Result};
5 | use reqwest::Url;
6 | use std::str::FromStr;
7 |
8 | /// `value_parser` to parse a `str` to `OracleKind`.
9 | #[inline]
10 | pub fn parse_model(value: &str) -> Result {
11 | Model::try_from(value.to_string()).map_err(|e| eyre!(e))
12 | }
13 |
14 | /// `value_parser` to parse a `str` to `Url`.
15 | #[inline]
16 | pub fn parse_url(value: &str) -> Result {
17 | Url::parse(value).map_err(Into::into)
18 | }
19 |
20 | /// `value_parser` to parse a hexadecimal `str` to 256-bit type `B256`.
21 | #[inline]
22 | pub fn parse_secret_key(value: &str) -> Result {
23 | B256::from_hex(value).map_err(Into::into)
24 | }
25 |
26 | /// `value_parser` to parse a `str` to `OracleKind`.
27 | ///
28 | /// This could be done with `ValueEnum` as well, but we prefer this to not
29 | /// require `clap` in another crate for just that reason.
30 | #[inline]
31 | pub fn parse_oracle_kind(value: &str) -> Result {
32 | match value {
33 | "generator" => Ok(OracleKind::Generator),
34 | "validator" => Ok(OracleKind::Validator),
35 | _ => Err(eyre!("Invalid OracleKind: {}", value)),
36 | }
37 | }
38 |
39 | /// `value parser` to parse a `str` to `BlockNumberOrTag`
40 | /// where if it can be parsed as `u64`, we call `BlockNumberOrTag::from_u64`
41 | /// otherwise we call `BlockNumberOrTag::from_str`.
42 | #[inline]
43 | pub fn parse_block_number_or_tag(value: &str) -> Result {
44 | match value.parse::() {
45 | // parse block no from its decimal representation
46 | Ok(block_number) => Ok(BlockNumberOrTag::from(block_number)),
47 | // parse block no from hex, or parse its tag
48 | Err(_) => BlockNumberOrTag::from_str(value).map_err(Into::into),
49 | }
50 | }
51 | #[cfg(test)]
52 | mod tests {
53 | use super::*;
54 |
55 | #[test]
56 | fn test_parse_model() {
57 | let model_str = "llama3.1:latest";
58 | let result = parse_model(model_str);
59 | assert!(result.is_ok());
60 | let model = result.unwrap();
61 | assert_eq!(model, Model::try_from(model_str.to_string()).unwrap());
62 | }
63 |
64 | #[test]
65 | fn test_parse_url() {
66 | let url_str = "https://example.com";
67 | let result = parse_url(url_str);
68 | assert!(result.is_ok());
69 | let url = result.unwrap();
70 | assert_eq!(url, Url::parse(url_str).unwrap());
71 | }
72 |
73 | #[test]
74 | fn test_parse_secret_key() {
75 | let hex_str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
76 | let result = parse_secret_key(hex_str);
77 | assert!(result.is_ok());
78 |
79 | let secret_key = result.unwrap();
80 | assert_eq!(secret_key, B256::from_hex(hex_str).unwrap());
81 | }
82 |
83 | #[test]
84 | fn test_parse_block_number_or_tag() {
85 | let block_number_str = "12345";
86 | let result = parse_block_number_or_tag(block_number_str);
87 | assert!(result.is_ok());
88 | let block_number_or_tag = result.unwrap();
89 | assert_eq!(block_number_or_tag, BlockNumberOrTag::from(12345u64));
90 |
91 | let block_tag_str = "latest";
92 | let result = parse_block_number_or_tag(block_tag_str);
93 | assert!(result.is_ok());
94 | let block_number_or_tag = result.unwrap();
95 | assert_eq!(block_number_or_tag, BlockNumberOrTag::Latest);
96 |
97 | let block_hex_str = "0x3039";
98 | let result = parse_block_number_or_tag(block_hex_str);
99 | assert!(result.is_ok());
100 | let block_number_or_tag = result.unwrap();
101 | assert_eq!(
102 | block_number_or_tag,
103 | BlockNumberOrTag::from_str(block_hex_str).unwrap()
104 | );
105 | }
106 |
107 | #[test]
108 | fn test_parse_oracle_kind() {
109 | let kind_str = "generator";
110 | let result = parse_oracle_kind(kind_str);
111 | assert!(result.is_ok());
112 | let kind = result.unwrap();
113 | assert_eq!(kind, OracleKind::Generator);
114 |
115 | let kind_str = "validator";
116 | let result = parse_oracle_kind(kind_str);
117 | assert!(result.is_ok());
118 | let kind = result.unwrap();
119 | assert_eq!(kind, OracleKind::Validator);
120 |
121 | let kind_str = "invalid";
122 | let result = parse_oracle_kind(kind_str);
123 | assert!(result.is_err());
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/core/src/compute/execute.rs:
--------------------------------------------------------------------------------
1 | use core::time::Duration;
2 | use dkn_workflows::{ExecutionError, Executor, Model, ProgramMemory, Workflow};
3 | use eyre::Context;
4 |
5 | const NUM_RETRIES: usize = 4;
6 |
7 | /// A wrapper for executing a workflow with retries.
8 | ///
9 | /// - Creates an `Executor` with the given model.
10 | /// - Executes the given workflow with the executor over an empty memory.
11 | /// - If the execution fails due to timeout, retries up to 3 times with slightly increasing timeout durations.
12 | pub async fn execute_workflow_with_timedout_retries(
13 | workflow: &Workflow,
14 | model: Model,
15 | duration: Duration,
16 | ) -> eyre::Result {
17 | let executor = Executor::new(model);
18 |
19 | let mut retries = 0;
20 | while retries < NUM_RETRIES {
21 | let mut memory = ProgramMemory::new();
22 | tokio::select! {
23 | result = executor.execute(None, workflow, &mut memory) => {
24 | if let Err(ExecutionError::WorkflowFailed(reason)) = result {
25 | // handle Workflow failed errors with retries
26 | log::warn!("Execution gave WorkflowFailed error with: {}", reason);
27 | if retries < NUM_RETRIES {
28 | retries += 1;
29 | log::warn!("Retrying {}/{}", retries, NUM_RETRIES);
30 | continue;
31 | }
32 | } else {
33 | return result.wrap_err("could not execute workflow");
34 | }
35 | },
36 | // normally the workflow has a timeout logic as well, but it doesnt work that well, and may get stuck
37 | _ = tokio::time::sleep(duration) => {
38 | // if we have retries left, log a warning and continue
39 | // note that other errors will be returned as is
40 | log::warn!("Execution timed out");
41 | if retries < NUM_RETRIES {
42 | retries += 1;
43 | log::warn!("Retrying {}/{}", retries, NUM_RETRIES);
44 | continue;
45 | }
46 | }
47 | };
48 | }
49 |
50 | // all retries failed
51 | Err(eyre::eyre!(
52 | "Execution failed after {} retries",
53 | NUM_RETRIES
54 | ))
55 | }
56 |
--------------------------------------------------------------------------------
/core/src/compute/generation/execute.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use alloy::primitives::U256;
4 | use dkn_workflows::{MessageInput, Model};
5 | use eyre::{eyre, Context, Result};
6 |
7 | use super::request::GenerationRequest;
8 | use super::workflow::*;
9 |
10 | use crate::compute::execute_workflow_with_timedout_retries;
11 | use crate::compute::parse_downloadable;
12 | use crate::DriaOracle;
13 |
14 | /// Executes a request using the given model, and optionally a node.
15 | /// Returns the raw string output.
16 | pub async fn execute_generation(
17 | request: &GenerationRequest,
18 | model: Model,
19 | node: Option<&DriaOracle>,
20 | ) -> Result {
21 | log::debug!(
22 | "Executing {} generation request with: {}",
23 | request.request_type(),
24 | model
25 | );
26 |
27 | match request {
28 | // workflows are executed directly without any prompts
29 | // as we expect their memory to be pre-filled
30 | GenerationRequest::Workflow(workflow) => {
31 | let duration = Duration::from_secs(workflow.get_config().max_time);
32 | execute_workflow_with_timedout_retries(workflow, model, duration).await
33 | }
34 |
35 | // string requests are used with the generation workflow with a given prompt
36 | GenerationRequest::String(input) => {
37 | let (workflow, duration) = make_generation_workflow(input.clone())?;
38 | execute_workflow_with_timedout_retries(&workflow, model, duration).await
39 | }
40 |
41 | // chat history requests are used with the chat workflow
42 | // and the existing history is fetched & parsed from previous requests
43 | GenerationRequest::ChatHistory(chat_request) => {
44 | let mut history = if chat_request.history_id == 0 {
45 | // if task id is zero, there is no prior history
46 | Vec::new()
47 | } else if let Some(node) = node {
48 | let history_id = U256::from(chat_request.history_id);
49 | // if task id is non-zero, we need the node to get the history
50 | // first make sure that next-task-id is larger than the history
51 | if history_id >= node.coordinator.nextTaskId().call().await?._0 {
52 | return Err(eyre!(
53 | "chat history cant exist as its larger than the latest task id"
54 | ));
55 | }
56 |
57 | let history_task = node
58 | .coordinator
59 | .getBestResponse(history_id)
60 | .call()
61 | .await
62 | .wrap_err("could not get chat history task from contract")?
63 | ._0;
64 |
65 | // parse it as chat history output
66 | let history_str = parse_downloadable(&history_task.output).await?;
67 |
68 | // if its a previous message array, we can parse it directly
69 | if let Ok(messages) = serde_json::from_str::>(&history_str) {
70 | messages
71 | } else {
72 | // otherwise, we can fallback to fetching input manually and creating a new history on-the-fly
73 | let request = node.coordinator.requests(history_id).call().await?;
74 | let input = parse_downloadable(&request.input).await?;
75 |
76 | // create a new history with the input
77 | vec![
78 | MessageInput::new_user_message(input),
79 | MessageInput::new_assistant_message(history_str),
80 | ]
81 | }
82 | } else {
83 | return Err(eyre!("node is required for chat history"));
84 | };
85 |
86 | // prepare the workflow with chat history
87 | let (workflow, duration) =
88 | make_chat_workflow(history.clone(), chat_request.content.clone(), None, None)?;
89 | let output = execute_workflow_with_timedout_retries(&workflow, model, duration).await?;
90 |
91 | // append user input to chat history
92 | history.push(MessageInput::new_assistant_message(output));
93 |
94 | // return the stringified output
95 | serde_json::to_string(&history).wrap_err("could not serialize chat history")
96 | }
97 | }
98 | }
99 |
100 | #[cfg(test)]
101 | mod tests {
102 | use super::*;
103 | use crate::compute::generation::request::{ChatHistoryRequest, GenerationRequest};
104 | #[tokio::test]
105 | #[ignore = "requires Ollama"]
106 | async fn test_ollama_generation() {
107 | dotenvy::dotenv().unwrap();
108 | let request = GenerationRequest::String("What is the result of 2 + 2?".to_string());
109 | let output = execute_generation(&request, Model::Llama3_1_8B, None)
110 | .await
111 | .unwrap();
112 |
113 | println!("Output:\n{}", output);
114 | assert!(output.contains('4'));
115 | }
116 |
117 | #[tokio::test]
118 | #[ignore = "requires OpenAI API key"]
119 | async fn test_openai_generation() {
120 | dotenvy::dotenv().unwrap();
121 | let request = GenerationRequest::String("What is the result of 2 + 2?".to_string());
122 | let output = execute_generation(&request, Model::GPT4Turbo, None)
123 | .await
124 | .unwrap();
125 |
126 | println!("Output:\n{}", output);
127 | assert!(output.contains('4'));
128 | }
129 |
130 | #[tokio::test]
131 | #[ignore = "requires OpenAI API key"]
132 | async fn test_openai_chat() {
133 | dotenvy::dotenv().unwrap();
134 | let request = ChatHistoryRequest {
135 | history_id: 0,
136 | content: "What is 2+2?".to_string(),
137 | };
138 | let request_bytes = serde_json::to_vec(&request).unwrap();
139 | let request = GenerationRequest::try_parse_bytes(&request_bytes.into())
140 | .await
141 | .unwrap();
142 | let output = execute_generation(&request, Model::GPT4Turbo, None)
143 | .await
144 | .unwrap();
145 |
146 | println!("Output:\n{}", output);
147 | assert!(output.contains('4'));
148 | }
149 |
150 | #[tokio::test]
151 | #[ignore = "requires OpenAI API key"]
152 | async fn test_workflow_on_arweave() {
153 | // cargo test --package dria-oracle --lib --all-features -- compute::generation::execute::tests::test_raw_workflow --exact --show-output --ignored
154 | dotenvy::dotenv().unwrap();
155 |
156 | let contract_result = hex_literal::hex!("7b2261727765617665223a223658797a572d71666e7670756b787344535a444b2d4f514a6e715a686b62703044624e4e6649696c7a706f227d");
157 | let request = GenerationRequest::try_parse_bytes(&contract_result.into())
158 | .await
159 | .unwrap();
160 | let output = execute_generation(&request, Model::GPT4o, None)
161 | .await
162 | .unwrap();
163 |
164 | println!("{}", output);
165 | }
166 |
167 | #[tokio::test]
168 | #[ignore = "requires OpenRouter API key"]
169 | async fn test_workflow_fails_with_short_time() {
170 | dotenvy::dotenv().unwrap();
171 | let _ = env_logger::builder()
172 | .filter_level(log::LevelFilter::Off)
173 | .filter_module("dria_oracle", log::LevelFilter::Info)
174 | .is_test(true)
175 | .try_init();
176 |
177 | let (workflow, _) =
178 | make_chat_workflow(Vec::new(), "What is 2+2".into(), Some(1), None).unwrap();
179 | let request = GenerationRequest::Workflow(workflow);
180 | let result = execute_generation(&request, Model::ORDeepSeek2_5, None).await;
181 | assert!(result.is_err());
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/core/src/compute/generation/handler.rs:
--------------------------------------------------------------------------------
1 | use crate::{compute::generation::execute::execute_generation, mine_nonce, DriaOracle};
2 | use alloy::{
3 | primitives::{FixedBytes, U256},
4 | rpc::types::TransactionReceipt,
5 | };
6 | use dria_oracle_contracts::{bytes32_to_string, bytes_to_string};
7 | use dria_oracle_storage::ArweaveStorage;
8 | use eyre::Result;
9 |
10 | use super::postprocess::*;
11 | use super::request::GenerationRequest;
12 |
13 | /// Handles a generation request.
14 | ///
15 | /// 1. First, we check if we have already responded to the task.
16 | /// Contract will revert even if we dont do this check ourselves, but its better to provide the error here.
17 | ///
18 | /// 2. Then, we check if our models are compatible with the request. If not, we return an error.
19 | pub async fn handle_generation(
20 | node: &DriaOracle,
21 | task_id: U256,
22 | protocol: FixedBytes<32>,
23 | ) -> Result> {
24 | log::info!("Handling generation task {}", task_id);
25 |
26 | // check if we have responded to this generation already
27 | log::debug!("Checking existing generation responses");
28 | let responses = node.coordinator.getResponses(task_id).call().await?._0;
29 | if responses.iter().any(|r| r.responder == node.address()) {
30 | log::debug!("Already responded to {} with generation", task_id);
31 | return Ok(None);
32 | }
33 |
34 | // fetch the request from contract
35 | log::debug!("Fetching the task request");
36 | let request = node.coordinator.requests(task_id).call().await?;
37 |
38 | // choose model based on the request
39 | log::debug!("Choosing model to use");
40 | let models_string = bytes_to_string(&request.models)?;
41 | let models_vec = models_string.split(',').map(|s| s.to_string()).collect();
42 | let model = match node.workflows.get_any_matching_model(models_vec) {
43 | Ok((_, model)) => model,
44 | Err(e) => {
45 | log::error!(
46 | "No matching model found: {}, falling back to random model.",
47 | e
48 | );
49 |
50 | node.workflows
51 | .get_matching_model("*".to_string())
52 | .expect("should return at least one model")
53 | .1
54 | }
55 | };
56 | log::debug!("Using model: {} from {}", model, models_string);
57 |
58 | // parse protocol string early, in case it cannot be parsed
59 | let protocol_string = bytes32_to_string(&protocol)?;
60 |
61 | // execute task
62 | log::debug!("Executing the workflow");
63 | let input = GenerationRequest::try_parse_bytes(&request.input).await?;
64 | let output = execute_generation(&input, model, Some(node)).await?;
65 | log::debug!("Output: {}", output);
66 |
67 | // post-processing
68 | log::debug!(
69 | "Post-processing the output for protocol: {}",
70 | protocol_string
71 | );
72 | let (output, metadata, use_storage) =
73 | match protocol_string.split('/').next().unwrap_or_default() {
74 | SwanPurchasePostProcessor::PROTOCOL => {
75 | SwanPurchasePostProcessor::new("", " ").post_process(output)
76 | }
77 | _ => IdentityPostProcessor.post_process(output),
78 | }?;
79 |
80 | // uploading to storage
81 | let arweave = ArweaveStorage::new_from_env()?;
82 | let output = if use_storage {
83 | log::debug!("Uploading output to storage");
84 | arweave.put_if_large(output).await?
85 | } else {
86 | log::debug!("Not uploading output to storage");
87 | output
88 | };
89 | log::debug!("Uploading metadata to storage");
90 | let metadata = arweave.put_if_large(metadata).await?;
91 |
92 | // mine nonce
93 | log::debug!("Mining nonce for task");
94 | let nonce = mine_nonce(
95 | request.parameters.difficulty,
96 | &request.requester,
97 | &node.address(),
98 | &request.input,
99 | &task_id,
100 | )
101 | .nonce;
102 |
103 | // respond
104 | log::debug!("Responding with generation");
105 | let tx_receipt = node
106 | .respond_generation(task_id, output, metadata, nonce)
107 | .await?;
108 | Ok(Some(tx_receipt))
109 | }
110 |
--------------------------------------------------------------------------------
/core/src/compute/generation/mod.rs:
--------------------------------------------------------------------------------
1 | mod execute;
2 |
3 | mod postprocess;
4 |
5 | mod workflow;
6 |
7 | mod handler;
8 | pub use handler::handle_generation;
9 |
10 | mod request;
11 |
--------------------------------------------------------------------------------
/core/src/compute/generation/postprocess/identity.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::Bytes;
2 | use eyre::Result;
3 |
4 | use super::PostProcess;
5 |
6 | /// An identity post-processor that does nothing.
7 | /// Input is directed to output and metadata is empty.
8 | #[derive(Default)]
9 | pub struct IdentityPostProcessor;
10 |
11 | impl PostProcess for IdentityPostProcessor {
12 | const PROTOCOL: &'static str = "";
13 |
14 | fn post_process(&self, input: String) -> Result<(Bytes, Bytes, bool)> {
15 | Ok((input.into(), Default::default(), true))
16 | }
17 | }
18 |
19 | #[cfg(test)]
20 | mod tests {
21 | use super::*;
22 |
23 | #[test]
24 | fn test_identity_post_processor() {
25 | let input = "hello".to_string();
26 | let (output, metadata, _) = IdentityPostProcessor.post_process(input).unwrap();
27 | assert_eq!(output, Bytes::from("hello"));
28 | assert_eq!(metadata, Bytes::default());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/core/src/compute/generation/postprocess/mod.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::Bytes;
2 |
3 | mod identity;
4 | pub use identity::*;
5 |
6 | mod swan;
7 | pub use swan::*;
8 |
9 | /// A `PostProcess` is a trait that defines a post-processing step for a workflow at application level.
10 | /// It's input is the raw output from the LLM, and it splits it into an output and metadata.
11 | /// The output is the main thing that is used within the contract, metadata is externally checked.
12 | pub trait PostProcess {
13 | /// Protocol string name, for instance if protocol is `foobar/1.0`, this should be `foobar`.
14 | const PROTOCOL: &'static str;
15 |
16 | /// A post-processing step that takes the raw output from the LLM and splits it into an output and metadata.
17 | ///
18 | /// Returns:
19 | /// - The output that is used within the contract.
20 | /// - The metadata that is externally checked.
21 | /// - A boolean indicating if the output should be uploaded to a storage if large enough.
22 | fn post_process(&self, input: String) -> eyre::Result<(Bytes, Bytes, bool)>;
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/compute/generation/request.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::Bytes;
2 | use dkn_workflows::Workflow;
3 | use eyre::Result;
4 |
5 | use crate::compute::parse_downloadable;
6 |
7 | /// A request with chat history.
8 | #[derive(Debug, serde::Serialize, serde::Deserialize)]
9 | pub struct ChatHistoryRequest {
10 | /// Task Id of which the output will act like history.
11 | pub history_id: usize,
12 | /// Message content.
13 | pub content: String,
14 | }
15 |
16 | /// An oracle request.
17 | #[derive(Debug)]
18 | pub enum GenerationRequest {
19 | /// A chat-history request, usually indicates a previous response to be continued upon.
20 | ChatHistory(ChatHistoryRequest),
21 | /// A Workflow object, can be executed directly.
22 | Workflow(Workflow),
23 | /// A plain string request, can be executed with a generation workflow.
24 | String(String),
25 | }
26 |
27 | impl GenerationRequest {
28 | /// Returns hte name of the request type, mostly for diagnostics.
29 | pub fn request_type(&self) -> &str {
30 | match self {
31 | Self::ChatHistory(_) => "chat",
32 | Self::Workflow(_) => "workflow",
33 | Self::String(_) => "string",
34 | }
35 | }
36 |
37 | /// Given an input of byte-slice, parses it into a valid request type.
38 | pub async fn try_parse_bytes(input_bytes: &Bytes) -> Result {
39 | let input_string = parse_downloadable(input_bytes).await?;
40 | log::debug!("Parsing input string: {}", input_string);
41 | Ok(Self::try_parse_string(input_string).await)
42 | }
43 |
44 | /// Given an input of string, parses it into a valid request type.
45 | pub async fn try_parse_string(input_string: String) -> Self {
46 | if let Ok(chat_input) = serde_json::from_str::(&input_string) {
47 | GenerationRequest::ChatHistory(chat_input)
48 | } else if let Ok(workflow) = serde_json::from_str::(&input_string) {
49 | GenerationRequest::Workflow(workflow)
50 | } else {
51 | GenerationRequest::String(input_string)
52 | }
53 | }
54 | }
55 |
56 | #[cfg(test)]
57 | mod tests {
58 | use super::*;
59 |
60 | // only implemented for testing purposes
61 | // because `Workflow` does not implement `PartialEq`
62 | impl PartialEq for GenerationRequest {
63 | fn eq(&self, other: &Self) -> bool {
64 | match (self, other) {
65 | (Self::ChatHistory(a), Self::ChatHistory(b)) => {
66 | a.content == b.content && a.history_id == b.history_id
67 | }
68 | (Self::Workflow(_), Self::Workflow(_)) => true, // not implemented
69 | (Self::String(a), Self::String(b)) => a == b,
70 | _ => false,
71 | }
72 | }
73 | }
74 |
75 | #[tokio::test]
76 | async fn test_parse_request_string() {
77 | let request_str = "foobar";
78 | let entry = GenerationRequest::try_parse_bytes(&request_str.as_bytes().into()).await;
79 | assert_eq!(
80 | entry.unwrap(),
81 | GenerationRequest::String(request_str.into())
82 | );
83 | }
84 |
85 | #[tokio::test]
86 | async fn test_parse_request_arweave() {
87 | // contains the string: "\"Hello, Arweave!\""
88 | let arweave_key = serde_json::json!({
89 | "arweave": "Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA"
90 | })
91 | .to_string();
92 | let expected_str = "\"Hello, Arweave!\"";
93 |
94 | let entry = GenerationRequest::try_parse_bytes(&arweave_key.into()).await;
95 | assert_eq!(
96 | entry.unwrap(),
97 | GenerationRequest::String(expected_str.into())
98 | );
99 | }
100 |
101 | #[tokio::test]
102 | async fn test_parse_request_chat() {
103 | let request = ChatHistoryRequest {
104 | history_id: 0,
105 | content: "foobar".to_string(),
106 | };
107 | let request_bytes = serde_json::to_vec(&request).unwrap();
108 | let entry = GenerationRequest::try_parse_bytes(&request_bytes.into()).await;
109 | assert_eq!(entry.unwrap(), GenerationRequest::ChatHistory(request));
110 | }
111 |
112 | #[tokio::test]
113 | async fn test_parse_request_workflow() {
114 | // contains a workflow
115 | let arweave_key = serde_json::json!({
116 | "arweave": "ALRD6i-Xm7xSyl5hF-Tc9WRvsc5C71_TzV3fh1PVgkw"
117 | })
118 | .to_string();
119 | let workflow = GenerationRequest::try_parse_bytes(&arweave_key.into())
120 | .await
121 | .unwrap();
122 | if let GenerationRequest::Workflow(_) = workflow {
123 | /* do nothing */
124 | } else {
125 | panic!("Expected workflow, got something else");
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/core/src/compute/generation/workflow.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use dkn_workflows::{MessageInput, Workflow};
4 | use serde_json::json;
5 |
6 | const DEFAULT_MAX_TIME: u64 = 50;
7 | const DEFAULT_MAX_STEPS: u64 = 10;
8 |
9 | /// Creates a generation workflow with the given input.
10 | ///
11 | /// It is an alias for `make_chat_workflow` with a single message alone.
12 | pub fn make_generation_workflow(input: String) -> Result<(Workflow, Duration), serde_json::Error> {
13 | make_chat_workflow(Vec::new(), input, None, None)
14 | }
15 |
16 | /// Creates a chat workflow with the given input.
17 | ///
18 | /// `messages` is the existing message history, which will be used as context for the `input` message.
19 | pub fn make_chat_workflow(
20 | mut messages: Vec,
21 | input: String,
22 | max_time_sec: Option,
23 | max_steps: Option,
24 | ) -> Result<(Workflow, Duration), serde_json::Error> {
25 | // add the new input to the message history as a user message
26 | messages.push(MessageInput::new_user_message(input));
27 |
28 | // we do like this in-case a dynamic assign is needed
29 | let max_time_sec = max_time_sec.unwrap_or(DEFAULT_MAX_TIME);
30 | let max_steps = max_steps.unwrap_or(DEFAULT_MAX_STEPS);
31 |
32 | let workflow = json!({
33 | "config": {
34 | "max_steps": max_steps,
35 | "max_time": max_time_sec,
36 | "tools": [""]
37 | },
38 | "tasks": [
39 | {
40 | "id": "A",
41 | "name": "Generate with history",
42 | "description": "Expects an array of messages for generation",
43 | "operator": "generation",
44 | "messages": messages,
45 | "outputs": [
46 | {
47 | "type": "write",
48 | "key": "result",
49 | "value": "__result"
50 | }
51 | ]
52 | },
53 | {
54 | "id": "__end",
55 | "operator": "end",
56 | "messages": [{ "role": "user", "content": "End of the task" }],
57 | }
58 | ],
59 | "steps": [ { "source": "A", "target": "__end" } ],
60 | "return_value": {
61 | "input": {
62 | "type": "read",
63 | "key": "result"
64 | }
65 | }
66 | });
67 |
68 | let workflow = serde_json::from_value(workflow)?;
69 |
70 | Ok((workflow, Duration::from_secs(max_time_sec)))
71 | }
72 |
--------------------------------------------------------------------------------
/core/src/compute/handler.rs:
--------------------------------------------------------------------------------
1 | use crate::DriaOracle;
2 | use dria_oracle_contracts::{OracleKind, TaskStatus};
3 |
4 | use alloy::{
5 | primitives::{FixedBytes, U256},
6 | rpc::types::TransactionReceipt,
7 | };
8 | use eyre::Result;
9 |
10 | use super::{handle_generation, handle_validation};
11 |
12 | /// Handles a task request.
13 | ///
14 | /// - Generation tasks are forwarded to `handle_generation`
15 | /// - Validation tasks are forwarded to `handle_validation`
16 | pub async fn handle_request(
17 | node: &DriaOracle,
18 | status: TaskStatus,
19 | task_id: U256,
20 | protocol: FixedBytes<32>,
21 | ) -> Result> {
22 | log::debug!("Received event for task {} ()", task_id);
23 |
24 | // we check the `statusAfter` field of the event, which indicates the final status of the listened task
25 | let response_receipt = match status {
26 | TaskStatus::PendingGeneration => {
27 | if node.kinds.contains(&OracleKind::Generator) {
28 | handle_generation(node, task_id, protocol).await?
29 | } else {
30 | log::debug!(
31 | "Ignoring generation task {} as you are not generator.",
32 | task_id
33 | );
34 | return Ok(None);
35 | }
36 | }
37 | TaskStatus::PendingValidation => {
38 | if node.kinds.contains(&OracleKind::Validator) {
39 | handle_validation(node, task_id).await?
40 | } else {
41 | log::debug!(
42 | "Ignoring generation task {} as you are not validator.",
43 | task_id
44 | );
45 | return Ok(None);
46 | }
47 | }
48 | TaskStatus::Completed => {
49 | return Ok(None);
50 | }
51 | // this is unexpected to happen unless the contract has an eror
52 | // but we dont have to return an error just for this
53 | TaskStatus::None => {
54 | log::error!("None status received in an event: {}", task_id);
55 | return Ok(None);
56 | }
57 | };
58 |
59 | if let Some(receipt) = &response_receipt {
60 | log::info!(
61 | "Task {} processed successfully. (tx: {})",
62 | task_id,
63 | receipt.transaction_hash
64 | );
65 | } else {
66 | log::debug!("Task {} ignored.", task_id)
67 | }
68 |
69 | Ok(response_receipt)
70 | }
71 |
--------------------------------------------------------------------------------
/core/src/compute/mod.rs:
--------------------------------------------------------------------------------
1 | mod handler;
2 | pub use handler::handle_request;
3 |
4 | mod nonce;
5 | pub use nonce::mine_nonce;
6 |
7 | mod generation;
8 | pub use generation::handle_generation;
9 |
10 | pub mod validation;
11 | pub use validation::handle_validation;
12 |
13 | mod utils;
14 | use utils::parse_downloadable;
15 |
16 | mod execute;
17 | use execute::execute_workflow_with_timedout_retries;
18 |
--------------------------------------------------------------------------------
/core/src/compute/nonce.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::{keccak256, Address, Bytes, U256};
2 | use alloy::sol_types::SolValue;
3 |
4 | pub struct NonceResult {
5 | pub nonce: U256,
6 | pub candidate: U256,
7 | pub target: U256,
8 | }
9 |
10 | /// Mines a nonce for the oracle proof-of-work.
11 | ///
12 | /// Returns (nonce, candidate, target).
13 | pub fn mine_nonce(
14 | difficulty: u8,
15 | requester: &Address,
16 | responder: &Address,
17 | input: &Bytes,
18 | task_id: &U256,
19 | ) -> NonceResult {
20 | let big_one = U256::from(1);
21 | let mut nonce = U256::ZERO;
22 |
23 | // target is (2^256-1) / 2^difficulty
24 | let target = U256::MAX >> difficulty;
25 |
26 | loop {
27 | // encode packed
28 | let mut message = Vec::new();
29 | task_id.abi_encode_packed_to(&mut message);
30 | input.abi_encode_packed_to(&mut message);
31 | requester.abi_encode_packed_to(&mut message);
32 | responder.abi_encode_packed_to(&mut message);
33 | nonce.abi_encode_packed_to(&mut message);
34 |
35 | // check hash
36 | let digest = keccak256(message.clone());
37 | let candidate = U256::from_be_bytes(*digest); // big-endian!
38 | if candidate < target {
39 | return NonceResult {
40 | nonce,
41 | candidate,
42 | target,
43 | };
44 | }
45 |
46 | nonce += big_one;
47 | }
48 | }
49 |
50 | #[cfg(test)]
51 | mod tests {
52 | use super::*;
53 | use crate::{DriaOracle, DriaOracleConfig};
54 | use alloy::{
55 | primitives::{address, Bytes, U256},
56 | sol,
57 | };
58 | use eyre::Result;
59 |
60 | sol! {
61 | #[allow(missing_docs)]
62 | // solc v0.8.26; solc tests/contracts/TestNonce.sol --via-ir --optimize --bin
63 | #[sol(rpc, bytecode="608080604052346015576101bc908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6359327f7c14610025575f80fd5b346101825760c0366003190112610182576024359067ffffffffffffffff8211610182573660238301121561018257816004013567ffffffffffffffff811161018257366024828501011161018257604435906001600160a01b038216820361018257606435906001600160a01b03821682036101825760a4359260ff841680940361018257604092828693602460208601996004358b5201868601378301916bffffffffffffffffffffffff199060601b16848301526bffffffffffffffffffffffff199060601b166054820152608435606882015203016028810183526067601f1991011682019282841067ffffffffffffffff85111761016e5760a0928492836040525f19901c908051832090608085525180938160808701528686015e5f84840186015281811115602085015260408401526060830152601f01601f19168101030190f35b634e487b7160e01b5f52604160045260245ffd5b5f80fdfea26469706673582212200bd4b20c7c71d8e44c60f255bf0a5e937f501f0e718e64687e36e0ddbc0d491864736f6c634300081a0033")]
64 | contract TestNonce {
65 | function assertValidNonce(uint256 taskId, bytes calldata input, address requester, address responder, uint256 nonce, uint8 difficulty) external
66 | view
67 | returns (bytes memory message, bool result, bytes32 candidate, uint256 target)
68 | {
69 | message = abi.encodePacked(taskId, input, requester, responder, nonce);
70 | target = type(uint256).max >> uint256(difficulty);
71 | candidate = keccak256(message);
72 | result = uint256(candidate) <= target;
73 | }
74 | }
75 | }
76 |
77 | #[tokio::test]
78 | async fn test_nonce_contract() -> Result<()> {
79 | dotenvy::dotenv().unwrap();
80 |
81 | let config = DriaOracleConfig::new_from_env()?;
82 | let node = DriaOracle::new(config).await?;
83 | let contract = TestNonce::deploy(&node.provider).await?;
84 |
85 | // prepare parameters
86 | let difficulty = 2u8;
87 | let task_id = U256::from(1);
88 | let requester = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
89 | let responder = node.address();
90 | let input = Bytes::from_iter("im some bytes yallllll".bytes());
91 |
92 | // call contract
93 | let NonceResult {
94 | nonce,
95 | candidate,
96 | target,
97 | } = mine_nonce(difficulty, &requester, &responder, &input, &task_id);
98 | // println!("Nonce: {}", nonce);
99 | // println!("Target: {:x}", target);
100 | // println!("Candidate: {:x}", candidate);
101 | let contract_bytes = contract
102 | .assertValidNonce(task_id, input, requester, responder, nonce, difficulty)
103 | .call()
104 | .await?;
105 |
106 | // println!("\nResult: {}", contract_bytes.result);
107 | // println!("Target: {:x}", contract_bytes.target);
108 | // println!("Candidate: {:x}", contract_bytes.candidate);
109 | // println!("Message:\n{:x}", contract_bytes.message);
110 | assert_eq!(contract_bytes.target, target);
111 | assert_eq!(U256::from_be_bytes(contract_bytes.candidate.0), candidate);
112 | assert_eq!(contract_bytes.result, true);
113 | Ok(())
114 | }
115 |
116 | #[test]
117 | fn test_nonce_local() {
118 | let requester = address!("0877022A137b8E8CE1C3020B9f047651dD02E37B");
119 | let responder = address!("0877022A137b8E8CE1C3020B9f047651dD02E37B");
120 | let input = vec![0x01, 0x02, 0x03].into();
121 | let task_id = U256::from(0x1234);
122 | let difficulty = 2;
123 |
124 | let NonceResult {
125 | nonce,
126 | candidate,
127 | target,
128 | } = mine_nonce(difficulty, &requester, &responder, &input, &task_id);
129 | assert!(!nonce.is_zero());
130 |
131 | println!("Nonce: {}", nonce);
132 | println!("Target: {:x}", target);
133 | println!("Candidate: {:x}", candidate);
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/core/src/compute/utils.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::Bytes;
2 | use dria_oracle_contracts::bytes_to_string;
3 | use dria_oracle_storage::{ArweaveStorage, IsExternalStorage};
4 | use eyre::{Context, Result};
5 |
6 | /// Parses a given bytes input to a string,
7 | /// and if it is a storage key identifier it automatically downloads the data from Arweave.
8 | pub async fn parse_downloadable(input_bytes: &Bytes) -> Result {
9 | // first, convert to string
10 | let mut input_string = bytes_to_string(input_bytes)?;
11 |
12 | // then, check storage
13 | if let Some(key) = ArweaveStorage::is_key(&input_string) {
14 | // if its a txid, we download the data and parse it again
15 | let input_bytes_from_arweave = ArweaveStorage::new_readonly()
16 | .get(key)
17 | .await
18 | .wrap_err("could not download from Arweave")?;
19 |
20 | // convert the input to string
21 | input_string = bytes_to_string(&input_bytes_from_arweave)?;
22 | }
23 |
24 | Ok(input_string)
25 | }
26 |
--------------------------------------------------------------------------------
/core/src/compute/validation/execute.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::U256;
2 | use dkn_workflows::Model;
3 | use eyre::{Context, Result};
4 |
5 | use crate::compute::execute::execute_workflow_with_timedout_retries;
6 |
7 | use super::workflow::*;
8 |
9 | #[derive(Debug, serde::Deserialize, serde::Serialize)]
10 | pub struct ValidationResult {
11 | /// How helpful the response is.
12 | helpfulness: u8,
13 | /// How closely the response follows the instruction.
14 | instruction_following: u8,
15 | /// The final score of the response, which we write to the contract.
16 | final_score: u8,
17 | /// The truthfulness of the response.
18 | /// If the response is correct but it doesn't make sense w.r.t instruction,
19 | /// this may still be correct.
20 | truthfulness: u8,
21 | /// The rationale for the scores reported.
22 | rationale: String,
23 | }
24 |
25 | impl ValidationResult {
26 | /// Clamps the score to the range `[1, 5]` and scales it to the range `[1-255]`.
27 | pub fn final_score_as_solidity_type(&self) -> U256 {
28 | U256::from(match self.final_score.clamp(1, 5) {
29 | 1 => 51,
30 | 2 => 102,
31 | 3 => 153,
32 | 4 => 204,
33 | 5 => 255,
34 | _ => unreachable!(),
35 | })
36 | }
37 | }
38 |
39 | /// Validates the given results.
40 | pub async fn execute_validations(
41 | instruction: String,
42 | generations: Vec,
43 | model: Model,
44 | ) -> Result> {
45 | let (workflow, duration) = make_validation_workflow(instruction, generations)?;
46 |
47 | log::debug!("Executing validation request with: {}", model);
48 | let result_str = execute_workflow_with_timedout_retries(&workflow, model, duration).await?;
49 |
50 | // first parse as vec of string
51 | // then parse each string as a ValidationResult
52 | // FIXME: this is a workflows bug, can return a single parseable string instead of array of parsable strings later
53 | let result: Vec = serde_json::from_str::>(&result_str)
54 | .wrap_err("could not parse validation results")?
55 | .into_iter()
56 | .map(|s| serde_json::from_str::(&s))
57 | .collect::, _>>()
58 | .wrap_err("could not parse validation results")?;
59 | Ok(result)
60 | }
61 |
62 | #[cfg(test)]
63 | mod tests {
64 | use super::*;
65 |
66 | #[tokio::test]
67 | #[ignore = "requires OpenAI API key"]
68 | async fn test_validation_multiple() {
69 | dotenvy::dotenv().unwrap();
70 | let _ = env_logger::builder()
71 | .filter_level(log::LevelFilter::Off)
72 | .filter_module("dria_oracle", log::LevelFilter::Debug)
73 | .is_test(true)
74 | .try_init();
75 |
76 | let instruction = "What is 2 + 2".to_string();
77 | let generations: Vec = [
78 | "2 + 2 is 4.", // correct
79 | "2 + 2 is 889992.", // incorrect
80 | "Bonito applebum", // completely irrelevant
81 | "2 + 2 is 4 because apples are green.", // correct but irrational
82 | "2 + 2 is not 5.", // correct but irrelevant
83 | ]
84 | .iter()
85 | .map(|s| s.to_string())
86 | .collect();
87 |
88 | let model = Model::GPT4oMini;
89 | let results = execute_validations(instruction, generations.clone(), model)
90 | .await
91 | .unwrap();
92 |
93 | assert_eq!(
94 | results.len(),
95 | generations.len(),
96 | "expected same number of results"
97 | );
98 | assert!(
99 | results[0].final_score == 5,
100 | "expected top score from correct response"
101 | );
102 | assert!(
103 | results[1].final_score < 3,
104 | "expected low score from wrong response"
105 | );
106 | assert!(
107 | results[2].final_score < 3,
108 | "expected low score from irrelevant response"
109 | );
110 | assert!(
111 | results[3].final_score < 3,
112 | "expected low score from correct but irrational response"
113 | );
114 | assert!(
115 | results[4].final_score < 3,
116 | "expected low score from correct but irrelevant response"
117 | );
118 | }
119 |
120 | #[tokio::test]
121 | #[ignore = "requires OpenAI API key"]
122 | async fn test_validation_single() {
123 | dotenvy::dotenv().unwrap();
124 | let _ = env_logger::builder()
125 | .filter_level(log::LevelFilter::Off)
126 | .filter_module("dria_oracle", log::LevelFilter::Debug)
127 | .is_test(true)
128 | .try_init();
129 |
130 | let instruction = "Can humans eat mango fruit?".to_string();
131 | let generations: Vec = ["Yes they can."].iter().map(|s| s.to_string()).collect();
132 |
133 | let model = Model::GPT4oMini;
134 | let results = execute_validations(instruction, generations.clone(), model)
135 | .await
136 | .unwrap();
137 |
138 | assert!(
139 | results[0].final_score == 5,
140 | "expected top score from correct response"
141 | );
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/core/src/compute/validation/handler.rs:
--------------------------------------------------------------------------------
1 | use crate::{compute::parse_downloadable, mine_nonce, DriaOracle};
2 | use alloy::{primitives::U256, rpc::types::TransactionReceipt};
3 | use dkn_workflows::Model;
4 | use dria_oracle_storage::ArweaveStorage;
5 | use eyre::{eyre, Context, Result};
6 |
7 | use super::execute::execute_validations;
8 |
9 | /// Handles a validation request.
10 | pub async fn handle_validation(
11 | node: &DriaOracle,
12 | task_id: U256,
13 | ) -> Result> {
14 | log::info!("Handling validation task {}", task_id);
15 |
16 | // check if already responded as generator, because we cant validate our own answer
17 | log::debug!("Checking if we are a generator for this task");
18 | let responses = node.coordinator.getResponses(task_id).call().await?._0;
19 | if responses.iter().any(|r| r.responder == node.address()) {
20 | log::debug!(
21 | "Cant validate {} with your own generation response",
22 | task_id
23 | );
24 | return Ok(None);
25 | }
26 |
27 | // check if we have validated anyways
28 | log::debug!("Checking if we have validated already");
29 | let validations = node.coordinator.getValidations(task_id).call().await?._0;
30 | if validations.iter().any(|v| v.validator == node.address()) {
31 | return Err(eyre!("Already validated {}", task_id));
32 | }
33 |
34 | // fetch the request from contract
35 | log::debug!("Fetching the task request");
36 | let request = node.coordinator.requests(task_id).call().await?;
37 |
38 | // fetch each generation response & download its metadata
39 | log::debug!("Fetching response messages");
40 | let responses = node.coordinator.getResponses(task_id).call().await?._0;
41 | let mut generations = Vec::new();
42 | for response in responses {
43 | let metadata_str = parse_downloadable(&response.metadata).await?;
44 | generations.push(metadata_str);
45 | }
46 | let input = parse_downloadable(&request.input).await?;
47 |
48 | // validate each response
49 | log::debug!("Computing validation scores");
50 | let model = Model::GPT4o; // all validations use Gpt 4o
51 | let validations = execute_validations(input, generations, model).await?;
52 | let scores = validations
53 | .iter()
54 | .map(|v| v.final_score_as_solidity_type())
55 | .collect::>();
56 | let metadata =
57 | serde_json::to_string(&validations).wrap_err("could not serialize validations")?;
58 | log::debug!("Validation metadata:\n{}", metadata);
59 |
60 | // uploading to storage
61 | log::debug!("Uploading metadata to storage");
62 | let arweave = ArweaveStorage::new_from_env()?;
63 | let metadata = arweave.put_if_large(metadata.into()).await?;
64 |
65 | // mine nonce
66 | log::debug!("Mining nonce for task");
67 | let nonce = mine_nonce(
68 | request.parameters.difficulty,
69 | &request.requester,
70 | &node.address(),
71 | &request.input,
72 | &task_id,
73 | )
74 | .nonce;
75 |
76 | // respond
77 | log::debug!("Responding with validation");
78 | let tx_receipt = node
79 | .respond_validation(task_id, scores, metadata, nonce)
80 | .await?;
81 | Ok(Some(tx_receipt))
82 | }
83 |
--------------------------------------------------------------------------------
/core/src/compute/validation/mod.rs:
--------------------------------------------------------------------------------
1 | mod execute;
2 | mod handler;
3 | mod workflow;
4 |
5 | pub use handler::handle_validation;
6 |
--------------------------------------------------------------------------------
/core/src/compute/validation/workflow.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use dkn_workflows::Workflow;
4 | use serde_json::json;
5 |
6 | pub fn make_validation_workflow(
7 | instruction: String,
8 | mut generations: Vec,
9 | ) -> Result<(Workflow, Duration), serde_json::Error> {
10 | // workflow processes the array in reverse order, so we reverse the input outside
11 | // to get the correct order in results
12 | generations.reverse();
13 |
14 | let max_time_sec = (generations.len() as u64) * 8 + 10; // we need at most few seconds per generation, plus some leeway here
15 |
16 | let workflow = json!({
17 | "config": {
18 | "max_steps": generations.len() + 5, // we need one step per generation, plus some leeway here
19 | "max_time": max_time_sec,
20 | "tools": ["ALL"],
21 | "max_tokens": 1000 // max tokens do not need to change
22 | },
23 | "external_memory": {
24 | "instruction": instruction,
25 | "generations": generations
26 | },
27 | "tasks": [
28 | {
29 | "id": "evaluate",
30 | "name": "Task",
31 | "description": "Task Description",
32 | "messages": [
33 | {
34 | "role": "user",
35 | "content": "\n# Instruction Following Assessment\n\nEvaluate alignment between output and intent. Assess understanding of task goal and restrictions.\n\n**Instruction Components**: Task Goal (intended outcome), Restrictions (text styles, formats, or designated methods, etc).\n\n**Scoring**: Rate outputs 1 to 5:\n\n1. **Irrelevant**: No alignment.\n\n2. **Partial Focus**: Addresses one aspect poorly.\n\n3. **Partial Compliance**:\n\n- (1) Meets goal or restrictions, neglecting other.\n\n- (2) Acknowledges both but slight deviations.\n\n4. **Almost There**: Near alignment, minor deviations.\n\n5. **Comprehensive Compliance**: Fully aligns, meets all requirements.\n\n## Format:\n\n### Input\n\nInstruction: Write a compelling product description \n\nTexts:\n\n The sleek smartphone features a 6.1\" display\n\n Buy this phone it has good screen\n\n Experience crystal-clear visuals on the stunning 6.1\" OLED display\n\n### Output\n\n### Output for Text 1\n\n{\n \"score\": 3,\n \"rationale\": \"Basic description with specs but lacks compelling language.\"\n}\n\n### Output for Text 2\n\n{\n \"score\": 1,\n \"rationale\": \"Too informal and vague. Missing specific details.\"\n}\n\n### Output for Text 3\n\n{\n \"score\": 5,\n \"rationale\": \"Uses engaging language. Includes specific features. Creates desire.\"\n}\n\n---\n \n## Annotation\n\n### Input\n\nInstruction: {{instruction}}\n\nTexts:\n\n{{generations}}\n\n### Output"
36 | }
37 | ],
38 | "schema": "{\"properties\": {\"instruction_following\": {\"description\": \"The score for the instruction following ability.\", \"gte\": 1, \"lte\": 5, \"title\": \"Instruction Following\", \"type\": \"integer\"}, \"truthfulness\": {\"description\": \"The score for the truthfulness of the response.\", \"gte\": 1, \"lte\": 5, \"title\": \"Truthfulness\", \"type\": \"integer\"}, \"helpfulness\": {\"description\": \"The score for the helpfulness of the response.\", \"gte\": 1, \"lte\": 5, \"title\": \"Helpfulness\", \"type\": \"integer\"}, \"final_score\": {\"description\": \"The overall score of the evaluation.\", \"gte\": 1, \"lte\": 5, \"title\": \"Final Score\", \"type\": \"integer\"}, \"rationale\": {\"description\": \"The rationale of the evaluation.\", \"title\": \"Rationale\", \"type\": \"string\"}}, \"required\": [\"instruction_following\", \"truthfulness\", \"helpfulness\", \"final_score\", \"rationale\"], \"title\": \"Evaluation\", \"type\": \"object\", \"additionalProperties\": false}",
39 | "inputs": [
40 | {
41 | "name": "instruction",
42 | "value": {
43 | "type": "read",
44 | "key": "instruction"
45 | },
46 | "required": true
47 | },
48 | {
49 | "name": "generations",
50 | "value": {
51 | "type": "pop",
52 | "key": "generations"
53 | },
54 | "required": true
55 | }
56 | ],
57 | "operator": "generation",
58 | "outputs": [
59 | {
60 | "type": "push",
61 | "key": "search_result",
62 | "value": "__result"
63 | }
64 | ]
65 | },
66 | {
67 | "id": "_end",
68 | "name": "Task",
69 | "description": "Task Description",
70 | "messages": [
71 | {
72 | "role": "user",
73 | "content": ""
74 | }
75 | ],
76 | "operator": "end"
77 | }
78 | ],
79 | "steps": [
80 | {
81 | "source": "evaluate",
82 | "target": "_end",
83 | "condition": {
84 | "input": {
85 | "type": "size",
86 | "key": "generations"
87 | },
88 | "expected": "0",
89 | "expression": "Equal",
90 | "target_if_not": "evaluate"
91 | }
92 | }
93 | ],
94 | "return_value": {
95 | "input": {
96 | "type": "get_all",
97 | "key": "search_result"
98 | },
99 | "to_json": true
100 | }
101 | });
102 |
103 | let workflow = serde_json::from_value(workflow)?;
104 |
105 | Ok((workflow, Duration::from_secs(max_time_sec)))
106 | }
107 |
--------------------------------------------------------------------------------
/core/src/configurations/mod.rs:
--------------------------------------------------------------------------------
1 | use alloy::{
2 | hex::FromHex, network::EthereumWallet, primitives::B256, signers::local::PrivateKeySigner,
3 | transports::http::reqwest::Url,
4 | };
5 |
6 | use eyre::{Context, Result};
7 | use std::env;
8 | use std::time::Duration;
9 |
10 | /// Configuration for the Dria Oracle.
11 | #[derive(Debug, Clone)]
12 | pub struct DriaOracleConfig {
13 | /// Wallet for the oracle.
14 | pub wallet: EthereumWallet,
15 | /// RPC URL for the oracle, decides the connected chain.
16 | pub rpc_url: Url,
17 | /// Optional transaction timeout, is useful to avoid getting stuck at `get_receipt()` when making a transaction.
18 | pub tx_timeout: Option,
19 | }
20 |
21 | impl DriaOracleConfig {
22 | pub fn new(private_key: &B256, rpc_url: Url) -> Result {
23 | let signer =
24 | PrivateKeySigner::from_bytes(private_key).wrap_err("could not parse private key")?;
25 | let wallet = EthereumWallet::from(signer);
26 |
27 | Ok(Self {
28 | wallet,
29 | rpc_url,
30 | tx_timeout: None,
31 | })
32 | }
33 |
34 | /// Creates the config from the environment variables.
35 | ///
36 | /// Required environment variables:
37 | /// - `SECRET_KEY`
38 | /// - `RPC_URL`
39 | pub fn new_from_env() -> Result {
40 | // parse private key
41 | let private_key_hex = env::var("SECRET_KEY")?;
42 | let secret_key = B256::from_hex(private_key_hex).wrap_err("could not decode secret key")?;
43 |
44 | // parse rpc url
45 | let rpc_url_env = env::var("RPC_URL")?;
46 | let rpc_url = Url::parse(&rpc_url_env).wrap_err("could not parse RPC_URL")?;
47 |
48 | Self::new(&secret_key, rpc_url)
49 | }
50 |
51 | /// Change the transaction timeout.
52 | ///
53 | /// This will make transaction wait for the given duration before timing out,
54 | /// otherwise the node may get stuck waiting for a lost transaction.
55 | pub fn with_tx_timeout(mut self, tx_timeout: Duration) -> Self {
56 | self.tx_timeout = Some(tx_timeout);
57 | self
58 | }
59 |
60 | /// Change the RPC URL.
61 | pub fn with_rpc_url(mut self, rpc_url: Url) -> Self {
62 | self.rpc_url = rpc_url;
63 | self
64 | }
65 |
66 | /// Change the underlying wallet.
67 | pub fn with_wallet(mut self, wallet: EthereumWallet) -> Self {
68 | self.wallet = wallet;
69 | self
70 | }
71 |
72 | /// Change the signer with a new one with the given secret key.
73 | pub fn with_secret_key(&mut self, secret_key: &B256) -> Result<&mut Self> {
74 | let signer =
75 | PrivateKeySigner::from_bytes(secret_key).wrap_err("could not parse private key")?;
76 | self.wallet.register_default_signer(signer);
77 | Ok(self)
78 | }
79 |
80 | /// Change the signer with a new one.
81 | pub fn with_signer(&mut self, signer: PrivateKeySigner) -> &mut Self {
82 | self.wallet.register_default_signer(signer);
83 | self
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/core/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![doc = include_str!("../../README.md")]
2 |
3 | mod cli;
4 | pub use cli::{handle_command, Cli};
5 |
6 | mod node;
7 | pub use node::DriaOracle;
8 |
9 | /// Node configurations.
10 | mod configurations;
11 | pub use configurations::DriaOracleConfig;
12 |
13 | mod compute;
14 | pub use compute::{handle_generation, handle_request, handle_validation, mine_nonce};
15 |
--------------------------------------------------------------------------------
/core/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use clap::Parser;
4 | use dria_oracle::{Cli, DriaOracle, DriaOracleConfig};
5 |
6 | #[tokio::main]
7 | async fn main() -> eyre::Result<()> {
8 | // default commands such as version and help exit at this point
9 | let cli = Cli::parse();
10 |
11 | // read env w.r.t cli argument, defaults to `.env`
12 | let dotenv_result = dotenvy::from_path(&cli.env);
13 |
14 | // init env logger
15 | let log_level = match cli.debug {
16 | true => log::LevelFilter::Debug,
17 | false => log::LevelFilter::Info,
18 | };
19 | env_logger::builder()
20 | .format_timestamp(Some(env_logger::TimestampPrecision::Millis))
21 | .filter(None, log::LevelFilter::Off)
22 | .filter_module("dria_oracle", log_level)
23 | .filter_module("dkn_workflows", log_level)
24 | .filter_module("dria_oracle_contracts", log_level)
25 | .filter_module("dria_oracle_storage", log_level)
26 | .parse_default_env()
27 | .init();
28 |
29 | // log about env usage after env logger init is executed
30 | match dotenv_result {
31 | Ok(_) => eprintln!("Loaded .env file at: {}", cli.env.display()),
32 | Err(e) => eprintln!("Could not load .env file: {}", e),
33 | }
34 |
35 | // read required env variables
36 | let secret_key = Cli::read_secret_key()?;
37 | let rpc_url = Cli::read_rpc_url()?;
38 | let tx_timeout = Cli::read_tx_timeout()?;
39 |
40 | // create config
41 | let config = DriaOracleConfig::new(&secret_key, rpc_url)?
42 | .with_tx_timeout(Duration::from_secs(tx_timeout));
43 |
44 | // create node
45 | let node = DriaOracle::new(config).await?;
46 | log::info!("{}", node);
47 |
48 | // handle cli command
49 | dria_oracle::handle_command(cli.command, node).await?;
50 |
51 | log::info!("Bye!");
52 | Ok(())
53 | }
54 |
--------------------------------------------------------------------------------
/core/src/node/anvil.rs:
--------------------------------------------------------------------------------
1 | //! Anvil-related utilities.
2 | //!
3 | //! This module is only available when the `anvil` feature is enabled,
4 | //! which is only expected to happen in tests.
5 |
6 | use super::DriaOracle;
7 |
8 | use alloy::network::{Ethereum, EthereumWallet};
9 | use alloy::primitives::Address;
10 | use alloy::primitives::{utils::parse_ether, U256};
11 | use alloy::providers::ext::AnvilApi;
12 | use alloy::providers::{PendingTransactionBuilder, Provider, ProviderBuilder};
13 | use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
14 | use alloy::signers::local::PrivateKeySigner;
15 | use alloy::transports::http::Http;
16 | use eyre::Result;
17 | use reqwest::{Client, Url};
18 |
19 | impl DriaOracle {
20 | /// We dedicate an unused port to Anvil.
21 | pub const ANVIL_PORT: u16 = 8545;
22 | /// Default ETH funding amount for generated wallets.
23 | pub const ANVIL_FUND_ETHER: &'static str = "10000";
24 |
25 | /// Generates a random wallet, funded with the given `fund` amount.
26 | ///
27 | /// If `fund` is not provided, 10K ETH is used.
28 | pub async fn anvil_new_funded_wallet(&self, fund: Option) -> Result {
29 | let fund = fund.unwrap_or_else(|| parse_ether(Self::ANVIL_FUND_ETHER).unwrap());
30 | let signer = PrivateKeySigner::random();
31 | self.provider
32 | .anvil_set_balance(signer.address(), fund)
33 | .await?;
34 | let wallet = EthereumWallet::from(signer);
35 | Ok(wallet)
36 | }
37 |
38 | /// Whitelists a given address, impersonates the owner in doing so.
39 | pub async fn anvil_whitelist_registry(&self, address: Address) -> Result {
40 | let owner = self.registry.owner().call().await?._0;
41 |
42 | // increase owner balance
43 | self.anvil_increase_balance(owner, parse_ether("1").unwrap())
44 | .await?;
45 |
46 | let tx = self
47 | .send_impersonated_transaction(
48 | self.registry
49 | .addToWhitelist(vec![address])
50 | .into_transaction_request(),
51 | owner,
52 | )
53 | .await?;
54 | let receipt = self.wait_for_tx(tx).await?;
55 |
56 | Ok(receipt)
57 | }
58 |
59 | /// Increases the balance of an account by the given amount.
60 | #[inline]
61 | pub async fn anvil_increase_balance(&self, address: Address, amount: U256) -> Result<()> {
62 | let balance = self.provider.get_balance(address).await?;
63 | self.provider
64 | .anvil_set_balance(address, balance + amount)
65 | .await?;
66 | Ok(())
67 | }
68 |
69 | /// Assumes that an Anvil instance is running already at the given port.
70 | ///
71 | /// We use this due to the issue: https://github.com/alloy-rs/alloy/issues/1918
72 | #[inline]
73 | pub async fn send_impersonated_transaction(
74 | &self,
75 | tx: TransactionRequest,
76 | from: Address,
77 | ) -> Result, Ethereum>> {
78 | let anvil = ProviderBuilder::new().on_http(Self::anvil_url());
79 |
80 | anvil.anvil_impersonate_account(from).await?;
81 | let pending_tx = anvil.send_transaction(tx.from(from)).await?;
82 | anvil.anvil_stop_impersonating_account(from).await?;
83 |
84 | Ok(pending_tx)
85 | }
86 |
87 | /// Returns the spawned Anvil URL, can be used with `ProviderBuilder::new().on_http(url)`.
88 | #[inline(always)]
89 | pub fn anvil_url() -> Url {
90 | Url::parse(&format!("http://localhost:{}", Self::ANVIL_PORT)).expect("could not parse URL")
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/core/src/node/coordinator.rs:
--------------------------------------------------------------------------------
1 | use super::DriaOracle;
2 | use alloy::eips::BlockNumberOrTag;
3 | use alloy::primitives::aliases::U40;
4 | use alloy::primitives::{Bytes, U256};
5 | use alloy::rpc::types::{Log, TransactionReceipt};
6 | use dria_oracle_contracts::string_to_bytes32;
7 | use eyre::{eyre, Result};
8 |
9 | use dria_oracle_contracts::OracleCoordinator::{
10 | getFeeReturn, getResponsesReturn, getValidationsReturn, requestsReturn,
11 | LLMOracleTaskParameters, StatusUpdate,
12 | };
13 |
14 | impl DriaOracle {
15 | /// Request an oracle task. This is not done by the oracle normally, but we have it added for testing purposes.
16 | pub async fn request(
17 | &self,
18 | input: Bytes,
19 | models: Bytes,
20 | difficulty: u8,
21 | num_gens: u64,
22 | num_vals: u64,
23 | protocol: String,
24 | ) -> Result {
25 | let parameters = LLMOracleTaskParameters {
26 | difficulty,
27 | numGenerations: U40::from(num_gens),
28 | numValidations: U40::from(num_vals),
29 | };
30 |
31 | let req = self
32 | .coordinator
33 | .request(string_to_bytes32(protocol)?, input, models, parameters);
34 | let tx = self.send_with_gas_hikes(req).await?;
35 | self.wait_for_tx(tx).await
36 | }
37 |
38 | /// Responds to a generation request with the response, metadata, and a valid nonce.
39 | pub async fn respond_generation(
40 | &self,
41 | task_id: U256,
42 | response: Bytes,
43 | metadata: Bytes,
44 | nonce: U256,
45 | ) -> Result {
46 | let req = self.coordinator.respond(task_id, nonce, response, metadata);
47 | let tx = self.send_with_gas_hikes(req).await?;
48 | self.wait_for_tx(tx).await
49 | }
50 |
51 | /// Responds to a validation request with the score, metadata, and a valid nonce.
52 | #[inline]
53 | pub async fn respond_validation(
54 | &self,
55 | task_id: U256,
56 | scores: Vec,
57 | metadata: Bytes,
58 | nonce: U256,
59 | ) -> Result {
60 | let req = self.coordinator.validate(task_id, nonce, scores, metadata);
61 | let tx = self.send_with_gas_hikes(req).await?;
62 | self.wait_for_tx(tx).await
63 | }
64 |
65 | /// Get previous tasks within the range of blocks.
66 | pub async fn get_tasks_in_range(
67 | &self,
68 | from_block: impl Into,
69 | to_block: impl Into,
70 | ) -> Result> {
71 | let tasks = self
72 | .coordinator
73 | .StatusUpdate_filter()
74 | .from_block(from_block)
75 | .to_block(to_block)
76 | .query()
77 | .await?;
78 |
79 | Ok(tasks)
80 | }
81 |
82 | /// Get task info for a given task id.
83 | pub async fn get_task(
84 | &self,
85 | task_id: U256,
86 | ) -> Result<(requestsReturn, getResponsesReturn, getValidationsReturn)> {
87 | // check if task id is valid
88 | if task_id.is_zero() {
89 | return Err(eyre!("Task ID must be non-zero."));
90 | } else if task_id >= self.coordinator.nextTaskId().call().await?._0 {
91 | return Err(eyre!("Task with id {} has not been created yet.", task_id));
92 | }
93 |
94 | // get task info
95 | let request = self.coordinator.requests(task_id).call().await?;
96 | let responses = self.coordinator.getResponses(task_id).call().await?;
97 | let validations = self.coordinator.getValidations(task_id).call().await?;
98 |
99 | Ok((request, responses, validations))
100 | }
101 |
102 | /// Get fee details for a given request setting.
103 | pub async fn get_request_fee(
104 | &self,
105 | difficulty: u8,
106 | num_gens: u64,
107 | num_vals: u64,
108 | ) -> Result {
109 | let parameters = LLMOracleTaskParameters {
110 | difficulty,
111 | numGenerations: U40::from(num_gens),
112 | numValidations: U40::from(num_vals),
113 | };
114 |
115 | let fees = self.coordinator.getFee(parameters).call().await?;
116 |
117 | Ok(fees)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/core/src/node/core.rs:
--------------------------------------------------------------------------------
1 | use alloy::contract::CallBuilder;
2 | use alloy::hex::FromHex;
3 | use alloy::providers::{PendingTransactionBuilder, WalletProvider};
4 | use alloy::transports::RpcError;
5 | use alloy::{
6 | network::EthereumWallet,
7 | primitives::Address,
8 | providers::{Provider, ProviderBuilder},
9 | };
10 | use alloy_chains::Chain;
11 | use dkn_workflows::{DriaWorkflowsConfig, Model, ModelProvider};
12 | use dria_oracle_contracts::ERC20::ERC20Instance;
13 | use dria_oracle_contracts::{
14 | contract_error_report, get_coordinator_address, OracleCoordinator, OracleKind, OracleRegistry,
15 | TokenBalance, ERC20,
16 | };
17 | use eyre::{eyre, Context, Result};
18 | use std::env;
19 |
20 | impl crate::DriaOracle {
21 | /// Creates a new Oracle node with the given private key and connected to the chain at the given RPC URL.
22 | ///
23 | /// If `anvil` feature is enabled, the node will connect to an Anvil fork of the chain.
24 | pub async fn new(config: crate::DriaOracleConfig) -> Result {
25 | #[cfg(not(feature = "anvil"))]
26 | let provider = ProviderBuilder::new()
27 | .with_recommended_fillers()
28 | .wallet(config.wallet.clone())
29 | .on_http(config.rpc_url.clone());
30 |
31 | #[cfg(feature = "anvil")]
32 | let provider = ProviderBuilder::new()
33 | .with_recommended_fillers()
34 | .wallet(config.wallet.clone())
35 | .on_anvil_with_config(|anvil| {
36 | anvil.fork(config.rpc_url.clone()).port(Self::ANVIL_PORT)
37 | });
38 |
39 | // fetch the chain id so that we can use the correct addresses
40 | let chain = Chain::from_id(provider.get_chain_id().await?)
41 | .named()
42 | .ok_or_else(|| eyre!("expected a named chain"))?;
43 |
44 | #[cfg(not(feature = "anvil"))]
45 | log::info!("Connected to {} network", chain);
46 | #[cfg(feature = "anvil")]
47 | log::info!("Connected to Anvil forked from {} network", chain);
48 |
49 | // get coordinator address from static list or the environment
50 | // (address within env can have 0x at the start, or not, does not matter)
51 | // and then create the coordinator instance
52 | let coordinator_address = if let Ok(addr) = env::var("COORDINATOR_ADDRESS") {
53 | Address::from_hex(addr).wrap_err("could not parse coordinator address in env")?
54 | } else {
55 | get_coordinator_address(chain)?
56 | };
57 | let coordinator = OracleCoordinator::new(coordinator_address, provider.clone());
58 |
59 | // get registry address from the coordinator & create instance
60 | let registry_address = coordinator
61 | .registry()
62 | .call()
63 | .await
64 | .wrap_err("could not get registry address from the coordinator")?
65 | ._0;
66 | let registry = OracleRegistry::new(registry_address, provider.clone());
67 |
68 | // get token address from the coordinator & create instance
69 | let token_address = coordinator
70 | .feeToken()
71 | .call()
72 | .await
73 | .wrap_err("could not get token address from the coordinator")?
74 | ._0;
75 | let token = ERC20::new(token_address, provider.clone());
76 |
77 | let node = Self {
78 | config,
79 | provider,
80 | token,
81 | coordinator,
82 | registry,
83 | kinds: Vec::default(), // TODO: take this from main config
84 | workflows: DriaWorkflowsConfig::default(), // TODO: take this from main config
85 | };
86 |
87 | Ok(node)
88 | }
89 |
90 | /// Creates a new node that uses the given wallet as its signer.
91 | pub fn connect(&self, wallet: EthereumWallet) -> Self {
92 | // first, clone the provider and set the wallet
93 | let mut provider = self.provider.clone();
94 | *provider.wallet_mut() = wallet.clone();
95 |
96 | // then instantiate the contract instances with the new provider
97 | let token = ERC20Instance::new(*self.token.address(), provider.clone());
98 | let coordinator = OracleCoordinator::new(*self.coordinator.address(), provider.clone());
99 | let registry = OracleRegistry::new(*self.registry.address(), provider.clone());
100 |
101 | Self {
102 | provider,
103 | config: self.config.clone().with_wallet(wallet),
104 | kinds: self.kinds.clone(),
105 | workflows: self.workflows.clone(),
106 | token,
107 | coordinator,
108 | registry,
109 | }
110 | }
111 |
112 | /// Given the kinds and models, prepares the configurations for the oracle.
113 | ///
114 | /// - If `kinds` is empty, it will check the registrations and use them as kinds.
115 | /// - If `models` is empty, gives an error.
116 | pub async fn prepare_oracle(
117 | &mut self,
118 | mut kinds: Vec,
119 | models: Vec,
120 | ) -> Result<()> {
121 | if kinds.is_empty() {
122 | // if kinds are not provided, use the registrations as kinds
123 | log::debug!("No kinds provided. Checking registrations.");
124 | for kind in [OracleKind::Generator, OracleKind::Validator] {
125 | if self.is_registered(kind).await? {
126 | kinds.push(kind);
127 | }
128 | }
129 |
130 | if kinds.is_empty() {
131 | return Err(eyre!("You are not registered as any type of oracle."))?;
132 | }
133 | } else {
134 | // otherwise, make sure we are registered to required kinds
135 | for kind in &kinds {
136 | if !self.is_registered(*kind).await? {
137 | return Err(eyre!("You need to register as {} first.", kind))?;
138 | }
139 | }
140 | }
141 |
142 | // prepare model config & check services
143 | let mut model_config = DriaWorkflowsConfig::new(models);
144 | let ollama_config = model_config.ollama.clone();
145 | model_config = model_config.with_ollama_config(
146 | ollama_config
147 | .with_min_tps(5.0)
148 | .with_timeout(std::time::Duration::from_secs(150)),
149 | );
150 | model_config.check_services().await?;
151 | if model_config.models.is_empty() {
152 | return Err(eyre!("No models provided."))?;
153 | }
154 |
155 | // validator-specific checks here
156 | if kinds.contains(&OracleKind::Validator) {
157 | // make sure we have GPT4o model
158 | if !model_config
159 | .models
160 | .contains(&(ModelProvider::OpenAI, Model::GPT4o))
161 | {
162 | return Err(eyre!("Validator must have GPT4o model."))?;
163 | }
164 |
165 | // make sure node is whitelisted
166 | if !self.is_whitelisted(self.address()).await? {
167 | return Err(eyre!("You are not whitelisted in the registry."))?;
168 | }
169 | }
170 |
171 | self.workflows = model_config;
172 | self.kinds = kinds;
173 |
174 | Ok(())
175 | }
176 |
177 | /// Returns the native token (ETH) balance of a given address.
178 | #[inline]
179 | pub async fn get_native_balance(&self, address: Address) -> Result {
180 | let balance = self.provider.get_balance(address).await?;
181 | Ok(TokenBalance::new(balance, "ETH", None))
182 | }
183 |
184 | /// Returns the address of the configured wallet.
185 | #[inline(always)]
186 | pub fn address(&self) -> Address {
187 | self.config.wallet.default_signer().address()
188 | }
189 |
190 | /// Waits for a transaction to be mined, returning the receipt.
191 | #[inline]
192 | pub async fn wait_for_tx(
193 | &self,
194 | tx: PendingTransactionBuilder,
195 | ) -> Result
196 | where
197 | T: alloy::transports::Transport + Clone,
198 | N: alloy::network::Network,
199 | {
200 | let tx_hash = *tx.tx_hash();
201 | log::info!("Waiting for tx: {:?}", tx_hash);
202 | let receipt = tx
203 | .with_timeout(self.config.tx_timeout)
204 | .get_receipt()
205 | .await
206 | .wrap_err(format!("tx {} failed", tx_hash))?;
207 | Ok(receipt)
208 | }
209 |
210 | /// Given a request, retries sending it with increasing gas prices to avoid
211 | /// the "tx underpriced" errors.
212 | #[inline]
213 | pub async fn send_with_gas_hikes(
214 | &self,
215 | req: CallBuilder,
216 | ) -> Result>
217 | where
218 | T: alloy::transports::Transport + Clone,
219 | P: alloy::providers::Provider + Clone,
220 | D: alloy::contract::CallDecoder + Clone,
221 | N: alloy::network::Network,
222 | {
223 | // gas price hikes to try in increasing order, first is 0 to simply use the
224 | // initial gas fee for the first attempt
225 | const GAS_PRICE_HIKES: [u128; 4] = [0, 12, 24, 36];
226 |
227 | // try and send tx, with increasing gas prices for few attempts
228 | let initial_gas_price = self.provider.get_gas_price().await?;
229 | for (attempt_no, increase_percentage) in GAS_PRICE_HIKES.iter().enumerate() {
230 | // set gas price
231 | let gas_price = initial_gas_price + (initial_gas_price / 100) * increase_percentage;
232 |
233 | // try to send tx with gas price
234 | match req.clone().gas_price(gas_price).send().await {
235 | // if all is well, we can return the tx
236 | Ok(tx) => {
237 | return Ok(tx);
238 | }
239 | // if we get an RPC error; specifically, if the tx is underpriced, we try again with higher gas
240 | Err(alloy::contract::Error::TransportError(RpcError::ErrorResp(err))) => {
241 | // TODO: kind of a code-smell, can we do better check here?
242 | if err.message.contains("underpriced") {
243 | log::warn!(
244 | "{} with gas {} in attempt {}",
245 | err.message,
246 | gas_price,
247 | attempt_no + 1,
248 | );
249 |
250 | // wait just a little bit
251 | tokio::time::sleep(std::time::Duration::from_millis(300)).await;
252 |
253 | continue;
254 | } else {
255 | // otherwise let it be handled by the error report
256 | return Err(contract_error_report(
257 | alloy::contract::Error::TransportError(RpcError::ErrorResp(err)),
258 | ));
259 | }
260 | }
261 | // if we get any other error, we report it
262 | Err(err) => return Err(contract_error_report(err)),
263 | };
264 | }
265 |
266 | // all attempts failed
267 | Err(eyre!("Failed all attempts send tx due to underpriced gas."))
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/core/src/node/mod.rs:
--------------------------------------------------------------------------------
1 | use alloy::network::Ethereum;
2 | use dkn_workflows::DriaWorkflowsConfig;
3 | use dria_oracle_contracts::OracleKind;
4 | use dria_oracle_contracts::{OracleCoordinator, OracleRegistry, ERC20};
5 |
6 | mod coordinator;
7 | mod core;
8 | mod registry;
9 | mod token;
10 |
11 | mod types;
12 | use types::*;
13 |
14 | #[cfg(feature = "anvil")]
15 | mod anvil;
16 |
17 | use super::DriaOracleConfig;
18 | pub struct DriaOracle {
19 | pub config: DriaOracleConfig,
20 | /// Contract addresses for the oracle, respects the connected chain.
21 | // pub addresses: ContractAddresses,
22 | pub token: ERC20::ERC20Instance,
23 | pub coordinator: OracleCoordinator::OracleCoordinatorInstance<
24 | DriaOracleTransport,
25 | DriaOracleProvider,
26 | Ethereum,
27 | >,
28 | pub registry:
29 | OracleRegistry::OracleRegistryInstance,
30 | /// Underlying provider type.
31 | pub provider: DriaOracleProvider,
32 | /// Kinds of this oracle, i.e. `generator`, `validator`.
33 | pub kinds: Vec,
34 | /// Workflows config, defines the available models & services.
35 | pub workflows: DriaWorkflowsConfig,
36 | }
37 |
38 | impl std::fmt::Display for DriaOracle {
39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 | write!(
41 | f,
42 | "Dria Oracle Node v{}\nOracle Address: {}\nRPC URL: {}\nCoordinator: {}\nTx timeout: {}s",
43 | env!("CARGO_PKG_VERSION"),
44 | self.address(),
45 | self.config.rpc_url,
46 | self.coordinator.address(),
47 | self.config.tx_timeout.map(|t| t.as_secs()).unwrap_or_default()
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/core/src/node/registry.rs:
--------------------------------------------------------------------------------
1 | use alloy::{primitives::Address, rpc::types::TransactionReceipt};
2 | use dria_oracle_contracts::{OracleKind, TokenBalance};
3 | use eyre::Result;
4 |
5 | impl crate::DriaOracle {
6 | /// Register the oracle with the registry.
7 | #[inline]
8 | pub async fn register_kind(&self, kind: OracleKind) -> Result {
9 | let req = self.registry.register(kind.into());
10 | let tx = self.send_with_gas_hikes(req).await?;
11 |
12 | self.wait_for_tx(tx).await
13 | }
14 |
15 | /// Unregister from the oracle registry.
16 | #[inline]
17 | pub async fn unregister_kind(&self, kind: OracleKind) -> Result {
18 | let req = self.registry.unregister(kind.into());
19 | let tx = self.send_with_gas_hikes(req).await?;
20 |
21 | self.wait_for_tx(tx).await
22 | }
23 |
24 | /// Returns the amount of tokens to be staked to registry.
25 | pub async fn get_registry_stake_amount(&self, kind: OracleKind) -> Result {
26 | let stake_amount = self.registry.getStakeAmount(kind.into()).call().await?._0;
27 |
28 | // return the symbol as well
29 | let token_symbol = self.token.symbol().call().await?._0;
30 |
31 | Ok(TokenBalance::new(
32 | stake_amount,
33 | token_symbol,
34 | Some(*self.token.address()),
35 | ))
36 | }
37 |
38 | /// Returns whether the oracle is registered as a given kind.
39 | #[inline]
40 | pub async fn is_registered(&self, kind: OracleKind) -> Result {
41 | let is_registered = self
42 | .registry
43 | .isRegistered(self.address(), kind.into())
44 | .call()
45 | .await?;
46 | Ok(is_registered._0)
47 | }
48 |
49 | /// Returns whether a given address is whitelisted or not.
50 | #[inline]
51 | pub async fn is_whitelisted(&self, address: Address) -> Result {
52 | let is_whitelisted = self.registry.isWhitelisted(address).call().await?;
53 | Ok(is_whitelisted._0)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/core/src/node/token.rs:
--------------------------------------------------------------------------------
1 | use super::DriaOracle;
2 | use alloy::primitives::{Address, U256};
3 | use alloy::rpc::types::TransactionReceipt;
4 | use dria_oracle_contracts::TokenBalance;
5 | use eyre::Result;
6 |
7 | impl DriaOracle {
8 | /// Returns the token balance of a given address.
9 | pub async fn get_token_balance(&self, address: Address) -> Result {
10 | let token_balance = self.token.balanceOf(address).call().await?._0;
11 | let token_symbol = self.token.symbol().call().await?._0;
12 |
13 | Ok(TokenBalance::new(
14 | token_balance,
15 | token_symbol,
16 | Some(*self.token.address()),
17 | ))
18 | }
19 |
20 | /// Transfer tokens from one address to another, calls `transferFrom` of the ERC20 contract.
21 | ///
22 | /// Assumes that approvals are made priorly.
23 | pub async fn transfer_from(
24 | &self,
25 | from: Address,
26 | to: Address,
27 | amount: U256,
28 | ) -> Result {
29 | let req = self.token.transferFrom(from, to, amount);
30 | let tx = self.send_with_gas_hikes(req).await?;
31 | self.wait_for_tx(tx).await
32 | }
33 |
34 | /// Approves the `spender` to spend `amount` tokens on behalf of the caller.
35 | pub async fn approve(&self, spender: Address, amount: U256) -> Result {
36 | let req = self.token.approve(spender, amount);
37 | let tx = self.send_with_gas_hikes(req).await?;
38 | self.wait_for_tx(tx).await
39 | }
40 |
41 | /// Returns the allowance of a given `spender` address to spend tokens on behalf of `owner` address.
42 | pub async fn allowance(&self, owner: Address, spender: Address) -> Result {
43 | let token_symbol = self.token.symbol().call().await?._0;
44 | let allowance = self.token.allowance(owner, spender).call().await?._0;
45 |
46 | Ok(TokenBalance::new(
47 | allowance,
48 | token_symbol,
49 | Some(*self.token.address()),
50 | ))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/core/src/node/types.rs:
--------------------------------------------------------------------------------
1 | //! This module exports the transport and provider type based on the `anvil` feature.
2 | //!
3 | //! - If `anvil` is enabled, then Anvil-compatible provider type is created.
4 | //! - Otherwise, default provider is created for Ethereum-like networks.
5 |
6 | use alloy::providers::fillers::{
7 | BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller,
8 | };
9 | use alloy::{
10 | network::{Ethereum, EthereumWallet},
11 | providers::{Identity, RootProvider},
12 | };
13 |
14 | #[cfg(not(feature = "anvil"))]
15 | pub type DriaOracleTransport = alloy::transports::http::Http;
16 |
17 | #[cfg(feature = "anvil")]
18 | pub type DriaOracleTransport = alloy::transports::BoxTransport;
19 |
20 | #[cfg(not(feature = "anvil"))]
21 | pub type DriaOracleProvider = FillProvider<
22 | JoinFill<
23 | JoinFill<
24 | Identity,
25 | JoinFill>>,
26 | >,
27 | WalletFiller,
28 | >,
29 | RootProvider,
30 | DriaOracleTransport,
31 | Ethereum,
32 | >;
33 |
34 | #[cfg(feature = "anvil")]
35 | pub type DriaOracleProvider = FillProvider<
36 | JoinFill<
37 | JoinFill<
38 | Identity,
39 | JoinFill>>,
40 | >,
41 | WalletFiller,
42 | >,
43 | alloy::providers::layers::AnvilProvider, DriaOracleTransport>,
44 | DriaOracleTransport,
45 | Ethereum,
46 | >;
47 |
--------------------------------------------------------------------------------
/core/tests/abi_encode_test.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "anvil")]
2 |
3 | use std::str::FromStr;
4 |
5 | use alloy::{
6 | network::EthereumWallet,
7 | node_bindings::Anvil,
8 | primitives::{address, keccak256, Address, Bytes, U256},
9 | providers::ProviderBuilder,
10 | signers::local::PrivateKeySigner,
11 | sol,
12 | sol_types::SolValue,
13 | };
14 | use eyre::Result;
15 |
16 | sol! {
17 | #[allow(missing_docs)]
18 | // solc v0.8.26; solc tests/contracts/TestSolidityPacked.sol --via-ir --optimize --bin
19 | #[sol(rpc, bytecode="608080604052346015576102bd908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063026131d7146101115780639dac80891461007f5763aa1e84de1461003a575f80fd5b3461007b57602036600319011261007b5760043567ffffffffffffffff811161007b5761006d60209136906004016101a9565b818151910120604051908152f35b5f80fd5b3461007b5761010d60206100f96059610097366101ff565b916040979397519788956bffffffffffffffffffffffff199060601b1685870152603486015263ffffffff60e01b9060e01b166054850152151560f81b60588401528051918291018484015e81015f838201520301601f198101835282610173565b604051918291602083526020830190610263565b0390f35b3461007b5761010d6101696100f963ffffffff61012d366101ff565b604080516001600160a01b03909616602087015285019390935293166060830152911515608082015260a08082015292839160c0830190610263565b03601f1981018352825b90601f8019910116810190811067ffffffffffffffff82111761019557604052565b634e487b7160e01b5f52604160045260245ffd5b81601f8201121561007b5780359067ffffffffffffffff821161019557604051926101de601f8401601f191660200185610173565b8284526020838301011161007b57815f926020809301838601378301015290565b60a060031982011261007b576004356001600160a01b038116810361007b57916024359160443563ffffffff8116810361007b5791606435801515810361007b57916084359067ffffffffffffffff821161007b57610260916004016101a9565b90565b805180835260209291819084018484015e5f828201840152601f01601f191601019056fea2646970667358221220e3f817afaaaef1121ac298a855627f97dac246dd3de24d8585bef6c9fce3ab5664736f6c634300081a0033")]
20 | contract TestSolidityPacked {
21 | function encodePacked(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) {
22 | return abi.encodePacked(someAddress, someNumber, someShort, someBool, someBytes);
23 | }
24 |
25 | function encode(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) {
26 | return abi.encode(someAddress, someNumber, someShort, someBool, someBytes);
27 | }
28 |
29 | function hash(bytes memory data) public pure returns (bytes32) {
30 | return keccak256(data);
31 | }
32 | }
33 | }
34 |
35 | #[tokio::test]
36 | async fn test_encode_packed() -> Result<()> {
37 | let anvil = Anvil::new().try_spawn()?;
38 | let signer: PrivateKeySigner = anvil.keys()[0].clone().into();
39 | let wallet = EthereumWallet::from(signer);
40 | let rpc_url = anvil.endpoint().parse()?;
41 | let provider = ProviderBuilder::new()
42 | .with_recommended_fillers()
43 | .wallet(wallet)
44 | .on_http(rpc_url);
45 | let contract = TestSolidityPacked::deploy(&provider).await?;
46 |
47 | // prepare parameters
48 | let some_address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
49 | let some_number = U256::from(0x12345678);
50 | let some_short = 0x69u32;
51 | let some_bool = true;
52 | let some_bytes = Bytes::from_static(&[0x42u8; 42]);
53 |
54 | // call contract
55 | let contract_bytes = contract
56 | .encodePacked(
57 | some_address,
58 | some_number,
59 | some_short,
60 | some_bool,
61 | some_bytes.clone(),
62 | )
63 | .call()
64 | .await?
65 | ._0;
66 |
67 | // encode with alloy
68 | type Data = (Address, U256, u32, bool, Bytes);
69 | let data = Data::from((some_address, some_number, some_short, some_bool, some_bytes));
70 | assert_eq!("(address,uint256,uint32,bool,bytes)", data.sol_name());
71 | let local_bytes = Bytes::from(data.abi_encode_packed());
72 |
73 | assert_eq!(contract_bytes, local_bytes);
74 |
75 | Ok(())
76 | }
77 |
78 | #[tokio::test]
79 | async fn test_encode() -> Result<()> {
80 | let anvil = Anvil::new().try_spawn()?;
81 | let signer: PrivateKeySigner = anvil.keys()[0].clone().into();
82 | let wallet = EthereumWallet::from(signer);
83 | let rpc_url = anvil.endpoint().parse()?;
84 | let provider = ProviderBuilder::new()
85 | .with_recommended_fillers()
86 | .wallet(wallet)
87 | .on_http(rpc_url);
88 | let contract = TestSolidityPacked::deploy(&provider).await?;
89 |
90 | // prepare parameters
91 | let some_address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
92 | let some_number = U256::from(0x12345678);
93 | let some_short = 0x69u32;
94 | let some_bool = true;
95 | let some_bytes = Bytes::from_static(&[0x42u8; 42]);
96 |
97 | // call contract
98 | let contract_bytes = contract
99 | .encode(
100 | some_address,
101 | some_number,
102 | some_short,
103 | some_bool,
104 | some_bytes.clone(),
105 | )
106 | .call()
107 | .await?
108 | ._0;
109 |
110 | // encode with alloy
111 | type Data = (Address, U256, u32, bool, Bytes);
112 | let data = Data::from((some_address, some_number, some_short, some_bool, some_bytes));
113 | assert_eq!("(address,uint256,uint32,bool,bytes)", data.sol_name());
114 | let local_bytes = Bytes::from(data.abi_encode_sequence());
115 |
116 | assert_eq!(contract_bytes, local_bytes);
117 |
118 | Ok(())
119 | }
120 |
121 | #[tokio::test]
122 | async fn test_decode() -> Result<()> {
123 | let b = Bytes::from_str("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000060000000000000000000000007befbfbd18777d12efbfbdefbfbdefbfbd79466b6fefbfbd6cefbfbd01efbfbd380000000000000000000000007c467217056444efbfbdd99ac6ab0231efbfbd55efbfbdefbfbd240f0000000000000000000000005defbfbd0b2b3d4eefbfbd3b45efbfbd42efbfbd0457efbfbdefbfbd1b64664f000000000000000000000000efbfbdefbfbd66efbfbdefbfbdefbfbd13efbfbd56efbfbd4d11efbfbdefbfbdefbfbd5befbfbd7869130000000000000000000000001b0cefbfbd01efbfbdefbfbdefbfbd2862efbfbd5130efbfbdefbfbdefbfbd03efbfbd2defbfbd6800000000000000000000000052efbfbd7a5b2f152133efbfbd773d2969efbfbd15efbfbdefbfbdefbfbdefbfbd")?;
124 | let addr = Vec::::abi_decode(&b, false)?;
125 | println!("{:?}", addr);
126 |
127 | Ok(())
128 | }
129 | #[tokio::test]
130 | async fn test_hash() -> Result<()> {
131 | let anvil = Anvil::new().try_spawn()?;
132 | let signer: PrivateKeySigner = anvil.keys()[0].clone().into();
133 | let wallet = EthereumWallet::from(signer);
134 | let rpc_url = anvil.endpoint().parse()?;
135 | let provider = ProviderBuilder::new()
136 | .with_recommended_fillers()
137 | .wallet(wallet)
138 | .on_http(rpc_url);
139 | let contract = TestSolidityPacked::deploy(&provider).await?;
140 |
141 | let some_bytes = Bytes::from_static(&hex_literal::hex!("deadbeef123deadbeef123deadbeef"));
142 |
143 | let contract_bytes = contract.hash(some_bytes.clone()).call().await?._0;
144 | let local_bytes = keccak256(some_bytes);
145 |
146 | assert_eq!(contract_bytes, local_bytes);
147 |
148 | Ok(())
149 | }
150 |
--------------------------------------------------------------------------------
/core/tests/contracts/TestNonce.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity 0.8.26;
3 |
4 | /// @dev solc v0.8.26; solc tests/contracts/TestNonce.sol --via-ir --optimize --bin
5 | /// @dev you may have to do: `solc-select install 0.8.26`
6 | contract TestNonce {
7 | function assertValidNonce(uint256 taskId, bytes calldata input, address requester, address responder, uint256 nonce, uint8 difficulty)
8 | external
9 | pure
10 | returns (bytes memory message, bool result, bytes32 candidate, uint256 target)
11 | {
12 | message = abi.encodePacked(taskId, input, requester, responder, nonce);
13 | target = type(uint256).max >> uint256(difficulty);
14 | candidate = keccak256(message);
15 | result = uint256(candidate) <= target;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/tests/contracts/TestSolidityPacked.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity 0.8.26;
3 |
4 | /// @dev solc v0.8.26; solc tests/contracts/TestSolidityPacked.sol --via-ir --optimize --bin
5 | /// @dev you may have to do: `solc-select install 0.8.26`
6 | contract TestSolidityPacked {
7 | function encodePacked(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) {
8 | return abi.encodePacked(someAddress, someNumber, someShort, someBool, someBytes);
9 | }
10 |
11 | function encode(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) {
12 | return abi.encode(someAddress, someNumber, someShort, someBool, someBytes);
13 | }
14 |
15 | function hash(bytes memory data) public pure returns (bytes32) {
16 | return keccak256(data);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/tests/oracle_test.rs:
--------------------------------------------------------------------------------
1 | //! Test the oracle with a simple 2 + 2 question.
2 | //!
3 | //! ```sh
4 | //! cargo test --package dria-oracle --test oracle_test --all-features -- test_oracle_two_plus_two --exact --show-output
5 | //! ```
6 | #![cfg(feature = "anvil")]
7 |
8 | use alloy::{eips::BlockNumberOrTag, primitives::utils::parse_ether};
9 | use dkn_workflows::Model;
10 | use dria_oracle::{handle_request, DriaOracle, DriaOracleConfig};
11 | use dria_oracle_contracts::{bytes_to_string, string_to_bytes, OracleKind, TaskStatus, WETH};
12 | use eyre::Result;
13 |
14 | #[tokio::test]
15 | async fn test_oracle_two_plus_two() -> Result<()> {
16 | dotenvy::dotenv().unwrap();
17 | let _ = env_logger::builder()
18 | .filter_level(log::LevelFilter::Off)
19 | .filter_module("dria_oracle", log::LevelFilter::Debug)
20 | .filter_module("oracle_test", log::LevelFilter::Debug)
21 | .is_test(true)
22 | .try_init();
23 |
24 | // task setup
25 | let model = Model::GPT4o;
26 | let difficulty = 1;
27 | let models = string_to_bytes(model.to_string());
28 | let protocol = format!("test/{}", env!("CARGO_PKG_VERSION"));
29 | let input = string_to_bytes("What is the result of 2 + 2?".to_string());
30 |
31 | // node setup
32 | let config = DriaOracleConfig::new_from_env()?;
33 | let node = DriaOracle::new(config).await?;
34 |
35 | // setup accounts
36 | let requester = node.connect(node.anvil_new_funded_wallet(None).await?);
37 | let mut generator = node.connect(node.anvil_new_funded_wallet(None).await?);
38 | let mut validator = node.connect(node.anvil_new_funded_wallet(None).await?);
39 |
40 | // buy some WETH for all people
41 | let amount = parse_ether("100")?;
42 | for node in [&requester, &generator, &validator] {
43 | let token = WETH::new(*node.token.address(), &node.provider);
44 | let balance_before = node.get_token_balance(node.address()).await?;
45 |
46 | let call = token.deposit().value(amount);
47 | let _ = call.send().await?.get_receipt().await?;
48 |
49 | let balance_after = node.get_token_balance(node.address()).await?;
50 | assert!(balance_after.amount > balance_before.amount);
51 | }
52 |
53 | // whitelist validator with impersonation
54 | node.anvil_whitelist_registry(validator.address()).await?;
55 | assert!(node.is_whitelisted(validator.address()).await?);
56 |
57 | // register & prepare generator oracle
58 | generator.register(OracleKind::Generator).await?;
59 | generator
60 | .prepare_oracle(vec![OracleKind::Generator], vec![model.clone()])
61 | .await?;
62 | assert!(generator.is_registered(OracleKind::Generator).await?);
63 |
64 | // register & prepare validator oracle
65 | validator.register(OracleKind::Validator).await?;
66 | validator
67 | .prepare_oracle(vec![OracleKind::Validator], vec![model])
68 | .await?;
69 | assert!(validator.is_registered(OracleKind::Validator).await?);
70 |
71 | // approve some tokens for the coordinator from requester
72 | requester
73 | .approve(*node.coordinator.address(), amount)
74 | .await?;
75 |
76 | // make a request with just one generation and validation request
77 | let request_receipt = requester
78 | .request(input, models, difficulty, 1, 1, protocol)
79 | .await?;
80 |
81 | // handle generation by reading the latest event
82 | let tasks = node
83 | .get_tasks_in_range(
84 | request_receipt.block_number.unwrap(),
85 | BlockNumberOrTag::Latest,
86 | )
87 | .await?;
88 | assert!(tasks.len() == 1);
89 | let event = tasks[0].0.clone();
90 | let task_id = event.taskId;
91 | assert_eq!(event.statusBefore, TaskStatus::None as u8);
92 | assert_eq!(event.statusAfter, TaskStatus::PendingGeneration as u8);
93 | let generation_receipt = handle_request(
94 | &generator,
95 | TaskStatus::PendingGeneration,
96 | event.taskId,
97 | event.protocol,
98 | )
99 | .await?
100 | .unwrap();
101 |
102 | // handle validation by reading the latest event
103 | let tasks = node
104 | .get_tasks_in_range(
105 | generation_receipt.block_number.unwrap(),
106 | BlockNumberOrTag::Latest,
107 | )
108 | .await?;
109 | assert!(tasks.len() == 1);
110 | let (event, _) = tasks.into_iter().next().unwrap();
111 | assert_eq!(event.taskId, task_id);
112 | assert_eq!(event.statusBefore, TaskStatus::PendingGeneration as u8);
113 | assert_eq!(event.statusAfter, TaskStatus::PendingValidation as u8);
114 | let validation_receipt = handle_request(
115 | &validator,
116 | TaskStatus::PendingValidation,
117 | event.taskId,
118 | event.protocol,
119 | )
120 | .await?
121 | .unwrap();
122 |
123 | let tasks = node
124 | .get_tasks_in_range(
125 | validation_receipt.block_number.unwrap(),
126 | BlockNumberOrTag::Latest,
127 | )
128 | .await?;
129 | assert!(tasks.len() == 1);
130 | let (event, _) = tasks.into_iter().next().unwrap();
131 | assert_eq!(event.taskId, task_id);
132 | assert_eq!(event.statusBefore, TaskStatus::PendingValidation as u8);
133 | assert_eq!(event.statusAfter, TaskStatus::Completed as u8);
134 |
135 | // get responses
136 | let responses = node.coordinator.getResponses(task_id).call().await?._0;
137 | assert_eq!(responses.len(), 1);
138 | let response = responses.into_iter().next().unwrap();
139 | let output_string = bytes_to_string(&response.output)?;
140 | assert!(output_string.contains("4"), "output must contain 4");
141 | assert!(!response.score.is_zero(), "score must be non-zero");
142 | log::debug!("Output: {}", output_string);
143 |
144 | Ok(())
145 | }
146 |
--------------------------------------------------------------------------------
/core/tests/registry_test.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "anvil")]
2 |
3 | use dria_oracle::{DriaOracle, DriaOracleConfig};
4 | use dria_oracle_contracts::OracleKind;
5 | use eyre::Result;
6 |
7 | #[tokio::test]
8 | async fn test_registry() -> Result<()> {
9 | dotenvy::dotenv().unwrap();
10 | let _ = env_logger::builder()
11 | .filter_level(log::LevelFilter::Off)
12 | .filter_module("dria_oracle", log::LevelFilter::Debug)
13 | .filter_module("registry_test", log::LevelFilter::Debug)
14 | .is_test(true)
15 | .try_init();
16 |
17 | let config = DriaOracleConfig::new_from_env()?;
18 | let node = DriaOracle::new(config).await?;
19 |
20 | // tries to register if registered, or opposite, to trigger an error
21 | const KIND: OracleKind = OracleKind::Generator;
22 | let result = if node.is_registered(KIND).await? {
23 | log::info!("Oracle is registered, we will try to register again to get an error.");
24 | node.register_kind(KIND).await
25 | } else {
26 | log::info!("Oracle is not registered, we will try to unregister again to get an error.");
27 | node.unregister_kind(KIND).await
28 | };
29 | assert!(result.is_err());
30 |
31 | // both errors include the node address in their message, which we look for here:
32 | let err = result.unwrap_err();
33 | err.to_string().contains(&node.address().to_string());
34 |
35 | Ok(())
36 | }
37 |
--------------------------------------------------------------------------------
/core/tests/request_test.rs:
--------------------------------------------------------------------------------
1 | //! Tests the request command, resulting in a task being created in the coordinator contract.
2 | //!
3 | //! 1. Requester buys some WETH, and it is approved within the request command.
4 | //! 2. Requester requests a task with a given input, models, difficulty, num_gens, and num_vals.
5 | //! 3. The task is created in the coordinator contract.
6 | //!
7 | //! ```sh
8 | //! cargo test --package dria-oracle --test request_test --all-features -- test_request --exact --show-output
9 | //! ```
10 | #![cfg(feature = "anvil")]
11 |
12 | use alloy::primitives::{aliases::U40, utils::parse_ether};
13 | use dkn_workflows::Model;
14 | use dria_oracle::{DriaOracle, DriaOracleConfig};
15 | use dria_oracle_contracts::{bytes_to_string, WETH};
16 | use eyre::Result;
17 |
18 | #[tokio::test]
19 | async fn test_request() -> Result<()> {
20 | dotenvy::dotenv().unwrap();
21 | let _ = env_logger::builder()
22 | .filter_level(log::LevelFilter::Off)
23 | .filter_module("dria_oracle", log::LevelFilter::Debug)
24 | .filter_module("request_test", log::LevelFilter::Debug)
25 | .is_test(true)
26 | .try_init();
27 |
28 | // task setup
29 | let (difficulty, num_gens, num_vals) = (1, 1, 1);
30 | let protocol = format!("test/{}", env!("CARGO_PKG_VERSION"));
31 | let models = vec![Model::GPT4Turbo];
32 | let input = "What is the result of 2 + 2?".to_string();
33 |
34 | // node setup
35 | let config = DriaOracleConfig::new_from_env()?;
36 | let node = DriaOracle::new(config).await?;
37 |
38 | // setup account & buy some WETH
39 | let requester = node.connect(node.anvil_new_funded_wallet(None).await?);
40 | let token = WETH::new(*requester.token.address(), &requester.provider);
41 | let _ = token.deposit().value(parse_ether("100")?).send().await?;
42 |
43 | // request a task, and see it in the coordinator
44 | let task_id = node.coordinator.nextTaskId().call().await?._0;
45 | requester
46 | .request_task(&input, models, difficulty, num_gens, num_vals, protocol)
47 | .await?;
48 |
49 | // get the task info
50 | let (request, _, _) = node.get_task(task_id).await?;
51 | assert_eq!(input, bytes_to_string(&request.input).unwrap());
52 | assert_eq!(difficulty, request.parameters.difficulty);
53 | assert_eq!(U40::from(num_gens), request.parameters.numGenerations);
54 | assert_eq!(U40::from(num_vals), request.parameters.numValidations);
55 |
56 | Ok(())
57 | }
58 |
--------------------------------------------------------------------------------
/core/tests/swan_test.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "anvil")]
2 |
3 | use alloy::{eips::BlockNumberOrTag, primitives::utils::parse_ether};
4 | use dkn_workflows::Model;
5 | use dria_oracle::{handle_request, DriaOracle, DriaOracleConfig};
6 | use dria_oracle_contracts::{string_to_bytes, OracleKind, TaskStatus, WETH};
7 | use eyre::Result;
8 |
9 | #[tokio::test]
10 | async fn test_swan() -> Result<()> {
11 | dotenvy::dotenv().unwrap();
12 | let _ = env_logger::builder()
13 | .filter_level(log::LevelFilter::Off)
14 | .filter_module("dria_oracle", log::LevelFilter::Debug)
15 | .filter_module("swan_test", log::LevelFilter::Debug)
16 | .is_test(true)
17 | .try_init();
18 |
19 | // task setup
20 | let difficulty = 1;
21 | let models = string_to_bytes(Model::GPT4Turbo.to_string());
22 | let protocol = format!("swan/0.0.1-test");
23 | let input = string_to_bytes(
24 | r#"
25 | Print the exact text below, do not reply with anything else:
26 |
27 |
28 | 0x36f55f830D6E628a78Fcb70F73f9D005BaF88eE3
29 | 0x671527de058BaD60C6151cA29d501C87439bCF62
30 | 0x66FC9dC1De3db773891753CD257359A26e876305
31 |
32 | "#
33 | .to_string(),
34 | );
35 |
36 | // node setup
37 | let config = DriaOracleConfig::new_from_env()?;
38 | let node = DriaOracle::new(config).await?;
39 |
40 | // setup accounts
41 | let requester = node.connect(node.anvil_new_funded_wallet(None).await?);
42 | let mut generator = node.connect(node.anvil_new_funded_wallet(None).await?);
43 | let mut validator = node.connect(node.anvil_new_funded_wallet(None).await?);
44 |
45 | // buy some WETH for all people
46 | log::info!("Buying WETH for all accounts");
47 | let amount = parse_ether("100").unwrap();
48 | for node in [&requester, &generator, &validator] {
49 | let balance_before = node.get_token_balance(node.address()).await?;
50 |
51 | let token = WETH::new(*node.token.address(), &node.provider);
52 | let call = token.deposit().value(amount);
53 | let _ = call.send().await?.get_receipt().await?;
54 |
55 | let balance_after = node.get_token_balance(node.address()).await?;
56 | assert!(balance_after.amount > balance_before.amount);
57 | }
58 |
59 | // whitelist validator with impersonation
60 | node.anvil_whitelist_registry(validator.address()).await?;
61 | assert!(node.is_whitelisted(validator.address()).await?);
62 |
63 | // register & prepare generator oracle
64 | generator.register(OracleKind::Generator).await?;
65 | generator
66 | .prepare_oracle(vec![OracleKind::Generator], vec![Model::GPT4Turbo])
67 | .await?;
68 | assert!(generator.is_registered(OracleKind::Generator).await?);
69 |
70 | // register & prepare validator oracle
71 | validator.register(OracleKind::Validator).await?;
72 | validator
73 | .prepare_oracle(vec![OracleKind::Validator], vec![Model::GPT4o])
74 | .await?;
75 | assert!(validator.is_registered(OracleKind::Validator).await?);
76 |
77 | // approve some tokens for the coordinator from requester
78 | requester
79 | .approve(*node.coordinator.address(), amount)
80 | .await?;
81 |
82 | // make a request with just one generation and validation request
83 | let request_receipt = requester
84 | .request(input, models, difficulty, 1, 1, protocol)
85 | .await?;
86 |
87 | // handle generation by reading the latest event
88 | let tasks = node
89 | .get_tasks_in_range(
90 | request_receipt.block_number.unwrap(),
91 | BlockNumberOrTag::Latest,
92 | )
93 | .await?;
94 | assert!(tasks.len() == 1);
95 | let (event, _) = tasks.into_iter().next().unwrap();
96 | let task_id = event.taskId;
97 | assert_eq!(event.statusBefore, TaskStatus::None as u8);
98 | assert_eq!(event.statusAfter, TaskStatus::PendingGeneration as u8);
99 | let generation_receipt = handle_request(
100 | &generator,
101 | TaskStatus::PendingGeneration,
102 | event.taskId,
103 | event.protocol,
104 | )
105 | .await?
106 | .unwrap();
107 |
108 | // handle validation by reading the latest event
109 | let tasks = node
110 | .get_tasks_in_range(
111 | generation_receipt.block_number.unwrap(),
112 | BlockNumberOrTag::Latest,
113 | )
114 | .await?;
115 | assert!(tasks.len() == 1);
116 | let (event, _) = tasks.into_iter().next().unwrap();
117 | assert_eq!(event.taskId, task_id);
118 | assert_eq!(event.statusBefore, TaskStatus::PendingGeneration as u8);
119 | assert_eq!(event.statusAfter, TaskStatus::PendingValidation as u8);
120 | let validation_receipt = handle_request(
121 | &validator,
122 | TaskStatus::PendingValidation,
123 | event.taskId,
124 | event.protocol,
125 | )
126 | .await?
127 | .unwrap();
128 |
129 | let tasks = node
130 | .get_tasks_in_range(
131 | validation_receipt.block_number.unwrap(),
132 | BlockNumberOrTag::Latest,
133 | )
134 | .await?;
135 | assert!(tasks.len() == 1);
136 | let (event, _) = tasks.into_iter().next().unwrap();
137 | assert_eq!(event.taskId, task_id);
138 | assert_eq!(event.statusBefore, TaskStatus::PendingValidation as u8);
139 | assert_eq!(event.statusAfter, TaskStatus::Completed as u8);
140 |
141 | // get responses
142 | let responses = node.coordinator.getResponses(task_id).call().await?._0;
143 | assert_eq!(responses.len(), 1);
144 | let response = responses.into_iter().next().unwrap();
145 | println!("Output: {:?}", response.output);
146 | assert!(!response.score.is_zero(), "score must be non-zero");
147 |
148 | Ok(())
149 | }
150 |
--------------------------------------------------------------------------------
/core/tests/weth_test.rs:
--------------------------------------------------------------------------------
1 | //! Using the forked blockchain, creates two accounts (alice, bob) and then,
2 | //!
3 | //! 1. Alice buys WETH
4 | //! 2. Alice approves Bob
5 | //! 3. Bob transfers WETH from Alice
6 | #![cfg(feature = "anvil")]
7 |
8 | use alloy::primitives::utils::parse_ether;
9 | use dria_oracle::{DriaOracle, DriaOracleConfig};
10 | use dria_oracle_contracts::WETH;
11 | use eyre::Result;
12 |
13 | #[tokio::test]
14 | async fn test_weth_transfer() -> Result<()> {
15 | dotenvy::dotenv().unwrap();
16 |
17 | // amount of WETH that will be transferred
18 | let amount = parse_ether("100")?;
19 |
20 | let config = DriaOracleConfig::new_from_env()?;
21 | let node = DriaOracle::new(config).await?;
22 |
23 | // setup alice
24 | let alice = node.connect(node.anvil_new_funded_wallet(None).await?);
25 | let alice_token = WETH::new(*node.token.address(), &alice.provider);
26 |
27 | // setup bob
28 | let bob = node.connect(node.anvil_new_funded_wallet(None).await?);
29 | let bob_token = WETH::new(*node.token.address(), &bob.provider);
30 |
31 | // record existing balances
32 | let alice_balance_before = node.get_token_balance(alice.address()).await?;
33 | let bob_balance_before = node.get_token_balance(bob.address()).await?;
34 |
35 | // alice buys WETH
36 | let call = alice_token.deposit().value(amount);
37 | let _ = call.send().await?.get_receipt().await?;
38 | let alice_balance_after = node.get_token_balance(alice.address()).await?;
39 | assert_eq!(
40 | alice_balance_after.amount - alice_balance_before.amount,
41 | amount
42 | );
43 |
44 | // alice approves bob
45 | let call = alice_token.approve(bob.address(), amount);
46 | let _ = call.send().await?.get_receipt().await?;
47 |
48 | // bob transfers WETH from alice
49 | let call = bob_token.transferFrom(alice.address(), bob.address(), amount);
50 | let _ = call.send().await?.get_receipt().await?;
51 |
52 | // balances should be updated
53 | let alice_balance_after = node.get_token_balance(alice.address()).await?;
54 | let bob_balance_after = node.get_token_balance(bob.address()).await?;
55 | assert_eq!(alice_balance_after.amount, alice_balance_before.amount);
56 | assert_eq!(bob_balance_after.amount - bob_balance_before.amount, amount);
57 |
58 | Ok(())
59 | }
60 |
--------------------------------------------------------------------------------
/misc/arweave.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A helper script to print the content of an Arweave transaction.
3 | *
4 | * Usage:
5 | *
6 | * ```sh
7 | * # calldata as-is
8 | * bun run ./misc/arweave.js 0x7b2261727765617665223a224d49555775656361634b417a62755442335a6a57613463784e6461774d71435a704550694f71675a625a63227d
9 | *
10 | * # as an object (with escaped quotes)
11 | * bun run ./misc/arweave.js "{\"arweave\":\"MIUWuecacKAzbuTB3ZjWa4cxNdawMqCZpEPiOqgZbZc\"}"
12 | *
13 | * # base64 txid
14 | * bun run ./misc/arweave.js MIUWuecacKAzbuTB3ZjWa4cxNdawMqCZpEPiOqgZbZc
15 | * ```
16 | *
17 | * Can be piped to `pbcopy` on macOS to copy the output to clipboard.
18 | */
19 |
20 | async function main() {
21 | // parse input
22 | let input = process.argv[2];
23 | if (!input) {
24 | console.error("No input provided.");
25 | return;
26 | }
27 |
28 | let arweaveTxId;
29 | if (input.startsWith("0x")) {
30 | // if it starts with 0x, we assume its all hex
31 | arweaveTxId = JSON.parse(
32 | Buffer.from(input.slice(2), "hex").toString()
33 | ).arweave;
34 | } else if (input.startsWith("{")) {
35 | // if it starts with {, we assume its a JSON string
36 | console.log("input", input);
37 | arweaveTxId = JSON.parse(input).arweave;
38 | } else {
39 | // otherwise, we assume its a base64 txid
40 | arweaveTxId = input;
41 | }
42 |
43 | // construct the URL
44 | // download the actual response from Arweave
45 | const url = `https://arweave.net/${arweaveTxId}`;
46 | const res = await fetch(url);
47 | console.log(await res.text());
48 | }
49 |
50 | main().catch(console.error);
51 |
--------------------------------------------------------------------------------
/secrets/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dria-oracle-storage"
3 | description = "Dria Oracle External Storages"
4 | version.workspace = true
5 | edition.workspace = true
6 | license.workspace = true
7 | authors.workspace = true
8 |
9 | [dependencies]
10 | # arweave uploader
11 | # NOTE: there are many unused stuff here, but everything breaks if you use the minimal set
12 | # because Bundlr SDK is not maintained at all
13 | bundlr-sdk = { version = "0.5.0" }
14 |
15 | alloy.workspace = true
16 | eyre.workspace = true
17 | log.workspace = true
18 |
19 | async-trait.workspace = true
20 |
21 | reqwest.workspace = true
22 | serde.workspace = true
23 | serde_json.workspace = true
24 |
25 | [dev-dependencies]
26 | env_logger.workspace = true
27 | dotenvy.workspace = true
28 | tokio.workspace = true
29 |
--------------------------------------------------------------------------------
/storage/src/arweave.rs:
--------------------------------------------------------------------------------
1 | use alloy::primitives::Bytes;
2 | use async_trait::async_trait;
3 | use bundlr_sdk::{currency::arweave::ArweaveBuilder, tags::Tag, BundlrBuilder};
4 | use eyre::{eyre, Context, Result};
5 | use reqwest::{Client, Url};
6 | use std::{env, path::PathBuf};
7 |
8 | use super::IsExternalStorage;
9 |
10 | const DEFAULT_UPLOAD_BASE_URL: &str = "https://node1.bundlr.network";
11 | const DEFAULT_DOWNLOAD_BASE_URL: &str = "https://arweave.net";
12 | const DEFAULT_BYTE_LIMIT: usize = 1024; // 1KB
13 |
14 | #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
15 | pub struct ArweaveKey {
16 | /// The base64url encoded key, can be used to download data directly.
17 | pub arweave: String,
18 | }
19 |
20 | /// External data storage for Arweave.
21 | ///
22 | /// - `put` corresponds to uploading (via Irys)
23 | /// - `get` corresponds to downloading
24 | pub struct ArweaveStorage {
25 | /// Path to Arweave keypair (usually JSON)
26 | wallet: Option,
27 | /// Base URL for uploading data on Arweave, e.g.:
28 | /// - https://gateway.irys.xyz
29 | /// - https://node1.bundlr.network
30 | upload_base_url: Url,
31 | /// Base URL for downloading data from Arweave, e.g.:
32 | /// - https://arweave.net
33 | download_base_url: Url,
34 | /// Reqwest client for downloads.
35 | client: Client,
36 | /// Byte limit for the data to be considered for Arweave.
37 | ///
38 | /// - If the data exceeds this limit, it will be uploaded to Arweave.
39 | /// - Otherwise, it will be stored as is.
40 | byte_limit: usize,
41 | }
42 |
43 | impl ArweaveStorage {
44 | /// Creates an Arweave storage client with the given wallet's path.
45 | pub fn new(wallet: &str) -> Result {
46 | let ar = Self::new_readonly();
47 | Ok(ar.with_wallet(wallet))
48 | }
49 |
50 | /// Creates an Arweave storage client without a wallet, used only for downloads.
51 | pub fn new_readonly() -> Self {
52 | Self {
53 | wallet: None,
54 | upload_base_url: Url::parse(DEFAULT_UPLOAD_BASE_URL).unwrap(),
55 | download_base_url: Url::parse(DEFAULT_DOWNLOAD_BASE_URL).unwrap(),
56 | byte_limit: DEFAULT_BYTE_LIMIT,
57 | client: Client::new(),
58 | }
59 | }
60 |
61 | /// Sets the wallet path for the Arweave storage.
62 | pub fn with_wallet(mut self, wallet: &str) -> Self {
63 | self.wallet = Some(PathBuf::from(wallet));
64 | self
65 | }
66 |
67 | /// Sets the byte limit for the data to be considered for Arweave, default is 1024 bytes (1KB).
68 | ///
69 | /// - If the data exceeds this limit, it will be uploaded to Arweave.
70 | /// - Otherwise, it will be stored as is.
71 | ///
72 | /// If this is too large, you may spend quite a bit of gas fees.
73 | pub fn with_upload_byte_limit(mut self, limit: usize) -> Self {
74 | self.byte_limit = limit;
75 | self
76 | }
77 |
78 | /// Sets the download base URL for Arweave.
79 | ///
80 | /// We don't need to change this usually, as `http://arweave.net` is enough.
81 | pub fn with_download_base_url(mut self, url: &str) -> Result {
82 | self.download_base_url = Url::parse(url).wrap_err("could not parse download base URL")?;
83 | Ok(self)
84 | }
85 |
86 | /// Sets the upload base URL for Arweave.
87 | pub fn with_upload_base_url(mut self, url: &str) -> Result {
88 | self.upload_base_url = Url::parse(url).wrap_err("could not parse upload base URL")?;
89 | Ok(self)
90 | }
91 |
92 | /// Creates a new Arweave instance from the environment variables.
93 | ///
94 | /// - `ARWEAVE_WALLET_PATH` is required
95 | /// - `ARWEAVE_BASE_URL` is optional
96 | /// - `ARWEAVE_BYTE_LIMIT` is optional
97 | ///
98 | /// All these variables have defaults if they are missing.
99 | pub fn new_from_env() -> Result {
100 | // use wallet from env
101 | let wallet =
102 | env::var("ARWEAVE_WALLET_PATH").wrap_err("could not read wallet path from env")?;
103 | let mut ar = Self::new(&wallet)?;
104 |
105 | // get base url if it exists
106 | if let Ok(base_url) = env::var("ARWEAVE_BASE_URL") {
107 | ar = ar.with_upload_base_url(&base_url)?;
108 | }
109 |
110 | // update upload byte limit if needed
111 | if let Ok(byte_limit) = env::var("ARWEAVE_BYTE_LIMIT") {
112 | ar = ar.with_upload_byte_limit(byte_limit.parse().unwrap_or(DEFAULT_BYTE_LIMIT));
113 | }
114 |
115 | Ok(ar)
116 | }
117 |
118 | /// Puts the value if it is larger than the byte limit.
119 | #[inline]
120 | pub async fn put_if_large(&self, value: Bytes) -> Result {
121 | let value_size = value.len();
122 | if value_size > self.byte_limit {
123 | log::info!(
124 | "Uploading large ({}B > {}B) value to Arweave",
125 | value_size,
126 | self.byte_limit
127 | );
128 | let key = self.put(value.clone()).await?;
129 | let key_str = serde_json::to_string(&key).wrap_err("could not serialize key")?;
130 | Ok(key_str.into())
131 | } else {
132 | Ok(value)
133 | }
134 | }
135 | }
136 |
137 | #[async_trait(?Send)]
138 | impl IsExternalStorage for ArweaveStorage {
139 | type Key = ArweaveKey;
140 | type Value = Bytes;
141 |
142 | async fn get(&self, key: Self::Key) -> Result {
143 | let url = self.download_base_url.join(&key.arweave)?;
144 |
145 | log::debug!("Fetching from Arweave: {}", url);
146 | let response = self
147 | .client
148 | .get(url)
149 | .send()
150 | .await
151 | .wrap_err("failed to fetch from Arweave")?;
152 |
153 | if !response.status().is_success() {
154 | return Err(eyre!("Failed to fetch from Arweave: {}", response.status()));
155 | }
156 |
157 | let response_bytes = response.bytes().await?;
158 | Ok(response_bytes.into())
159 | }
160 |
161 | async fn put(&self, value: Self::Value) -> Result {
162 | let wallet_path = self
163 | .wallet
164 | .as_ref()
165 | .ok_or_else(|| eyre!("Wallet path is not set"))?;
166 |
167 | #[derive(Debug, serde::Deserialize)]
168 | #[serde(rename_all = "camelCase")]
169 | #[allow(unused)]
170 | struct UploadResponse {
171 | block: u64,
172 | deadline_height: u64,
173 | id: String,
174 | public: String,
175 | signature: String,
176 | timestamp: u64,
177 | validator_signatures: Vec,
178 | version: String,
179 | }
180 |
181 | // ensure that wallet exists
182 | // NOTE: we do this here instead of `new` so that we can work without any wallet
183 | // in case we only want to download data.
184 | if !wallet_path.try_exists()? {
185 | return Err(eyre!("Wallet does not exist at {}.", wallet_path.display()));
186 | }
187 |
188 | // create tag
189 | let base_tag = Tag::new(
190 | "User-Agent",
191 | &format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
192 | );
193 |
194 | // create Arweave currency instance
195 | let currency = ArweaveBuilder::new()
196 | .keypair_path(wallet_path.clone())
197 | .build()?;
198 |
199 | // create the Bundlr instance
200 | let bundlr = BundlrBuilder::new()
201 | .url(self.upload_base_url.clone())
202 | .currency(currency)
203 | .fetch_pub_info()
204 | .await?
205 | .build()?;
206 |
207 | // create & sign transaction
208 | let mut tx = bundlr.create_transaction(value.into(), vec![base_tag])?;
209 | bundlr.sign_transaction(&mut tx).await?;
210 | let response_body = bundlr.send_transaction(tx).await?;
211 | let res = serde_json::from_value::(response_body)?;
212 |
213 | log::debug!("Uploaded to Arweave: {:#?}", res);
214 | log::info!("Uploaded at {}", self.upload_base_url.join(&res.id)?);
215 |
216 | // the key is in base64 format, we want to convert that to hexadecimals
217 | Ok(ArweaveKey { arweave: res.id })
218 | }
219 |
220 | /// Check if key is an Arweave key, which is a JSON object of type `{arweave: string}`
221 | /// where the `arweave` field contains the base64url encoded txid.
222 | ///
223 | /// For example:
224 | ///
225 | /// ```json
226 | /// { arweave: "Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA" }
227 | /// ```
228 | #[inline(always)]
229 | fn is_key(key: impl AsRef) -> Option {
230 | serde_json::from_str::(key.as_ref()).ok()
231 | }
232 |
233 | #[inline(always)]
234 | fn describe() -> &'static str {
235 | "Arweave"
236 | }
237 | }
238 |
239 | #[cfg(test)]
240 | mod tests {
241 | use super::*;
242 |
243 | #[tokio::test]
244 | #[ignore = "run manually"]
245 | async fn test_download_data() -> Result<()> {
246 | dotenvy::dotenv().unwrap();
247 |
248 | // https://gateway.irys.xyz/Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA
249 | let tx_id = "Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA".to_string();
250 | let key = ArweaveKey { arweave: tx_id };
251 | let arweave = ArweaveStorage::new_from_env()?;
252 |
253 | let result = arweave.get(key).await?;
254 | let val = serde_json::from_slice::(&result)?;
255 | assert_eq!(val, "Hello, Arweave!");
256 |
257 | Ok(())
258 | }
259 |
260 | #[tokio::test]
261 | #[ignore = "run manually with Arweave wallet"]
262 | async fn test_upload_and_download_data() -> Result<()> {
263 | dotenvy::dotenv().unwrap();
264 |
265 | let arweave = ArweaveStorage::new_from_env()?;
266 | let input = b"Hi there Im a test data".to_vec();
267 |
268 | // put data
269 | let key = arweave.put(input.clone().into()).await?;
270 | println!("{:?}", key);
271 |
272 | // get it again
273 | let result = arweave.get(key).await?;
274 | assert_eq!(input, result);
275 |
276 | Ok(())
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/storage/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod arweave;
2 | pub use arweave::ArweaveStorage;
3 |
4 | mod traits;
5 | pub use traits::IsExternalStorage;
6 |
--------------------------------------------------------------------------------
/storage/src/traits.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use eyre::Result;
3 |
4 | /// A generalized external storage trait.
5 | ///
6 | /// Putting a value should return a unique key, even for the same value uploaded multiple times.
7 | /// Getting a value should be done with that returned key.
8 | ///
9 | /// Note that the `async_trait` has `?Send` specified, as by default it makes them `Send` but Arweave does not have it.
10 | #[async_trait(?Send)]
11 | pub trait IsExternalStorage {
12 | type Key: Clone;
13 | type Value: Clone + std::fmt::Debug;
14 |
15 | /// Returns the value (if exists) at the given key.
16 | /// Returns an error if the key is invalid or the value does not exist.
17 | async fn get(&self, key: Self::Key) -> Result;
18 |
19 | /// Puts the value and returns the generated key.
20 | async fn put(&self, value: Self::Value) -> Result;
21 |
22 | /// Checks if the given string constitutes a key, and returns it.
23 | fn is_key(key: impl AsRef) -> Option;
24 |
25 | /// Describes the implementation.
26 | fn describe() -> &'static str;
27 | }
28 |
--------------------------------------------------------------------------------