Skip to content

Commit 260466a

Browse files
authored
Merge pull request #1 from scio-labs/feat/azero-offchain-resolver
AzeroID off-chain resolver implementation
2 parents 099b7e9 + 118c9d5 commit 260466a

14 files changed

+6508
-27
lines changed

packages/contracts/.env.example

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
NETWORK=sepolia
2+
REMOTE_GATEWAY=
3+
DEPLOYER_KEY=
4+
SIGNER_ADDR=
5+
INFURA_ID=
6+
ETHERSCAN_API_KEY=

packages/contracts/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,30 @@ This library facilitates checking signatures over CCIP read responses.
1616

1717
### [OffchainResolver.sol](contracts/OffchainResolver.sol)
1818
This contract implements the offchain resolution system. Set this contract as the resolver for a name, and that name and all its subdomains that are not present in the ENS registry will be resolved via the provided gateway by supported clients.
19+
20+
## Deployment instruction
21+
22+
1. Set the following env variables:
23+
24+
* *REMOTE_GATEWAY*
25+
The target url (default: localhost:8080)
26+
27+
* *DEPLOYER_KEY* (*mandatory)
28+
The private key use to deploy the contract (also the contract owner)
29+
30+
* *SIGNER_KEY* (*mandatory)
31+
The public key which is approved as the trusted signer
32+
33+
* *INFURA_ID*
34+
API key for network provider
35+
36+
* *ETHERSCAN_API_KEY*
37+
38+
* *NETWORK*
39+
The target network (default: sepolia)
40+
41+
2. Run the following command
42+
43+
```bash
44+
./deploy.sh
45+
```

packages/contracts/contracts/OffchainResolver.sol

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.4;
33

4+
import "@openzeppelin/contracts/access/Ownable.sol";
45
import "@ensdomains/ens-contracts/contracts/resolvers/SupportsInterface.sol";
56
import "./IExtendedResolver.sol";
67
import "./SignatureVerifier.sol";
@@ -13,11 +14,12 @@ interface IResolverService {
1314
* Implements an ENS resolver that directs all queries to a CCIP read gateway.
1415
* Callers must implement EIP 3668 and ENSIP 10.
1516
*/
16-
contract OffchainResolver is IExtendedResolver, SupportsInterface {
17+
contract OffchainResolver is Ownable, IExtendedResolver, SupportsInterface {
1718
string public url;
1819
mapping(address=>bool) public signers;
1920

2021
event NewSigners(address[] signers);
22+
event SignersRemoved(address[] signers);
2123
error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData);
2224

2325
constructor(string memory _url, address[] memory _signers) {
@@ -28,6 +30,24 @@ contract OffchainResolver is IExtendedResolver, SupportsInterface {
2830
emit NewSigners(_signers);
2931
}
3032

33+
function setUrl(string calldata _url) external onlyOwner {
34+
url = _url;
35+
}
36+
37+
function addSigners(address[] calldata _signers) external onlyOwner {
38+
for(uint i = 0; i < _signers.length; i++) {
39+
signers[_signers[i]] = true;
40+
}
41+
emit NewSigners(_signers);
42+
}
43+
44+
function removeSigners(address[] calldata _signers) external onlyOwner {
45+
for(uint i = 0; i < _signers.length; i++) {
46+
signers[_signers[i]] = false;
47+
}
48+
emit SignersRemoved(_signers);
49+
}
50+
3151
function makeSignatureHash(address target, uint64 expires, bytes memory request, bytes memory result) external pure returns(bytes32) {
3252
return SignatureVerifier.makeSignatureHash(target, expires, request, result);
3353
}

packages/contracts/deploy.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
4+
# Can only be overwritten from the .env file, not from the command line!
5+
NETWORK=sepolia
6+
7+
# Load .env
8+
source .env
9+
10+
# Deploy OffchainResolver
11+
npx hardhat --network $NETWORK deploy --tags demo

packages/contracts/hardhat.config.js

+16-15
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,13 @@ require('@nomiclabs/hardhat-ethers');
44
require('@nomiclabs/hardhat-waffle');
55
require('hardhat-deploy');
66
require('hardhat-deploy-ethers');
7+
require('dotenv').config();
78

89
real_accounts = undefined;
9-
if (process.env.DEPLOYER_KEY && process.env.OWNER_KEY) {
10-
real_accounts = [process.env.OWNER_KEY, process.env.DEPLOYER_KEY];
11-
}
12-
const gatewayurl =
13-
'https://offchain-resolver-example.uc.r.appspot.com/{sender}/{data}.json';
14-
15-
let devgatewayurl = 'http://localhost:8080/{sender}/{data}.json';
16-
if (process.env.REMOTE_GATEWAY) {
17-
devgatewayurl =
18-
`${process.env.REMOTE_GATEWAY}/{sender}/{data}.json`;
10+
if (process.env.DEPLOYER_KEY) {
11+
real_accounts = [process.env.DEPLOYER_KEY];
1912
}
13+
const gatewayurl = process.env.REMOTE_GATEWAY || 'http://localhost:8080/';
2014
/**
2115
* @type import('hardhat/config').HardhatUserConfig
2216
*/
@@ -26,7 +20,7 @@ module.exports = {
2620
networks: {
2721
hardhat: {
2822
throwOnCallFailures: false,
29-
gatewayurl: devgatewayurl,
23+
gatewayurl,
3024
},
3125
ropsten: {
3226
url: `https://ropsten.infura.io/v3/${process.env.INFURA_ID}`,
@@ -49,6 +43,13 @@ module.exports = {
4943
accounts: real_accounts,
5044
gatewayurl,
5145
},
46+
sepolia: {
47+
url: `https://sepolia.infura.io/v3/${process.env.INFURA_ID}`,
48+
tags: ['test', 'demo'],
49+
chainId: 11155111,
50+
accounts: real_accounts,
51+
gatewayurl,
52+
},
5253
mainnet: {
5354
url: `https://mainnet.infura.io/v3/${process.env.INFURA_ID}`,
5455
tags: ['demo'],
@@ -61,11 +62,11 @@ module.exports = {
6162
apiKey: process.env.ETHERSCAN_API_KEY,
6263
},
6364
namedAccounts: {
64-
signer: {
65-
default: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
66-
},
6765
deployer: {
68-
default: 1,
66+
default: 0,
67+
},
68+
signer: {
69+
default: process.env.SIGNER_ADDR,
6970
},
7071
},
7172
};

packages/contracts/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"dependencies": {
3030
"@ensdomains/ens-contracts": "^0.0.8",
3131
"@nomiclabs/hardhat-etherscan": "^3.0.0",
32+
"dotenv": "^16.4.5",
3233
"hardhat-deploy-ethers": "^0.3.0-beta.13"
3334
}
3435
}

packages/gateway/README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ You can run the gateway as a command line tool; in its default configuration it
66

77
```
88
yarn && yarn build
9-
yarn start --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --data test.eth.json
9+
yarn start --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --provider-url <url>
1010
```
1111

1212
`private-key` should be an Ethereum private key that will be used to sign messages. You should configure your resolver contract to expect messages to be signed using the corresponding address.
1313

14-
`data` is the path to the data file; an example file is provided in `test.eth.json`.
14+
`<url>` is the websocket endpoint of the target substrate chain (default: wss://ws.test.azero.dev)
1515

1616
## Customisation
1717
The JSON backend is implemented in [json.ts](src/json.ts), and implements the `Database` interface from [server.ts](src/server.ts). You can replace this with your own database service by implementing the methods provided in that interface. If a record does not exist, you should return the zero value for that type - for example, requests for nonexistent text records should be responded to with the empty string, and requests for nonexistent addresses should be responded to with the all-zero address.
@@ -25,3 +25,6 @@ const db = JSONDatabase.fromFilename(options.data, parseInt(options.ttl));
2525
const app = makeApp(signer, '/', db);
2626
app.listen(parseInt(options.port));
2727
```
28+
29+
## AZERO-ID implementation
30+
The AzeroId gateway implementation in [azero-id.ts](src/azero-id.ts) reads the state from the AZERO-ID registry contract on AlephZero network. [supported-tlds.json](src/supported-tlds.json) stores the TLDs mapped to their target registry. Update it as per need.

packages/gateway/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@
5858
"dependencies": {
5959
"@chainlink/ccip-read-server": "^0.2.1",
6060
"@chainlink/ethers-ccip-read-provider": "^0.2.3",
61+
"@ensdomains/address-encoder": "^1.1.2",
6162
"@ensdomains/ens-contracts": "^0.0.8",
6263
"@ensdomains/offchain-resolver-contracts": "^0.2.1",
64+
"@polkadot/api": "^12.3.1",
65+
"@polkadot/api-contract": "^12.3.1",
6366
"commander": "^8.3.0",
6467
"dotenv": "^15.0.0",
6568
"ethers": "^5.7.2"

packages/gateway/src/azero-id.ts

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import abi from './metadata.json';
2+
import { ApiPromise, WsProvider } from '@polkadot/api';
3+
import { Database } from './server';
4+
import { ContractPromise } from '@polkadot/api-contract';
5+
import type { WeightV2 } from '@polkadot/types/interfaces';
6+
import { getCoderByCoinType } from "@ensdomains/address-encoder";
7+
import { createDotAddressDecoder } from '@ensdomains/address-encoder/utils'
8+
import { hexlify } from 'ethers/lib/utils';
9+
10+
const AZERO_COIN_TYPE = 643;
11+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
12+
const EMPTY_CONTENT_HASH = '0x';
13+
14+
export interface GasLimit {
15+
refTime: number,
16+
proofSize: number,
17+
}
18+
19+
export class AzeroId implements Database {
20+
ttl: number;
21+
tldToContract: Map<string, ContractPromise>;
22+
maxGasLimit: WeightV2;
23+
24+
constructor(ttl: number, tldToContract: Map<string, ContractPromise>, maxGasLimit: WeightV2) {
25+
this.ttl = ttl;
26+
this.tldToContract = tldToContract;
27+
this.maxGasLimit = maxGasLimit;
28+
}
29+
30+
static async init(ttl: number, providerURL: string, tldToContractAddress: Map<string, string>, gasLimit: GasLimit) {
31+
const wsProvider = new WsProvider(providerURL);
32+
const api = await ApiPromise.create({ provider: wsProvider });
33+
34+
const tldToContract = new Map<string, ContractPromise>();
35+
tldToContractAddress.forEach((addr, tld) => {
36+
tldToContract.set(tld, new ContractPromise(api, abi, addr))
37+
})
38+
39+
const maxGasLimit = api.registry.createType('WeightV2', gasLimit) as WeightV2;
40+
41+
return new AzeroId(
42+
ttl,
43+
tldToContract,
44+
maxGasLimit,
45+
);
46+
}
47+
48+
async addr(name: string, coinType: number) {
49+
coinType = Number(coinType); // convert BigNumber to number
50+
console.log("addr", name, coinType);
51+
52+
let value;
53+
if (coinType == AZERO_COIN_TYPE) {
54+
value = await this.fetchA0ResolverAddress(name);
55+
} else {
56+
let alias = AzeroId.getAlias(""+coinType);
57+
if (alias !== undefined) {
58+
const serviceKey = "address." + alias;
59+
value = await this.fetchRecord(name, serviceKey);
60+
}
61+
if (value === undefined) {
62+
const serviceKey = "address." + coinType;
63+
value = await this.fetchRecord(name, serviceKey);
64+
}
65+
}
66+
67+
if (value === undefined) {
68+
value = coinType == 60? ZERO_ADDRESS:'0x';
69+
} else {
70+
value = AzeroId.encodeAddress(value, coinType);
71+
}
72+
73+
return { addr: value, ttl: this.ttl };
74+
}
75+
76+
async text(name: string, key: string) {
77+
console.log("text", name, key);
78+
const value = await this.fetchRecord(name, key) || '';
79+
return { value, ttl: this.ttl };
80+
}
81+
82+
contenthash(name: string) {
83+
console.log("contenthash", name);
84+
return { contenthash: EMPTY_CONTENT_HASH, ttl: this.ttl };
85+
}
86+
87+
private async fetchRecord(domain: string, key: string) {
88+
let {name, contract} = this.processName(domain);
89+
const resp: any = await contract.query.getRecord(
90+
'',
91+
{
92+
gasLimit: this.maxGasLimit
93+
},
94+
name,
95+
key
96+
);
97+
98+
return resp.output?.toHuman().Ok.Ok;
99+
}
100+
101+
private async fetchA0ResolverAddress(domain: string) {
102+
let {name, contract} = this.processName(domain);
103+
const resp: any = await contract.query.getAddress(
104+
'',
105+
{
106+
gasLimit: this.maxGasLimit
107+
},
108+
name
109+
);
110+
111+
return resp.output?.toHuman().Ok.Ok;
112+
}
113+
114+
private processName(domain: string) {
115+
const labels = domain.split('.');
116+
console.log("Labels:", labels);
117+
118+
const name = labels.shift() || '';
119+
const tld = labels.join('.');
120+
const contract = this.tldToContract.get(tld);
121+
122+
if (contract === undefined) {
123+
throw new Error(`TLD (.${tld}) not supported`);
124+
}
125+
126+
return {name, contract};
127+
}
128+
129+
static getAlias(coinType: string) {
130+
const alias = new Map<string, string>([
131+
['0', 'btc'],
132+
['60', 'eth'],
133+
['354', 'dot'],
134+
['434', 'ksm'],
135+
['501', 'sol'],
136+
]);
137+
138+
return alias.get(coinType);
139+
}
140+
141+
static encodeAddress(addr: string, coinType: number) {
142+
const isEvmCoinType = (c: number) => {
143+
return c == 60 || (c & 0x80000000)!=0
144+
}
145+
146+
if (coinType == AZERO_COIN_TYPE) {
147+
const azeroCoder = createDotAddressDecoder(42);
148+
return hexlify(azeroCoder(addr));
149+
}
150+
if (isEvmCoinType(coinType) && !addr.startsWith('0x')) {
151+
addr = '0x' + addr;
152+
}
153+
154+
try {
155+
const coder = getCoderByCoinType(coinType);
156+
return hexlify(coder.decode(addr));
157+
} catch {
158+
return addr;
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)