Skip to content

Commit 1f3e255

Browse files
authored
Merge pull request #1 from compound-finance/hayesgm/start-to-add-yul-support
Start to add Sleuth Query Language and Yul resolution support
2 parents 763e6da + f3ee89f commit 1f3e255

37 files changed

+2612
-95
lines changed

README.md

+50-1
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,56 @@ contract BlockNumber {
2222
```ts
2323
import { Sleuth } from '@compound-finance/sleuth';
2424

25+
let blockNumberQuery = await Sleuth.querySol(fs.readFileSync('./MyQuery.sol', 'utf8'));
2526
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';
2635

27-
let blockNumber = await sleuth.query(fs.readFileSync('./MyQuery.sol', 'utf8'));
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);
2839
```
2940

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+
3075
## Getting Started
3176

3277
Install Sleuth:
@@ -66,6 +111,10 @@ await sleuth.query("SELECT comet.name FROM comet(0xc3...) WHERE comet.id = 5");
66111

67112
There's so much we could do here and it sounds really fun!
68113

114+
### Parser
115+
116+
There's an early version up and running, which you can use with Sleuth. See [/parser](/parser) for more information.
117+
69118
## License
70119

71120
MIT

cli/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './sleuth';

cli/sleuth.ts

+189-18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
const solc = require('solc');
21
import { Provider } from '@ethersproject/providers';
32
import { Contract } from '@ethersproject/contracts';
4-
import { AbiCoder } from '@ethersproject/abi';
3+
import { AbiCoder, FormatTypes, FunctionFragment, Fragment, Interface, ParamType } from '@ethersproject/abi';
54
import { keccak256 } from '@ethersproject/keccak256';
65
import { getContractAddress } from '@ethersproject/address';
6+
import { parse } from '../parser/pkg/parser';
77

88
interface Opts {
99
network?: string,
@@ -15,24 +15,179 @@ const defaultOpts = {
1515
version: 1
1616
};
1717

18-
const sleuthDeployer = process.env['SLEUTH_ADDRESS'] ?? '0x84C3e20985d9E7aEc46F80d2EB52b731D8CC40F8';
18+
const sleuthDeployer = process.env['SLEUTH_DEPLOYER'] ?? '0x84C3e20985d9E7aEc46F80d2EB52b731D8CC40F8';
19+
20+
interface Query<T, A extends any[] = []> {
21+
bytecode: string,
22+
callargs?: string,
23+
fn: FunctionFragment
24+
}
25+
26+
interface Source {
27+
name: string,
28+
address: string,
29+
iface: Interface
30+
}
31+
32+
interface SolidityQueryOpts {
33+
queryFunctionName?: string;
34+
}
35+
36+
interface SolcInput {
37+
language?: string,
38+
sources: {
39+
[fileName: string]: {
40+
content: string
41+
}
42+
},
43+
settings: object
44+
}
45+
46+
interface SolcContract {
47+
evm?: {
48+
bytecode?: {
49+
object: string
50+
}
51+
},
52+
bytecode?: {
53+
object: string
54+
},
55+
abi: Fragment[]
56+
}
57+
58+
interface SolcOutput {
59+
contracts: {
60+
[fileName: string]: {
61+
[contractName: string]: SolcContract
62+
}
63+
},
64+
errors?: string[],
65+
}
66+
67+
function solcCompile(input: SolcInput): SolcOutput {
68+
let solc;
69+
try {
70+
solc = require('solc');
71+
} catch (e) {
72+
throw new Error(`solc.js yarn dependency not found. Please build with optional dependencies included`);
73+
}
74+
return JSON.parse(solc.compile(JSON.stringify(input)));
75+
}
76+
77+
function hexify(v: string): string {
78+
return v.startsWith('0x') ? v : `0x${v}`;
79+
}
1980

2081
export class Sleuth {
2182
provider: Provider;
2283
network: string;
2384
version: number;
2485
sleuthAddr: string;
86+
sources: Source[];
87+
coder: AbiCoder;
2588

2689
constructor(provider: Provider, opts: Opts = {}) {
2790
this.provider = provider;
2891
this.network = opts.network ?? defaultOpts.network;
2992
this.version = opts.version ?? defaultOpts.version;
3093
this.sleuthAddr = getContractAddress({ from: sleuthDeployer, nonce: this.version - 1 });
31-
console.log('Sleuth address', this.sleuthAddr);
94+
this.sources = [];
95+
this.coder = new AbiCoder();
3296
}
3397

34-
async query(q: string) {
98+
query<T>(q: string): Query<T, []> {
99+
let registrations = this.sources.map((source) => {
100+
let iface = JSON.stringify(source.iface.format(FormatTypes.full));
101+
return `REGISTER CONTRACT ${source.name} AT ${source.address} WITH INTERFACE ${iface};`
102+
}).join("\n");
103+
let fullQuery = `${registrations}${q}`;
104+
console.log("Full Query", fullQuery);
105+
let [tuple, yul] = parse(fullQuery).split(';', 2);
106+
console.log("Tuple", tuple, "Yul", yul);
35107
const input = {
108+
language: 'Yul',
109+
sources: {
110+
'query.yul': {
111+
content: yul
112+
}
113+
},
114+
settings: {
115+
outputSelection: {
116+
'*': {
117+
'*': ['*']
118+
}
119+
}
120+
}
121+
};
122+
123+
let result = solcCompile(input);
124+
console.log(result.contracts['query.yul']);
125+
if (result.errors && result.errors.length > 0) {
126+
throw new Error("Compilation Error: " + JSON.stringify(result.errors));
127+
}
128+
129+
let bytecode = result?.contracts['query.yul']?.Query?.evm?.bytecode?.object;
130+
131+
if (!bytecode) {
132+
throw new Error(`Missing bytecode from compilation result: ${JSON.stringify(result)}`);
133+
}
134+
135+
return {
136+
bytecode: bytecode,
137+
fn: FunctionFragment.from({
138+
name: 'query',
139+
inputs: [],
140+
outputs: ParamType.from(tuple).components,
141+
stateMutability: 'pure',
142+
type: 'function'
143+
})
144+
};
145+
}
146+
147+
static querySol<T, A extends any[] = []>(q: string | object, opts: SolidityQueryOpts = {}): Query<T, A> {
148+
if (typeof(q) === 'string') {
149+
let r;
150+
try {
151+
// Try to parse as JSON, if that fails, then consider a query
152+
r = JSON.parse(q);
153+
} catch (e) {
154+
// Ignore
155+
}
156+
157+
if (r) {
158+
return this.querySolOutput(r, opts);
159+
} else {
160+
// This must be a source file, try to compile
161+
return this.querySolSource(q, opts);
162+
}
163+
164+
} else {
165+
// This was passed in as a pre-parsed contract. Or at least, it should have been.
166+
return this.querySolOutput(q as SolcContract, opts);
167+
}
168+
}
169+
170+
static querySolOutput<T, A extends any[] = []>(c: SolcContract, opts: SolidityQueryOpts = {}): Query<T, A> {
171+
let queryFunctionName = opts.queryFunctionName ?? 'query';
172+
let b = c.evm?.bytecode?.object ?? c.bytecode?.object;
173+
if (!b) {
174+
throw new Error(`Missing (evm.)bytecode.object in contract ${JSON.stringify(c, null, 4)}`);
175+
}
176+
let abi = c.abi;
177+
let queryAbi = abi.find(({type, name}: any) => type === 'function' && name === queryFunctionName);
178+
if (!queryAbi) {
179+
throw new Error(`Query must include function \`${queryFunctionName}()\``);
180+
}
181+
182+
return {
183+
bytecode: b,
184+
fn: queryAbi as FunctionFragment
185+
};
186+
}
187+
188+
static querySolSource<T, A extends any[] = []>(q: string, opts: SolidityQueryOpts = {}): Query<T, A> {
189+
let fnName = opts.queryFunctionName ?? 'query';
190+
let input = {
36191
language: 'Solidity',
37192
sources: {
38193
'query.sol': {
@@ -48,8 +203,8 @@ export class Sleuth {
48203
}
49204
};
50205

51-
let result = JSON.parse(solc.compile(JSON.stringify(input)));
52-
if (result.errors) {
206+
let result = solcCompile(input);
207+
if (result.errors && result.errors.length > 0) {
53208
throw new Error("Compilation Error: " + JSON.stringify(result.errors));
54209
}
55210
let contract = result.contracts['query.sol'];
@@ -62,19 +217,35 @@ export class Sleuth {
62217
} else if (Object.keys(contract).length > 1) {
63218
console.warn(`Query contains multiple contracts, using ${Object.keys(contract)[0]}`);
64219
}
65-
let b = c.evm.bytecode.object;
66-
let abi = c.abi;
67-
let queryAbi = abi.find(({type, name}: any) => type === 'function' && name === 'query');
68-
if (!queryAbi) {
69-
throw new Error(`Query must include function \`query()\``);
220+
return this.querySolOutput(c, opts);
221+
}
222+
223+
async addSource(name: string, address: string, iface: string[] | Interface) {
224+
if (Array.isArray(iface)) {
225+
iface = new Interface(iface);
70226
}
71-
let sleuthCtx = new Contract(this.sleuthAddr, ['function query(bytes) public view returns (bytes)'], this.provider);
72-
let queryResult = await sleuthCtx.query('0x' + b);
73-
let res = new AbiCoder().decode(queryAbi.outputs, queryResult);
74-
if (res.length === 1) {
75-
return res[0]
227+
this.sources.push({name, address, iface});
228+
}
229+
230+
async fetch<T, A extends any[] = []>(q: Query<T, A>, args?: A): Promise<T> {
231+
let sleuthCtx = new Contract(this.sleuthAddr, [
232+
'function query(bytes,bytes) public view returns (bytes)'
233+
], this.provider);
234+
let iface = new Interface([q.fn]);
235+
let argsCoded = iface.encodeFunctionData(q.fn.name, args ?? []);
236+
let queryResult = await sleuthCtx.query(hexify(q.bytecode), argsCoded);
237+
console.log(q.fn);
238+
console.log(queryResult);
239+
let r = this.coder.decode(q.fn.outputs ?? [], queryResult) as unknown;
240+
if (Array.isArray(r) && r.length === 1) {
241+
return r[0] as T;
76242
} else {
77-
return res;
243+
return r as T;
78244
}
79245
}
246+
247+
async fetchSql<T>(q: string): Promise<T> {
248+
let query = this.query<T>(q);
249+
return this.fetch<T, []>(query, []);
250+
}
80251
}

cli/test/client.test.ts

-25
This file was deleted.

0 commit comments

Comments
 (0)