diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dccda4a..a65db8d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node: ['16.x', '18.x'] + node: ['18.x'] os: [ubuntu-latest] steps: 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..d1ae040 --- /dev/null +++ b/packages/contracts/contracts/relayer/evm/RegistrationProxy.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/access/Ownable.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, + uint256 value, + uint256 ttl + ); + + event ResultInfo( + uint256 indexed id, + bool success, + uint256 refundAmt + ); + + struct Record { + // string name; + address initiator; + uint256 value; + uint256 ttl; + Status status; + } + + uint256 public id; + uint256 public holdPeriod; + mapping(uint256 => Record) public idToRecord; + + constructor(uint256 _holdPeriod) Ownable() { + holdPeriod = _holdPeriod; + } + + function setHoldPeriod(uint256 _holdPeriod) external onlyOwner { + holdPeriod = _holdPeriod; + } + + function register( + string calldata name, + string calldata recipient, + uint8 yearsToRegister + ) external payable { + uint256 ttl = block.timestamp + holdPeriod; + uint256 _id = id++; + + idToRecord[_id] = Record(msg.sender, msg.value, ttl, Status.PENDING); + + emit InitiateRequest( + _id, + name, + recipient, + yearsToRegister, + msg.value, + ttl + ); + } + + 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; + payable(record.initiator).transfer(refundAmt); + + 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; + payable(record.initiator).transfer(record.value); + + emit ResultInfo(_id, false, record.value); + } +} 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..1ebcaac --- /dev/null +++ b/packages/contracts/contracts/relayer/wasm/lib.rs @@ -0,0 +1,169 @@ +#![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::storage::Mapping; + + #[ink(event)] + pub struct Success { + id: u128, + price: Balance, + } + + #[ink(storage)] + pub struct RegistrationProxy { + admin: AccountId, + /// Two-step ownership transfer AccountId + pending_admin: Option, + registry_addr: AccountId, + controllers: 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, + /// Failed to register + RegisterFailed(u8), + /// Unable to retrieve the price + PriceFetchFailed(u8), + } + + impl RegistrationProxy { + #[ink(constructor)] + pub fn new(admin: AccountId, registry_addr: AccountId) -> Self { + Self { + admin, + pending_admin: None, + registry_addr, + controllers: Mapping::default(), + } + } + + #[ink(message, payable)] + pub fn fund_me(&mut self) -> Result<(), Error> { + Ok(()) + } + + #[ink(message)] + pub fn register( + &mut self, + id: u128, + name: String, + recipient: AccountId, + years_to_register: u8, + max_fees: Balance, + ) -> Result { + self.ensure_controller()?; + + let price = self.get_name_price(&name, recipient, years_to_register)?; + if price > max_fees { + return Err(Error::TooExpensive); + } else if price > self.env().balance() { + return Err(Error::InsufficientBalance); + } + + const REGISTER_SELECTOR: [u8; 4] = ink::selector_bytes!("register_on_behalf_of"); + let result = build_call::() + .call(self.registry_addr) + .call_v1() + .exec_input( + ExecutionInput::new(Selector::new(REGISTER_SELECTOR)) + .push_arg(name) + .push_arg(recipient) + .push_arg(years_to_register) + .push_arg::>(None) + .push_arg::>(None), + ) + .returns::>() + .transferred_value(price) + .params() + .invoke(); + + result.map_err(Error::RegisterFailed)?; + + self.env().emit_event(Success { + id, + price, + }); + + Ok(price) + } + + #[ink(message)] + pub fn set_controller(&mut self, controller: AccountId, enable: bool) -> Result<(), Error> { + self.ensure_admin().expect("Not Authorised"); + + 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 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 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::), u8>>() + .params() + .invoke(); + + let (base_price, premium, _, _) = result.map_err(Error::PriceFetchFailed)?; + 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) + } + } +} diff --git a/packages/gateway/.env.example b/packages/gateway/.env.example new file mode 100644 index 0000000..c6cacd4 --- /dev/null +++ b/packages/gateway/.env.example @@ -0,0 +1,10 @@ +PORT=8080 +TTL=300 +WASM_PROVIDER_URL='wss://ws.test.azero.dev' +EVM_PROVIDER_URL='https://ethereum-sepolia.publicnode.com' +EVM_RELAYER_CONTRACT='0x2BaD727319377af238a7F6D166494118Ca9D0497' +WASM_RELAYER_CONTRACT='5GNDka5xV9y9nsES2gqYQponJ8vJaAmoJjMUvrqGqPF65q7P' +BUFFER_DURATION_IN_MIN= 10 +GATEWAY_SIGNER_KEY= +EVM_PRIVATE_KEY= +WASM_PRIVATE_KEY= diff --git a/packages/gateway/src/metadata.json b/packages/gateway/src/artefacts/azns_registry.json similarity index 100% rename from packages/gateway/src/metadata.json rename to packages/gateway/src/artefacts/azns_registry.json diff --git a/packages/gateway/src/artefacts/evm.json b/packages/gateway/src/artefacts/evm.json new file mode 100644 index 0000000..bc4600c --- /dev/null +++ b/packages/gateway/src/artefacts/evm.json @@ -0,0 +1,319 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_holdPeriod", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "recipient", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "yearsToRegister", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ttl", + "type": "uint256" + } + ], + "name": "InitiateRequest", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "refundAmt", + "type": "uint256" + } + ], + "name": "ResultInfo", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "controllers", + "outputs": [ + { + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "failure", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "holdPeriod", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "id", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "idToRecord", + "outputs": [ + { + "internalType": "address", + "name": "initiator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ttl", + "type": "uint256" + }, + { + "internalType": "enum RegistrationProxy.Status", + "name": "status", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "recipient", + "type": "string" + }, + { + "internalType": "uint8", + "name": "yearsToRegister", + "type": "uint8" + } + ], + "name": "register", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "controller", + "type": "address" + }, + { + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "name": "setController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_holdPeriod", + "type": "uint256" + } + ], + "name": "setHoldPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAmt", + "type": "uint256" + } + ], + "name": "success", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/gateway/src/artefacts/wasm.json b/packages/gateway/src/artefacts/wasm.json new file mode 100644 index 0000000..fb7b0ae --- /dev/null +++ b/packages/gateway/src/artefacts/wasm.json @@ -0,0 +1,1140 @@ +{ + "source": { + "hash": "0x19af38a9ba0bb74c28516eb1c25947eb6d88ad579f1f7ebc983084fd0e6b7762", + "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": false, + "returnType": { + "displayName": [ + "ink_primitives", + "ConstructorResult" + ], + "type": 10 + }, + "selector": "0x9bae9d5e" + } + ], + "docs": [], + "environment": { + "accountId": { + "displayName": [ + "AccountId" + ], + "type": 0 + }, + "balance": { + "displayName": [ + "Balance" + ], + "type": 15 + }, + "blockNumber": { + "displayName": [ + "BlockNumber" + ], + "type": 25 + }, + "chainExtension": { + "displayName": [ + "ChainExtension" + ], + "type": 26 + }, + "hash": { + "displayName": [ + "Hash" + ], + "type": 21 + }, + "maxEventTopics": 4, + "staticBufferSize": 16384, + "timestamp": { + "displayName": [ + "Timestamp" + ], + "type": 24 + } + }, + "events": [ + { + "args": [ + { + "docs": [], + "indexed": false, + "label": "id", + "type": { + "displayName": [ + "u128" + ], + "type": 15 + } + }, + { + "docs": [], + "indexed": false, + "label": "price", + "type": { + "displayName": [ + "Balance" + ], + "type": 15 + } + } + ], + "docs": [], + "label": "Success", + "module_path": "wasm::registration_proxy", + "signature_topic": "0x096bb2d50ce5aa43ab85a5681d5e8498e69e9fba99c0e016e6dfe3a2236e62ab" + } + ], + "lang_error": { + "displayName": [ + "ink", + "LangError" + ], + "type": 11 + }, + "messages": [ + { + "args": [], + "default": false, + "docs": [], + "label": "fund_me", + "mutates": true, + "payable": true, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 12 + }, + "selector": "0x0848523c" + }, + { + "args": [ + { + "label": "id", + "type": { + "displayName": [ + "u128" + ], + "type": 15 + } + }, + { + "label": "name", + "type": { + "displayName": [ + "String" + ], + "type": 16 + } + }, + { + "label": "recipient", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "label": "years_to_register", + "type": { + "displayName": [ + "u8" + ], + "type": 2 + } + }, + { + "label": "max_fees", + "type": { + "displayName": [ + "Balance" + ], + "type": 15 + } + } + ], + "default": false, + "docs": [], + "label": "register", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 17 + }, + "selector": "0x229b553f" + }, + { + "args": [ + { + "label": "controller", + "type": { + "displayName": [ + "AccountId" + ], + "type": 0 + } + }, + { + "label": "enable", + "type": { + "displayName": [ + "bool" + ], + "type": 19 + } + } + ], + "default": false, + "docs": [], + "label": "set_controller", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 12 + }, + "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": 20 + }, + "selector": "0x493a701c" + }, + { + "args": [ + { + "label": "code_hash", + "type": { + "displayName": [ + "Hash" + ], + "type": 21 + } + } + ], + "default": false, + "docs": [], + "label": "upgrade_contract", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 10 + }, + "selector": "0x1345543d" + }, + { + "args": [], + "default": false, + "docs": [], + "label": "get_admin", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 22 + }, + "selector": "0x57b8a8a7" + }, + { + "args": [], + "default": false, + "docs": [], + "label": "get_pending_admin", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 23 + }, + "selector": "0xbcd31d76" + }, + { + "args": [ + { + "label": "account", + "type": { + "displayName": [ + "Option" + ], + "type": 9 + } + } + ], + "default": false, + "docs": [], + "label": "transfer_ownership", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 12 + }, + "selector": "0x107e33ea" + }, + { + "args": [], + "default": false, + "docs": [], + "label": "accept_ownership", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 12 + }, + "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" + } + ], + "name": "RegistrationProxy" + } + }, + "root_key": "0x00000000", + "ty": 8 + } + }, + "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": { + "fields": [ + { + "name": "admin", + "type": 0, + "typeName": ",>>::Type" + }, + { + "name": "pending_admin", + "type": 9, + "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" + } + ] + } + }, + "path": [ + "wasm", + "registration_proxy", + "RegistrationProxy" + ] + } + }, + { + "id": 9, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 0, + "name": "None" + }, + { + "fields": [ + { + "type": 0 + } + ], + "index": 1, + "name": "Some" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + } + ], + "path": [ + "Option" + ] + } + }, + { + "id": 10, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 3 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 11 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 3 + }, + { + "name": "E", + "type": 11 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 11, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 1, + "name": "CouldNotReadInput" + } + ] + } + }, + "path": [ + "ink_primitives", + "LangError" + ] + } + }, + { + "id": 12, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 13 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 11 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 13 + }, + { + "name": "E", + "type": 11 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 13, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 3 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 14 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 3 + }, + { + "name": "E", + "type": 14 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 14, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 0, + "name": "NotAdmin" + }, + { + "index": 1, + "name": "NotController" + }, + { + "index": 2, + "name": "InsufficientBalance" + }, + { + "index": 3, + "name": "TooExpensive" + }, + { + "fields": [ + { + "type": 2, + "typeName": "u8" + } + ], + "index": 4, + "name": "RegisterFailed" + }, + { + "fields": [ + { + "type": 2, + "typeName": "u8" + } + ], + "index": 5, + "name": "PriceFetchFailed" + } + ] + } + }, + "path": [ + "wasm", + "registration_proxy", + "Error" + ] + } + }, + { + "id": 15, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 16, + "type": { + "def": { + "primitive": "str" + } + } + }, + { + "id": 17, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 18 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 11 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 18 + }, + { + "name": "E", + "type": 11 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 18, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 15 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 14 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 15 + }, + { + "name": "E", + "type": 14 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 19, + "type": { + "def": { + "primitive": "bool" + } + } + }, + { + "id": 20, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 19 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 11 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 19 + }, + { + "name": "E", + "type": 11 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 21, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 1, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "Hash" + ] + } + }, + { + "id": 22, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 0 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 11 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + }, + { + "name": "E", + "type": 11 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 23, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 9 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 11 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 9 + }, + { + "name": "E", + "type": 11 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 24, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 25, + "type": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 26, + "type": { + "def": { + "variant": {} + }, + "path": [ + "ink_env", + "types", + "NoChainExtension" + ] + } + } + ], + "version": 5 +} \ No newline at end of file diff --git a/packages/gateway/src/azero-id.ts b/packages/gateway/src/azero-id.ts index c86ab27..697fecb 100644 --- a/packages/gateway/src/azero-id.ts +++ b/packages/gateway/src/azero-id.ts @@ -1,71 +1,75 @@ -import abi from './metadata.json'; +import abi from './artefacts/azns_registry.json'; import { ApiPromise, WsProvider } from '@polkadot/api'; import { Database } from './server'; import { ContractPromise } from '@polkadot/api-contract'; -import type { WeightV2 } from '@polkadot/types/interfaces'; -import { getCoderByCoinType } from "@ensdomains/address-encoder"; -import { createDotAddressDecoder } from '@ensdomains/address-encoder/utils' +import { WeightV2 } from '@polkadot/types/interfaces'; +import { getCoderByCoinType } from '@ensdomains/address-encoder'; +import { createDotAddressDecoder } from '@ensdomains/address-encoder/utils'; import { hexlify } from 'ethers/lib/utils'; +import { GasLimit } from './utils'; const AZERO_COIN_TYPE = 643; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const EMPTY_CONTENT_HASH = '0x'; -export interface GasLimit { - refTime: number, - proofSize: number, -} - export class AzeroId implements Database { ttl: number; tldToContract: Map; maxGasLimit: WeightV2; - constructor(ttl: number, tldToContract: Map, maxGasLimit: WeightV2) { + constructor( + ttl: number, + tldToContract: Map, + maxGasLimit: WeightV2 + ) { this.ttl = ttl; this.tldToContract = tldToContract; this.maxGasLimit = maxGasLimit; } - static async init(ttl: number, providerURL: string, tldToContractAddress: Map, gasLimit: GasLimit) { + static async init( + ttl: number, + providerURL: string, + tldToContractAddress: Map, + gasLimit: GasLimit + ) { const wsProvider = new WsProvider(providerURL); const api = await ApiPromise.create({ provider: wsProvider }); - + const tldToContract = new Map(); tldToContractAddress.forEach((addr, tld) => { - tldToContract.set(tld, new ContractPromise(api, abi, addr)) - }) + tldToContract.set(tld, new ContractPromise(api, abi, addr)); + }); - const maxGasLimit = api.registry.createType('WeightV2', gasLimit) as WeightV2; + const maxGasLimit = api.registry.createType( + 'WeightV2', + gasLimit + ) as WeightV2; - return new AzeroId( - ttl, - tldToContract, - maxGasLimit, - ); + return new AzeroId(ttl, tldToContract, maxGasLimit); } async addr(name: string, coinType: number) { coinType = Number(coinType); // convert BigNumber to number - console.log("addr", name, coinType); - + console.log('addr', name, coinType); + let value; - if (coinType == AZERO_COIN_TYPE) { + if (coinType === AZERO_COIN_TYPE) { value = await this.fetchA0ResolverAddress(name); } else { - let alias = AzeroId.getAlias(""+coinType); + let alias = AzeroId.getAlias('' + coinType); if (alias !== undefined) { - const serviceKey = "address." + alias; + const serviceKey = 'address.' + alias; value = await this.fetchRecord(name, serviceKey); } if (value === undefined) { - const serviceKey = "address." + coinType; + const serviceKey = 'address.' + coinType; value = await this.fetchRecord(name, serviceKey); } } if (value === undefined) { - value = coinType == 60? ZERO_ADDRESS:'0x'; + value = coinType === 60 ? ZERO_ADDRESS : '0x'; } else { value = AzeroId.encodeAddress(value, coinType); } @@ -74,36 +78,36 @@ export class AzeroId implements Database { } async text(name: string, key: string) { - console.log("text", name, key); - const value = await this.fetchRecord(name, key) || ''; + console.log('text', name, key); + const value = (await this.fetchRecord(name, key)) || ''; return { value, ttl: this.ttl }; } contenthash(name: string) { - console.log("contenthash", name); + console.log('contenthash', name); return { contenthash: EMPTY_CONTENT_HASH, ttl: this.ttl }; } private async fetchRecord(domain: string, key: string) { - let {name, contract} = this.processName(domain); + let { name, contract } = this.processName(domain); const resp: any = await contract.query.getRecord( '', { - gasLimit: this.maxGasLimit + gasLimit: this.maxGasLimit, }, name, key ); - + return resp.output?.toHuman().Ok.Ok; } private async fetchA0ResolverAddress(domain: string) { - let {name, contract} = this.processName(domain); + let { name, contract } = this.processName(domain); const resp: any = await contract.query.getAddress( '', { - gasLimit: this.maxGasLimit + gasLimit: this.maxGasLimit, }, name ); @@ -113,7 +117,7 @@ export class AzeroId implements Database { private processName(domain: string) { const labels = domain.split('.'); - console.log("Labels:", labels); + console.log('Labels:', labels); const name = labels.shift() || ''; const tld = labels.join('.'); @@ -123,7 +127,7 @@ export class AzeroId implements Database { throw new Error(`TLD (.${tld}) not supported`); } - return {name, contract}; + return { name, contract }; } static getAlias(coinType: string) { @@ -140,10 +144,10 @@ export class AzeroId implements Database { static encodeAddress(addr: string, coinType: number) { const isEvmCoinType = (c: number) => { - return c == 60 || (c & 0x80000000)!=0 - } + return c === 60 || (c & 0x80000000) !== 0; + }; - if (coinType == AZERO_COIN_TYPE) { + if (coinType === AZERO_COIN_TYPE) { const azeroCoder = createDotAddressDecoder(42); return hexlify(azeroCoder(addr)); } diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts index 8aad458..002caab 100644 --- a/packages/gateway/src/index.ts +++ b/packages/gateway/src/index.ts @@ -1,45 +1,110 @@ import { makeApp } from './server'; -import { Command } from 'commander'; -import { readFileSync } from 'fs'; import { ethers } from 'ethers'; -import { AzeroId, GasLimit } from './azero-id'; +import { AzeroId } from './azero-id'; +import { GasLimit } from './utils'; +import { Relayer } from './registration-relayer'; +import { Keyring } from '@polkadot/keyring'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { waitReady } from '@polkadot/wasm-crypto'; import supportedTLDs from './supported-tlds.json'; +import dotenv from 'dotenv'; -const program = new Command(); -program - .requiredOption( - '-k --private-key ', - 'Private key to sign responses with. Prefix with @ to read from a file' - ) - .option('--provider-url ', 'Provider URL of the substrate chain', 'wss://ws.test.azero.dev') - .option('-t --ttl ', 'TTL for signatures', '300') - .option('-p --port ', 'Port number to serve on', '8080'); -program.parse(process.argv); -const options = program.opts(); -let privateKey = options.privateKey; -if (privateKey.startsWith('@')) { - privateKey = ethers.utils.arrayify( - readFileSync(privateKey.slice(1), { encoding: 'utf-8' }) +dotenv.config(); + +async function gateway( + port: number, + ttl: number, + wasmProviderURL: string, + tldToContractAddress: Map, + wasmGasLimit: GasLimit, + evmSigningKey: ethers.utils.SigningKey +) { + const db = await AzeroId.init( + ttl, + wasmProviderURL, + tldToContractAddress, + wasmGasLimit + ); + + const app = makeApp(evmSigningKey, '/', db); + const address = ethers.utils.computeAddress(evmSigningKey.privateKey); + console.log(`Serving on port ${port} with signing address ${address}`); + app.listen(port); +} + +async function relayer( + evmSigner: ethers.Signer, + evmRelayerAddr: string, + wasmProviderURL: string, + wasmRelayerAddr: string, + wasmSigner: KeyringPair, + wasmGasLimit: GasLimit, + bufferDurationInMinutes: number +) { + const relayer = await Relayer.init( + evmSigner, + evmRelayerAddr, + wasmProviderURL, + wasmRelayerAddr, + wasmSigner, + wasmGasLimit, + bufferDurationInMinutes ); + + relayer.start(); } -const address = ethers.utils.computeAddress(privateKey); -const signer = new ethers.utils.SigningKey(privateKey); - -// TODO: make it configurable -const defaultGasLimit: GasLimit = { - refTime: 10_000_000_000, - proofSize: 1_000_000, -}; - -const tldToContractAddress = new Map(Object.entries(supportedTLDs)); - -AzeroId.init( - parseInt(options.ttl), - options.providerUrl, - tldToContractAddress, - defaultGasLimit, -).then(db => { - const app = makeApp(signer, '/', db); - console.log(`Serving on port ${options.port} with signing address ${address}`); - app.listen(parseInt(options.port)); -}); + +async function main() { + const port = parseInt(process.env.PORT || '8080'); + const ttl = parseInt(process.env.TTL || '300'); + const wasmProviderURL = + process.env.WASM_PROVIDER_URL || 'wss://ws.test.azero.dev'; + const tldToContractAddress = new Map( + Object.entries(supportedTLDs) + ); + const wasmGasLimit: GasLimit = { + refTime: 100_000_000_000, + proofSize: 1_000_000, + }; + const evmGatewaySigningKey = new ethers.utils.SigningKey( + process.env.GATEWAY_SIGNER_KEY || '' + ); + const evmRelayerSigningKey = new ethers.utils.SigningKey( + process.env.EVM_PRIVATE_KEY || '' + ); + const evmProvider = ethers.getDefaultProvider( + process.env.EVM_PROVIDER_URL || 'https://ethereum-sepolia.publicnode.com' + ); + const evmSigner = new ethers.Wallet(evmRelayerSigningKey, evmProvider); + const evmRelayerAddr = process.env.EVM_RELAYER_CONTRACT || ''; + const wasmRelayerAddr = process.env.WASM_RELAYER_CONTRACT || ''; + const wasmSigner = await waitReady().then(() => + new Keyring({ type: 'sr25519' }).createFromUri( + process.env.WASM_PRIVATE_KEY || '' + ) + ); + const bufferDurationInMinutes = parseInt( + process.env.BUFFER_DURATION_IN_MIN || '10' + ); + + await gateway( + port, + ttl, + wasmProviderURL, + tldToContractAddress, + wasmGasLimit, + evmGatewaySigningKey + ); + + await relayer( + evmSigner, + evmRelayerAddr, + wasmProviderURL, + wasmRelayerAddr, + wasmSigner, + wasmGasLimit, + bufferDurationInMinutes + ); +} + +main(); diff --git a/packages/gateway/src/registration-relayer.ts b/packages/gateway/src/registration-relayer.ts new file mode 100644 index 0000000..f017441 --- /dev/null +++ b/packages/gateway/src/registration-relayer.ts @@ -0,0 +1,204 @@ +import { ContractReceipt, ContractTransaction, ethers } from 'ethers'; +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { ContractPromise } from '@polkadot/api-contract'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { WeightV2 } from '@polkadot/types/interfaces'; +import evmABI from './artefacts/evm.json'; +import wasmABI from './artefacts/wasm.json'; +import { GasLimit } from './utils'; + +export class Relayer { + evm: ethers.Contract; + wasm: ContractPromise; + wasmSigner: KeyringPair; + wasmGasLimit: WeightV2; + bufferDuration: number; + + constructor( + evm: ethers.Contract, + wasm: ContractPromise, + wasmSigner: KeyringPair, + wasmGasLimit: WeightV2, + bufferDuration: number // In minutes + ) { + this.evm = evm; + this.wasm = wasm; + this.wasmSigner = wasmSigner; + this.wasmGasLimit = wasmGasLimit; + this.bufferDuration = bufferDuration * 60 * 1000; // converted to milliseconds + } + + static async init( + evmSigner: ethers.Signer, + evmAddr: string, + wasmProviderURL: string, + wasmAddr: string, + wasmSigner: KeyringPair, + wasmGasLimit: GasLimit, + bufferDuration: number // In minutes + ): Promise { + const evm = new ethers.Contract(evmAddr, evmABI, evmSigner); + + const wsProvider = new WsProvider(wasmProviderURL); + const api = await ApiPromise.create({ provider: wsProvider }); + const wasm = new ContractPromise(api, wasmABI, wasmAddr); + const weightV2 = api.registry.createType( + 'WeightV2', + wasmGasLimit + ) as WeightV2; + + return new Relayer(evm, wasm, wasmSigner, weightV2, bufferDuration); + } + + start() { + this.evm.on( + 'InitiateRequest', + (id, name, recipient, yearsToRegister, value, ttl) => { + console.log('New request:', Number(id), name); + + if (this.isTTLInRange(Number(ttl))) { + this.executeRequest( + Number(id), + name, + recipient, + Number(yearsToRegister), + value + ); + } else { + // Ignore the request + console.log( + 'Request %d skipped as its expiry-time falls short', + Number(id) + ); + } + } + ); + } + + /// @dev ttl is expected to be in seconds + isTTLInRange(ttl: number) { + ttl = ttl * 1000; // convert seconds to milliseconds + const currentTimestamp = Date.now(); + return currentTimestamp + this.bufferDuration <= ttl; + } + + async executeRequest( + id: number, + name: string, + recipient: string, + yearsToRegister: number, + maxFeesInEVM: number + ) { + const maxFeesInWASM = this.valueEVM2WASM(maxFeesInEVM); + + // TODO: first dry-run to save Tx which would fail + + await this.wasm.tx + .register( + { + gasLimit: this.wasmGasLimit, + }, + id, + name, + recipient, + yearsToRegister, + maxFeesInWASM + ) + .signAndSend(this.wasmSigner, ({ events = [], status }) => { + if (status.isFinalized) { + let successEventRecord = events.find(eventRecord => { + const isContractEvent = this.wasm.api.events.contracts + .ContractEmitted.is; + const verifyEventEmitter = (addr: any) => + eventRecord.event.data.at(0)?.eq(addr); + + const successEventExists = (eventRecord: any, id: number) => { + const decoded = this.wasm.abi.decodeEvent(eventRecord); + const emittedID = Number(decoded.args[0]); + return ( + decoded.event.identifier === + 'wasm::registration_proxy::Success' && emittedID === id + ); + }; + + return ( + isContractEvent(eventRecord.event) && + verifyEventEmitter(this.wasm.address) && + successEventExists(eventRecord, id) + ); + }); + + if (successEventRecord === undefined) { + // Failure + console.log('Failed to register'); + this.failure(id); + } else { + // Success + const decoded = this.wasm.abi.decodeEvent(successEventRecord); + const priceInWASM = Number(decoded.args[1]); + console.log('Registered successfully with price:', priceInWASM); + + const refundInWASM = maxFeesInWASM - priceInWASM; + const refundInEVM = this.valueWASM2EVM(refundInWASM); + this.success(id, refundInEVM); + } + } + }); + } + + private async failure(id: number) { + const tx: ContractTransaction = await this.evm.failure(id); + const receipt: ContractReceipt = await tx.wait(); + + const relaySuccess = receipt.events?.some(eve => + isResultInfoEvent(eve, this.evm.address, id, false) + ); + + if (relaySuccess) { + console.log('Failure status relayed back successfully'); + } else { + console.log('Failure status was NOT relayed back'); + } + } + + private async success(id: number, refundInEVM: number) { + const tx: ContractTransaction = await this.evm.success(id, refundInEVM); + const receipt: ContractReceipt = await tx.wait(); + + const relaySuccess = receipt.events?.some(eve => + isResultInfoEvent(eve, this.evm.address, id, true) + ); + + if (relaySuccess) { + console.log('Success status relayed back successfully'); + } else { + // ALERT: RELAYER FAILURE + throw new Error('FAILURE: Success status could not be relayed back'); + } + } + + private valueEVM2WASM(valueInEVM: number) { + // TODO: set value converter properly + return valueInEVM + 10000000000000; + } + + private valueWASM2EVM(valueInEVM: number) { + // TODO: set value converter properly + if (valueInEVM <= 0) return 0; + return 0; + } +} + +function isResultInfoEvent( + eve: ethers.Event, + addr: string, + id: number, + success: boolean +): boolean { + return ( + eve.address === addr && + eve.event === 'ResultInfo' && + Number(eve.args?.at(0)) === id && + eve.args?.at(1) === success + ); +} diff --git a/packages/gateway/src/utils.ts b/packages/gateway/src/utils.ts index 228afbf..ba8ead9 100644 --- a/packages/gateway/src/utils.ts +++ b/packages/gateway/src/utils.ts @@ -1 +1,6 @@ export const ETH_COIN_TYPE = 60; + +export interface GasLimit { + refTime: number; + proofSize: number; +}