diff --git a/README.md b/README.md index 30f7344..27be4f0 100644 --- a/README.md +++ b/README.md @@ -14,25 +14,32 @@ It specifically targets the [AZERO.ID](https://azero.id) registry, though, it ca | | **Testnet¹** | **Mainnet²** | | --------------------- | -------------------------------------------- | -------------------------------------------- | | **Resolver Contract** | `0x5cf63C14b82C6E1B95023d8D23e682d12761F56C` | `0x723f6C968609F62583504DD67307A4Ae4c9Fd886` | -| **Gateway** | https://tzero-id-gateway.nameverse.io | https://azero-id-gateway.nameverse.io | -| **ENS Domain** | `*.tzero-id.eth`, `tzero.eth` | `*.azero-id.eth` | +| **Gateway** | https://gateway.tzero.id | https://gateway.azero.id | +| **ENS Domains** | `.tzero.id`³, `.tzero-id.eth` | `.azero.id`³, `.azero-id.eth` | +| **RegistrationProxy** | TODO | TODO | +| **Wasm⁴** | TODO | TODO | - ¹ Testnet: Ethereum Sepolia & Aleph Zero Testnet
- ² Mainnet: Ethereum Mainnet & Aleph Zero Mainnet
+ ¹ Ethereum Sepolia & Aleph Zero Testnet
+ ² Ethereum Mainnet & Aleph Zero Mainnet
+ ³ Regular ENS Domains imported via DNSSEC
+ Deployed on substrate chain
## Packages -### [Solidity Contracts](packages/contracts) +### [Solidity Contracts](packages/contracts/README.md) The smart contract provides a resolver stub that implement CCIP Read (EIP 3668) and ENS wildcard resolution (ENSIP 10). When queried for a name, it directs the client to query the gateway server. When called back with the gateway server response, the resolver verifies the signature was produced by an authorised signer, and returns the response to the client. -### [Gateway Server](packages/gateway) +### [Gateway & Relayer Server](packages/server/README.md) -The gateway server implements CCIP Read (EIP 3668), and answers requests by looking up the names on the registry Aleph Zero. Once a record is retrieved, it is signed using a user-provided key to assert its validity, and both record and signature are returned to the caller so they can be provided to the contract that initiated the request. It's designed to be deployed as a Cloudflare worker. +The server serves as both a EVM Registration Proxy (Relayer) and as a CCIP Read Resolver (Gateway) for ENS resolution. -### [Demo Client](packages/client) +- **Gateway**: Implements CCIP Read (EIP 3668), and answers requests by looking up the names on the registry Aleph Zero. Once a record is retrieved, it is signed using a user-provided key to assert its validity, and both record and signature are returned to the caller so they can be provided to the contract that initiated the request. It's designed to be deployed as a Cloudflare worker. +- **Relayer**: Relays registration requests from EVM chain to the substrate chain. `InitiateRequest` event is emitted when `RegistrationProxy::register()` is invoked. Its `TxHash` and optionally `reqId` is submitted to the relayer that parses and executes it on the substrate chain and then relays back the result to the EVM chain. Multiple payment options (native token, ERC20, and theoretically traditional payment as well) are supported by the relayer. + +### [Demo Client](packages/client/README.md) A simple script that resolves a given domain through the ENS protocol (using the gateway server) and verifies the response with the result from the registry contracts directly on the Aleph Zero network. @@ -44,7 +51,7 @@ A simple script that resolves a given domain through the ENS protocol (using the > - Install [Bun](https://bun.sh/) > - Clone this repository -1. Run the gateway server ([packages/gateway/README.md](packages/gateway/README.md)) +1. Run the gateway server ([packages/server/README.md](packages/server/README.md)) 1. Use the worker url as environment variable when deploying the contracts 2. Deploy the contracts ([packages/contracts/README.md](packages/contracts/README.md)) 1. Assign the new resolver to your ENS name diff --git a/bun.lockb b/bun.lockb index 18e61c8..3c702aa 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/client/index.ts b/packages/client/index.ts index 0501839..4299ca3 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -37,29 +37,32 @@ const transport = process.env.INFURA_API_KEY const viemClient = createPublicClient({ chain: evmChain, transport }) const evmChainName = isMainnet ? 'Ethereum Mainnet' : 'Ethereum Sepolia' -const ensDomain = isMainnet ? `${domain}-id.eth` : `${domain}-id.eth` +const ensDomain = isMainnet ? `${domain}.id` : `${domain}.id` +// const ensDomain = isMainnet ? `${domain}-id.eth` : `${domain}-id.eth` const resolver = await viemClient.getEnsResolver({ name: normalize(ensDomain), }) spinner.info(`[${evmChainName}] Found Resolver: ${resolver}`).start() -const gatewayUrl = isMainnet - ? `https://azero-id-gateway.nameverse.io` - : `https://tzero-id-gateway.nameverse.io` +const gatewayUrl = isMainnet ? `https://gateway.azero.id` : `https://gateway.tzero.id` spinner.info(`[${evmChainName}] Gateway URL: ${gatewayUrl}`) spinner.start(`Fetching ENS Address on EVM via Gateway (${gatewayUrl})…`) + +const startTime = performance.now() const evmAddress = await viemClient.getEnsAddress({ name: normalize(ensDomain), coinType: 643, - // universalResolverAddress: resolver, + universalResolverAddress: resolver, }) +const endTime = performance.now() +const duration = endTime - startTime const evmAddressSs58 = evmAddress ? new AccountId32(evmAddress).address() : null if (evmAddress && evmAddressSs58) { spinner.success( - `[${evmChainName}] Resolved address of ${ensDomain}: ${evmAddressSs58} (${evmAddress})`, + `[${evmChainName}] Resolved address of ${ensDomain}: ${evmAddressSs58} (${evmAddress}) in ${duration.toFixed(0)}ms`, ) } else { spinner.error(`[${evmChainName}] Couldn't resolve address of ${ensDomain}`) diff --git a/packages/client/package.json b/packages/client/package.json index 80fbe0e..8e0592d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { - "name": "@scio-labs/azero-offchain-resolver-client", - "version": "0.0.1", + "name": "@scio-labs/azero-offchain-client", + "version": "0.2.0", "repository": "git@github.com:scio-labs/offchain-resolver-ts.git", "author": "Scio Labs (https://scio.xyz)", "license": "MIT", @@ -16,7 +16,7 @@ "@types/bun": "latest", "@wagmi/cli": "^2.1.16", "dedot": "^0.6.0", - "viem": "^2.21.22", - "yocto-spinner": "^0.1.0" + "viem": "^2.21.34", + "yocto-spinner": "^0.1.1" } } diff --git a/packages/contracts/.env.example b/packages/contracts/.env.example index e1b3eff..66325c8 100644 --- a/packages/contracts/.env.example +++ b/packages/contracts/.env.example @@ -6,9 +6,27 @@ DEPLOYER_KEY=TODO SIGNER_ADDR=TODO NETWORK=sepolia REMOTE_GATEWAY=https://tzero-id-gateway.nameverse.io/ +HOLD_PERIOD=TODO + +RPC_URL='wss://ws.test.azero.dev' +WASM_PRIVATE_KEY=TODO +WASM_PATH=TODO +ABI_PATH=TODO +ADMIN=TODO +REGISTRY_ADDR='5FsB91tXSEuMj6akzdPczAtmBaVKToqHmtAwSUzXh49AYzaD' +PRE_FUND_AMOUNT=0 # Mainnet # DEPLOYER_KEY=TODO # SIGNER_ADDR=TODO # NETWORK=mainnet # REMOTE_GATEWAY=https://azero-id-gateway.nameverse.io/ +# HOLD_PERIOD=TODO + +# RPC_URL='wss://ws.azero.dev' +# WASM_PRIVATE_KEY=TODO +# WASM_PATH=TODO +# ABI_PATH=TODO +# ADMIN=TODO +# REGISTRY_ADDR='5CTQBfBC9SfdrCDBJdfLiyW2pg9z5W6C6Es8sK313BLnFgDf' +# PRE_FUND_AMOUNT=0 \ No newline at end of file diff --git a/packages/contracts/README.md b/packages/contracts/README.md index a677489..0c4174b 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -1,23 +1,11 @@ -# ENS Offchain Gateway for Aleph Zero – Solidity Contracts +# Contracts -> **See [README.md](../../README.md) for more information.** - -## Contracts - -### [IExtendedResolver.sol](contracts/IExtendedResolver.sol) - -This is the interface for wildcard resolution specified in ENSIP 10. In time this will likely be moved to the [@ensdomains/ens-contracts](https://github.com/ensdomains/ens-contracts) repository. - -### [SignatureVerifier.sol](contracts/SignatureVerifier.sol) - -This library facilitates checking signatures over CCIP read responses. - -### [OffchainResolver.sol](contracts/OffchainResolver.sol) - -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. +There are two directories under the `contracts` dir - `gateway` and `relayer`. Each containing the contracts for respective purposes. ## Getting Started +Deploy smart contracts for both gateway and relayer: + ```bash # Install dependencies bun install diff --git a/packages/contracts/contracts/IExtendedResolver.sol b/packages/contracts/contracts/gateway/IExtendedResolver.sol similarity index 100% rename from packages/contracts/contracts/IExtendedResolver.sol rename to packages/contracts/contracts/gateway/IExtendedResolver.sol diff --git a/packages/contracts/contracts/OffchainResolver.sol b/packages/contracts/contracts/gateway/OffchainResolver.sol similarity index 100% rename from packages/contracts/contracts/OffchainResolver.sol rename to packages/contracts/contracts/gateway/OffchainResolver.sol diff --git a/packages/contracts/contracts/gateway/README.md b/packages/contracts/contracts/gateway/README.md new file mode 100644 index 0000000..a66345d --- /dev/null +++ b/packages/contracts/contracts/gateway/README.md @@ -0,0 +1,15 @@ +# ENS Offchain Gateway for Aleph Zero – Solidity Contracts + +## Contracts + +### [IExtendedResolver.sol](./IExtendedResolver.sol) + +This is the interface for wildcard resolution specified in ENSIP 10. In time this will likely be moved to the [@ensdomains/ens-contracts](https://github.com/ensdomains/ens-contracts) repository. + +### [SignatureVerifier.sol](./SignatureVerifier.sol) + +This library facilitates checking signatures over CCIP read responses. + +### [OffchainResolver.sol](./OffchainResolver.sol) + +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. diff --git a/packages/contracts/contracts/SignatureVerifier.sol b/packages/contracts/contracts/gateway/SignatureVerifier.sol similarity index 100% rename from packages/contracts/contracts/SignatureVerifier.sol rename to packages/contracts/contracts/gateway/SignatureVerifier.sol diff --git a/packages/contracts/contracts/SupportsInterface.sol b/packages/contracts/contracts/gateway/SupportsInterface.sol similarity index 100% rename from packages/contracts/contracts/SupportsInterface.sol rename to packages/contracts/contracts/gateway/SupportsInterface.sol diff --git a/packages/contracts/contracts/imports.sol b/packages/contracts/contracts/gateway/imports.sol similarity index 100% rename from packages/contracts/contracts/imports.sol rename to packages/contracts/contracts/gateway/imports.sol diff --git a/packages/contracts/contracts/relayer/README.md b/packages/contracts/contracts/relayer/README.md new file mode 100644 index 0000000..dc3e26c --- /dev/null +++ b/packages/contracts/contracts/relayer/README.md @@ -0,0 +1,15 @@ +# Relayer contracts + +## [RegistrationProxy](./evm/RegistrationProxy.sol) + +It is a solidity contract and is deployed on the EVM chain. + +## [Wasm](./wasm/lib.rs) + +It is a wasm contract and is deployed on the substrate chain. + +## Note + +1. Admin is assigned as a controller by default during init (for both `RegistrationProxy` & `wasm`). The admin can assign other accounts as a controller as well by invoking the `setController()` method. + +2. `wasm` contract has a payable method `fundMe()` which can be used to fund its reserve. \ No newline at end of file diff --git a/packages/contracts/contracts/relayer/evm/Controllable.sol b/packages/contracts/contracts/relayer/evm/Controllable.sol new file mode 100644 index 0000000..8c4bd2a --- /dev/null +++ b/packages/contracts/contracts/relayer/evm/Controllable.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +abstract contract Controllable is Ownable { + mapping(address => bool) public controllers; + + modifier onlyController { + require(controllers[msg.sender]); + _; + } + + function setController(address controller, bool status) external onlyOwner { + controllers[controller] = status; + } +} diff --git a/packages/contracts/contracts/relayer/evm/RegistrationProxy.sol b/packages/contracts/contracts/relayer/evm/RegistrationProxy.sol new file mode 100644 index 0000000..53791cf --- /dev/null +++ b/packages/contracts/contracts/relayer/evm/RegistrationProxy.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./Controllable.sol"; + +contract RegistrationProxy is Ownable, Controllable { + enum Status { + NON_EXISTENT, + PENDING, + SUCCESS, + FAILURE + } + + event InitiateRequest( + uint256 indexed id, + string name, + string recipient, + uint8 yearsToRegister, + string[2][] metadata, + address paymentToken, + uint256 value, + uint256 ttl + ); + + event ResultInfo(uint256 indexed id, bool success, uint256 refundAmt); + + struct Record { + // string name; + address initiator; + address paymentToken; + uint256 value; + uint256 ttl; + Status status; + } + + uint256 public id; + uint256 public holdPeriod; + mapping(address => uint256) public lockedFunds; + mapping(uint256 => Record) public idToRecord; + mapping(address => bool) public whitelistedTokens; + + constructor(uint256 _holdPeriod) Ownable() { + holdPeriod = _holdPeriod; + controllers[msg.sender] = true; + } + + function setHoldPeriod(uint256 _holdPeriod) external onlyOwner { + holdPeriod = _holdPeriod; + } + + function setWhitelistToken(address paymentToken, bool state) external onlyOwner { + whitelistedTokens[paymentToken] = state; + } + + function register( + string calldata name, + string calldata recipient, + uint8 yearsToRegister, + string[2][] calldata metadata + ) external payable { + _register(name, recipient, yearsToRegister, metadata, address(0), msg.value); + } + + function register( + string calldata name, + string calldata recipient, + uint8 yearsToRegister, + string[2][] calldata metadata, + address paymentToken, + uint256 value + ) external { + _collectPayment(msg.sender, paymentToken, value); + _register(name, recipient, yearsToRegister, metadata, paymentToken, value); + } + + function success(uint256 _id, uint256 refundAmt) external onlyController { + Record memory record = idToRecord[_id]; + require(record.status == Status.PENDING, "Invalid state"); + require(refundAmt <= record.value, "Refund exceeds received value"); + + record.status = Status.SUCCESS; + idToRecord[_id] = record; + _transferFunds(record.initiator, record.paymentToken, record.value); + lockedFunds[record.paymentToken] -= record.value; + + emit ResultInfo(_id, true, refundAmt); + } + + function failure(uint256 _id) external { + Record memory record = idToRecord[_id]; + require(record.status == Status.PENDING, "Invalid state"); + require(controllers[msg.sender] || record.ttl < block.timestamp, "Only controller can respond till TTL"); + + record.status = Status.FAILURE; + idToRecord[_id] = record; + _transferFunds(record.initiator, record.paymentToken, record.value); + lockedFunds[record.paymentToken] -= record.value; + + emit ResultInfo(_id, false, record.value); + } + + function withdrawFunds(address beneficiary, address paymentToken, uint256 value) external onlyOwner { + uint256 maxWithdrawableBalance = _contractBalance(paymentToken) - lockedFunds[paymentToken]; + require(value <= maxWithdrawableBalance, "Insufficient Balance"); + _transferFunds(beneficiary, paymentToken, value); + } + + function _register( + string calldata name, + string calldata recipient, + uint8 yearsToRegister, + string[2][] calldata metadata, + address paymentToken, + uint256 value + ) private returns (uint256 _id) { + _id = id++; + uint256 ttl = block.timestamp + holdPeriod; + + lockedFunds[paymentToken] += msg.value; + idToRecord[_id] = Record(msg.sender, paymentToken, value, ttl, Status.PENDING); + + emit InitiateRequest(_id, name, recipient, yearsToRegister, metadata, paymentToken, value, ttl); + } + + function _contractBalance(address paymentToken) private view returns (uint256) { + if (paymentToken == address(0)) return address(this).balance; + return IERC20(paymentToken).balanceOf(address(this)); + } + + function _transferFunds(address beneficiary, address paymentToken, uint256 value) private { + if (paymentToken == address(0)) { + payable(beneficiary).transfer(value); + } else { + require(IERC20(paymentToken).transfer(beneficiary, value), "erc20: transfer failed"); + } + } + + function _collectPayment(address from, address paymentToken, uint256 value) private { + require(whitelistedTokens[paymentToken], "given token not accepted"); + require(IERC20(paymentToken).transferFrom(from, address(this), value), "erc20: payment failed"); + } +} diff --git a/packages/contracts/contracts/relayer/wasm/.gitignore b/packages/contracts/contracts/relayer/wasm/.gitignore new file mode 100755 index 0000000..8de8f87 --- /dev/null +++ b/packages/contracts/contracts/relayer/wasm/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/packages/contracts/contracts/relayer/wasm/Cargo.toml b/packages/contracts/contracts/relayer/wasm/Cargo.toml new file mode 100755 index 0000000..1562a19 --- /dev/null +++ b/packages/contracts/contracts/relayer/wasm/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "wasm" +version = "0.1.0" +authors = ["[your_name] <[your_email]>"] +edition = "2021" + +[dependencies] +ink = { version = "5.0.0", default-features = false } +zink = { git = "https://github.com/scio-labs/zink" } + +[dev-dependencies] +ink_e2e = { version = "5.0.0" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/packages/contracts/contracts/relayer/wasm/lib.rs b/packages/contracts/contracts/relayer/wasm/lib.rs new file mode 100644 index 0000000..1c100e9 --- /dev/null +++ b/packages/contracts/contracts/relayer/wasm/lib.rs @@ -0,0 +1,279 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[zink::coating(Ownable2Step[ + Error = Error::NotAdmin +])] +#[ink::contract] +mod registration_proxy { + use ink::env::call::{build_call, ExecutionInput, Selector}; + use ink::prelude::string::String; + use ink::prelude::vec::Vec; + use ink::storage::Mapping; + + #[ink(event)] + pub struct Success { + id: u128, + price: Balance, + } + + #[derive(Debug)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum InnerResult { + Pass(Balance), + Fail(Error), + } + + #[ink(storage)] + pub struct RegistrationProxy { + admin: AccountId, + /// Two-step ownership transfer AccountId + pending_admin: Option, + registry_addr: AccountId, + controllers: Mapping, + used_ids: Mapping, + } + + #[derive(Debug)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum Error { + /// Caller not allowed to call privileged calls. + NotAdmin, + /// Caller is not approved as controller + NotController, + /// Insufficient balance in the contract + InsufficientBalance, + /// Price exceeds the mentioned cap + TooExpensive, + /// Given Id is already used + DuplicateId, + /// Failed to register + RegisterFailed(u8), + /// Failed to register with Id not marked + RegisterFailedWithRevert(u8), + /// Unable to retrieve the price + PriceFetchFailed(u8), + } + + impl RegistrationProxy { + #[ink(constructor, payable)] + pub fn new(admin: AccountId, registry_addr: AccountId) -> Self { + let mut contract = Self { + admin, + pending_admin: None, + registry_addr, + controllers: Mapping::default(), + used_ids: Mapping::default(), + }; + + contract.controllers.insert(admin, &()); + contract + } + + #[ink(message, payable)] + pub fn fund_me(&mut self) -> Result<(), Error> { + Ok(()) + } + + #[ink(message)] + pub fn withdraw_funds( + &mut self, + account: AccountId, + balance: Balance, + ) -> Result<(), Error> { + self.ensure_admin()?; + self.env() + .transfer(account, balance) + .map_err(|_| Error::InsufficientBalance) + } + + #[ink(message)] + pub fn register( + &mut self, + id: u128, + name: String, + recipient: AccountId, + years_to_register: u8, + records: Vec<(String, String)>, + max_fees: Balance, + ) -> Result { + self.ensure_controller()?; + self.ensure_unqiue_id(id)?; + + let price = match self.get_name_price(&name, recipient, years_to_register) { + Ok(price) => price, + Err(err) => return Ok(InnerResult::Fail(err)), + }; + if price > max_fees { + return Ok(InnerResult::Fail(Error::TooExpensive)); + } else if price > self.env().balance() { + return Ok(InnerResult::Fail(Error::InsufficientBalance)); + } + + // register with custody + let result = self.do_register(&name, years_to_register, price); + if let Err(e) = result { + return Ok(InnerResult::Fail(Error::RegisterFailed(e))); + } + + // set metadata and then transfer name ownership + // TODO: evaluate if this action can ever fail (leaving behind unmarked id) + // TODO: if yes, then explore attack vector(s) (result: free domain) + self.do_set_metadata(&name, records) + .map_err(Error::RegisterFailedWithRevert)?; + self.do_transfer_name(&name, &recipient) + .map_err(Error::RegisterFailedWithRevert)?; + + self.env().emit_event(Success { id, price }); + + Ok(InnerResult::Pass(price)) + } + + #[ink(message)] + pub fn set_controller(&mut self, controller: AccountId, enable: bool) -> Result<(), Error> { + self.ensure_admin()?; + + if enable { + self.controllers.insert(controller, &()); + } else { + self.controllers.remove(controller); + } + + Ok(()) + } + + #[ink(message)] + pub fn is_controller(&self, controller: AccountId) -> bool { + self.controllers.contains(controller) + } + + #[ink(message)] + pub fn is_used_id(&self, id: u128) -> bool { + self.used_ids.contains(id) + } + + #[ink(message)] + pub fn upgrade_contract(&mut self, code_hash: Hash) { + self.ensure_admin().expect("Not Authorised"); + + self.env().set_code_hash(&code_hash).unwrap_or_else(|err| { + panic!( + "Failed to `set_code_hash` to {:?} due to {:?}", + code_hash, err + ) + }); + ink::env::debug_println!("Switched code hash to {:?}.", code_hash); + } + + fn do_register(&self, name: &str, years_to_register: u8, price: u128) -> Result<(), u8> { + const REGISTER_SELECTOR: [u8; 4] = ink::selector_bytes!("register_v2"); + + let referrer: Option = None; + let bonus_name: Option = None; + let set_as_primary_name = false; + + build_call::() + .call(self.registry_addr) + .call_v1() + .exec_input( + ExecutionInput::new(Selector::new(REGISTER_SELECTOR)) + .push_arg(name) + .push_arg(years_to_register) + .push_arg(referrer) + .push_arg(bonus_name) + .push_arg(set_as_primary_name), + ) + .returns::>() + .transferred_value(price) + .params() + .invoke() + } + + fn do_set_metadata(&self, name: &str, records: Vec<(String, String)>) -> Result<(), u8> { + const UPDATE_RECORDS_SELECTOR: [u8; 4] = ink::selector_bytes!("update_records"); + + let records: Vec<(String, Option)> = + records.into_iter().map(|(k, v)| (k, Some(v))).collect(); + let remove_rest = true; + + build_call::() + .call(self.registry_addr) + .call_v1() + .exec_input( + ExecutionInput::new(Selector::new(UPDATE_RECORDS_SELECTOR)) + .push_arg(name) + .push_arg(records) + .push_arg(remove_rest), + ) + .returns::>() + .params() + .invoke() + } + + fn do_transfer_name(&self, name: &str, to: &AccountId) -> Result<(), u8> { + const TRANSFER_SELECTOR: [u8; 4] = ink::selector_bytes!("transfer"); + + let keep_records = true; + let keep_controller = false; + let keep_resolving = false; + let data: Vec = Vec::new(); + + build_call::() + .call(self.registry_addr) + .call_v1() + .exec_input( + ExecutionInput::new(Selector::new(TRANSFER_SELECTOR)) + .push_arg(to) + .push_arg(name) + .push_arg(keep_records) + .push_arg(keep_controller) + .push_arg(keep_resolving) + .push_arg(data), + ) + .returns::>() + .params() + .invoke() + } + + fn get_name_price( + &self, + name: &str, + recipient: AccountId, + years_to_register: u8, + ) -> Result { + const GET_NAME_PRICE_SELECTOR: [u8; 4] = ink::selector_bytes!("get_name_price"); + + let result = build_call::() + .call(self.registry_addr) + .call_v1() + .exec_input( + ExecutionInput::new(Selector::new(GET_NAME_PRICE_SELECTOR)) + .push_arg(name) + .push_arg(recipient) + .push_arg(years_to_register) + .push_arg::>(None), + ) + .returns::), u16>>() + .params() + .invoke(); + + let (base_price, premium, _, _) = + result.map_err(|e| Error::PriceFetchFailed((e >> 8) as u8))?; + let price = base_price.checked_add(premium).expect("Overflow"); + + Ok(price) + } + + fn ensure_controller(&self) -> Result<(), Error> { + let caller = self.env().caller(); + self.controllers.get(caller).ok_or(Error::NotController) + } + + fn ensure_unqiue_id(&mut self, id: u128) -> Result<(), Error> { + if self.used_ids.contains(id) { + return Err(Error::DuplicateId); + } + self.used_ids.insert(id, &()); + Ok(()) + } + } +} diff --git a/packages/contracts/deploy.sh b/packages/contracts/deploy.sh index 08277a6..d6c4f9f 100755 --- a/packages/contracts/deploy.sh +++ b/packages/contracts/deploy.sh @@ -7,5 +7,16 @@ source .env # Clean bun run clean +echo "Deploying GATEWAY contracts..." # Deploy & Verify OffchainResolver -bunx hardhat --network $NETWORK deploy --tags gateway --reset \ No newline at end of file +bunx hardhat --network $NETWORK deploy --tags gateway --reset + +echo "Deploying RELAYER contracts..." +# Deploy & Verify RegistrationProxy +bunx hardhat --network $NETWORK deploy --tags relayer --reset + +# Deploy wasm contract +bunx ts-node scripts/deploy_wasm.ts + +# Verify Manually +# bunx hardhat verify --network sepolia
--constructor-args .js \ No newline at end of file diff --git a/packages/contracts/deploy/20_registration_proxy.js b/packages/contracts/deploy/20_registration_proxy.js new file mode 100644 index 0000000..268d2fe --- /dev/null +++ b/packages/contracts/deploy/20_registration_proxy.js @@ -0,0 +1,27 @@ +const { ethers, run } = require('hardhat') + +module.exports = async ({ getNamedAccounts, deployments, network }) => { + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + if (!network.config.holdPeriod) { + throw "holdPeriod is missing on hardhat.config.js"; + } + + const args = [network.config.holdPeriod]; + console.log("Constructor arguments:", args); + + console.log("Deploying RegistrationProxy"); + const { address } = await deploy("RegistrationProxy", { + from: deployer, + args, + log: true, + }); + + console.log('Verifying contract…') + await run('verify:verify', { + address, + constructorArguments: args, + }) +} +module.exports.tags = ['relayer'] diff --git a/packages/contracts/hardhat.config.js b/packages/contracts/hardhat.config.js index cbccf2a..97d1283 100644 --- a/packages/contracts/hardhat.config.js +++ b/packages/contracts/hardhat.config.js @@ -11,6 +11,7 @@ if (process.env.DEPLOYER_KEY) { real_accounts = [process.env.DEPLOYER_KEY]; } const gatewayurl = process.env.REMOTE_GATEWAY || "http://localhost:8080/"; +const holdPeriod = process.env.HOLD_PERIOD || "3600"; // default: 1hr (3600 seconds) /** * @type import('hardhat/config').HardhatUserConfig @@ -35,6 +36,7 @@ module.exports = { hardhat: { throwOnCallFailures: false, gatewayurl, + holdPeriod, }, sepolia: { url: `https://sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`, @@ -42,6 +44,7 @@ module.exports = { chainId: 11155111, accounts: real_accounts, gatewayurl, + holdPeriod, }, mainnet: { url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, @@ -49,6 +52,7 @@ module.exports = { chainId: 1, accounts: real_accounts, gatewayurl, + holdPeriod, }, }, etherscan: { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 31a63dc..2330ff3 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,11 +1,11 @@ { - "name": "@scio-labs/azero-offchain-resolver-contracts", - "version": "0.0.1", + "name": "@scio-labs/azero-offchain-contracts", + "version": "0.2.0", "repository": "git@github.com:scio-labs/offchain-resolver-ts.git", "author": "Scio Labs (https://scio.xyz)", "license": "MIT", "files": [ - "contracts/*.sol", + "contracts/**/*.sol", "artifacts/contracts/**/*.json" ], "scripts": { @@ -29,6 +29,7 @@ "hardhat-deploy-ethers": "^0.4.2" }, "dependencies": { - "@ensdomains/ens-contracts": "^1.2.2" + "@ensdomains/ens-contracts": "^1.2.2", + "@polkadot/keyring": "^13.2.2" } } diff --git a/packages/contracts/scripts/deploy_wasm.ts b/packages/contracts/scripts/deploy_wasm.ts new file mode 100644 index 0000000..04cdcc2 --- /dev/null +++ b/packages/contracts/scripts/deploy_wasm.ts @@ -0,0 +1,88 @@ +import { Keyring } from '@polkadot/keyring' +import { LegacyClient, WsProvider } from 'dedot' +import { ContractDeployer, ContractMetadata } from 'dedot/contracts' +import { stringToHex } from 'dedot/utils' +import dotenv from 'dotenv' +import { readFile } from 'fs/promises' + +dotenv.config(); + +async function main() { + // Destructure environment variables + const { + WASM_PRIVATE_KEY, + RPC_URL, + WASM_PATH, + ABI_PATH, + ADMIN, + REGISTRY_ADDR, + PRE_FUND_AMOUNT, + } = process.env + if ( + !WASM_PRIVATE_KEY || + !RPC_URL || + !WASM_PATH || + !ABI_PATH || + !ADMIN || + !REGISTRY_ADDR || + !PRE_FUND_AMOUNT + ) { + throw new Error('Missing environment variables') + } + + // instanciate an api client + const provider = new WsProvider(RPC_URL) + const client = await LegacyClient.new({ + provider, + cacheMetadata: false, + }) + + // create a ContractDeployer instance + const wasm = await readFile(WASM_PATH) + const abi = JSON.parse(await readFile(ABI_PATH,'utf-8')) + const deployer = new ContractDeployer( + client, + abi as ContractMetadata, + wasm as unknown as string + ) + + // Dry run the constructor call for validation and gas estimation + const signer = new Keyring().createFromUri(WASM_PRIVATE_KEY) + const value = BigInt(PRE_FUND_AMOUNT) + const salt = stringToHex(""+Math.random()) + const { raw } = await deployer.query.new(ADMIN, REGISTRY_ADDR, { caller: signer.address, salt, value }) + + // Submitting the transaction to instanciate the contract + let contractAddress: string + await deployer.tx.new(ADMIN, REGISTRY_ADDR, { gasLimit: raw.gasRequired, salt, value }) + .signAndSend(signer, ({ status, events}) => { + if (status.type === 'Finalized') { + const instantiatedEvent = client.events.contracts.Instantiated.find(events) + contractAddress = instantiatedEvent.palletEvent.data.contract.address() + } + }); + + const waitForResponse = (): Promise => { + return new Promise((resolve) => { + const interval = setInterval(() => { + if (contractAddress !== undefined) { + clearInterval(interval) + resolve(contractAddress) + } + }, 1000) + }) + } + + return waitForResponse() +} + +main() + .then((contractAddress) => { + console.log("Deployed address for wasm contract:", contractAddress) + }) + .catch((error) => { + console.error(error) + process.exit(1) + }) + .finally(() => process.exit(0)) + \ No newline at end of file diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 0000000..e55a4ed --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "resolveJsonModule": true, + }, +} \ No newline at end of file diff --git a/packages/gateway/.dev.vars.example b/packages/gateway/.dev.vars.example deleted file mode 100644 index 14d9133..0000000 --- a/packages/gateway/.dev.vars.example +++ /dev/null @@ -1 +0,0 @@ -OG_PRIVATE_KEY="TODO" \ No newline at end of file diff --git a/packages/gateway/README.md b/packages/gateway/README.md deleted file mode 100644 index 1b5a597..0000000 --- a/packages/gateway/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# ENS Offchain Gateway for Aleph Zero – Gateway Server (Worker) - -> **See [README.md](../../README.md) for more information.** - -## Getting Started - -### Development - -```bash -# Install dependencies -bun install - -# Create `.dev.vars` & Set your private key -cp .dev.vars.example .dev.vars - -# Edit `wrangler.toml` if needed - -# Run development server -bun run dev - -# Build for production -bun run build -``` - -### Deployment - -```bash -# Put your private key in the Cloudflare secrets manager -bunx wrangler secret put OG_PRIVATE_KEY --env - -# Deploy the worker -bunx wrangler deploy --env -``` diff --git a/packages/gateway/src/azero-id-resolver.ts b/packages/gateway/src/azero-id-resolver.ts deleted file mode 100644 index d9b83d9..0000000 --- a/packages/gateway/src/azero-id-resolver.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { getCoderByCoinType } from '@ensdomains/address-encoder' -import { createDotAddressDecoder } from '@ensdomains/address-encoder/utils' -import { LegacyClient, WsProvider } from 'dedot' -import { Contract, ContractMetadata } from 'dedot/contracts' -import { toHex } from 'viem' -import { AznsRegistryContractApi } from '../types/azns-registry' -import contractMetadata from './metadata/azns-registry.json' -import { Database } from './server' - -const AZERO_COIN_TYPE = 643 -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -const EMPTY_CONTENT_HASH = '0x' - -export class AzeroIdResolver implements Database { - ttl: number - tldToContractAddress: Record - azeroRpcUrl: string - azeroClient: LegacyClient | undefined - _tldToContract = new Map>() - - constructor(ttl: number, azeroRpcUrl: string, tldToContractAddress: Record) { - this.ttl = ttl - this.azeroRpcUrl = azeroRpcUrl - this.tldToContractAddress = tldToContractAddress - } - - async getContract(tld: string) { - if (!this.tldToContractAddress[tld]) return null - if (this._tldToContract.has(tld)) return this._tldToContract.get(tld)! - - // Initialize Substrate API - if (!this.azeroClient) { - console.log('nere') - // TODO @Dennis Efficiently preload chain metadata - // const metadata = nodeMetadata as Record - const provider = new WsProvider(this.azeroRpcUrl) - this.azeroClient = await LegacyClient.new({ provider, cacheMetadata: false }) - } - - // Initialize Contract Instance - const defaultCaller = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' // Alice - const contract = new Contract( - this.azeroClient, - contractMetadata as ContractMetadata, - this.tldToContractAddress[tld], - { defaultCaller }, - ) - this._tldToContract.set(tld, contract) - - return contract - } - - async addr(name: string, coinType: number) { - coinType = Number(coinType) - - let value - if (coinType == AZERO_COIN_TYPE) { - value = await this.fetchA0ResolverAddress(name) - } else { - let alias = AzeroIdResolver.getAlias('' + coinType) - if (alias !== undefined) { - const serviceKey = 'address.' + alias - value = await this.fetchRecord(name, serviceKey) - } - if (value === undefined) { - const serviceKey = 'address.' + coinType - value = await this.fetchRecord(name, serviceKey) - } - } - - if (value === undefined) { - value = coinType == 60 ? ZERO_ADDRESS : '0x' - } else { - value = AzeroIdResolver.encodeAddress(value, coinType) - } - - return { addr: value, ttl: this.ttl } - } - - async text(name: string, key: string) { - const value = (await this.fetchRecord(name, key)) || '' - return { value, ttl: this.ttl } - } - - contenthash(name: string) { - return { contenthash: EMPTY_CONTENT_HASH, ttl: this.ttl } - } - - private async fetchRecord(domain: string, key: string) { - let { name, contract } = await this.processName(domain) - - const { data } = await contract.query.getRecord(name, key, {}) - if (!data.isOk) { - throw new Error(`Failed to fetch record: ${data.err}`) - } - - return data.value - } - - private async fetchA0ResolverAddress(domain: string) { - let { name, contract } = await this.processName(domain) - - const { data } = await contract.query.getAddress(name, {}) - if (!data.isOk) { - throw new Error(`Failed to fetch resolver address: ${data.err}`) - } - - return data.value.address() - } - - private async processName(domain: string) { - const labels = domain.split('.') - - const name = labels.shift() || '' - const tld = labels.join('.') - - const contract = await this.getContract(tld) - if (!contract) { - throw new Error(`TLD (.${tld}) not supported`) - } - - return { name, contract } - } - - static getAlias(coinType: string) { - const alias = new Map([ - ['0', 'btc'], - ['60', 'eth'], - ['354', 'dot'], - ['434', 'ksm'], - ['501', 'sol'], - ]) - - return alias.get(coinType) - } - - static encodeAddress(addr: string, coinType: number) { - const isEvmCoinType = (c: number) => { - return c == 60 || (c & 0x80000000) != 0 - } - - if (coinType == AZERO_COIN_TYPE) { - const azeroCoder = createDotAddressDecoder(42) - return toHex(azeroCoder(addr)) - } - if (isEvmCoinType(coinType) && !addr.startsWith('0x')) { - addr = '0x' + addr - } - - try { - const coder = getCoderByCoinType(coinType) - return toHex(coder.decode(addr)) - } catch { - return addr - } - } -} diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts deleted file mode 100644 index 23a73e3..0000000 --- a/packages/gateway/src/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ethers } from 'ethers' -import { AutoRouter, AutoRouterType } from 'itty-router' -import { privateKeyToAccount } from 'viem/accounts' -import { AzeroIdResolver } from './azero-id-resolver' -import { makeServer } from './server' - -let router: AutoRouterType | undefined -function initRouter(env: any) { - console.log('Initializing…') - - // Destructure environment variables - const { OG_PRIVATE_KEY, SUPPORTED_TLDS, OG_TTL, AZERO_RPC_URL } = env - if (!Object.keys(SUPPORTED_TLDS || {}).length || !OG_PRIVATE_KEY || !OG_TTL || !AZERO_RPC_URL) { - throw new Error('Missing environment variables') - } - - // Initialize the Database-like Resolver - const db = new AzeroIdResolver(parseInt(OG_TTL), AZERO_RPC_URL, SUPPORTED_TLDS) - - // Initialize the CCIP-Read Handler - const signer = new ethers.utils.SigningKey(OG_PRIVATE_KEY) - const gateway = makeServer(signer, db) - - // Setup itty-router (used by `@ensdomains/ccip-read-cf-worker`) - const router = AutoRouter() - .get('/', () => new Response('AZERO.ID Gateway is running… 🌉', { status: 200 })) - .get(`/:sender/:callData.json`, gateway.handleRequest.bind(gateway)) - .post('/', gateway.handleRequest.bind(gateway)) - - const { address } = privateKeyToAccount(OG_PRIVATE_KEY) - console.log(`Initialized with signing address ${address}`) - - return router -} - -export default { - fetch(request: Request, env: any) { - try { - router = router || initRouter(env) - return router.fetch(request) - } catch (e) { - console.error(e) - return new Response('Internal server error', { status: 500 }) - } - }, -} diff --git a/packages/gateway/src/server.ts b/packages/gateway/src/server.ts deleted file mode 100644 index b9bed92..0000000 --- a/packages/gateway/src/server.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Server } from '@ensdomains/ccip-read-cf-worker' -import { abi as Resolver_abi } from '@ensdomains/ens-contracts/artifacts/contracts/resolvers/Resolver.sol/Resolver.json' -import { abi as IResolverService_abi } from '@ensdomains/offchain-resolver-contracts/artifacts/contracts/OffchainResolver.sol/IResolverService.json' -import { BytesLike, ethers } from 'ethers' -import { hexConcat, Result } from 'ethers/lib/utils' - -const ETH_COIN_TYPE = 60 - -const Resolver = new ethers.utils.Interface(Resolver_abi) - -interface DatabaseResult { - result: any[] - ttl: number -} - -type PromiseOrResult = T | Promise - -export interface Database { - addr(name: string, coinType: number): PromiseOrResult<{ addr: string; ttl: number }> - text(name: string, key: string): PromiseOrResult<{ value: string; ttl: number }> - contenthash(name: string): PromiseOrResult<{ contenthash: string; ttl: number }> -} - -function decodeDnsName(dnsname: Buffer) { - const labels = [] - let idx = 0 - while (true) { - const len = dnsname.readUInt8(idx) - if (len === 0) break - labels.push(dnsname.slice(idx + 1, idx + len + 1).toString('utf8')) - idx += len + 1 - } - return labels.join('.') -} - -const queryHandlers: { - [key: string]: (db: Database, name: string, args: Result) => Promise -} = { - 'addr(bytes32)': async (db, name, _args) => { - const { addr, ttl } = await db.addr(name, ETH_COIN_TYPE) - return { result: [addr], ttl } - }, - 'addr(bytes32,uint256)': async (db, name, args) => { - const { addr, ttl } = await db.addr(name, args[0]) - return { result: [addr], ttl } - }, - 'text(bytes32,string)': async (db, name, args) => { - const { value, ttl } = await db.text(name, args[0]) - return { result: [value], ttl } - }, - 'contenthash(bytes32)': async (db, name, _args) => { - const { contenthash, ttl } = await db.contenthash(name) - return { result: [contenthash], ttl } - }, -} - -async function query( - db: Database, - name: string, - data: string, -): Promise<{ result: BytesLike; validUntil: number }> { - // Parse the data nested inside the second argument to `resolve` - const { signature, args } = Resolver.parseTransaction({ data }) - - if (ethers.utils.nameprep(name) !== name) { - throw new Error('Name must be normalised') - } - - if (ethers.utils.namehash(name) !== args[0]) { - throw new Error('Name does not match namehash') - } - - const handler = queryHandlers[signature] - if (handler === undefined) { - throw new Error(`Unsupported query function ${signature}`) - } - - const { result, ttl } = await handler(db, name, args.slice(1)) - return { - result: Resolver.encodeFunctionResult(signature, result), - validUntil: Math.floor(Date.now() / 1000 + ttl), - } -} - -export function makeServer(signer: ethers.utils.SigningKey, db: Database) { - const server = new Server() - server.add(IResolverService_abi, [ - { - type: 'resolve', - func: async ([encodedName, data]: Result, request) => { - const name = decodeDnsName(Buffer.from(encodedName.slice(2), 'hex')) - // Query the database - const { result, validUntil } = await query(db, name, data) - - // Hash and sign the response - let messageHash = ethers.utils.solidityKeccak256( - ['bytes', 'address', 'uint64', 'bytes32', 'bytes32'], - [ - '0x1900', - request?.to, - validUntil, - ethers.utils.keccak256(request?.data || '0x'), - ethers.utils.keccak256(result), - ], - ) - const sig = signer.signDigest(messageHash) - const sigData = hexConcat([sig.r, sig.s, ethers.utils.hexlify(sig.v)]) - return [result, validUntil, sigData] - }, - }, - ]) - return server -} diff --git a/packages/gateway/wrangler.toml b/packages/gateway/wrangler.toml deleted file mode 100644 index 3b8b64f..0000000 --- a/packages/gateway/wrangler.toml +++ /dev/null @@ -1,49 +0,0 @@ -account_id = "702b4f69f0033f956c18b119f81ea1d6" - -compatibility_date = "2024-09-23" -compatibility_flags = ["nodejs_compat"] - -main = "./src/index.ts" - -workers_dev = false - -[observability] -enabled = true - -[placement] -mode = "smart" - -[dev] -port = 8080 - -[build] -command = "bun run build" - -# Testnet (Aleph Zero Testnet) - -[env.testnet] -name = "tzero-id-gateway" - -[env.testnet.route] -pattern = "tzero-id-gateway.nameverse.io" -custom_domain = true - -[env.testnet.vars] -OG_TTL = "60" -AZERO_RPC_URL = "wss://ws.test.azero.dev" -SUPPORTED_TLDS = { "tzero-id.eth" = "5FsB91tXSEuMj6akzdPczAtmBaVKToqHmtAwSUzXh49AYzaD", "tzero.eth" = "5FsB91tXSEuMj6akzdPczAtmBaVKToqHmtAwSUzXh49AYzaD" } - -# Mainnet (Aleph Zero Mainnet) - -[env.mainnet] -name = "azero-id-gateway" - -[env.mainnet.route] -pattern = "azero-id-gateway.nameverse.io" -custom_domain = true - -[env.mainnet.vars] -OG_TTL = "60" -AZERO_RPC_URL = "wss://ws.azero.dev" -SUPPORTED_TLDS = { "azero-id.eth" = "5CTQBfBC9SfdrCDBJdfLiyW2pg9z5W6C6Es8sK313BLnFgDf" } -# SUPPORTED_TLDS = { "azero-id.eth" = "5CTQBfBC9SfdrCDBJdfLiyW2pg9z5W6C6Es8sK313BLnFgDf", "azero.eth" = "5CTQBfBC9SfdrCDBJdfLiyW2pg9z5W6C6Es8sK313BLnFgDf" } diff --git a/packages/server/.dev.vars.example b/packages/server/.dev.vars.example new file mode 100644 index 0000000..7a8ccd3 --- /dev/null +++ b/packages/server/.dev.vars.example @@ -0,0 +1,4 @@ +OG_PRIVATE_KEY="TODO" +INFURA_API_KEY="TODO" +EVM_RELAYER_PRIVATE_KEY="TODO" +WASM_PRIVATE_KEY="TODO" \ No newline at end of file diff --git a/packages/gateway/.prettierignore b/packages/server/.prettierignore similarity index 100% rename from packages/gateway/.prettierignore rename to packages/server/.prettierignore diff --git a/packages/server/README.md b/packages/server/README.md new file mode 100644 index 0000000..8c0fb55 --- /dev/null +++ b/packages/server/README.md @@ -0,0 +1,47 @@ +# ENS Offchain Gateway & Registration Relayer for AZERO.ID + +> **See [README.md](../../README.md) for more information.** + +## Getting Started + +### API Info + +`/relay` endpoint expects following arguments: + +1. `txhash` (mandatory): The EVM transaction that contains the `InitiateRequest` event from our target contract. +2. `reqId` (optional): It is required when a given `txHash` contains more than one `InitiateRequest` event (can happen if someone creates a bulk-registration contract on top). + +### Development + +```bash +# Install dependencies +bun install + +# Create `.dev.vars` & Set your private key +cp .dev.vars.example .dev.vars + +# [Optional] Edit `wrangler.toml` if needed + +# [Optional] (Re)generate contract types from (Solidity & ink!) +bunx wagmi generate +bunx dedot typink -m /path/to/contract/metadata.json -o ./types/contract-name + +# Run development server +bun run dev + +# Build for production +bun run build +``` + +### Deployment + +```bash +# Put your private key in the Cloudflare secrets manager +bunx wrangler secret put OG_PRIVATE_KEY --env +bunx wrangler secret put INFURA_API_KEY --env +bunx wrangler secret put EVM_RELAYER_PRIVATE_KEY --env +bunx wrangler secret put WASM_PRIVATE_KEY --env + +# Deploy the worker +bunx wrangler deploy --env +``` diff --git a/packages/gateway/package.json b/packages/server/package.json similarity index 79% rename from packages/gateway/package.json rename to packages/server/package.json index d2632ee..18d46c1 100644 --- a/packages/gateway/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { - "name": "@scio-labs/azero-offchain-resolver-gateway", - "version": "0.0.1", + "name": "@scio-labs/azero-offchain-server", + "version": "0.2.0", "repository": "git@github.com:scio-labs/offchain-resolver-ts.git", "author": "Scio Labs (https://scio.xyz)", "license": "MIT", @@ -24,20 +24,19 @@ "clean": "rm -fr node_modules dist" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20241004.0", - "wrangler": "^3.80.4" + "@cloudflare/workers-types": "^4.20241022.0", + "wrangler": "^3.83.0", + "@wagmi/cli": "^2.1.16" }, "dependencies": { "@chainlink/ethers-ccip-read-provider": "^0.2.3", "@ensdomains/address-encoder": "^1.1.2", - "@polkadot/api": "^14.0.1", - "@polkadot/api-contract": "^14.0.1", - "dedot": "^0.6.0", - "viem": "^2.21.22", "@ensdomains/ccip-read-cf-worker": "^0.0.3", "@ensdomains/ens-contracts": "^1.2.2", "@ensdomains/offchain-resolver-contracts": "^0.2.1", + "dedot": "^0.6.0", "ethers": "^5.7.2", - "itty-router": "^5.0.18" + "itty-router": "^5.0.18", + "viem": "^2.21.34" } } diff --git a/packages/server/src/azero-id-relayer.ts b/packages/server/src/azero-id-relayer.ts new file mode 100644 index 0000000..2d8e8fb --- /dev/null +++ b/packages/server/src/azero-id-relayer.ts @@ -0,0 +1,390 @@ +import { KeyringPair } from '@polkadot/keyring/types' +import { LegacyClient, WsProvider } from 'dedot' +import { Contract, ContractMetadata } from 'dedot/contracts' +import { + BaseError, + type Chain, + ContractFunctionRevertedError, + createPublicClient, + createWalletClient, + http, + parseEventLogs, + PublicClient, + WaitForTransactionReceiptReturnType, + WalletClient, + zeroAddress +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { mainnet, sepolia } from 'viem/chains' +import { WasmContractApi } from '../types/wasm' +import { registrationProxyAbi } from '../wagmi.generated' +import wasmRelayerMetadata from './metadata/wasmRelayer.json' +import Logger from './utils/logger' + +const log = Logger.getInstance(); + +class AzeroIdRelayer { + private isPaused: boolean + private azeroRpcUrl: string + private evmRpcUrl: string + private evmChain: Chain + private evmRelayerAddress: `0x${string}` + private wasmRelayerAddress: string + private wasmSigner: KeyringPair + private evmSignerKey: `0x${string}` + private bufferDuration: number + private _azeroClient: LegacyClient | undefined + private _evmClient: PublicClient | undefined + private _evmWallet: WalletClient | undefined + private _wasmRelayerContract: Contract | undefined + + constructor( + azeroRpcUrl: string, + evmRpcUrl: string, + evmRelayerAddress: `0x${string}`, + wasmRelayerAddress: string, + wasmSigner: KeyringPair, + evmSignerKey: `0x${string}`, + bufferDurationInMinutes: number + ) { + this.isPaused = false + this.azeroRpcUrl = azeroRpcUrl + this.evmRpcUrl = evmRpcUrl + this.evmChain = evmRpcUrl.includes('mainnet') ? mainnet : sepolia + if (!evmRpcUrl.includes('mainnet') && !evmRpcUrl.includes('sepolia')) { + throw new Error('Invalid EVM RPC URL') + } + this.evmRelayerAddress = evmRelayerAddress + this.wasmRelayerAddress = wasmRelayerAddress + this.wasmSigner = wasmSigner + this.evmSignerKey = evmSignerKey + this.bufferDuration = bufferDurationInMinutes * 60 * 1000; // converted to milliseconds + } + + private async getAzeroClient() { + if (!this._azeroClient) { + const provider = new WsProvider(this.azeroRpcUrl) + this._azeroClient = await LegacyClient.new({ + provider, + cacheMetadata: false, + }) + } + return this._azeroClient + } + + private getEvmClient() { + if (!this._evmClient) { + const transport = http(this.evmRpcUrl) + this._evmClient = createPublicClient({ chain: this.evmChain, transport }) + } + return this._evmClient + } + + private getEvmWallet() { + if (!this._evmWallet) + this._evmWallet = createWalletClient({ + account: privateKeyToAccount(this.evmSignerKey), + chain: this.evmChain, + transport: http(this.evmRpcUrl), + }) + + return this._evmWallet + } + + private async getWasmRelayerContract() { + if (!this._wasmRelayerContract) { + this._wasmRelayerContract = new Contract( + await this.getAzeroClient(), + wasmRelayerMetadata as ContractMetadata, + this.wasmRelayerAddress + ) + } + return this._wasmRelayerContract + } + + async handleRequest(request: Request): Promise { + const evmClient = this.getEvmClient() + + const { txHash, reqId } = await request.json() + if (!txHash) return new Response('Bad Request', { status: 400 }) + + // Wait for the transaction receipt + let receipt: WaitForTransactionReceiptReturnType | undefined + try { + receipt = await evmClient.waitForTransactionReceipt({ + hash: txHash, + retryCount: 3, + retryDelay: 1000, + }) + } catch (error) { + log.error('Error waiting for transaction receipt:', error) + return new Response('Transaction not found', { status: 404 }) + } + + // Parse the events from the transaction receipt + const logs = parseEventLogs({ + logs: receipt.logs, + abi: registrationProxyAbi, + eventName: 'InitiateRequest' + }).filter(log => log.address === this.evmRelayerAddress.toLowerCase() && + (reqId === undefined) ? true : log.args.id == reqId) + + if (logs.length === 0) return new Response('No event found', { status: 404 }) + if (logs.length !== 1) return new Response(`Multiple events found; specify 'reqId'`, { status: 400 }) + + const { id, name, recipient, yearsToRegister, metadata, paymentToken, value, ttl } = logs[0].args + + return this.processRegistrationRequest( + id, + name, + recipient, + yearsToRegister, + metadata as unknown as Array<[string, string]>, + paymentToken, + value, + ttl + ) + } + + private async processRegistrationRequest( + id: bigint, + name: string, + recipient: string, + yearsToRegister: number, + metadata: Array<[string, string]>, + paymentToken: `0x${string}`, + value: bigint, + ttl: bigint + ): Promise { + log.info('New request:', id, name); + + if (this.isPaused) { + log.info(`(Request Id: ${id}) skipped; Not accepting any new requests`) + return new Response('Relayer is paused, Request skipped', { status: 503 }) + } else if (this.isTTLValid(Number(ttl))) { + return this.relayRequestToWasm( + id, + name, + recipient, + Number(yearsToRegister), + metadata, + paymentToken, + value + ); + } else { + // Ignore the request + log.info( + `(Request Id: ${id}) skipped as its expiry-time falls short` + ); + return new Response('TTL expired', { status: 500 }) + } + } + + private async relayRequestToWasm( + id: bigint, + name: string, + recipient: string, + yearsToRegister: number, + metadata: Array<[string, string]>, + paymentToken: `0x${string}`, + maxFeesInEVM: bigint + ): Promise { + const wasmRelayerContract = await this.getWasmRelayerContract() + const maxFeesInWASM = this.valueEVM2WASM(maxFeesInEVM, paymentToken) + + // first dry-run to save Tx that would fail + const { data, raw } = await wasmRelayerContract.query.register( + id, + name, + recipient, + yearsToRegister, + metadata, + maxFeesInWASM, + { + caller: this.wasmSigner.address + } + ) + + if (data.isErr) { + log.error(`(Request Id: ${id}) cannot make transaction due to error:`, data.err); + // relay failure status back to EVM + if (data.err.type === 'DuplicateId') return new Response('Duplicate request', { status: 500 }) + return this.failure(id) + } + + let response: Promise | undefined + + await wasmRelayerContract.tx.register( + id, + name, + recipient, + yearsToRegister, + metadata, + maxFeesInWASM, + { + gasLimit: raw.gasRequired + } + ).signAndSend(this.wasmSigner, ({ status, events }) => { + if (status.type === 'Finalized') { + const successEvent = wasmRelayerContract.events.Success.find(events) + + if (successEvent === undefined) { + // Failure + log.info(`(Request Id: ${id}) Failed to register`); + response = this.failure(id); + } else { + // Success + const priceInWASM = successEvent.data.price; + log.info(`(Request Id: ${id}) Registered successfully with price ${priceInWASM} (in Wasm)`); + try { + const refundInEVM = maxFeesInEVM - this.valueWASM2EVM(priceInWASM, paymentToken); + response = this.success(id, refundInEVM); + } catch (error: any) { + const errMsg = `ALERT: Success status could not be relayed back\nError log: ${error.message}` + log.error(`(Request Id: ${id})`, errMsg) + response = (async () => new Response(errMsg, { status: 500 }))() + } + } + } + }) + + const waitForResponse = (): Promise => { + return new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (response !== undefined) { + clearInterval(interval) + response.then(resolve).catch(reject) + } + }, 1000) + }) + } + + return waitForResponse() + } + + private async success(id: bigint, refundInEVM: bigint): Promise { + const evmClient = this.getEvmClient() + const evmWallet = this.getEvmWallet() + + let receipt + try { + const hash = await evmWallet.writeContract({ + address: this.evmRelayerAddress, + abi: registrationProxyAbi, + functionName: 'success', + args: [id, refundInEVM], + account: privateKeyToAccount(this.evmSignerKey), + chain: this.evmChain + }) + receipt = await evmClient.waitForTransactionReceipt({ hash }) + } catch (err) { + // ALERT: RELAYER FAILURE + this.isPaused = true + const errMsg = `ALERT: Success status could not be relayed back\nError log: ${err ?? ''}` + log.error(`(Request Id: ${id})`, errMsg) + return new Response(errMsg, { status: 500 }) + } + + const logs = parseEventLogs({ + logs: receipt.logs, + abi: registrationProxyAbi, + eventName: 'ResultInfo', + args: { + id, + success: true + } + }) + + const relaySuccess = logs.some((log) => { + return log.address === this.evmRelayerAddress.toLowerCase() + }) + + if (relaySuccess) { + log.info(`(Request Id: ${id}) Success status relayed back successfully`); + return new Response('Success', { status: 200 }) + } else { + // ALERT: RELAYER FAILURE + this.isPaused = true + const errMsg = `ALERT: success status could not be relayed back` + log.error(`(Request Id: ${id})`, errMsg) + return new Response(errMsg, { status: 500 }) + } + } + + private async failure(id: bigint): Promise { + const evmClient = this.getEvmClient() + const evmWallet = this.getEvmWallet() + + const request = await this._mockFailure(id) + if (request === undefined) return new Response(`Failure state couldn't be relayed back`, { status: 501 }) + const hash = await evmWallet.writeContract(request) + const receipt = await evmClient.waitForTransactionReceipt({ hash }) + + const logs = parseEventLogs({ + logs: receipt.logs, + abi: registrationProxyAbi, + eventName: 'ResultInfo', + args: { + id, + success: false + } + }) + + const relaySuccess = logs.some((log) => { + return log.address === this.evmRelayerAddress.toLowerCase() + }) + + if (relaySuccess) { + log.info(`(Request Id: ${id}) Failure status relayed back successfully`); + return new Response('Failure status relayed back successfully', { status: 500 }) + } else { + log.warn(`(Request Id: ${id}) Failure status was NOT relayed back`); + return new Response('Failure status was NOT relayed back', { status: 500 }) + } + } + + // AZERO decimals on EVM: 18 + // AZERO decimals on WASM: 12 + private valueEVM2WASM(valueInEVM: bigint, fromPaymentToken: `0x${string}`): bigint { + if (fromPaymentToken !== zeroAddress) throw new Error(`Token(${fromPaymentToken}) not supported`) + return valueInEVM / BigInt(1_000_000) + } + + private valueWASM2EVM(valueInWASM: bigint, toPaymentToken: `0x${string}`): bigint { + if (toPaymentToken !== zeroAddress) throw new Error(`Token(${toPaymentToken}) not supported`) + return valueInWASM * BigInt(1_000_000) + } + + /// @dev ttl is expected to be in seconds + isTTLValid(ttl: number) { + ttl = ttl * 1000; // convert seconds to milliseconds + const currentTimestamp = Date.now(); + return currentTimestamp + this.bufferDuration <= ttl; + } + + private async _mockFailure(id: bigint) { + const evmClient = this.getEvmClient() + + try { + const { request } = await evmClient.simulateContract({ + address: this.evmRelayerAddress, + abi: registrationProxyAbi, + functionName: 'failure', + args: [id], + account: privateKeyToAccount(this.evmSignerKey) + }) + return request + } catch (err) { + if (err instanceof BaseError) { + const revertError = err.walk(err => err instanceof ContractFunctionRevertedError) + if (revertError instanceof ContractFunctionRevertedError) { + log.error( + `(Request Id: ${id}) Failure state couldn't be relayed back with reason (${revertError.reason})` + ) + } + } + } + } +} + +export { AzeroIdRelayer } diff --git a/packages/server/src/azero-id-resolver.ts b/packages/server/src/azero-id-resolver.ts new file mode 100644 index 0000000..bf60967 --- /dev/null +++ b/packages/server/src/azero-id-resolver.ts @@ -0,0 +1,221 @@ +import { getCoderByCoinType } from '@ensdomains/address-encoder' +import { createDotAddressDecoder } from '@ensdomains/address-encoder/utils' +import { LegacyClient, WsProvider } from 'dedot' +import { Contract, ContractMetadata } from 'dedot/contracts' +import { toHex, zeroAddress } from 'viem' +import { AznsRegistryContractApi } from '../types/azns-registry' +import { makeDedotFakeClient } from './dedot-fake-client' +import contractMetadata from './metadata/azns-registry.json' +import { Database } from './server' +import Logger from './utils/logger' + +const log = Logger.getInstance() + +const AZERO_COIN_TYPE = 643 +const ALICE_SS58 = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' + +export class AzeroIdResolver implements Database { + private provider: WsProvider | undefined + private client: LegacyClient | undefined + private contractForAddress = new Map>() + + constructor( + private ttl: number, + private azeroRpcUrl: string, + private tldToContractAddress: Record, + ) {} + + private async getContract(tld: string) { + if (!this.tldToContractAddress[tld]) return null + const contractAddress = this.tldToContractAddress[tld] + + if (this.contractForAddress.has(contractAddress)) + return this.contractForAddress.get(contractAddress) + + // Initialize Substrate API + // if (!this.azeroClient) { + // log.debug(`[Resolver] Initializing Client for '${this.azeroRpcUrl}'`) + // const provider = new WsProvider(this.azeroRpcUrl) + // this.azeroClient = await LegacyClient.new({ + // provider, + // cacheMetadata: true, + // cacheStorage: new KVCacheStorage(this.kv), + // }) + // } else if (this.azeroClient.status !== 'connected') { + // await this.azeroClient.connect() + // } + + // Initialize optimized RPC client + if (!this.client || !this.provider) { + log.debug(`[Resolver] Initializing Fake Client for '${this.azeroRpcUrl}'`) + this.provider = new WsProvider(this.azeroRpcUrl) + this.client = await makeDedotFakeClient(this.provider) + } + + if (this.provider.status !== 'connected') { + await this.provider.connect() + } + + // Initialize Contract Instance + log.debug(`[Resolver] Initializing Contract Instance for '${contractAddress}'`) + const contract = new Contract( + this.client, + contractMetadata as ContractMetadata, + contractAddress, + { defaultCaller: ALICE_SS58 }, + ) + this.contractForAddress.set(contractAddress, contract) + + return contract + } + + async addr(name: string, coinType: number) { + log.debug("[Resolver] Called 'addr':", name, coinType) + coinType = Number(coinType) + + let value + if (coinType == AZERO_COIN_TYPE) { + value = await this.fetchA0ResolverAddress(name) + } else { + let alias = AzeroIdResolver.getAlias('' + coinType) + if (alias !== undefined) { + const serviceKey = 'address.' + alias + value = await this.fetchRecord(name, serviceKey) + } + if (value === undefined) { + const serviceKey = 'address.' + coinType + value = await this.fetchRecord(name, serviceKey) + } + } + + if (value === undefined) { + value = coinType == 60 ? zeroAddress : '0x' + } else { + value = AzeroIdResolver.encodeAddress(value, coinType) + } + + log.debug("[Resolver] Returning 'addr':", value) + return { addr: value, ttl: this.ttl } + } + + async text(domain: string, key: string) { + log.debug("[Resolver] Called 'text':", domain, key) + + let value = '' + + // Static root domain values + const isRootDomain = Object.keys(this.tldToContractAddress).includes(domain) + if (isRootDomain) { + if (key === 'avatar') { + value = 'https://azero.id/og/logo.png' + } else if (key === 'description') { + value = `This is an official AZERO.ID root domain. Resolve your domain on ENS like ".${domain}". Powered by our Aleph Zero CCIP-Read Gateway Service.` + } else if (key === 'url') { + value = `https://azero.id` + } else if (key === 'com.twitter') { + value = `@azero_id` + } + } else { + value = (await this.fetchRecord(domain, key)) || '' + } + + log.debug("[Resolver] Returning 'text':", value) + return { value, ttl: this.ttl } + } + + async contenthash(domain: string) { + log.debug("[Resolver] Called 'contenthash' (NOT SUPPORTED):", domain) + + return { contenthash: '0x', ttl: this.ttl } + } + + private async fetchRecord(domain: string, key: string) { + let { name, contract } = await this.processName(domain) + + const { data } = await contract.query.getRecord(name, key, {}) + + if (data.isErr && data.err.type === 'RecordNotFound') { + // Return certain fallback values + if (key === 'avatar') return 'https://azero.id/og/logo.png' + if (key === 'description') + return 'This domain is registered at AZERO.ID and resolves through ENS. Powered by our Aleph Zero CCIP-Read Gateway Service.' + + return undefined + } + if (!data.isOk) { + log.error(`[Resolver] Failed to fetch record with key '${key}' for '${domain}': ${data.err}`) + throw new Error(`Failed to fetch record: ${data.err}`) + } + + log.debug(`[Resolver] Fetched record with key '${key}' for '${domain}':`, data.value) + return data.value + } + + private async fetchA0ResolverAddress(domain: string) { + let { name, contract } = await this.processName(domain) + + const { data } = await contract.query.getAddress(name, {}) + if (!data.isOk) { + log.error(`[Resolver] Failed to fetch Aleph Zero address for '${domain}': ${data.err}`) + throw new Error(`Failed to fetch resolver address: ${data.err}`) + } + const address = data.value.address() + + log.debug(`[Resolver] Fetched Aleph Zero address for '${domain}':`, address) + return address + } + + private async processName(domain: string) { + const labels = domain.split('.') + const name = labels.shift() || '' + let tld = labels.join('.') + + // Assign TLD to root domains + const isRootDomain = Object.keys(this.tldToContractAddress).includes(domain) + if (isRootDomain) { + tld = domain + } + + const contract = await this.getContract(tld) + if (!contract) { + log.warn(`[Resolver] Requested TLD '.${tld}' is not supported`) + throw new Error(`Requested TLD '.${tld}' is not supported`) + } + + return { name, contract } + } + + static getAlias(coinType: string) { + // TODO @Dennis Consider switching to an 'address.' format + const alias = new Map([ + ['0', 'btc'], + ['60', 'eth'], + ['354', 'dot'], + ['434', 'ksm'], + ['501', 'sol'], + ]) + + return alias.get(coinType) + } + + static encodeAddress(addr: string, coinType: number) { + const isEvmCoinType = (c: number) => { + return c == 60 || (c & 0x80000000) != 0 + } + + if (coinType == AZERO_COIN_TYPE) { + const azeroCoder = createDotAddressDecoder(42) + return toHex(azeroCoder(addr)) + } + if (isEvmCoinType(coinType) && !addr.startsWith('0x')) { + addr = '0x' + addr + } + + try { + const coder = getCoderByCoinType(coinType) + return toHex(coder.decode(addr)) + } catch { + return addr + } + } +} diff --git a/packages/server/src/dedot-fake-client.ts b/packages/server/src/dedot-fake-client.ts new file mode 100644 index 0000000..e3cc123 --- /dev/null +++ b/packages/server/src/dedot-fake-client.ts @@ -0,0 +1,68 @@ +// Source: https://gist.github.com/sinzii/942f034dcb870c98fde7221c1d61e817 + +import { $, LegacyClient, WsProvider } from 'dedot' +import { SpWeightsWeightV2Weight } from 'dedot/chaintypes' +import { $AccountId32, $Bytes, $DispatchError, AccountId32Like, BytesLike } from 'dedot/codecs' +import { concatU8a, u8aToHex } from 'dedot/utils' + +const $Weight = $.Struct({ + refTime: $.compactU64, + proofSize: $.compactU64, +}) + +const CODECS = [$AccountId32, $AccountId32, $.u128, $.Option($Weight), $.Option($.u128), $Bytes] + +export async function makeDedotFakeClient(provider: WsProvider) { + const $ContractResult = $.Struct({ + gasConsumed: $Weight, + gasRequired: $Weight, + storageDeposit: $.Enum({ + Refund: $.u128, + Charge: $.u128, + }), + debugMessage: $.PrefixedHex, + result: $.Result( + $.Struct({ flags: $.Struct({ bits: $.u32 }), data: $.PrefixedHex }), + $DispatchError, + ), + events: $.Option($.RawHex), + }) + + const client = { + call: { + contractsApi: { + call: async ( + origin: AccountId32Like, + dest: AccountId32Like, + value: bigint, + gasLimit: SpWeightsWeightV2Weight | undefined, + storageDepositLimit: bigint | undefined, + inputData: BytesLike, + ) => { + const func = 'ContractsApi_call' + const params = [origin, dest, value, gasLimit, storageDepositLimit, inputData] + + const formattedInputs = params.map((param, index) => CODECS[index].tryEncode(param)) + const bytes = u8aToHex(concatU8a(...formattedInputs)) + + const result = await provider.send('state_call', [func, bytes]) + return $ContractResult.tryDecode(result) + }, + }, + }, + } as unknown as LegacyClient + + return client + + // https://github.com/scio-labs/inkathon/blob/052eb14208d77ed4f592e6310a3f4461872c7ea4/contracts/deployments/greeter/alephzero-testnet.ts#L1 + // const GREETER_CONTRACT_ADDRESS = '5CDia8Y46K7CbD2vLej2SjrvxpfcbrLVqK2He3pTJod2Eyik'; + + // const greeter = new Contract( + // fakeClient, greeterMetadata as any, GREETER_CONTRACT_ADDRESS, + // { defaultCaller: '5EeG3x2qiUMU8LkRz4WGyy9kFhLY3u1AQwZz9aidvis58jqj' } + // ); + // const { data: message } = await greeter.query.greet(); + // console.log(message); + + // await provider.disconnect(); +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..12514f3 --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,90 @@ +import { Keyring } from '@polkadot/keyring' +import { AutoRouter, cors, error } from 'itty-router' +import { privateKeyToAccount } from 'viem/accounts' +import { AzeroIdRelayer } from './azero-id-relayer' +import { AzeroIdResolver } from './azero-id-resolver' +import { makeServer } from './server' +import cachify from './utils/cahify' +import Logger from './utils/logger' + +const log = Logger.getInstance() + +function initRouter(env: any) { + log.debug('Initializing Router…') + + // Destructure environment variables + const { + OG_PRIVATE_KEY, + SUPPORTED_TLDS, + OG_TTL, + AZERO_RPC_URL, + EVM_RPC_BASE_URL, + INFURA_API_KEY, + EVM_RELAYER_CONTRACT, + WASM_RELAYER_CONTRACT, + WASM_PRIVATE_KEY, + EVM_RELAYER_PRIVATE_KEY, + BUFFER_DURATION_IN_MIN, + } = env + if ( + !Object.keys(SUPPORTED_TLDS || {}).length || + !OG_PRIVATE_KEY || + !OG_TTL || + !AZERO_RPC_URL || + !EVM_RPC_BASE_URL || + !INFURA_API_KEY || + !EVM_RELAYER_CONTRACT || + !WASM_RELAYER_CONTRACT || + !WASM_PRIVATE_KEY || + !EVM_RELAYER_PRIVATE_KEY || + !BUFFER_DURATION_IN_MIN + ) { + throw new Error('Missing environment variables') + } + + // Initialize the Resolver & Gateway + const resolver = new AzeroIdResolver(OG_TTL, AZERO_RPC_URL, SUPPORTED_TLDS) + const gateway = makeServer(OG_PRIVATE_KEY, resolver) + + // Initialize the Relayer + const evmRpcUrl = `${EVM_RPC_BASE_URL}/${INFURA_API_KEY}` + const wasmSigner = new Keyring().createFromUri(WASM_PRIVATE_KEY) + const relayer = new AzeroIdRelayer( + AZERO_RPC_URL, + evmRpcUrl, + EVM_RELAYER_CONTRACT, + WASM_RELAYER_CONTRACT, + wasmSigner, + EVM_RELAYER_PRIVATE_KEY, + BUFFER_DURATION_IN_MIN, + ) + + // Setup itty-router (used by `@ensdomains/ccip-read-cf-worker`) + const { preflight, corsify } = cors() + const router = AutoRouter({ + before: [preflight], + finally: [corsify, cachify(OG_TTL)], + }) + .get('/', () => new Response('AZERO.ID Gateway is running… 🌉', { status: 200 })) + // Gateway + .get(`/:sender/:callData.json`, gateway.handleRequest.bind(gateway)) + .post('/', gateway.handleRequest.bind(gateway)) + // Relayer + .post(`/relay`, relayer.handleRequest.bind(relayer)) + + const { address } = privateKeyToAccount(OG_PRIVATE_KEY) + const evmRelayerSigner = privateKeyToAccount(EVM_RELAYER_PRIVATE_KEY) + log.info(`Initialized Gateway & Relayer with signer '${address}'`) + log.info(`Initialized EVM relayer with Signing Address '${evmRelayerSigner.address}'`) + log.info(`Initialized Substrate relayer with Signing Address '${wasmSigner.address}'`) + + return router +} + +export default { + fetch(request: Request, env: any) { + log.setLogLevel(env.LOGLEVEL || 1) + const router = initRouter(env) + return router.fetch(request).catch(error) + }, +} diff --git a/packages/gateway/src/metadata/aleph-node.json b/packages/server/src/metadata/aleph-node.json similarity index 100% rename from packages/gateway/src/metadata/aleph-node.json rename to packages/server/src/metadata/aleph-node.json diff --git a/packages/gateway/src/metadata/azns-registry.json b/packages/server/src/metadata/azns-registry.json similarity index 100% rename from packages/gateway/src/metadata/azns-registry.json rename to packages/server/src/metadata/azns-registry.json diff --git a/packages/server/src/metadata/wasmRelayer.json b/packages/server/src/metadata/wasmRelayer.json new file mode 100644 index 0000000..f5e026d --- /dev/null +++ b/packages/server/src/metadata/wasmRelayer.json @@ -0,0 +1,1371 @@ +{ + "source": { + "hash": "0xfaaa69559a036ce61966c509e41edada7bf1bcbf7c4a73daa336fbc61d382b46", + "language": "ink! 5.0.0", + "compiler": "rustc 1.81.0", + "build_info": { + "build_mode": "Release", + "cargo_contract_version": "4.1.1", + "rust_toolchain": "stable-aarch64-apple-darwin", + "wasm_opt_settings": { + "keep_debug_symbols": false, + "optimization_passes": "Z" + } + } + }, + "contract": { + "name": "wasm", + "version": "0.1.0", + "authors": [ + "[your_name] <[your_email]>" + ] + }, + "image": null, + "spec": { + "constructors": [ + { + "args": [ + { + "label": "admin", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "label": "registry_addr", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [], + "label": "new", + "payable": true, + "returnType": { + "displayName": [ + "ink_primitives", + "ConstructorResult" + ], + "type": 14 + }, + "selector": "0x9bae9d5e" + } + ], + "docs": [], + "environment": { + "accountId": { + "displayName": [ + "AccountId" + ], + "type": 0 + }, + "balance": { + "displayName": [ + "Balance" + ], + "type": 9 + }, + "blockNumber": { + "displayName": [ + "BlockNumber" + ], + "type": 31 + }, + "chainExtension": { + "displayName": [ + "ChainExtension" + ], + "type": 32 + }, + "hash": { + "displayName": [ + "Hash" + ], + "type": 27 + }, + "maxEventTopics": 4, + "staticBufferSize": 16384, + "timestamp": { + "displayName": [ + "Timestamp" + ], + "type": 30 + } + }, + "events": [ + { + "args": [ + { + "docs": [], + "indexed": false, + "label": "id", + "type": { + "displayName": [ + "u128" + ], + "type": 9 + } + }, + { + "docs": [], + "indexed": false, + "label": "price", + "type": { + "displayName": [ + "Balance" + ], + "type": 9 + } + } + ], + "docs": [], + "label": "Success", + "module_path": "wasm::registration_proxy", + "signature_topic": "0x096bb2d50ce5aa43ab85a5681d5e8498e69e9fba99c0e016e6dfe3a2236e62ab" + } + ], + "lang_error": { + "displayName": [ + "ink", + "LangError" + ], + "type": 15 + }, + "messages": [ + { + "args": [], + "default": false, + "docs": [], + "label": "fund_me", + "mutates": true, + "payable": true, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 16 + }, + "selector": "0x0848523c" + }, + { + "args": [ + { + "label": "account", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "label": "balance", + "type": { + "displayName": [ + "Balance" + ], + "type": 9 + } + } + ], + "default": false, + "docs": [], + "label": "withdraw_funds", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 16 + }, + "selector": "0xe7cda623" + }, + { + "args": [ + { + "label": "id", + "type": { + "displayName": [ + "u128" + ], + "type": 9 + } + }, + { + "label": "name", + "type": { + "displayName": [ + "String" + ], + "type": 19 + } + }, + { + "label": "recipient", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "label": "years_to_register", + "type": { + "displayName": [ + "u8" + ], + "type": 2 + } + }, + { + "label": "records", + "type": { + "displayName": [ + "Vec" + ], + "type": 20 + } + }, + { + "label": "max_fees", + "type": { + "displayName": [ + "Balance" + ], + "type": 9 + } + } + ], + "default": false, + "docs": [], + "label": "register", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 22 + }, + "selector": "0x229b553f" + }, + { + "args": [ + { + "label": "controller", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "label": "enable", + "type": { + "displayName": [ + "bool" + ], + "type": 25 + } + } + ], + "default": false, + "docs": [], + "label": "set_controller", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 16 + }, + "selector": "0xc5e161ea" + }, + { + "args": [ + { + "label": "controller", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [], + "label": "is_controller", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 26 + }, + "selector": "0x493a701c" + }, + { + "args": [ + { + "label": "id", + "type": { + "displayName": [ + "u128" + ], + "type": 9 + } + } + ], + "default": false, + "docs": [], + "label": "is_used_id", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 26 + }, + "selector": "0x6adfbd21" + }, + { + "args": [ + { + "label": "code_hash", + "type": { + "displayName": [ + "Hash" + ], + "type": 27 + } + } + ], + "default": false, + "docs": [], + "label": "upgrade_contract", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 14 + }, + "selector": "0x1345543d" + }, + { + "args": [], + "default": false, + "docs": [], + "label": "get_admin", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 28 + }, + "selector": "0x57b8a8a7" + }, + { + "args": [], + "default": false, + "docs": [], + "label": "get_pending_admin", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 29 + }, + "selector": "0xbcd31d76" + }, + { + "args": [ + { + "label": "account", + "type": { + "displayName": [ + "Option" + ], + "type": 13 + } + } + ], + "default": false, + "docs": [], + "label": "transfer_ownership", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 16 + }, + "selector": "0x107e33ea" + }, + { + "args": [], + "default": false, + "docs": [], + "label": "accept_ownership", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 16 + }, + "selector": "0xb55be9f0" + } + ] + }, + "storage": { + "root": { + "layout": { + "struct": { + "fields": [ + { + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 0 + } + }, + "name": "admin" + }, + { + "layout": { + "enum": { + "dispatchKey": "0x00000000", + "name": "Option", + "variants": { + "0": { + "fields": [], + "name": "None" + }, + "1": { + "fields": [ + { + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 0 + } + }, + "name": "0" + } + ], + "name": "Some" + } + } + } + }, + "name": "pending_admin" + }, + { + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 0 + } + }, + "name": "registry_addr" + }, + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0x09621f32", + "ty": 3 + } + }, + "root_key": "0x09621f32", + "ty": 4 + } + }, + "name": "controllers" + }, + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0x4a930302", + "ty": 3 + } + }, + "root_key": "0x4a930302", + "ty": 8 + } + }, + "name": "used_ids" + } + ], + "name": "RegistrationProxy" + } + }, + "root_key": "0x00000000", + "ty": 12 + } + }, + "types": [ + { + "id": 0, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 1, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "AccountId" + ] + } + }, + { + "id": 1, + "type": { + "def": { + "array": { + "len": 32, + "type": 2 + } + } + } + }, + { + "id": 2, + "type": { + "def": { + "primitive": "u8" + } + } + }, + { + "id": 3, + "type": { + "def": { + "tuple": [] + } + } + }, + { + "id": 4, + "type": { + "def": { + "composite": {} + }, + "params": [ + { + "name": "K", + "type": 0 + }, + { + "name": "V", + "type": 3 + }, + { + "name": "KeyType", + "type": 5 + } + ], + "path": [ + "ink_storage", + "lazy", + "mapping", + "Mapping" + ] + } + }, + { + "id": 5, + "type": { + "def": { + "composite": {} + }, + "params": [ + { + "name": "L", + "type": 6 + }, + { + "name": "R", + "type": 7 + } + ], + "path": [ + "ink_storage_traits", + "impls", + "ResolverKey" + ] + } + }, + { + "id": 6, + "type": { + "def": { + "composite": {} + }, + "path": [ + "ink_storage_traits", + "impls", + "AutoKey" + ] + } + }, + { + "id": 7, + "type": { + "def": { + "composite": {} + }, + "params": [ + { + "name": "ParentKey", + "type": 3 + } + ], + "path": [ + "ink_storage_traits", + "impls", + "ManualKey" + ] + } + }, + { + "id": 8, + "type": { + "def": { + "composite": {} + }, + "params": [ + { + "name": "K", + "type": 9 + }, + { + "name": "V", + "type": 3 + }, + { + "name": "KeyType", + "type": 10 + } + ], + "path": [ + "ink_storage", + "lazy", + "mapping", + "Mapping" + ] + } + }, + { + "id": 9, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 10, + "type": { + "def": { + "composite": {} + }, + "params": [ + { + "name": "L", + "type": 6 + }, + { + "name": "R", + "type": 11 + } + ], + "path": [ + "ink_storage_traits", + "impls", + "ResolverKey" + ] + } + }, + { + "id": 11, + "type": { + "def": { + "composite": {} + }, + "params": [ + { + "name": "ParentKey", + "type": 3 + } + ], + "path": [ + "ink_storage_traits", + "impls", + "ManualKey" + ] + } + }, + { + "id": 12, + "type": { + "def": { + "composite": { + "fields": [ + { + "name": "admin", + "type": 0, + "typeName": ",>>::Type" + }, + { + "name": "pending_admin", + "type": 13, + "typeName": " as::ink::storage::traits::AutoStorableHint<::\nink::storage::traits::ManualKey<801277377u32, ()>,>>::Type" + }, + { + "name": "registry_addr", + "type": 0, + "typeName": ",>>::Type" + }, + { + "name": "controllers", + "type": 4, + "typeName": " as::ink::storage::traits::AutoStorableHint\n<::ink::storage::traits::ManualKey<840917513u32, ()>,>>::Type" + }, + { + "name": "used_ids", + "type": 8, + "typeName": " as::ink::storage::traits::AutoStorableHint<::\nink::storage::traits::ManualKey<33788746u32, ()>,>>::Type" + } + ] + } + }, + "path": [ + "wasm", + "registration_proxy", + "RegistrationProxy" + ] + } + }, + { + "id": 13, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 0, + "name": "None" + }, + { + "fields": [ + { + "type": 0 + } + ], + "index": 1, + "name": "Some" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + } + ], + "path": [ + "Option" + ] + } + }, + { + "id": 14, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 3 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 15 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 3 + }, + { + "name": "E", + "type": 15 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 15, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 1, + "name": "CouldNotReadInput" + } + ] + } + }, + "path": [ + "ink_primitives", + "LangError" + ] + } + }, + { + "id": 16, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 17 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 15 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 17 + }, + { + "name": "E", + "type": 15 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 17, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 3 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 18 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 3 + }, + { + "name": "E", + "type": 18 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 18, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 0, + "name": "NotAdmin" + }, + { + "index": 1, + "name": "NotController" + }, + { + "index": 2, + "name": "InsufficientBalance" + }, + { + "index": 3, + "name": "TooExpensive" + }, + { + "index": 4, + "name": "DuplicateId" + }, + { + "fields": [ + { + "type": 2, + "typeName": "u8" + } + ], + "index": 5, + "name": "RegisterFailed" + }, + { + "fields": [ + { + "type": 2, + "typeName": "u8" + } + ], + "index": 6, + "name": "RegisterFailedWithRevert" + }, + { + "fields": [ + { + "type": 2, + "typeName": "u8" + } + ], + "index": 7, + "name": "PriceFetchFailed" + } + ] + } + }, + "path": [ + "wasm", + "registration_proxy", + "Error" + ] + } + }, + { + "id": 19, + "type": { + "def": { + "primitive": "str" + } + } + }, + { + "id": 20, + "type": { + "def": { + "sequence": { + "type": 21 + } + } + } + }, + { + "id": 21, + "type": { + "def": { + "tuple": [ + 19, + 19 + ] + } + } + }, + { + "id": 22, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 23 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 15 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 23 + }, + { + "name": "E", + "type": 15 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 23, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 24 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 18 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 24 + }, + { + "name": "E", + "type": 18 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 24, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 9, + "typeName": "Balance" + } + ], + "index": 0, + "name": "Pass" + }, + { + "fields": [ + { + "type": 18, + "typeName": "Error" + } + ], + "index": 1, + "name": "Fail" + } + ] + } + }, + "path": [ + "wasm", + "registration_proxy", + "InnerResult" + ] + } + }, + { + "id": 25, + "type": { + "def": { + "primitive": "bool" + } + } + }, + { + "id": 26, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 25 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 15 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 25 + }, + { + "name": "E", + "type": 15 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 27, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 1, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "Hash" + ] + } + }, + { + "id": 28, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 0 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 15 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + }, + { + "name": "E", + "type": 15 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 29, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 13 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 15 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 13 + }, + { + "name": "E", + "type": 15 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 30, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 31, + "type": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 32, + "type": { + "def": { + "variant": {} + }, + "path": [ + "ink_env", + "types", + "NoChainExtension" + ] + } + } + ], + "version": 5 +} \ No newline at end of file diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts new file mode 100644 index 0000000..724eb37 --- /dev/null +++ b/packages/server/src/server.ts @@ -0,0 +1,140 @@ +import { Server } from '@ensdomains/ccip-read-cf-worker' +import { abi as IResolverService_abi } from '@ensdomains/offchain-resolver-contracts/artifacts/contracts/OffchainResolver.sol/IResolverService.json' +import { Result } from 'ethers/lib/utils' +import { + AbiFunction, + concat, + decodeAbiParameters, + encodeAbiParameters, + encodePacked, + Hex, + keccak256, + namehash, + parseAbiItem, + toFunctionHash, + toHex, +} from 'viem' +import { sign } from 'viem/accounts' +import { normalize } from 'viem/ens' + +const ETH_COIN_TYPE = 60 + +interface DatabaseResult { + result: any[] + ttl: number +} + +export interface Database { + addr(name: string, coinType: number): Promise<{ addr: string; ttl: number }> + text(name: string, key: string): Promise<{ value: string; ttl: number }> + contenthash(name: string): Promise<{ contenthash: string; ttl: number }> +} + +function decodeDnsName(dnsname: Buffer) { + const labels = [] + let idx = 0 + while (true) { + const len = dnsname.readUInt8(idx) + if (len === 0) break + labels.push(dnsname.slice(idx + 1, idx + len + 1).toString('utf8')) + idx += len + 1 + } + return labels.join('.') +} + +const queryHandlers: { + [key: string]: (db: Database, name: string, args: unknown[]) => Promise +} = { + 'function addr(bytes32 node) returns (address)': async (db, name, _) => { + const { addr, ttl } = await db.addr(name, ETH_COIN_TYPE) + return { result: [addr], ttl } + }, + 'function addr(bytes32,uint256) returns (bytes)': async (db, name, args) => { + const { addr, ttl } = await db.addr(name, args[0] as number) + return { result: [addr], ttl } + }, + 'function text(bytes32,string) returns (string)': async (db, name, args) => { + const { value, ttl } = await db.text(name, args[0] as string) + return { result: [value], ttl } + }, + 'function contenthash(bytes32) returns (bytes)': async (db, name, _args) => { + const { contenthash, ttl } = await db.contenthash(name) + return { result: [contenthash], ttl } + }, +} + +const querySelectors = Object.keys(queryHandlers).map((fn) => + toFunctionHash(fn).slice(0, 10).toLowerCase(), +) + +async function query( + db: Database, + name: string, + callData: Hex, +): Promise<{ result: Hex; validUntil: number }> { + // Determine handler + const selector = callData.slice(0, 10).toLowerCase() + const selectorIndex = querySelectors.indexOf(selector) + if (selectorIndex === -1) { + throw new Error(`Unsupported query selector ${selector}`) + } + + // Parse function arguments + const signature = Object.keys(queryHandlers)[selectorIndex] + const functionAbi = parseAbiItem(signature) as AbiFunction + const args = decodeAbiParameters(functionAbi.inputs, `0x${callData.slice(10)}`) + + // Sanity check domain + if (normalize(name) !== name) { + throw new Error('Name must be normalised') + } + if (namehash(name) !== args[0]) { + throw new Error('Name does not match namehash') + } + + // Execute handler and encode result + const handler = queryHandlers[signature] + const { result, ttl } = await handler(db, name, args.slice(1)) + const encodedResult = encodeAbiParameters(functionAbi.outputs, result) + + return { + result: encodedResult, + validUntil: Math.floor(Date.now() / 1000 + ttl), + } +} + +export function makeServer(privateKey: Hex, db: Database) { + const server = new Server() + server.add(IResolverService_abi, [ + { + type: 'resolve', + func: async ([encodedName, data]: Result, request) => { + const encodedNameBuffer = Buffer.from(encodedName.slice(2), 'hex') + const name = decodeDnsName(encodedNameBuffer) + + // Query the database (i.e. our Azero ID resolver) + const { result, validUntil } = await query(db, name, data) + + // Hash and sign the response + const messageHash = keccak256( + encodePacked( + ['bytes', 'address', 'uint64', 'bytes32', 'bytes32'], + [ + '0x1900', + request?.to as Hex, + BigInt(validUntil), + keccak256((request?.data as Hex) || '0x'), + keccak256(result as Hex), + ], + ), + ) + + const sig = await sign({ hash: messageHash, privateKey }) + const sigData = concat([sig.r, sig.s, toHex(sig.v!)]) + + return [result, validUntil, sigData] + }, + }, + ]) + return server +} diff --git a/packages/server/src/utils/cahify.ts b/packages/server/src/utils/cahify.ts new file mode 100644 index 0000000..2cbdaab --- /dev/null +++ b/packages/server/src/utils/cahify.ts @@ -0,0 +1,9 @@ +export default function cachify(ttl: number) { + return (res: Response) => { + res.headers.set( + 'Cache-Control', + `public, max-age=${ttl}, s-maxage=${ttl}, stale-while-revalidate=${ttl * 2}`, + ) + return res + } +} diff --git a/packages/server/src/utils/kv-cache.ts b/packages/server/src/utils/kv-cache.ts new file mode 100644 index 0000000..37be1f0 --- /dev/null +++ b/packages/server/src/utils/kv-cache.ts @@ -0,0 +1,35 @@ +import { KVNamespace } from '@cloudflare/workers-types' +import type { IStorage } from '@dedot/storage' + +export class KVCacheStorage implements IStorage { + constructor(private kv: KVNamespace) {} + + async get(key: string) { + return this.kv.get(key) + } + async set(key: string, value: string) { + await this.kv.put(key, value) + return value + } + async remove(key: string) { + return await this.kv.delete(key) + } + async keys() { + // HACK To speed up cache hits as dedot only checks if a key exists that starts with `RAW_META/`. + // See https://github.com/dedotdev/dedot/blob/80e64c7da483c4f5b757f6af7259aa4fdd5397fc/packages/api/src/client/BaseSubstrateClient.ts#L151-L153 + return ['RAW_META/'] + + // const { keys } = await this.kv.list() + // return keys.map((k) => k.name) + } + async clear() { + const keys = await this.keys() + for (const key of keys) { + await this.kv.delete(key) + } + } + async length() { + const keys = await this.keys() + return keys.length + } +} diff --git a/packages/server/src/utils/logger.ts b/packages/server/src/utils/logger.ts new file mode 100644 index 0000000..150b20e --- /dev/null +++ b/packages/server/src/utils/logger.ts @@ -0,0 +1,46 @@ +enum LogLevel { + SILENT = 0, + WARN = 1, + INFO = 2, + DEBUG = 3, +} + +class Logger { + private static instance: Logger + private currentLogLevel: LogLevel = LogLevel.INFO + + private constructor() {} + + public static getInstance(): Logger { + if (!Logger.instance) Logger.instance = new Logger() + return Logger.instance + } + + public setLogLevel(level: LogLevel): void { + this.currentLogLevel = level + } + + public debug(...args: any[]): void { + if (this.currentLogLevel >= LogLevel.DEBUG) { + console.log(...args) + } + } + + public info(...args: any[]): void { + if (this.currentLogLevel >= LogLevel.INFO) { + console.log(...args) + } + } + + public warn(...args: any[]): void { + if (this.currentLogLevel >= LogLevel.WARN) { + console.warn(...args) + } + } + + public error(...args: any[]): void { + console.error(...args) + } +} + +export default Logger diff --git a/packages/gateway/tsconfig.json b/packages/server/tsconfig.json similarity index 92% rename from packages/gateway/tsconfig.json rename to packages/server/tsconfig.json index 4157a49..ab64bd0 100644 --- a/packages/gateway/tsconfig.json +++ b/packages/server/tsconfig.json @@ -22,6 +22,6 @@ "noPropertyAccessFromIndexSignature": false, "outDir": "./dist", }, - "include": ["src","types"], + "include": ["src","types", "wagmi.generated.ts"], "exclude": ["node_modules", "dist"], } diff --git a/packages/gateway/types/aleph-node/consts.d.ts b/packages/server/types/aleph-node/consts.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/consts.d.ts rename to packages/server/types/aleph-node/consts.d.ts diff --git a/packages/gateway/types/aleph-node/errors.d.ts b/packages/server/types/aleph-node/errors.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/errors.d.ts rename to packages/server/types/aleph-node/errors.d.ts diff --git a/packages/gateway/types/aleph-node/events.d.ts b/packages/server/types/aleph-node/events.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/events.d.ts rename to packages/server/types/aleph-node/events.d.ts diff --git a/packages/gateway/types/aleph-node/index.d.ts b/packages/server/types/aleph-node/index.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/index.d.ts rename to packages/server/types/aleph-node/index.d.ts diff --git a/packages/gateway/types/aleph-node/json-rpc.d.ts b/packages/server/types/aleph-node/json-rpc.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/json-rpc.d.ts rename to packages/server/types/aleph-node/json-rpc.d.ts diff --git a/packages/gateway/types/aleph-node/query.d.ts b/packages/server/types/aleph-node/query.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/query.d.ts rename to packages/server/types/aleph-node/query.d.ts diff --git a/packages/gateway/types/aleph-node/runtime.d.ts b/packages/server/types/aleph-node/runtime.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/runtime.d.ts rename to packages/server/types/aleph-node/runtime.d.ts diff --git a/packages/gateway/types/aleph-node/tx.d.ts b/packages/server/types/aleph-node/tx.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/tx.d.ts rename to packages/server/types/aleph-node/tx.d.ts diff --git a/packages/gateway/types/aleph-node/types.d.ts b/packages/server/types/aleph-node/types.d.ts similarity index 100% rename from packages/gateway/types/aleph-node/types.d.ts rename to packages/server/types/aleph-node/types.d.ts diff --git a/packages/gateway/types/azns-registry/constructor-query.d.ts b/packages/server/types/azns-registry/constructor-query.d.ts similarity index 100% rename from packages/gateway/types/azns-registry/constructor-query.d.ts rename to packages/server/types/azns-registry/constructor-query.d.ts diff --git a/packages/gateway/types/azns-registry/constructor-tx.d.ts b/packages/server/types/azns-registry/constructor-tx.d.ts similarity index 100% rename from packages/gateway/types/azns-registry/constructor-tx.d.ts rename to packages/server/types/azns-registry/constructor-tx.d.ts diff --git a/packages/gateway/types/azns-registry/events.d.ts b/packages/server/types/azns-registry/events.d.ts similarity index 100% rename from packages/gateway/types/azns-registry/events.d.ts rename to packages/server/types/azns-registry/events.d.ts diff --git a/packages/gateway/types/azns-registry/index.d.ts b/packages/server/types/azns-registry/index.d.ts similarity index 100% rename from packages/gateway/types/azns-registry/index.d.ts rename to packages/server/types/azns-registry/index.d.ts diff --git a/packages/gateway/types/azns-registry/query.d.ts b/packages/server/types/azns-registry/query.d.ts similarity index 100% rename from packages/gateway/types/azns-registry/query.d.ts rename to packages/server/types/azns-registry/query.d.ts diff --git a/packages/gateway/types/azns-registry/tx.d.ts b/packages/server/types/azns-registry/tx.d.ts similarity index 100% rename from packages/gateway/types/azns-registry/tx.d.ts rename to packages/server/types/azns-registry/tx.d.ts diff --git a/packages/gateway/types/azns-registry/types.d.ts b/packages/server/types/azns-registry/types.d.ts similarity index 100% rename from packages/gateway/types/azns-registry/types.d.ts rename to packages/server/types/azns-registry/types.d.ts diff --git a/packages/server/types/wasm/constructor-query.d.ts b/packages/server/types/wasm/constructor-query.d.ts new file mode 100644 index 0000000..a25a5f6 --- /dev/null +++ b/packages/server/types/wasm/constructor-query.d.ts @@ -0,0 +1,32 @@ +// Generated by dedot cli + +import type { GenericSubstrateApi } from 'dedot/types' +import type { AccountId32Like, Result } from 'dedot/codecs' +import type { + GenericConstructorQuery, + GenericConstructorQueryCall, + GenericConstructorCallResult, + ConstructorCallOptions, + ContractInstantiateResult, +} from 'dedot/contracts' +import type { InkPrimitivesLangError } from './types' + +export interface ConstructorQuery + extends GenericConstructorQuery { + /** + * + * @param {AccountId32Like} admin + * @param {AccountId32Like} registryAddr + * @param {ConstructorCallOptions} options + * + * @selector 0x9bae9d5e + **/ + new: GenericConstructorQueryCall< + ChainApi, + ( + admin: AccountId32Like, + registryAddr: AccountId32Like, + options?: ConstructorCallOptions, + ) => Promise>> + > +} diff --git a/packages/server/types/wasm/constructor-tx.d.ts b/packages/server/types/wasm/constructor-tx.d.ts new file mode 100644 index 0000000..e60e031 --- /dev/null +++ b/packages/server/types/wasm/constructor-tx.d.ts @@ -0,0 +1,30 @@ +// Generated by dedot cli + +import type { GenericSubstrateApi } from 'dedot/types' +import type { AccountId32Like } from 'dedot/codecs' +import type { + GenericConstructorTx, + GenericConstructorTxCall, + ConstructorTxOptions, + GenericInstantiateSubmittableExtrinsic, +} from 'dedot/contracts' + +export interface ConstructorTx + extends GenericConstructorTx { + /** + * + * @param {AccountId32Like} admin + * @param {AccountId32Like} registryAddr + * @param {ConstructorTxOptions} options + * + * @selector 0x9bae9d5e + **/ + new: GenericConstructorTxCall< + ChainApi, + ( + admin: AccountId32Like, + registryAddr: AccountId32Like, + options: ConstructorTxOptions, + ) => GenericInstantiateSubmittableExtrinsic + > +} diff --git a/packages/server/types/wasm/events.d.ts b/packages/server/types/wasm/events.d.ts new file mode 100644 index 0000000..fd3c658 --- /dev/null +++ b/packages/server/types/wasm/events.d.ts @@ -0,0 +1,27 @@ +// Generated by dedot cli + +import type { GenericSubstrateApi } from 'dedot/types' +import type { GenericContractEvents, GenericContractEvent } from 'dedot/contracts' + +export interface ContractEvents + extends GenericContractEvents { + /** + * + * @signature_topic: 0x096bb2d50ce5aa43ab85a5681d5e8498e69e9fba99c0e016e6dfe3a2236e62ab + **/ + Success: GenericContractEvent< + 'Success', + { + /** + * + * @indexed: false + **/ + id: bigint + /** + * + * @indexed: false + **/ + price: bigint + } + > +} diff --git a/packages/server/types/wasm/index.d.ts b/packages/server/types/wasm/index.d.ts new file mode 100644 index 0000000..d46ba34 --- /dev/null +++ b/packages/server/types/wasm/index.d.ts @@ -0,0 +1,36 @@ +// Generated by dedot cli + +import type { VersionedGenericSubstrateApi, RpcVersion, RpcV2 } from 'dedot/types' +import type { GenericContractApi } from 'dedot/contracts' +import type { SubstrateApi } from 'dedot/chaintypes' +import type { InkPrimitivesLangError } from './types' +import { ContractQuery } from './query' +import { ContractTx } from './tx' +import { ConstructorQuery } from './constructor-query' +import { ConstructorTx } from './constructor-tx' +import { ContractEvents } from './events' + +export * from './types' + +/** + * @name: WasmContractApi + * @contractName: wasm + * @contractVersion: 0.1.0 + * @authors: [your_name] <[your_email]> + * @language: ink! 5.0.0 + **/ +export interface WasmContractApi< + Rv extends RpcVersion = RpcVersion, + ChainApi extends VersionedGenericSubstrateApi = SubstrateApi, +> extends GenericContractApi { + query: ContractQuery + tx: ContractTx + constructorQuery: ConstructorQuery + constructorTx: ConstructorTx + events: ContractEvents + + types: { + LangError: InkPrimitivesLangError + ChainApi: ChainApi[Rv] + } +} diff --git a/packages/server/types/wasm/query.d.ts b/packages/server/types/wasm/query.d.ts new file mode 100644 index 0000000..34d275a --- /dev/null +++ b/packages/server/types/wasm/query.d.ts @@ -0,0 +1,220 @@ +// Generated by dedot cli + +import type { GenericSubstrateApi } from 'dedot/types' +import type { Result, AccountId32Like, Hash, AccountId32 } from 'dedot/codecs' +import type { + GenericContractQuery, + GenericContractQueryCall, + ContractCallOptions, + GenericContractCallResult, + ContractCallResult, +} from 'dedot/contracts' +import type { + WasmRegistrationProxyError, + InkPrimitivesLangError, + WasmRegistrationProxyInnerResult, +} from './types' + +export interface ContractQuery + extends GenericContractQuery { + /** + * + * @param {ContractCallOptions} options + * + * @selector 0x0848523c + **/ + fundMe: GenericContractQueryCall< + ChainApi, + ( + options?: ContractCallOptions, + ) => Promise< + GenericContractCallResult< + Result<[], WasmRegistrationProxyError>, + ContractCallResult + > + > + > + + /** + * + * @param {AccountId32Like} account + * @param {bigint} balance + * @param {ContractCallOptions} options + * + * @selector 0xe7cda623 + **/ + withdrawFunds: GenericContractQueryCall< + ChainApi, + ( + account: AccountId32Like, + balance: bigint, + options?: ContractCallOptions, + ) => Promise< + GenericContractCallResult< + Result<[], WasmRegistrationProxyError>, + ContractCallResult + > + > + > + + /** + * + * @param {bigint} id + * @param {string} name + * @param {AccountId32Like} recipient + * @param {number} yearsToRegister + * @param {Array<[string, string]>} records + * @param {bigint} maxFees + * @param {ContractCallOptions} options + * + * @selector 0x229b553f + **/ + register: GenericContractQueryCall< + ChainApi, + ( + id: bigint, + name: string, + recipient: AccountId32Like, + yearsToRegister: number, + records: Array<[string, string]>, + maxFees: bigint, + options?: ContractCallOptions, + ) => Promise< + GenericContractCallResult< + Result, + ContractCallResult + > + > + > + + /** + * + * @param {AccountId32Like} controller + * @param {boolean} enable + * @param {ContractCallOptions} options + * + * @selector 0xc5e161ea + **/ + setController: GenericContractQueryCall< + ChainApi, + ( + controller: AccountId32Like, + enable: boolean, + options?: ContractCallOptions, + ) => Promise< + GenericContractCallResult< + Result<[], WasmRegistrationProxyError>, + ContractCallResult + > + > + > + + /** + * + * @param {AccountId32Like} controller + * @param {ContractCallOptions} options + * + * @selector 0x493a701c + **/ + isController: GenericContractQueryCall< + ChainApi, + ( + controller: AccountId32Like, + options?: ContractCallOptions, + ) => Promise>> + > + + /** + * + * @param {bigint} id + * @param {ContractCallOptions} options + * + * @selector 0x6adfbd21 + **/ + isUsedId: GenericContractQueryCall< + ChainApi, + ( + id: bigint, + options?: ContractCallOptions, + ) => Promise>> + > + + /** + * + * @param {Hash} codeHash + * @param {ContractCallOptions} options + * + * @selector 0x1345543d + **/ + upgradeContract: GenericContractQueryCall< + ChainApi, + ( + codeHash: Hash, + options?: ContractCallOptions, + ) => Promise>> + > + + /** + * + * @param {ContractCallOptions} options + * + * @selector 0x57b8a8a7 + **/ + getAdmin: GenericContractQueryCall< + ChainApi, + ( + options?: ContractCallOptions, + ) => Promise>> + > + + /** + * + * @param {ContractCallOptions} options + * + * @selector 0xbcd31d76 + **/ + getPendingAdmin: GenericContractQueryCall< + ChainApi, + ( + options?: ContractCallOptions, + ) => Promise>> + > + + /** + * + * @param {AccountId32Like | undefined} account + * @param {ContractCallOptions} options + * + * @selector 0x107e33ea + **/ + transferOwnership: GenericContractQueryCall< + ChainApi, + ( + account: AccountId32Like | undefined, + options?: ContractCallOptions, + ) => Promise< + GenericContractCallResult< + Result<[], WasmRegistrationProxyError>, + ContractCallResult + > + > + > + + /** + * + * @param {ContractCallOptions} options + * + * @selector 0xb55be9f0 + **/ + acceptOwnership: GenericContractQueryCall< + ChainApi, + ( + options?: ContractCallOptions, + ) => Promise< + GenericContractCallResult< + Result<[], WasmRegistrationProxyError>, + ContractCallResult + > + > + > +} diff --git a/packages/server/types/wasm/tx.d.ts b/packages/server/types/wasm/tx.d.ts new file mode 100644 index 0000000..bd85362 --- /dev/null +++ b/packages/server/types/wasm/tx.d.ts @@ -0,0 +1,121 @@ +// Generated by dedot cli + +import type { GenericSubstrateApi } from 'dedot/types' +import type { AccountId32Like, Hash } from 'dedot/codecs' +import type { + GenericContractTx, + GenericContractTxCall, + ContractTxOptions, + ContractSubmittableExtrinsic, +} from 'dedot/contracts' + +export interface ContractTx + extends GenericContractTx { + /** + * + * @param {ContractTxOptions} options + * + * @selector 0x0848523c + **/ + fundMe: GenericContractTxCall< + ChainApi, + (options: ContractTxOptions) => ContractSubmittableExtrinsic + > + + /** + * + * @param {AccountId32Like} account + * @param {bigint} balance + * @param {ContractTxOptions} options + * + * @selector 0xe7cda623 + **/ + withdrawFunds: GenericContractTxCall< + ChainApi, + ( + account: AccountId32Like, + balance: bigint, + options: ContractTxOptions, + ) => ContractSubmittableExtrinsic + > + + /** + * + * @param {bigint} id + * @param {string} name + * @param {AccountId32Like} recipient + * @param {number} yearsToRegister + * @param {Array<[string, string]>} records + * @param {bigint} maxFees + * @param {ContractTxOptions} options + * + * @selector 0x229b553f + **/ + register: GenericContractTxCall< + ChainApi, + ( + id: bigint, + name: string, + recipient: AccountId32Like, + yearsToRegister: number, + records: Array<[string, string]>, + maxFees: bigint, + options: ContractTxOptions, + ) => ContractSubmittableExtrinsic + > + + /** + * + * @param {AccountId32Like} controller + * @param {boolean} enable + * @param {ContractTxOptions} options + * + * @selector 0xc5e161ea + **/ + setController: GenericContractTxCall< + ChainApi, + ( + controller: AccountId32Like, + enable: boolean, + options: ContractTxOptions, + ) => ContractSubmittableExtrinsic + > + + /** + * + * @param {Hash} codeHash + * @param {ContractTxOptions} options + * + * @selector 0x1345543d + **/ + upgradeContract: GenericContractTxCall< + ChainApi, + (codeHash: Hash, options: ContractTxOptions) => ContractSubmittableExtrinsic + > + + /** + * + * @param {AccountId32Like | undefined} account + * @param {ContractTxOptions} options + * + * @selector 0x107e33ea + **/ + transferOwnership: GenericContractTxCall< + ChainApi, + ( + account: AccountId32Like | undefined, + options: ContractTxOptions, + ) => ContractSubmittableExtrinsic + > + + /** + * + * @param {ContractTxOptions} options + * + * @selector 0xb55be9f0 + **/ + acceptOwnership: GenericContractTxCall< + ChainApi, + (options: ContractTxOptions) => ContractSubmittableExtrinsic + > +} diff --git a/packages/server/types/wasm/types.d.ts b/packages/server/types/wasm/types.d.ts new file mode 100644 index 0000000..48dc329 --- /dev/null +++ b/packages/server/types/wasm/types.d.ts @@ -0,0 +1,37 @@ +// Generated by dedot cli + +import type { AccountId32 } from 'dedot/codecs' + +export type InkStorageLazyMapping = {} + +export type InkStorageTraitsImplsResolverKey = {} + +export type InkStorageTraitsImplsAutoKey = {} + +export type InkStorageTraitsImplsManualKey = {} + +export type WasmRegistrationProxy = { + admin: AccountId32 + pendingAdmin?: AccountId32 | undefined + registryAddr: AccountId32 + controllers: InkStorageLazyMapping + usedIds: InkStorageLazyMapping +} + +export type InkPrimitivesLangError = 'CouldNotReadInput' + +export type WasmRegistrationProxyError = + | { type: 'NotAdmin' } + | { type: 'NotController' } + | { type: 'InsufficientBalance' } + | { type: 'TooExpensive' } + | { type: 'DuplicateId' } + | { type: 'RegisterFailed'; value: number } + | { type: 'RegisterFailedWithRevert'; value: number } + | { type: 'PriceFetchFailed'; value: number } + +export type WasmRegistrationProxyInnerResult = + | { type: 'Pass'; value: bigint } + | { type: 'Fail'; value: WasmRegistrationProxyError } + +export type InkEnvNoChainExtension = null diff --git a/packages/server/wagmi.config.ts b/packages/server/wagmi.config.ts new file mode 100644 index 0000000..3ccf2f9 --- /dev/null +++ b/packages/server/wagmi.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@wagmi/cli' +import { hardhat } from '@wagmi/cli/plugins' + +export default defineConfig({ + out: 'wagmi.generated.ts', + contracts: [], + plugins: [ + hardhat({ + project: '../contracts', + }), + ], +}) diff --git a/packages/server/wagmi.generated.ts b/packages/server/wagmi.generated.ts new file mode 100644 index 0000000..d58aec1 --- /dev/null +++ b/packages/server/wagmi.generated.ts @@ -0,0 +1,913 @@ +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Controllable +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const controllableAbi = [ + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'previousOwner', internalType: 'address', type: 'address', indexed: true }, + { name: 'newOwner', internalType: 'address', type: 'address', indexed: true }, + ], + name: 'OwnershipTransferred', + }, + { + type: 'function', + inputs: [{ name: '', internalType: 'address', type: 'address' }], + name: 'controllers', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'owner', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'controller', internalType: 'address', type: 'address' }, + { name: 'status', internalType: 'bool', type: 'bool' }, + ], + name: 'setController', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ENS +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const ensAbi = [ + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'owner', internalType: 'address', type: 'address', indexed: true }, + { name: 'operator', internalType: 'address', type: 'address', indexed: true }, + { name: 'approved', internalType: 'bool', type: 'bool', indexed: false }, + ], + name: 'ApprovalForAll', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'label', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'owner', internalType: 'address', type: 'address', indexed: false }, + ], + name: 'NewOwner', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'resolver', internalType: 'address', type: 'address', indexed: false }, + ], + name: 'NewResolver', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'ttl', internalType: 'uint64', type: 'uint64', indexed: false }, + ], + name: 'NewTTL', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'owner', internalType: 'address', type: 'address', indexed: false }, + ], + name: 'Transfer', + }, + { + type: 'function', + inputs: [ + { name: 'owner', internalType: 'address', type: 'address' }, + { name: 'operator', internalType: 'address', type: 'address' }, + ], + name: 'isApprovedForAll', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'owner', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'recordExists', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'resolver', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'operator', internalType: 'address', type: 'address' }, + { name: 'approved', internalType: 'bool', type: 'bool' }, + ], + name: 'setApprovalForAll', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + ], + name: 'setOwner', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + { name: 'resolver', internalType: 'address', type: 'address' }, + { name: 'ttl', internalType: 'uint64', type: 'uint64' }, + ], + name: 'setRecord', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'resolver', internalType: 'address', type: 'address' }, + ], + name: 'setResolver', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'label', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + ], + name: 'setSubnodeOwner', + outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'label', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + { name: 'resolver', internalType: 'address', type: 'address' }, + { name: 'ttl', internalType: 'uint64', type: 'uint64' }, + ], + name: 'setSubnodeRecord', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'ttl', internalType: 'uint64', type: 'uint64' }, + ], + name: 'setTTL', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'ttl', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'view', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ENSRegistry +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const ensRegistryAbi = [ + { type: 'constructor', inputs: [], stateMutability: 'nonpayable' }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'owner', internalType: 'address', type: 'address', indexed: true }, + { name: 'operator', internalType: 'address', type: 'address', indexed: true }, + { name: 'approved', internalType: 'bool', type: 'bool', indexed: false }, + ], + name: 'ApprovalForAll', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'label', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'owner', internalType: 'address', type: 'address', indexed: false }, + ], + name: 'NewOwner', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'resolver', internalType: 'address', type: 'address', indexed: false }, + ], + name: 'NewResolver', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'ttl', internalType: 'uint64', type: 'uint64', indexed: false }, + ], + name: 'NewTTL', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { name: 'owner', internalType: 'address', type: 'address', indexed: false }, + ], + name: 'Transfer', + }, + { + type: 'function', + inputs: [ + { name: 'owner', internalType: 'address', type: 'address' }, + { name: 'operator', internalType: 'address', type: 'address' }, + ], + name: 'isApprovedForAll', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'owner', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'recordExists', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'resolver', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'operator', internalType: 'address', type: 'address' }, + { name: 'approved', internalType: 'bool', type: 'bool' }, + ], + name: 'setApprovalForAll', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + ], + name: 'setOwner', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + { name: 'resolver', internalType: 'address', type: 'address' }, + { name: 'ttl', internalType: 'uint64', type: 'uint64' }, + ], + name: 'setRecord', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'resolver', internalType: 'address', type: 'address' }, + ], + name: 'setResolver', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'label', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + ], + name: 'setSubnodeOwner', + outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'label', internalType: 'bytes32', type: 'bytes32' }, + { name: 'owner', internalType: 'address', type: 'address' }, + { name: 'resolver', internalType: 'address', type: 'address' }, + { name: 'ttl', internalType: 'uint64', type: 'uint64' }, + ], + name: 'setSubnodeRecord', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'node', internalType: 'bytes32', type: 'bytes32' }, + { name: 'ttl', internalType: 'uint64', type: 'uint64' }, + ], + name: 'setTTL', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'node', internalType: 'bytes32', type: 'bytes32' }], + name: 'ttl', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'view', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IERC20 +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const ierc20Abi = [ + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'owner', internalType: 'address', type: 'address', indexed: true }, + { name: 'spender', internalType: 'address', type: 'address', indexed: true }, + { name: 'value', internalType: 'uint256', type: 'uint256', indexed: false }, + ], + name: 'Approval', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'from', internalType: 'address', type: 'address', indexed: true }, + { name: 'to', internalType: 'address', type: 'address', indexed: true }, + { name: 'value', internalType: 'uint256', type: 'uint256', indexed: false }, + ], + name: 'Transfer', + }, + { + type: 'function', + inputs: [ + { name: 'owner', internalType: 'address', type: 'address' }, + { name: 'spender', internalType: 'address', type: 'address' }, + ], + name: 'allowance', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'spender', internalType: 'address', type: 'address' }, + { name: 'amount', internalType: 'uint256', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'account', internalType: 'address', type: 'address' }], + name: 'balanceOf', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'totalSupply', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'to', internalType: 'address', type: 'address' }, + { name: 'amount', internalType: 'uint256', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'from', internalType: 'address', type: 'address' }, + { name: 'to', internalType: 'address', type: 'address' }, + { name: 'amount', internalType: 'uint256', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'nonpayable', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IExtendedResolver +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const iExtendedResolverAbi = [ + { + type: 'function', + inputs: [ + { name: 'name', internalType: 'bytes', type: 'bytes' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + ], + name: 'resolve', + outputs: [{ name: '', internalType: 'bytes', type: 'bytes' }], + stateMutability: 'view', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IResolverService +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const iResolverServiceAbi = [ + { + type: 'function', + inputs: [ + { name: 'name', internalType: 'bytes', type: 'bytes' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + ], + name: 'resolve', + outputs: [ + { name: 'result', internalType: 'bytes', type: 'bytes' }, + { name: 'expires', internalType: 'uint64', type: 'uint64' }, + { name: 'sig', internalType: 'bytes', type: 'bytes' }, + ], + stateMutability: 'view', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ISupportsInterface +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const iSupportsInterfaceAbi = [ + { + type: 'function', + inputs: [{ name: 'interfaceID', internalType: 'bytes4', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'pure', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// OffchainResolver +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const offchainResolverAbi = [ + { + type: 'constructor', + inputs: [ + { name: '_url', internalType: 'string', type: 'string' }, + { name: '_signers', internalType: 'address[]', type: 'address[]' }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'error', + inputs: [ + { name: 'sender', internalType: 'address', type: 'address' }, + { name: 'urls', internalType: 'string[]', type: 'string[]' }, + { name: 'callData', internalType: 'bytes', type: 'bytes' }, + { name: 'callbackFunction', internalType: 'bytes4', type: 'bytes4' }, + { name: 'extraData', internalType: 'bytes', type: 'bytes' }, + ], + name: 'OffchainLookup', + }, + { + type: 'event', + anonymous: false, + inputs: [{ name: 'signers', internalType: 'address[]', type: 'address[]', indexed: false }], + name: 'NewSigners', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'previousOwner', internalType: 'address', type: 'address', indexed: true }, + { name: 'newOwner', internalType: 'address', type: 'address', indexed: true }, + ], + name: 'OwnershipTransferred', + }, + { + type: 'event', + anonymous: false, + inputs: [{ name: 'signers', internalType: 'address[]', type: 'address[]', indexed: false }], + name: 'SignersRemoved', + }, + { + type: 'function', + inputs: [{ name: '_signers', internalType: 'address[]', type: 'address[]' }], + name: 'addSigners', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'target', internalType: 'address', type: 'address' }, + { name: 'expires', internalType: 'uint64', type: 'uint64' }, + { name: 'request', internalType: 'bytes', type: 'bytes' }, + { name: 'result', internalType: 'bytes', type: 'bytes' }, + ], + name: 'makeSignatureHash', + outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], + stateMutability: 'pure', + }, + { + type: 'function', + inputs: [], + name: 'owner', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: '_signers', internalType: 'address[]', type: 'address[]' }], + name: 'removeSigners', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'name', internalType: 'bytes', type: 'bytes' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + ], + name: 'resolve', + outputs: [{ name: '', internalType: 'bytes', type: 'bytes' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'response', internalType: 'bytes', type: 'bytes' }, + { name: 'extraData', internalType: 'bytes', type: 'bytes' }, + ], + name: 'resolveWithProof', + outputs: [{ name: '', internalType: 'bytes', type: 'bytes' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: '_url', internalType: 'string', type: 'string' }], + name: 'setUrl', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: '', internalType: 'address', type: 'address' }], + name: 'signers', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'interfaceID', internalType: 'bytes4', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'pure', + }, + { + type: 'function', + inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [], + name: 'url', + outputs: [{ name: '', internalType: 'string', type: 'string' }], + stateMutability: 'view', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Ownable +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const ownableAbi = [ + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'previousOwner', internalType: 'address', type: 'address', indexed: true }, + { name: 'newOwner', internalType: 'address', type: 'address', indexed: true }, + ], + name: 'OwnershipTransferred', + }, + { + type: 'function', + inputs: [], + name: 'owner', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// RegistrationProxy +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const registrationProxyAbi = [ + { + type: 'constructor', + inputs: [{ name: '_holdPeriod', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'nonpayable', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'id', internalType: 'uint256', type: 'uint256', indexed: true }, + { name: 'name', internalType: 'string', type: 'string', indexed: false }, + { name: 'recipient', internalType: 'string', type: 'string', indexed: false }, + { name: 'yearsToRegister', internalType: 'uint8', type: 'uint8', indexed: false }, + { name: 'metadata', internalType: 'string[2][]', type: 'string[2][]', indexed: false }, + { name: 'paymentToken', internalType: 'address', type: 'address', indexed: false }, + { name: 'value', internalType: 'uint256', type: 'uint256', indexed: false }, + { name: 'ttl', internalType: 'uint256', type: 'uint256', indexed: false }, + ], + name: 'InitiateRequest', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'previousOwner', internalType: 'address', type: 'address', indexed: true }, + { name: 'newOwner', internalType: 'address', type: 'address', indexed: true }, + ], + name: 'OwnershipTransferred', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'id', internalType: 'uint256', type: 'uint256', indexed: true }, + { name: 'success', internalType: 'bool', type: 'bool', indexed: false }, + { name: 'refundAmt', internalType: 'uint256', type: 'uint256', indexed: false }, + ], + name: 'ResultInfo', + }, + { + type: 'function', + inputs: [{ name: '', internalType: 'address', type: 'address' }], + name: 'controllers', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: '_id', internalType: 'uint256', type: 'uint256' }], + name: 'failure', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [], + name: 'holdPeriod', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'id', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + name: 'idToRecord', + outputs: [ + { name: 'initiator', internalType: 'address', type: 'address' }, + { name: 'paymentToken', internalType: 'address', type: 'address' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + { name: 'ttl', internalType: 'uint256', type: 'uint256' }, + { name: 'status', internalType: 'enum RegistrationProxy.Status', type: 'uint8' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: '', internalType: 'address', type: 'address' }], + name: 'lockedFunds', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'owner', + outputs: [{ name: '', internalType: 'address', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'name', internalType: 'string', type: 'string' }, + { name: 'recipient', internalType: 'string', type: 'string' }, + { name: 'yearsToRegister', internalType: 'uint8', type: 'uint8' }, + { name: 'metadata', internalType: 'string[2][]', type: 'string[2][]' }, + { name: 'paymentToken', internalType: 'address', type: 'address' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + name: 'register', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'name', internalType: 'string', type: 'string' }, + { name: 'recipient', internalType: 'string', type: 'string' }, + { name: 'yearsToRegister', internalType: 'uint8', type: 'uint8' }, + { name: 'metadata', internalType: 'string[2][]', type: 'string[2][]' }, + ], + name: 'register', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'controller', internalType: 'address', type: 'address' }, + { name: 'status', internalType: 'bool', type: 'bool' }, + ], + name: 'setController', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: '_holdPeriod', internalType: 'uint256', type: 'uint256' }], + name: 'setHoldPeriod', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'paymentToken', internalType: 'address', type: 'address' }, + { name: 'state', internalType: 'bool', type: 'bool' }, + ], + name: 'setWhitelistToken', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: '_id', internalType: 'uint256', type: 'uint256' }, + { name: 'refundAmt', internalType: 'uint256', type: 'uint256' }, + ], + name: 'success', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: '', internalType: 'address', type: 'address' }], + name: 'whitelistedTokens', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'beneficiary', internalType: 'address', type: 'address' }, + { name: 'paymentToken', internalType: 'address', type: 'address' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + name: 'withdrawFunds', + outputs: [], + stateMutability: 'nonpayable', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SupportsInterface +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const supportsInterfaceAbi = [ + { + type: 'function', + inputs: [{ name: 'interfaceID', internalType: 'bytes4', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'pure', + }, +] as const diff --git a/packages/server/wrangler.toml b/packages/server/wrangler.toml new file mode 100644 index 0000000..9541cb6 --- /dev/null +++ b/packages/server/wrangler.toml @@ -0,0 +1,74 @@ +account_id = "702b4f69f0033f956c18b119f81ea1d6" + +compatibility_date = "2024-09-23" +compatibility_flags = ["nodejs_compat"] + +main = "./src/index.ts" + +workers_dev = false + +[observability] +enabled = true + +[placement] +mode = "smart" + +[dev] +port = 8080 + +[build] +command = "bun run build" + + +# Testnet (Aleph Zero Testnet & Ethereum Sepolia) +# Testnet: Aleph Zero Testnet +[env.testnet] +name = "tzero-id-gateway" + +# TODO @Dennis +# [[env.testnet.kv_namespaces]] +# binding = "GATEWAY_KV" +# id = "57c96f8e33034440bc2bc58e8546a3a9" +# preview_id = "36962a3091994602a0bf7b1580dabc5b" + +[env.testnet.route] +pattern = "gateway.tzero.id" +custom_domain = true + +# @dennis please set all the TODOs here +[env.testnet.vars] +LOGLEVEL = 3 # debug +# LOGLEVEL = 0 # silent +OG_TTL = 60 +AZERO_RPC_URL = "wss://ws.test.azero.dev" +EVM_RPC_BASE_URL = "https://sepolia.infura.io/v3" +SUPPORTED_TLDS = { "tzero.id" = "5FsB91tXSEuMj6akzdPczAtmBaVKToqHmtAwSUzXh49AYzaD", "tzero-id.eth" = "5FsB91tXSEuMj6akzdPczAtmBaVKToqHmtAwSUzXh49AYzaD" } +EVM_RELAYER_CONTRACT = "TODO" +WASM_RELAYER_CONTRACT = "TODO" +BUFFER_DURATION_IN_MIN = "10" + +# Mainnet (Aleph Zero Mainnet & Ethereum Mainnet) + +[env.mainnet] +name = "azero-id-gateway" + +# TODO @Dennis +# [[env.mainnet.kv_namespaces]] +# binding = "GATEWAY_KV" +# id = "57c96f8e33034440bc2bc58e8546a3a9" +# preview_id = "36962a3091994602a0bf7b1580dabc5b" + +[env.mainnet.route] +pattern = "gateway.azero.id" +custom_domain = true + +[env.mainnet.vars] +# LOGLEVEL = 3 # debug +LOGLEVEL = 0 # silent +OG_TTL = 60 +AZERO_RPC_URL = "wss://ws.azero.dev" +EVM_RPC_BASE_URL = "https://mainnet.infura.io/v3" +SUPPORTED_TLDS = { "azero.id" = "5CTQBfBC9SfdrCDBJdfLiyW2pg9z5W6C6Es8sK313BLnFgDf", "azero-id.eth" = "5CTQBfBC9SfdrCDBJdfLiyW2pg9z5W6C6Es8sK313BLnFgDf" } +EVM_RELAYER_CONTRACT = "TODO" +WASM_RELAYER_CONTRACT = "TODO" +BUFFER_DURATION_IN_MIN = "10"