├── .github
└── workflows
│ ├── deploy-release.yml
│ ├── deploy-wallet-connect.yml
│ ├── prepare-release.yml
│ └── test.yml
├── .gitignore
├── .gitmodules
├── README.md
├── cli
├── index.ts
├── sleuth.ts
└── test
│ └── sleuth.test.ts
├── foundry.toml
├── jest.config.js
├── logo.png
├── package.json
├── parser
├── .appveyor.yml
├── .gitignore
├── .travis.yml
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── pkg
│ ├── README.md
│ ├── package.json
│ ├── parser.d.ts
│ ├── parser.js
│ ├── parser_bg.js
│ ├── parser_bg.wasm
│ └── parser_bg.wasm.d.ts
├── src
│ ├── abi.rs
│ ├── lib.rs
│ ├── parse.rs
│ ├── query.rs
│ ├── resolve.rs
│ ├── sleuth.pest
│ ├── source.rs
│ ├── utils.rs
│ └── yul.rs
└── tests
│ └── web.rs
├── script
├── Sleuth.s.sol
├── deploy-release.sh
├── mainnet
│ └── deploy.sh
├── prepare-release.sh
└── test.sh
├── src
└── Sleuth.sol
├── test
├── Sleuth.t.sol
└── examples
│ ├── Birthday.sol
│ ├── BlockNumber.sol
│ ├── Fun.yul
│ └── Pair.sol
├── tsconfig.json
└── yarn.lock
/.github/workflows/deploy-release.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | release:
7 | name: release
8 | description: Release tag (e.g. v0.0.1)
9 |
10 | network:
11 | name: network
12 | type: choice
13 | options:
14 | - mainnet
15 | - sepolia
16 | - base
17 | - base_sepolia
18 |
19 | env:
20 | FOUNDRY_PROFILE: ci
21 |
22 | permissions:
23 | contents: write
24 |
25 | jobs:
26 | check:
27 | strategy:
28 | fail-fast: true
29 |
30 | name: Foundry project
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v3
34 | with:
35 | submodules: recursive
36 |
37 | - name: Install Foundry
38 | uses: foundry-rs/foundry-toolchain@v1
39 | with:
40 | version: nightly
41 |
42 | - name: Deploy Release
43 | run: |
44 | export RPC_URL=$(echo $deployer_config | jq -r ".$network.rpc_url")
45 | export DEPLOYER_PK=$(echo $deployer_config | jq -r ".$network.deployer_pk")
46 | script/deploy-release.sh $release
47 | env:
48 | deployer_config: ${{ secrets.deployer_config }}
49 | network: ${{ inputs.network }}
50 | release: ${{ inputs.release }}
51 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-wallet-connect.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Sleuth [Mainnet - WalletConnect]
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | deployer_address:
7 | description: WalletConnect address to deploy from
8 | required: true
9 |
10 | env:
11 | FOUNDRY_PROFILE: ci
12 |
13 | jobs:
14 | check:
15 | strategy:
16 | fail-fast: true
17 |
18 | name: Deploy Sleuth [Mainnet]
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Start Seacrest
22 | uses: hayesgm/seacrest@v1
23 | with:
24 | ethereum_url: "${{ secrets.ETH_MAINNET_URL }}"
25 |
26 | - uses: actions/checkout@v3
27 | with:
28 | submodules: recursive
29 |
30 | - name: Install Foundry
31 | uses: foundry-rs/foundry-toolchain@v1
32 | with:
33 | version: nightly
34 |
35 | - name: Run Forge build
36 | run: |
37 | forge --version
38 | forge build --sizes
39 |
40 | - name: Forge Deploy Sleuth [Mainnet]
41 | run: script/mainnet/deploy.sh
42 | env:
43 | ETHERSCAN_API_KEY: "${{ secrets.ETHERSCAN_API_KEY }}"
44 | ETH_FROM: "${{ inputs.deployer_address }}"
45 | RPC_URL: "http://localhost:8585"
46 |
--------------------------------------------------------------------------------
/.github/workflows/prepare-release.yml:
--------------------------------------------------------------------------------
1 | name: Prepare Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | env:
9 | FOUNDRY_PROFILE: ci
10 |
11 | permissions:
12 | contents: write
13 |
14 | jobs:
15 | check:
16 | strategy:
17 | fail-fast: true
18 |
19 | name: Foundry project
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v3
23 | with:
24 | submodules: recursive
25 |
26 | - name: Install Foundry
27 | uses: foundry-rs/foundry-toolchain@v1
28 | with:
29 | version: nightly
30 |
31 | - name: Run Forge build
32 | run: |
33 | forge build
34 |
35 | - name: Prepare Release
36 | run: |
37 | export RPC_URL=$(echo $deployer_config | jq -r ".$network.rpc_url")
38 | export CODE_JAR=$(echo $deployer_config | jq -r ".$network.code_jar")
39 | script/prepare-release.sh
40 | env:
41 | deployer_config: ${{ secrets.deployer_config }}
42 | network: sepolia
43 |
44 | - uses: ncipollo/release-action@v1
45 | with:
46 | artifacts: "release/Sleuth.json,release/Sleuth.sol,release/contracts.json,release/sleuth@*"
47 | bodyFile: "release/RELEASE.md"
48 | allowUpdates: true
49 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 |
6 | env:
7 | FOUNDRY_PROFILE: ci
8 |
9 | jobs:
10 | check:
11 | strategy:
12 | fail-fast: true
13 |
14 | name: Foundry project
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | with:
19 | submodules: recursive
20 |
21 | - name: Install Foundry
22 | uses: foundry-rs/foundry-toolchain@v1
23 | with:
24 | version: nightly
25 |
26 | - name: Run Forge build
27 | run: |
28 | forge --version
29 | forge build --sizes
30 | id: build
31 |
32 | - name: Run Forge tests
33 | run: |
34 | forge test -vvv
35 | id: test
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | out/
4 | release/
5 | .release-tmp/
6 |
7 | # Ignores development broadcast logs
8 | !/broadcast
9 | /broadcast/*/31337/
10 | /broadcast/**/dry-run/
11 |
12 | # Dotenv file
13 | .env
14 | dist
15 | node_modules
16 | yarn-error.log
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/forge-std"]
2 | path = lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sleuth
2 |
3 |
4 |
5 | ----
6 |
7 | Sleuth is an easy way to pull data from an EVM-compatible blockchain, allowing for complex queries, similar to an ethers-multicall. Sleuth works by deploying a smart contract and then invoking it in an `eth_call`. This allows you to use complex logic to pull data from many contracts or other items such as `eth_chainId` or `eth_blockNumber`, which you can use for data analysis or in your Web3 front-end. For example:
8 |
9 | **MyQuery.sol** [Note: this is not deployed, and is never deployed]
10 | ```sol
11 | // SPDX-License-Identifier: UNLICENSED
12 | pragma solidity ^0.8.16;
13 |
14 | contract BlockNumber {
15 | function query() external view returns (uint256) {
16 | return block.number;
17 | }
18 | }
19 | ```
20 |
21 | **MyView.ts**
22 | ```ts
23 | import { Sleuth } from '@compound-finance/sleuth';
24 |
25 | let blockNumberQuery = await Sleuth.querySol(fs.readFileSync('./MyQuery.sol', 'utf8'));
26 | let sleuth = new Sleuth(provider);
27 | let blockNumber = await sleuth.fetch(blockNumberQuery);
28 | ```
29 |
30 | You can also use pre-compiled contracts (e.g. if you check in the compilation artifacts from solc).
31 |
32 | **MyView.ts**
33 | ```ts
34 | import { Sleuth } from '@compound-finance/sleuth';
35 |
36 | let blockNumberQuery = await Sleuth.querySol(fs.readFileSync('./out/MyQuery.json', 'utf8'));
37 | let sleuth = new Sleuth(provider);
38 | let blockNumber = await sleuth.fetch(blockNumberQuery);
39 | ```
40 |
41 | ## Sleuth Query Language [Experimental]
42 |
43 | Sleuth also comes with a full query language, similar to SQL. You can specify contracts and load data from them. This is a WIP and subject to change.
44 |
45 | ```ts
46 | import { Sleuth } from '@compound-finance/sleuth';
47 |
48 | let sleuth = new Sleuth(provider);
49 |
50 | // Add a source so the query language knows the shape of the contracts you'll be querying.
51 | sleuth.addSource("comet", "0xc3d688B66703497DAA19211EEdff47f25384cdc3", ["function totalSupply() returns (uint256)"]);
52 |
53 | // Build a query
54 | let q = sleuth.query<[ BigNumber ]>("SELECT comet.totalSupply FROM comet;");
55 |
56 | // Fetch the data
57 | let [ totalSupply ] = await sleuth.fetch(q);
58 | ```
59 |
60 | or all in one:
61 |
62 | ```ts
63 | import { Sleuth } from '@compound-finance/sleuth';
64 |
65 | let sleuth = new Sleuth(provider);
66 |
67 | console.log(await sleuth.fetchSql(`
68 | REGISTER CONTRACT comet AT 0xc3d688B66703497DAA19211EEdff47f25384cdc3 WITH INTERFACE ["function totalSupply() returns (uint256)"];
69 | SELECT comet.totalSupply FROM comet;
70 | `));
71 | ```
72 |
73 | There's a lot more work in Sleuth Query Language to do, mostly around allowing you to pull in multiple "rows" since that's a core aspect of SQL, but for one-off queries, it's quite fun!
74 |
75 | ## Getting Started
76 |
77 | Install Sleuth:
78 |
79 | ```
80 | yarn add @compound-finance/sleuth
81 |
82 | # npm install --save @compound-finance/sleuth
83 | ```
84 |
85 | Next, simply build a Solidity file and build Sleuth, as above, to execute the query. E.g.
86 |
87 | ```ts
88 | import { Sleuth } from '@compound-finance/sleuth';
89 |
90 | let sleuth = new Sleuth(provider);
91 |
92 | let [name, age] = await sleuth.query(`
93 | // SPDX-License-Identifier: UNLICENSED
94 | pragma solidity ^0.8.16;
95 |
96 | contract SimpleQuery {
97 | function query() external pure returns (uint256, string memory) {
98 | return (55, "Bob Jones");
99 | }
100 | }
101 | `);
102 | ```
103 |
104 | ## Deploying the Sleuth contract on a network
105 |
106 | To deploy sleuth run the script under `script/mainnet/deploy.sh`, making sure to set the variables `ETHEREUM_PK` and `RPC_URL` (and optionally `ETHERSCAN_API_KEY`)
107 |
108 | ```sh
109 | ETHEREUM_PK=xxx RPC_URL=yyy ./deploy.sh
110 | ```
111 |
112 | If you run into an `error code -32000: invalid opcode: PUSH0` error, the network may not support the `PUSH0` opcode. Try adding the following to `foundry.toml` to deploy on an older Ethereum fork:
113 |
114 | ```toml
115 | evm_version = "london"
116 | solc = "0.8.23" // optional, to pin solc to a specific version
117 | ```
118 |
119 | ## Future Considerations
120 |
121 | Instead of having users build solidity files, it might be nice to build a proper query language. This could be SQL-like or ORM-style or anything that compiles to say Yul (the intermediate representation used by Solidity). We could then abstract the interface to something interesting, such as:
122 |
123 | ```ts
124 | await sleuth.query("SELECT comet.name FROM comet(0xc3...) WHERE comet.id = 5");
125 | ```
126 |
127 | There's so much we could do here and it sounds really fun!
128 |
129 | ### Parser
130 |
131 | There's an early version up and running, which you can use with Sleuth. See [/parser](/parser) for more information.
132 |
133 | ## License
134 |
135 | MIT
136 |
137 | Copyright 2022, Compound Labs, Inc. Geoffrey Hayes.
138 |
--------------------------------------------------------------------------------
/cli/index.ts:
--------------------------------------------------------------------------------
1 | export * from './sleuth';
2 |
--------------------------------------------------------------------------------
/cli/sleuth.ts:
--------------------------------------------------------------------------------
1 | import { Provider } from '@ethersproject/providers';
2 | import { Contract } from '@ethersproject/contracts';
3 | import { AbiCoder, FormatTypes, FunctionFragment, Fragment, Interface, ParamType } from '@ethersproject/abi';
4 | import { keccak256 } from '@ethersproject/keccak256';
5 | import { getContractAddress } from '@ethersproject/address';
6 | import { parse } from '../parser/pkg/parser';
7 |
8 | interface Opts {
9 | network?: string,
10 | version?: number,
11 | contractAddress?: string
12 | };
13 |
14 | const defaultOpts = {
15 | network: 'mainnet',
16 | version: 1
17 | };
18 |
19 | const sleuthDeployer = process.env['SLEUTH_DEPLOYER'] ?? '0x84C3e20985d9E7aEc46F80d2EB52b731D8CC40F8';
20 |
21 | interface Query {
22 | bytecode: string,
23 | callargs?: string,
24 | fn: FunctionFragment
25 | }
26 |
27 | interface Source {
28 | name: string,
29 | address: string,
30 | iface: Interface
31 | }
32 |
33 | interface SolidityQueryOpts {
34 | queryFunctionName?: string;
35 | }
36 |
37 | interface SolcInput {
38 | language?: string,
39 | sources: {
40 | [fileName: string]: {
41 | content: string
42 | }
43 | },
44 | settings: object
45 | }
46 |
47 | interface SolcContract {
48 | evm?: {
49 | bytecode?: {
50 | object: string
51 | }
52 | },
53 | bytecode?: {
54 | object: string
55 | },
56 | abi: Fragment[]
57 | }
58 |
59 | interface SolcOutput {
60 | contracts: {
61 | [fileName: string]: {
62 | [contractName: string]: SolcContract
63 | }
64 | },
65 | errors?: string[],
66 | }
67 |
68 | function solcCompile(input: SolcInput): SolcOutput {
69 | let solc;
70 | try {
71 | solc = require('solc');
72 | } catch (e) {
73 | throw new Error(`solc.js yarn dependency not found. Please build with optional dependencies included`);
74 | }
75 | return JSON.parse(solc.compile(JSON.stringify(input)));
76 | }
77 |
78 | function hexify(v: string): string {
79 | return v.startsWith('0x') ? v : `0x${v}`;
80 | }
81 |
82 | export class Sleuth {
83 | provider: Provider;
84 | network: string;
85 | version: number;
86 | sleuthAddr: string;
87 | sources: Source[];
88 | coder: AbiCoder;
89 |
90 | constructor(provider: Provider, opts: Opts = {}) {
91 | this.provider = provider;
92 | this.network = opts.network ?? defaultOpts.network;
93 | this.version = opts.version ?? defaultOpts.version;
94 | this.sleuthAddr = opts.contractAddress ?? getContractAddress({ from: sleuthDeployer, nonce: this.version - 1 });
95 | this.sources = [];
96 | this.coder = new AbiCoder();
97 | }
98 |
99 | query(q: string): Query {
100 | let registrations = this.sources.map((source) => {
101 | let iface = JSON.stringify(source.iface.format(FormatTypes.full));
102 | return `REGISTER CONTRACT ${source.name} AT ${source.address} WITH INTERFACE ${iface};`
103 | }).join("\n");
104 | let fullQuery = `${registrations}${q}`;
105 | console.log("Full Query", fullQuery);
106 | let [tuple, yul] = parse(fullQuery).split(';', 2);
107 | console.log("Tuple", tuple, "Yul", yul);
108 | const input = {
109 | language: 'Yul',
110 | sources: {
111 | 'query.yul': {
112 | content: yul
113 | }
114 | },
115 | settings: {
116 | outputSelection: {
117 | '*': {
118 | '*': ['*']
119 | }
120 | }
121 | }
122 | };
123 |
124 | let result = solcCompile(input);
125 | console.log(result.contracts['query.yul']);
126 | if (result.errors && result.errors.length > 0) {
127 | throw new Error("Compilation Error: " + JSON.stringify(result.errors));
128 | }
129 |
130 | let bytecode = result?.contracts['query.yul']?.Query?.evm?.bytecode?.object;
131 |
132 | if (!bytecode) {
133 | throw new Error(`Missing bytecode from compilation result: ${JSON.stringify(result)}`);
134 | }
135 |
136 | return {
137 | bytecode: bytecode,
138 | fn: FunctionFragment.from({
139 | name: 'query',
140 | inputs: [],
141 | outputs: ParamType.from(tuple).components,
142 | stateMutability: 'pure',
143 | type: 'function'
144 | })
145 | };
146 | }
147 |
148 | static querySol(q: string | object, opts: SolidityQueryOpts = {}): Query {
149 | if (typeof(q) === 'string') {
150 | let r;
151 | try {
152 | // Try to parse as JSON, if that fails, then consider a query
153 | r = JSON.parse(q);
154 | } catch (e) {
155 | // Ignore
156 | }
157 |
158 | if (r) {
159 | return this.querySolOutput(r, opts);
160 | } else {
161 | // This must be a source file, try to compile
162 | return this.querySolSource(q, opts);
163 | }
164 |
165 | } else {
166 | // This was passed in as a pre-parsed contract. Or at least, it should have been.
167 | return this.querySolOutput(q as SolcContract, opts);
168 | }
169 | }
170 |
171 | static querySolOutput(c: SolcContract, opts: SolidityQueryOpts = {}): Query {
172 | let queryFunctionName = opts.queryFunctionName ?? 'query';
173 | let b = c.evm?.bytecode?.object ?? c.bytecode?.object;
174 | if (!b) {
175 | throw new Error(`Missing (evm.)bytecode.object in contract ${JSON.stringify(c, null, 4)}`);
176 | }
177 | let abi = c.abi;
178 | let queryAbi = abi.find(({type, name}: any) => type === 'function' && name === queryFunctionName);
179 | if (!queryAbi) {
180 | throw new Error(`Query must include function \`${queryFunctionName}()\``);
181 | }
182 |
183 | return {
184 | bytecode: b,
185 | fn: queryAbi as FunctionFragment
186 | };
187 | }
188 |
189 | static querySolSource(q: string, opts: SolidityQueryOpts = {}): Query {
190 | let fnName = opts.queryFunctionName ?? 'query';
191 | let input = {
192 | language: 'Solidity',
193 | sources: {
194 | 'query.sol': {
195 | content: q
196 | }
197 | },
198 | settings: {
199 | outputSelection: {
200 | '*': {
201 | '*': ['*']
202 | }
203 | }
204 | }
205 | };
206 |
207 | let result = solcCompile(input);
208 | if (result.errors && result.errors.length > 0) {
209 | throw new Error("Compilation Error: " + JSON.stringify(result.errors));
210 | }
211 | let contract = result.contracts['query.sol'];
212 | if (!contract) {
213 | throw new Error(`Missing query.sol compiled contract in ${JSON.stringify(Object.keys(result.contracts))}`);
214 | }
215 | let c = Object.values(contract)[0] as any;
216 | if (!c) {
217 | throw new Error(`Query does not contain any contract definitions`);
218 | } else if (Object.keys(contract).length > 1) {
219 | console.warn(`Query contains multiple contracts, using ${Object.keys(contract)[0]}`);
220 | }
221 | return this.querySolOutput(c, opts);
222 | }
223 |
224 | async addSource(name: string, address: string, iface: string[] | Interface) {
225 | if (Array.isArray(iface)) {
226 | iface = new Interface(iface);
227 | }
228 | this.sources.push({name, address, iface});
229 | }
230 |
231 | async fetch(q: Query, args?: A): Promise {
232 | let sleuthCtx = new Contract(this.sleuthAddr, [
233 | 'function query(bytes,bytes) public view returns (bytes)'
234 | ], this.provider);
235 | let iface = new Interface([q.fn]);
236 | let argsCoded = iface.encodeFunctionData(q.fn.name, args ?? []);
237 | let queryResult = await sleuthCtx.query(hexify(q.bytecode), argsCoded);
238 | console.log(q.fn);
239 | console.log(queryResult);
240 | let r = this.coder.decode(q.fn.outputs ?? [], queryResult) as unknown;
241 | if (Array.isArray(r) && r.length === 1) {
242 | return r[0] as T;
243 | } else {
244 | return r as T;
245 | }
246 | }
247 |
248 | async fetchSql(q: string): Promise {
249 | let query = this.query(q);
250 | return this.fetch(query, []);
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/cli/test/sleuth.test.ts:
--------------------------------------------------------------------------------
1 | import { Sleuth } from '../sleuth';
2 | import { BigNumber } from '@ethersproject/bignumber';
3 | import { Provider, JsonRpcProvider } from '@ethersproject/providers';
4 | import * as fs from 'fs/promises';
5 | import * as path from 'path';
6 |
7 | describe('testing sleuthing', () => {
8 | let provider: Provider;
9 |
10 | beforeAll(() => {
11 | provider = new JsonRpcProvider('http://127.0.0.1:8599');
12 | });
13 |
14 | test('should return the block number via compilation', async () => {
15 | let sleuth = new Sleuth(provider);
16 | let solidity = await fs.readFile(path.join(__dirname, '../../src/examples/BlockNumber.sol'), 'utf8');
17 | let res = await sleuth.fetch(Sleuth.querySol(solidity));
18 | expect(res.toNumber()).toBe(1);
19 | });
20 |
21 | test('should return the block number via precompile', async () => {
22 | let sleuth = new Sleuth(provider);
23 | let solidity = await fs.readFile(path.join(__dirname, '../../out/BlockNumber.sol/BlockNumber.json'), 'utf8');
24 | console.log({solidity})
25 | let res = await sleuth.fetch(Sleuth.querySol(solidity));
26 | console.log("res", res);
27 | expect(res.toNumber()).toBe(1);
28 | });
29 |
30 | test('should handle args', async () => {
31 | let sleuth = new Sleuth(provider);
32 | let solidity = await fs.readFile(path.join(__dirname, '../../out/Birthday.sol/Birthday.json'), 'utf8');
33 | console.log({solidity})
34 | let res = await sleuth.fetch(Sleuth.querySol(solidity), [5]);
35 | console.log("res", res);
36 | expect(res.toNumber()).toBe(6);
37 | });
38 |
39 | test('should return the pair', async () => {
40 | let sleuth = new Sleuth(provider);
41 | let solidity = await fs.readFile(path.join(__dirname, '../../src/examples/Pair.sol'), 'utf8');
42 | let res = await sleuth.fetch(Sleuth.querySol<[BigNumber, string]>(solidity));
43 | console.log(res);
44 | expect(res[0].toNumber()).toBe(55);
45 | expect(res[1]).toEqual("hello");
46 | });
47 |
48 | test('should fail invalid', async () => {
49 | let sleuth = new Sleuth(provider);
50 | expect(() => sleuth.query("INSERT INTO users;")).toThrow();
51 | });
52 |
53 | test('should parse sleuth', async () => {
54 | let sleuth = new Sleuth(provider);
55 | let q = sleuth.query("SELECT block.number FROM block;");
56 | let number = await sleuth.fetch(q);
57 | // TODO: Check why named return types aren't working
58 | expect(number.toNumber()).toEqual(1);
59 | });
60 |
61 | test('should parse sleuth too', async () => {
62 | let sleuth = new Sleuth(provider);
63 | let q = sleuth.query<[BigNumber, string, BigNumber]>("SELECT block.number, \"dog\", 22 FROM block;");
64 | let [number, animal, age] = await sleuth.fetch(q);
65 | expect(number.toNumber()).toEqual(1);
66 | expect(animal).toEqual("dog");
67 | expect(age.toNumber()).toEqual(22);
68 | });
69 |
70 | test('including a call', async () => {
71 | let sleuth = new Sleuth(provider);
72 | sleuth.addSource("comet", "0xc3d688B66703497DAA19211EEdff47f25384cdc3", ["function totalSupply() returns (uint256)"]);
73 | let q = sleuth.query<[ BigNumber ]>("SELECT comet.totalSupply FROM comet;");
74 | let [ totalSupply ] = await sleuth.fetch(q);
75 | // TODO: Check why named return types aren't working
76 | expect(totalSupply.toNumber()).toEqual(160);
77 | });
78 |
79 | test('fetchSql query', async () => {
80 | let sleuth = new Sleuth(provider);
81 | let [ totalSupply ] = await sleuth.fetchSql<[ BigNumber ]>(`
82 | REGISTER CONTRACT comet AT 0xc3d688B66703497DAA19211EEdff47f25384cdc3 WITH INTERFACE ["function totalSupply() returns (uint256)"];
83 | SELECT comet.totalSupply FROM comet;
84 | `);
85 | expect(totalSupply.toNumber()).toEqual(160);
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = 'src'
3 | out = 'out'
4 | libs = ['lib']
5 | fs_permissions = [
6 | { access = "read", path = "./out"},
7 | { access = "read", path = "./.release-tmp"}
8 | ]
9 |
10 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {"^.+\\.(t|j)sx?$": ["@swc/jest"]},
3 | testEnvironment: 'node',
4 | testRegex: 'cli/test/.*\\.(test|spec)?\\.(ts|tsx)$',
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
6 | transformIgnorePatterns: ["node_modules/.*"],
7 | };
8 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compound-finance/sleuth/b46d95cb5e75ca97caae2dea06837ef95c426589/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@compound-finance/sleuth",
3 | "version": "1.0.1-alpha4",
4 | "main": "dist/index.js",
5 | "types": "dist/index.d.ts",
6 | "files": ["dist/**/*", "parser/pkg/**/*"],
7 | "repository": "https://github.com/compound-finance/sleuth",
8 | "author": "Geoffrey Hayes ",
9 | "license": "MIT",
10 | "scripts": {
11 | "test": "script/test.sh"
12 | },
13 | "devDependencies": {
14 | "@swc/core": "^1.3.22",
15 | "@swc/jest": "^0.2.24",
16 | "@types/jest": "^29.2.4",
17 | "jest": "^29.3.1",
18 | "ts-node": "^10.9.1",
19 | "typescript": "^4.9.4"
20 | },
21 | "dependencies": {
22 | "@ethersproject/contracts": "^5.7.0",
23 | "@ethersproject/providers": "^5.7.2"
24 | },
25 | "optionalDependencies": {
26 | "solc": "^0.8.17"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/parser/.appveyor.yml:
--------------------------------------------------------------------------------
1 | install:
2 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
3 | - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly
4 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
5 | - rustc -V
6 | - cargo -V
7 |
8 | build: false
9 |
10 | test_script:
11 | - cargo test --locked
12 |
--------------------------------------------------------------------------------
/parser/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | Cargo.lock
4 | bin/
5 | wasm-pack.log
6 |
--------------------------------------------------------------------------------
/parser/.travis.yml:
--------------------------------------------------------------------------------
1 | language: rust
2 | sudo: false
3 |
4 | cache: cargo
5 |
6 | matrix:
7 | include:
8 |
9 | # Builds with wasm-pack.
10 | - rust: beta
11 | env: RUST_BACKTRACE=1
12 | addons:
13 | firefox: latest
14 | chrome: stable
15 | before_script:
16 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
17 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
18 | - cargo install-update -a
19 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
20 | script:
21 | - cargo generate --git . --name testing
22 | # Having a broken Cargo.toml (in that it has curlies in fields) anywhere
23 | # in any of our parent dirs is problematic.
24 | - mv Cargo.toml Cargo.toml.tmpl
25 | - cd testing
26 | - wasm-pack build
27 | - wasm-pack test --chrome --firefox --headless
28 |
29 | # Builds on nightly.
30 | - rust: nightly
31 | env: RUST_BACKTRACE=1
32 | before_script:
33 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
34 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
35 | - cargo install-update -a
36 | - rustup target add wasm32-unknown-unknown
37 | script:
38 | - cargo generate --git . --name testing
39 | - mv Cargo.toml Cargo.toml.tmpl
40 | - cd testing
41 | - cargo check
42 | - cargo check --target wasm32-unknown-unknown
43 | - cargo check --no-default-features
44 | - cargo check --target wasm32-unknown-unknown --no-default-features
45 | - cargo check --no-default-features --features console_error_panic_hook
46 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
47 | - cargo check --no-default-features --features "console_error_panic_hook wee_alloc"
48 | - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc"
49 |
50 | # Builds on beta.
51 | - rust: beta
52 | env: RUST_BACKTRACE=1
53 | before_script:
54 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
55 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
56 | - cargo install-update -a
57 | - rustup target add wasm32-unknown-unknown
58 | script:
59 | - cargo generate --git . --name testing
60 | - mv Cargo.toml Cargo.toml.tmpl
61 | - cd testing
62 | - cargo check
63 | - cargo check --target wasm32-unknown-unknown
64 | - cargo check --no-default-features
65 | - cargo check --target wasm32-unknown-unknown --no-default-features
66 | - cargo check --no-default-features --features console_error_panic_hook
67 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
68 | # Note: no enabling the `wee_alloc` feature here because it requires
69 | # nightly for now.
70 |
--------------------------------------------------------------------------------
/parser/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "parser"
3 | version = "0.1.0"
4 | authors = ["Geoffrey Hayes "]
5 | edition = "2018"
6 |
7 | [lib]
8 | crate-type = ["cdylib", "rlib"]
9 |
10 | [features]
11 | default = ["console_error_panic_hook"]
12 |
13 | [dependencies]
14 | wasm-bindgen = "0.2.63"
15 |
16 | # The `console_error_panic_hook` crate provides better debugging of panics by
17 | # logging them with `console.error`. This is great for development, but requires
18 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
19 | # code size when deploying.
20 | console_error_panic_hook = { version = "0.1.6", optional = true }
21 |
22 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
23 | # compared to the default allocator's ~10K. It is slower than the default
24 | # allocator, however.
25 | wee_alloc = { version = "0.4.5", optional = true }
26 |
27 | pest = "2.5.1"
28 | pest_derive = "2.5.1"
29 | ethers = "1.0.2"
30 |
31 | [dev-dependencies]
32 | wasm-bindgen-test = "0.3.13"
33 |
34 | [profile.release]
35 | # Tell `rustc` to optimize for small code size.
36 | opt-level = "s"
37 |
--------------------------------------------------------------------------------
/parser/LICENSE_APACHE:
--------------------------------------------------------------------------------
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 [yyyy] [name of copyright owner]
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 |
--------------------------------------------------------------------------------
/parser/LICENSE_MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 Geoffrey Hayes
2 |
3 | Permission is hereby granted, free of charge, to any
4 | person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the
6 | Software without restriction, including without
7 | limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice
14 | shall be included in all copies or substantial portions
15 | of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 | DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/parser/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
wasm-pack-template
4 |
5 | A template for kick starting a Rust and WebAssembly project using wasm-pack.
6 |
7 |
19 |
20 | ## About
21 |
22 | [**📚 Read this template tutorial! 📚**][template-docs]
23 |
24 | This template is designed for compiling Rust libraries into WebAssembly and
25 | publishing the resulting package to NPM.
26 |
27 | Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other
28 | templates and usages of `wasm-pack`.
29 |
30 | [tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html
31 | [template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html
32 |
33 | ## 🚴 Usage
34 |
35 | ### 🐑 Use `cargo generate` to Clone this Template
36 |
37 | [Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate)
38 |
39 | ```
40 | cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
41 | cd my-project
42 | ```
43 |
44 | ### 🛠️ Build with `wasm-pack build`
45 |
46 | ```
47 | wasm-pack build -t nodejs
48 | ```
49 |
50 | ### 🔬 Test in Headless Browsers with `wasm-pack test`
51 |
52 | ```
53 | wasm-pack test --headless --firefox
54 | ```
55 |
56 | ### 🎁 Publish to NPM with `wasm-pack publish`
57 |
58 | ```
59 | wasm-pack publish
60 | ```
61 |
62 | ## 🔋 Batteries Included
63 |
64 | * [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating
65 | between WebAssembly and JavaScript.
66 | * [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook)
67 | for logging panic messages to the developer console.
68 | * [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized
69 | for small code size.
70 | * `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you
71 |
72 | ## License
73 |
74 | Licensed under either of
75 |
76 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
77 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
78 |
79 | at your option.
80 |
81 | ### Contribution
82 |
83 | Unless you explicitly state otherwise, any contribution intentionally
84 | submitted for inclusion in the work by you, as defined in the Apache-2.0
85 | license, shall be dual licensed as above, without any additional terms or
86 | conditions.
--------------------------------------------------------------------------------
/parser/pkg/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
wasm-pack-template
4 |
5 | A template for kick starting a Rust and WebAssembly project using wasm-pack.
6 |
7 |