Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multicall CCIP-read gateway #22

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f6a58af
Replace ethers-ccip-read-provider with ethers.js on client
makoto Mar 29, 2022
aac1828
Remove @chainlink/ethers-ccip-read-provider from contracts
makoto Mar 29, 2022
aed2da0
yarn lint --fix
makoto Mar 29, 2022
1802de4
Add mockprovider
makoto Mar 30, 2022
9043bb2
Fix failing test
makoto Mar 31, 2022
f9ebfc0
Merge sendRPC and callFunction into MockProvider
makoto Mar 31, 2022
1e7daab
Fix lint
makoto Mar 31, 2022
8772fe2
Add error class
makoto Mar 31, 2022
6cf5f7f
Fix lint error
makoto Mar 31, 2022
c8e083a
Rename to chainName
makoto Mar 31, 2022
eac7fce
multicoin
makoto Mar 31, 2022
b48058d
Change the default chainId to 3117
makoto Apr 4, 2022
20e3c42
Add litecoin to *.offchainexample.eth
makoto Apr 4, 2022
5f2e3ac
Resolve conflict
makoto Apr 4, 2022
3bcf20a
Remove console.log
makoto Apr 4, 2022
060e025
Fix lint error
makoto Apr 4, 2022
80bccfb
eth address
makoto Apr 4, 2022
0de1f23
Simplify multicoin call
makoto Apr 22, 2022
de14b44
Remove comment
makoto Apr 22, 2022
46a626f
Resolve conflicts
makoto Apr 22, 2022
e810b7f
Add contenthash example
makoto Apr 22, 2022
34bcc03
Merge branch 'main' into multicall
makoto Jul 4, 2022
72bfbb2
Change the console.log order
makoto Jul 14, 2022
202a137
Upgraded ens-contracts to 0.0.12
makoto Jul 18, 2022
084acbc
WIP add batchgateway and batchclient
makoto Jul 29, 2022
ae32ef9
Make multicoin work
makoto Aug 1, 2022
cd388ea
Return multiple query
makoto Aug 1, 2022
0540307
Tidy up
makoto Aug 1, 2022
c3d1e03
Refactoring
makoto Aug 1, 2022
2abac74
Add readme
makoto Aug 1, 2022
ecb2ea9
Update the lint error
makoto Aug 1, 2022
0ce7d6f
Update readme
makoto Aug 1, 2022
34864db
Fix lint error
makoto Aug 2, 2022
2c86d15
Fix lint error on batch-gateway
makoto Aug 3, 2022
b779881
Fix lint on gateway
makoto Aug 3, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 66 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Serving on port 8000 with signing address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb9

Take a look at the data in `test.eth.json` under `packages/gateway/`; it specifies addresses for the name `test.eth` and the wildcard `*.test.eth`.

Next, edit `packages/contracts/hardhat.config.js`; replacing the address on line 59 with the one output when you ran the command above. Then, in a new terminal, build and run a test node with an ENS registry and the offchain resolver deployed:
Next, edit `packages/contracts/hardhat.config.js`; replacing the address on line 59 with the one output when you ran the command above. Then, in a new terminal, build and run a test node with an ENS registry, the offchain resolver, and the Universal resolver deployed:

```
cd packages/contracts
Expand All @@ -60,9 +60,12 @@ You will see output similar to the following:

```
Compilation finished successfully
deploying "ENSRegistry" (tx: 0x8b353610592763c0abd8b06305e9e82c1b14afeecac99b1ce1ee54f5271baa2c)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 1084532 gas
deploying "OffchainResolver" (tx: 0xdb3142c2c4d214b58378a5261859a7f104908a38b4b9911bb75f8f21aa28e896)...: deployed at 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 with 1533637 gas
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:9545/
deploying "ENSRegistry" (tx: 0xded902cec50a22b4d797a27c88bf3a96a9d1bdbb41b6b40342cad729a18cee8d)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 743372 gas
deploying "OffchainResolver" (tx: 0xd07d38decf02acff262ef085420fc1956233b6eb4d025a594915839835c21f60)...: deployed at 0x8464135c8F25Da09e49BC8782676a84730C318bC with 2086937 gas
***registry 0x5FbDB2315678afecb367f032d93F642f64180aa3
deploying "UniversalResolver" (tx: 0x1b666104bb78a5a22b1914a5902426975a88eaaab23ec275c4b92fe4d7bc1008)...: deployed at 0x71C95911E9a5D330f4D621842EC243EE1343292e with 1566841 gas
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/


Accounts
========
Expand Down Expand Up @@ -103,6 +106,65 @@ Done in 0.23s.

Check these addresses against the gateway's `test.eth.json` and you will see that they match.


## Batch gateway

The normal gateway can only request single record at a time.
The batch gateway will make use of of `OffchainMulticallable.multicall` function that combines multiple calls.

To use the batch gateway, start the gateway server

```
yarn start:batch:gateway
yarn run v1.22.18
$ yarn workspace @ensdomains/offchain-resolver-batch-gateway start
$ node dist/index.js
Serving on port 8081
```

Then runs the batch client

```
yarn start:batch:client --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 --uAddress 0x71C95911E9a5D330f4D621842EC243EE1343292e foo.test.eth
yarn run v1.22.18
$ yarn workspace @ensdomains/offchain-resolver-batch-client start --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 --uAddress 0x71C95911E9a5D330f4D621842EC243EE1343292e foo.test.eth
$ node dist/index.js --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 --uAddress 0x71C95911E9a5D330f4D621842EC243EE1343292e foo.test.eth
{
name: 'foo.test.eth',
coinType: 60,
finalResult: [ '0x70997970c51812dc3a010c7d01b50e0d17dc79c8' ],
decodedResult: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
}
{
name: 'foo.test.eth',
coinType: 0,
finalResult: [ '0x0000000000000000000000000000000000000000' ],
decodedResult: 'bc1q9zpgru'
}
✨ Done in 1.27s.
```

### How it works.

The batch client and gateway go through the following sequence.

- Call `UniversalResolver.findResolver(dnsname)` to find the correct offchain resolver
- Encode `addr(node,coinType)` call into `addrData`
- Encode `resolver(dnsname, addrData)` into `callData`
- Combine `callData` into the array of `callDatas`
- Call `offchainResolver.multicall(callDatas)`
- Catch `OffchainLookup` error that encodes `Gateway.query(callDatas)` with callData and each gateway url
- Call the gateway server
- The batch gateway server decodes `Gateway.query(callDatas)` and call each gateway server in parallel
- Once the client receive the response, decode in the order of `Gateway.query` -> `ResolverService.resolve` -> `Resolver.addr(node, cointype)`
- Decode each coin cointype

### Todo

- Make batch gateway deployable
- Handle partial failure
- Fix lint errors

## Real-world usage

There are 5 main steps to using this in production:
Expand Down
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@
"author": "Nick Johnson <[email protected]>",
"license": "MIT",
"workspaces": [
"packages/contracts",
"packages/gateway",
"packages/client"
"packages/batchclient",
"packages/batchgateway",
"packages/client",
"packages/gateway"
],
"private": true,
"scripts": {
"start:gateway": "yarn workspace @ensdomains/offchain-resolver-gateway start",
"start:client": "yarn workspace @ensdomains/offchain-resolver-client start",
"start:batch:gateway": "yarn workspace @ensdomains/offchain-resolver-batch-gateway start",
"start:batch:client": "yarn workspace @ensdomains/offchain-resolver-batch-client start",
"test": "yarn workspaces run test",
"lint": "yarn workspaces run lint",
"build": "yarn workspaces run build",
"docs": "typedoc --entryPointStrategy packages packages/server packages/ethers-ccip-read-provider",
"clean": "rm -fr node_modules && yarn workspaces run clean"
},
"dependencies": {
"@ensdomains/ens-contracts": "^0.0.12"
}
}
4 changes: 4 additions & 0 deletions packages/batchclient/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.log
.DS_Store
node_modules
dist
21 changes: 21 additions & 0 deletions packages/batchclient/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Nick Johnson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
13 changes: 13 additions & 0 deletions packages/batchclient/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ENS offchain resolution client
This package implements a very simple dig-like tool for querying ENS names, with support for offchain resolution. It is intended as a way to test out your code and run the demo, rather than a production-quality tool.

Most of the code in this package is boilerplate implementing ENSIP 10; once support for this is added to Ethers, this code will be a lot simpler than it is now. Please do not use this code in production!

## Usage:
```
$ yarn start --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 test.eth
```

`--registry` is the address of the ENS registry. If you are running this on mainnet, this will be `0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e`.

`--provider` can optionally be specified to supply the URL to an Ethereum web3 provider; it defaults to `http://localhost:8545/`.
62 changes: 62 additions & 0 deletions packages/batchclient/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"version": "0.2.1",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"scripts": {
"start": "node dist/index.js",
"build": "tsdx build",
"lint": "tsdx lint",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why",
"clean": "rm -fr node_modules dist",
"test": "echo No tests"
},
"peerDependencies": {},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"name": "@ensdomains/offchain-resolver-batch-client",
"author": "Nick Johnson",
"module": "dist/client.esm.js",
"size-limit": [
{
"path": "dist/client.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/client.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@size-limit/preset-small-lib": "^7.0.5",
"husky": "^7.0.4",
"size-limit": "^7.0.5",
"tsdx": "^0.14.1",
"tslib": "^2.3.1",
"typescript": "^4.5.4"
},
"dependencies": {
"@ensdomains/address-encoder": "^0.2.17",
"commander": "^8.3.0",
"cross-fetch": "^3.1.5",
"ethers": "^5.6.2"
}
}
127 changes: 127 additions & 0 deletions packages/batchclient/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { formatsByCoinType } from '@ensdomains/address-encoder';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't all of this be replaced by a simple call to Ethers to resolve the name now?

import { Command } from 'commander';
import ethers from 'ethers';
import { abi as UniversalResolver_abi } from '@ensdomains/ens-contracts/artifacts/contracts/utils/UniversalResolver.sol/UniversalResolver.json';
import { abi as OffchainResolver_abi } from '@ensdomains/offchain-resolver-contracts/artifacts/contracts/OffchainResolver.sol/OffchainResolver.json';
import { abi as Gateway_abi } from '@ensdomains/ens-contracts/artifacts/contracts/utils/OffchainMulticallable.sol/BatchGateway.json';
import { abi as IResolverService_abi } from '@ensdomains/offchain-resolver-contracts/artifacts/contracts/OffchainResolver.sol/IResolverService.json';
import { abi as Resolver_abi } from '@ensdomains/ens-contracts/artifacts/contracts/resolvers/Resolver.sol/Resolver.json';
import fetch from 'cross-fetch';

const IResolverService = new ethers.utils.Interface(IResolverService_abi);

function getDnsName(name: string) {
const n = name.replace(/^\.|\.$/gm, '');

var bufLen = n === '' ? 1 : n.length + 2;
var buf = Buffer.allocUnsafe(bufLen);

let offset = 0;
if (n.length) {
const list = n.split('.');
for (let i = 0; i < list.length; i++) {
const len = buf.write(list[i], offset + 1);
buf[offset] = len;
offset += len + 1;
}
}
buf[offset++] = 0;
return (
'0x' +
buf.reduce(
(output, elem) => output + ('0' + elem.toString(16)).slice(-2),
''
)
);
}

const GatewayI = new ethers.utils.Interface(Gateway_abi);
const program = new Command();
program
.requiredOption('-r --registry <address>', 'ENS registry address')
.option('-p --provider <url>', 'web3 provider URL', 'http://localhost:8545/')
.option('-i --chainId <chainId>', 'chainId', '1337')
.option('-n --chainName <name>', 'chainName', 'unknown')
.option('-u --uAddress <uaddress>', 'Universal Resolver address')
.argument('<name>');

program.parse(process.argv);

const options = program.opts();
const ensAddress = options.registry;
const uAddress = options.uAddress;
const chainId = parseInt(options.chainId);
const chainName = options.chainName;
const provider = new ethers.providers.JsonRpcProvider(options.provider, {
chainId,
name: chainName,
ensAddress,
});
(async () => {
const name = program.args[0] || 'test.eth';
const node = ethers.utils.namehash(name);
const dnsName = getDnsName(name);
const uResolver = new ethers.Contract(
uAddress,
UniversalResolver_abi,
provider
);

const [resolverAddress] = await uResolver.callStatic.findResolver(dnsName);
if (resolverAddress) {
const offchainResolver = new ethers.Contract(
resolverAddress,
OffchainResolver_abi,
provider
);
const iface = new ethers.utils.Interface(Resolver_abi);
const coinTypes = [60, 0];
const callDatas = coinTypes.map(coinType => {
const addrData = iface.encodeFunctionData('addr(bytes32,uint256)', [
node,
coinType,
]);
return IResolverService.encodeFunctionData('resolve', [
dnsName,
addrData,
]);
});
try {
await offchainResolver.callStatic.multicall(callDatas);
} catch (e) {
if (e && e.errorArgs) {
const url = e.errorArgs.urls[0];
const lowerTo = e.errorArgs.sender.toLowerCase();
const callData = e.errorArgs.callData;
const gatewayUrl = url
.replace('{sender}', lowerTo)
.replace('{data}', callData);
const result = await fetch(gatewayUrl);
const { data: resultData } = await result.json();
const { responses: decodedQuery } = GatewayI.decodeFunctionResult(
'query',
resultData
);
for (let index = 0; index < decodedQuery.length; index++) {
const dq = decodedQuery[index];
const { result: addrResult } = IResolverService.decodeFunctionResult(
'resolve',
dq
);
const coinType = coinTypes[index];
const { encoder } = formatsByCoinType[coinType];
const finalResult = iface.decodeFunctionResult(
'addr(bytes32,uint256)',
addrResult
);
const hex = finalResult[0].slice(2);
const buffered = Buffer.from(hex, 'hex');
const decodedResult = encoder(buffered);
console.log({ name, coinType, finalResult, decodedResult });
}
} else {
console.log(105, e);
}
}
}
})();
36 changes: 36 additions & 0 deletions packages/batchclient/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"lib": ["dom", "esnext"],
"importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true,
// output .js.map sourcemap files for consumers
"sourceMap": true,
// match output dir to input dir. e.g. dist/index instead of dist/src/index
"rootDir": "./src",
// stricter type-checking for stronger correctness. Recommended by TS
"strict": true,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": true,
"noUnusedParameters": true,
// use Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "node",
// transpile JSX to React.createElement
"jsx": "react",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true,
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
"noEmit": true,
"resolveJsonModule": true,
}
}
Loading