From 51502b7f25f6c881719ecd3baea4f55d133cbd20 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Tue, 27 Jan 2026 10:58:48 -0300 Subject: [PATCH 1/5] chore: add l1 setup config --- foundry.lock | 5 + packages/client/package.json | 4 +- packages/client/src/read.ts | 6 +- packages/contracts/package.json | 1 + .../contracts/script/config/L1Config.s.sol | 158 ++++++++++++++++++ .../contracts/script/local/L1Resolver.sol | 62 +++++++ packages/contracts/src/L1Resolver.sol | 28 ++-- 7 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 foundry.lock create mode 100644 packages/contracts/script/config/L1Config.s.sol create mode 100644 packages/contracts/script/local/L1Resolver.sol diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..f6bd030 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,5 @@ +{ + "packages/contracts/lib/optimism-bedrock-contracts": { + "rev": "934341571f50fd676c07014a0b31652cb105e41d" + } +} \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json index fe771fc..fb9d423 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -5,8 +5,8 @@ "test:arb": "hardhat test ./test/arb.e2e.spec.ts --network localhost", "test:db": "hardhat test ./test/db.e2e.spec.ts --network localhost", "test:metadata": "hardhat test ./test/metadata.e2e.spec.ts --network localhost", - "start:read": "ts-node src/read.ts", - "start:write": "ts-node src/write.ts", + "read": "ts-node src/read.ts", + "write": "ts-node src/write.ts", "lint": "eslint . --ext .ts --fix" }, "devDependencies": { diff --git a/packages/client/src/read.ts b/packages/client/src/read.ts index 3d361b2..80ba341 100644 --- a/packages/client/src/read.ts +++ b/packages/client/src/read.ts @@ -8,9 +8,7 @@ import { Hex, createPublicClient, http } from 'viem' import { normalize } from 'viem/ens' import { getChain } from './client' -config({ - path: process.env.ENV_FILE || '../.env', -}) +config() const { CHAIN_ID: chainId = '31337', @@ -29,7 +27,7 @@ const client = createPublicClient({ // eslint-disable-next-line const _ = (async () => { - const name = normalize('lucas.arb.eth') + const name = normalize('blockful.eth') const twitter = await client.getEnsText({ name, diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 73be31e..14d4e54 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -9,6 +9,7 @@ "lint": "forge fmt", "build": "forge build", "dev:db": "forge script script/local/DatabaseResolver.sol --rpc-url http://localhost:8545 --broadcast -vvv --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "dev:eth": "forge script script/local/L1Resolver.sol --rpc-url http://localhost:8545 --broadcast -vvv --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", "dev:arb": "npm-run-all --sequential dev:arb:l2 dev:arb:l1", "dev:arb:l1": "forge script script/local/L1ArbitrumResolver.sol --rpc-url http://localhost:8545 --broadcast -vvv --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", "dev:arb:l2": "forge script script/local/L2ArbitrumResolver.sol --rpc-url http://localhost:8547 --broadcast -vvv --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659" diff --git a/packages/contracts/script/config/L1Config.s.sol b/packages/contracts/script/config/L1Config.s.sol new file mode 100644 index 0000000..1bca30a --- /dev/null +++ b/packages/contracts/script/config/L1Config.s.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +import {ENSRegistry} from "@ens-contracts/registry/ENSRegistry.sol"; +import {ReverseRegistrar} from + "@ens-contracts/reverseRegistrar/ReverseRegistrar.sol"; +import {NameWrapper} from "@ens-contracts/wrapper/NameWrapper.sol"; +import {UniversalResolver} from "@ens-contracts/utils/UniversalResolver.sol"; +import {BaseRegistrarImplementation} from + "@ens-contracts/ethregistrar/BaseRegistrarImplementation.sol"; +import {MockMetadataService} from "../mocks/MetadataService.sol"; +import {DummyOracle} from "@ens-contracts/ethregistrar/DummyOracle.sol"; +import { + StablePriceOracle, + AggregatorInterface +} from "@ens-contracts/ethregistrar/StablePriceOracle.sol"; +import {ETHRegistrarController} from + "@ens-contracts/ethregistrar/ETHRegistrarController.sol"; +import {PublicResolver} from "@ens-contracts/resolvers/PublicResolver.sol"; + +import {ENSHelper} from "../ENSHelper.sol"; +import {DeployHelper} from "../DeployHelper.sol"; +import {SubdomainController} from "../../src/SubdomainController.sol"; + +contract L1Config is Script, ENSHelper, DeployHelper { + + NetworkConfig public activeNetworkConfig; + + struct NetworkConfig { + ENSRegistry registry; + uint256 targetChainId; + PublicResolver resolver; + address registrar; + address nameWrapper; + } + + constructor(uint256 chainId, address sender) { + // if (chainId == 11155111) activeNetworkConfig = _getSepoliaConfig(); + // else if (chainId == 1) activeNetworkConfig = _getMainnetConfig(); + // else activeNetworkConfig = + _getAnvilConfig(sender); + } + + // function _getMainnetConfig() private view returns (NetworkConfig memory) { + // return NetworkConfig({ + // registry: ENSRegistry(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e), + // rollup: IRollupCore(0x5eF0D09d1E6204141B4d37530808eD19f60FBa35), + // targetChainId: 42161, + // resolver: getContractAddress( + // "L2ArbitrumResolver", "PublicResolver", 42161 + // ), + // registrar: getContractAddress("SubdomainController", 42161), + // nameWrapper: getContractAddress("NameWrapper", 42161) + // }); + // } + + // function _getSepoliaConfig() private view returns (NetworkConfig memory) { + // return NetworkConfig({ + // registry: ENSRegistry(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e), + // rollup: IRollupCore(0xd80810638dbDF9081b72C1B33c65375e807281C8), + // targetChainId: 421614, + // resolver: getContractAddress( + // "L2ArbitrumResolver", "PublicResolver", 421614 + // ), + // registrar: getContractAddress("SubdomainController", 421614), + // nameWrapper: getContractAddress("NameWrapper", 421614) + // }); + // } + + function _getAnvilConfig(address sender) + private + returns (NetworkConfig memory) + { + if (address(activeNetworkConfig.registry) != address(0)) { + return activeNetworkConfig; + } + + vm.startBroadcast(sender); + ENSRegistry registry = new ENSRegistry(); + + string[] memory urls = new string[](0); + UniversalResolver universalResolver = + new UniversalResolver(address(registry), urls); + + ReverseRegistrar registrar = new ReverseRegistrar(registry); + + // .reverse + registry.setSubnodeOwner(rootNode, labelhash("reverse"), sender); + // addr.reverse + registry.setSubnodeOwner( + namehash("reverse"), labelhash("addr"), address(registrar) + ); + + BaseRegistrarImplementation baseRegistrar = + new BaseRegistrarImplementation(registry, namehash("eth")); + + // .eth + registry.setSubnodeOwner( + rootNode, labelhash("eth"), address(baseRegistrar) + ); + + MockMetadataService metadata = new MockMetadataService( + "http://ens-metadata-service.appspot.com/name/0x{id}" + ); + NameWrapper nameWrapper = + new NameWrapper(registry, baseRegistrar, metadata); + baseRegistrar.addController(address(nameWrapper)); + + DummyOracle dummyOracle = new DummyOracle(16 gwei); + uint256[] memory rentPrices = new uint256[](5); + rentPrices[0] = 5; + rentPrices[1] = 4; + rentPrices[2] = 3; + rentPrices[3] = 2; + rentPrices[4] = 1; + StablePriceOracle priceOracle = new StablePriceOracle( + AggregatorInterface(address(dummyOracle)), rentPrices + ); + + ETHRegistrarController registrarController = new ETHRegistrarController( + baseRegistrar, priceOracle, 0, 1, registrar, nameWrapper, registry + ); + nameWrapper.setController(address(registrarController), true); + nameWrapper.setController(msg.sender, true); + + uint256 subdomainPrice = 0.001 ether; + SubdomainController subdomainController = + new SubdomainController(address(nameWrapper), subdomainPrice); + nameWrapper.setApprovalForAll(address(subdomainController), true); + + PublicResolver publicResolver = new PublicResolver( + registry, + nameWrapper, + address(subdomainController), + address(registrar) + ); + + registrar.setDefaultResolver(address(publicResolver)); + + vm.stopBroadcast(); + + console.log("Registry deployed at", address(registry)); + console.log("UniversalResolver deployed at", address(universalResolver)); + + activeNetworkConfig = NetworkConfig({ + registry: registry, + targetChainId: 31337, + resolver: publicResolver, + registrar: address(registrar), + nameWrapper: address(nameWrapper) + }); + + return activeNetworkConfig; + } + +} diff --git a/packages/contracts/script/local/L1Resolver.sol b/packages/contracts/script/local/L1Resolver.sol new file mode 100644 index 0000000..ae60982 --- /dev/null +++ b/packages/contracts/script/local/L1Resolver.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +import {ENSRegistry} from "@ens-contracts/registry/ENSRegistry.sol"; +import {ReverseRegistrar} from + "@ens-contracts/reverseRegistrar/ReverseRegistrar.sol"; +import {PublicResolver} from "@ens-contracts/resolvers/PublicResolver.sol"; + +import {L1Verifier} from "@evmgateway/L1Verifier.sol"; +import {ENSHelper} from "../ENSHelper.sol"; +import {L1Resolver} from "../../src/L1Resolver.sol"; +import {L1Config} from "../config/L1Config.s.sol"; + +contract L1ResolverScript is Script, ENSHelper { + + function run() external { + ( + ENSRegistry registry, + uint256 targetChainId, + PublicResolver resolver, + address registrar, + address nameWrapper + ) = (new L1Config(block.chainid, msg.sender)).activeNetworkConfig(); + + string[] memory urls = new string[](1); + urls[0] = "http://127.0.0.1:3000/{sender}/{data}.json"; + + string memory metadataUrl = "https://localhost:3000"; + + vm.startBroadcast(); + + L1Verifier verifier = new L1Verifier(urls); + L1Resolver l1resolver = new L1Resolver( + targetChainId, + address(resolver), + registrar, + nameWrapper, + verifier, + metadataUrl + ); + + // .eth + registry.setSubnodeOwner(rootNode, labelhash("eth"), msg.sender); + // blockful.eth + registry.setSubnodeRecord( + namehash("eth"), + labelhash("blockful"), + msg.sender, + address(l1resolver), + 100000 + ); + resolver.setAddr(namehash("blockful.eth"), msg.sender); + resolver.setText(namehash("blockful.eth"), "com.twitter", "@blockful"); + + vm.stopBroadcast(); + + console.log("L1Resolver deployed at", address(l1resolver)); + } + +} diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index 3979e2d..d771318 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -43,19 +43,19 @@ contract L1Resolver is using EVMFetcher for EVMFetcher.EVMFetchRequest; - //////// CONTRACT VARIABLE STATE //////// + // CONTRACT VARIABLE STATE // // address of each target contract mapping(bytes32 => address) public targets; - //////// CONTRACT IMMUTABLE STATE //////// + // CONTRACT IMMUTABLE STATE // // id of chain that is storing the domains uint256 public immutable chainId; // EVM Verifier to handle data validation based on Merkle Proof IEVMVerifier immutable verifier; - //////// CONSTANTS //////// + // CONSTANTS // /// Universal constant for the ETH coin type. uint256 constant COIN_TYPE_ETH = 60; @@ -70,7 +70,7 @@ contract L1Resolver is bytes32 public constant TARGET_REGISTRAR = keccak256("registrar"); bytes32 public constant TARGET_NAME_WRAPPER = keccak256("nameWrapper"); - //////// INITIALIZER //////// + // INITIALIZER // /** * @notice Initializes the contract with the initial parameters @@ -104,7 +104,7 @@ contract L1Resolver is setTarget(TARGET_NAME_WRAPPER, _target_nameWrapper); } - //////// EIP Operation Router //////// + // EIP Operation Router // /** * @notice Validates and processes write parameters for deferred storage mutations @@ -136,7 +136,7 @@ contract L1Resolver is revert FunctionNotSupported(); } - //////// ENSIP Wildcard Writing //////// + // ENSIP Wildcard Writing // function registerParams( bytes calldata, @@ -157,7 +157,7 @@ contract L1Resolver is getOperationHandler(msg.data); } - //////// ENSIP 10 //////// + // ENSIP 10 // /** * @dev Resolve and verify a record stored in l2 target address. It supports subdomain by fetching target recursively to the nearest parent. @@ -199,7 +199,7 @@ contract L1Resolver is } } - //////// ENS ERC-137 //////// + // ENS ERC-137 // /** * Sets the address associated with an ENS node. @@ -234,7 +234,7 @@ contract L1Resolver is return abi.encode(address(bytes20(values[1]))); } - //////// ENS ERC-2304 //////// + // ENS ERC-2304 // /** * Sets the address associated with an ENS node. @@ -291,7 +291,7 @@ contract L1Resolver is return abi.encode(values[1]); } - //////// ENS ERC-634 //////// + // ENS ERC-634 // /** * Sets the text data associated with an ENS node and key. @@ -347,7 +347,7 @@ contract L1Resolver is return abi.encode(string(values[1])); } - //////// ENS ERC-1577 //////// + // ENS ERC-1577 // /** * Sets the contenthash associated with an ENS node. @@ -387,7 +387,7 @@ contract L1Resolver is return values[1]; } - //////// ENS WRITE DEFERRAL RESOLVER (EIP-5559) //////// + // ENS WRITE DEFERRAL RESOLVER (EIP-5559) // /** * @notice Builds an OperationHandledOnchain error. @@ -396,7 +396,7 @@ contract L1Resolver is revert OperationHandledOnchain(chainId, target); } - //////// ENS ERC-165 //////// + // ENS ERC-165 // function supportsInterface(bytes4 interfaceID) public @@ -418,7 +418,7 @@ contract L1Resolver is || super.supportsInterface(interfaceID); } - //////// PUBLIC WRITE FUNCTIONS //////// + // PUBLIC WRITE FUNCTIONS // /** * @notice Sets the new metadata URL and emits a MetadataUrlSet event. From e283316168dcc477e9a36013c709ffd7d64437ea Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Tue, 27 Jan 2026 15:13:09 -0300 Subject: [PATCH 2/5] chore: read and write fully working locally --- packages/client/src/read.ts | 2 +- packages/client/src/write.ts | 13 ++-- .../contracts/script/config/L1Config.s.sol | 66 +++++++++---------- .../contracts/script/local/L1Resolver.sol | 18 ++--- 4 files changed, 44 insertions(+), 55 deletions(-) diff --git a/packages/client/src/read.ts b/packages/client/src/read.ts index 80ba341..b6c35f9 100644 --- a/packages/client/src/read.ts +++ b/packages/client/src/read.ts @@ -27,7 +27,7 @@ const client = createPublicClient({ // eslint-disable-next-line const _ = (async () => { - const name = normalize('blockful.eth') + const name = normalize('meditation.blockful.eth') const twitter = await client.getEnsText({ name, diff --git a/packages/client/src/write.ts b/packages/client/src/write.ts index c65f5f0..05858bd 100644 --- a/packages/client/src/write.ts +++ b/packages/client/src/write.ts @@ -19,7 +19,7 @@ import { } from 'viem' import { normalize, packetToBytes } from 'viem/ens' import { privateKeyToAccount } from 'viem/accounts' -import { addEnsContracts } from '@ensdomains/ensjs' +// import { addEnsContracts } from '@ensdomains/ensjs' import { abi } from '@blockful/contracts/out/DatabaseResolver.sol/DatabaseResolver.json' import { abi as urAbi } from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' @@ -27,9 +27,7 @@ import { abi as scAbi } from '@blockful/contracts/out/SubdomainController.sol/Su import { MessageData, DomainData } from '@blockful/gateway/src/types' import { getRevertErrorData, getChain, handleDBStorage } from './client' -config({ - path: process.env.ENV_FILE || '../.env', -}) +config() let { UNIVERSAL_RESOLVER_ADDRESS: universalResolver, @@ -46,7 +44,8 @@ if (!chain) { } const client = createPublicClient({ - chain: addEnsContracts(chain), + // chain: addEnsContracts(chain), + chain, transport: http(provider), }).extend(walletActions) console.log(`Connecting to ${chain?.name}.`) @@ -64,7 +63,7 @@ const _ = (async () => { }) } - const name = normalize('meditation.arb.eth') + const name = normalize('meditation.blockful.eth') const encodedName = toHex(packetToBytes(name)) const node = namehash(name) const signer = privateKeyToAccount(privateKey as Hex) @@ -199,7 +198,7 @@ const _ = (async () => { continue } default: - console.error('error registering domain: ', { err }) + throw err } } } diff --git a/packages/contracts/script/config/L1Config.s.sol b/packages/contracts/script/config/L1Config.s.sol index 1bca30a..7052dbb 100644 --- a/packages/contracts/script/config/L1Config.s.sol +++ b/packages/contracts/script/config/L1Config.s.sol @@ -7,6 +7,7 @@ import {ENSRegistry} from "@ens-contracts/registry/ENSRegistry.sol"; import {ReverseRegistrar} from "@ens-contracts/reverseRegistrar/ReverseRegistrar.sol"; import {NameWrapper} from "@ens-contracts/wrapper/NameWrapper.sol"; +import {INameWrapper} from "@ens-contracts/wrapper/INameWrapper.sol"; import {UniversalResolver} from "@ens-contracts/utils/UniversalResolver.sol"; import {BaseRegistrarImplementation} from "@ens-contracts/ethregistrar/BaseRegistrarImplementation.sol"; @@ -29,51 +30,41 @@ contract L1Config is Script, ENSHelper, DeployHelper { NetworkConfig public activeNetworkConfig; struct NetworkConfig { - ENSRegistry registry; uint256 targetChainId; PublicResolver resolver; address registrar; - address nameWrapper; + INameWrapper nameWrapper; } constructor(uint256 chainId, address sender) { - // if (chainId == 11155111) activeNetworkConfig = _getSepoliaConfig(); - // else if (chainId == 1) activeNetworkConfig = _getMainnetConfig(); - // else activeNetworkConfig = - _getAnvilConfig(sender); + if (chainId == 11155111) activeNetworkConfig = _getSepoliaConfig(); + else if (chainId == 1) activeNetworkConfig = _getMainnetConfig(); + else activeNetworkConfig = _getAnvilConfig(sender); } - // function _getMainnetConfig() private view returns (NetworkConfig memory) { - // return NetworkConfig({ - // registry: ENSRegistry(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e), - // rollup: IRollupCore(0x5eF0D09d1E6204141B4d37530808eD19f60FBa35), - // targetChainId: 42161, - // resolver: getContractAddress( - // "L2ArbitrumResolver", "PublicResolver", 42161 - // ), - // registrar: getContractAddress("SubdomainController", 42161), - // nameWrapper: getContractAddress("NameWrapper", 42161) - // }); - // } - - // function _getSepoliaConfig() private view returns (NetworkConfig memory) { - // return NetworkConfig({ - // registry: ENSRegistry(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e), - // rollup: IRollupCore(0xd80810638dbDF9081b72C1B33c65375e807281C8), - // targetChainId: 421614, - // resolver: getContractAddress( - // "L2ArbitrumResolver", "PublicResolver", 421614 - // ), - // registrar: getContractAddress("SubdomainController", 421614), - // nameWrapper: getContractAddress("NameWrapper", 421614) - // }); - // } + function _getMainnetConfig() private view returns (NetworkConfig memory) { + return NetworkConfig({ + targetChainId: 1, + resolver: PublicResolver(getContractAddress("PublicResolver", 1)), + registrar: getContractAddress("SubdomainController", 1), + nameWrapper: NameWrapper(getContractAddress("NameWrapper", 1)) + }); + } + + function _getSepoliaConfig() private view returns (NetworkConfig memory) { + return NetworkConfig({ + targetChainId: 11155111, + resolver: PublicResolver(getContractAddress("PublicResolver", 11155111)), + registrar: getContractAddress("SubdomainController", 11155111), + nameWrapper: NameWrapper(getContractAddress("NameWrapper", 11155111)) + }); + } function _getAnvilConfig(address sender) private returns (NetworkConfig memory) { - if (address(activeNetworkConfig.registry) != address(0)) { + if (address(activeNetworkConfig.nameWrapper) != address(0)) { return activeNetworkConfig; } @@ -123,7 +114,7 @@ contract L1Config is Script, ENSHelper, DeployHelper { baseRegistrar, priceOracle, 0, 1, registrar, nameWrapper, registry ); nameWrapper.setController(address(registrarController), true); - nameWrapper.setController(msg.sender, true); + nameWrapper.setController(sender, true); uint256 subdomainPrice = 0.001 ether; SubdomainController subdomainController = @@ -143,13 +134,16 @@ contract L1Config is Script, ENSHelper, DeployHelper { console.log("Registry deployed at", address(registry)); console.log("UniversalResolver deployed at", address(universalResolver)); + console.log("PublicResolver deployed at", address(publicResolver)); + console.log( + "SubdomainController deployed at", address(subdomainController) + ); activeNetworkConfig = NetworkConfig({ - registry: registry, targetChainId: 31337, resolver: publicResolver, - registrar: address(registrar), - nameWrapper: address(nameWrapper) + registrar: address(subdomainController), + nameWrapper: nameWrapper }); return activeNetworkConfig; diff --git a/packages/contracts/script/local/L1Resolver.sol b/packages/contracts/script/local/L1Resolver.sol index ae60982..d4da161 100644 --- a/packages/contracts/script/local/L1Resolver.sol +++ b/packages/contracts/script/local/L1Resolver.sol @@ -8,7 +8,9 @@ import {ReverseRegistrar} from "@ens-contracts/reverseRegistrar/ReverseRegistrar.sol"; import {PublicResolver} from "@ens-contracts/resolvers/PublicResolver.sol"; +import {INameWrapper} from "@ens-contracts/wrapper/INameWrapper.sol"; import {L1Verifier} from "@evmgateway/L1Verifier.sol"; + import {ENSHelper} from "../ENSHelper.sol"; import {L1Resolver} from "../../src/L1Resolver.sol"; import {L1Config} from "../config/L1Config.s.sol"; @@ -17,11 +19,10 @@ contract L1ResolverScript is Script, ENSHelper { function run() external { ( - ENSRegistry registry, uint256 targetChainId, PublicResolver resolver, address registrar, - address nameWrapper + INameWrapper nameWrapper ) = (new L1Config(block.chainid, msg.sender)).activeNetworkConfig(); string[] memory urls = new string[](1); @@ -36,21 +37,16 @@ contract L1ResolverScript is Script, ENSHelper { targetChainId, address(resolver), registrar, - nameWrapper, + address(nameWrapper), verifier, metadataUrl ); - // .eth - registry.setSubnodeOwner(rootNode, labelhash("eth"), msg.sender); // blockful.eth - registry.setSubnodeRecord( - namehash("eth"), - labelhash("blockful"), - msg.sender, - address(l1resolver), - 100000 + nameWrapper.registerAndWrapETH2LD( + "blockful", msg.sender, 31556952000, address(l1resolver), 1 ); + resolver.setAddr(namehash("blockful.eth"), msg.sender); resolver.setText(namehash("blockful.eth"), "com.twitter", "@blockful"); From f0867ee38c8af99dad706ac07e7f8def750b6208 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Tue, 27 Jan 2026 15:45:54 -0300 Subject: [PATCH 3/5] chore: update docs --- README.md | 50 +++++++++++++++++--------------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 08df816..cba456b 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ The smart contracts are the backbone of the External Resolver. They include the #### Database Resolver +A smart contract that stores ENS domain data off-chain in a database. It enables cost-effective domain management by handling storage operations through the Gateway. + #### L1 Resolver A smart contract that redirects requests to specified external contract deployed to any EVM compatible protocol. @@ -149,19 +151,7 @@ try { To run the External Resolver project in its entirety, you'll need to complete the installation process. Since we provide an off-chain resolver solution, it's essential to set up both the database and the Arbitrum Layer 2 environment. This will enable you to run comprehensive end-to-end tests and verify the functionality of the entire project. -### Prerequisites - -- [Foundry](https://book.getfoundry.sh) - - Run local node by calling `anvil` - -### Setup - -1. Clone this repository to your local machine. -2. Copy the `env.example` file to `.env` in the root directory. -3. Install dependencies: `npm install` -4. Build the contracts: `npm run build` - -#### Database Setup +### Database Setup 1. Run a local PostgreSQL instance (no initial data is inserted): @@ -184,7 +174,7 @@ To run the External Resolver project in its entirety, you'll need to complete th 4. Write properties to a given domain: ```bash - npm run client start:write:db + npm run client start:write:db ``` 5. Request domain properties through the client: @@ -193,7 +183,7 @@ To run the External Resolver project in its entirety, you'll need to complete th npm run client read ``` -##### Migrations +#### Migrations This repository relies on migrations to manage the database schema. To create a new migration, run the following command: @@ -207,7 +197,7 @@ To apply the migration, run the following command: npm run migration:generate -- -n ``` -#### Layer 2 Setup +### Layer 2 Setup 1. Deploy the contracts to the local Arbitrum node (follow the [Arbitrum's local node setup tutorial](https://docs.arbitrum.io/run-arbitrum-node/run-local-dev-node)): @@ -229,40 +219,34 @@ npm run migration:generate -- -n npm run client start ``` -## Deployment - -### Gateway - -Ensure you have the [Railway CLI](https://docs.railway.app/guides/cli) installed. +### Layer 1 Setup -1. Install the Railway CLI: +1. Run the local node: ```bash - npm i -g @railway/cli + anvil ``` -2. Log in to your Railway account: +2. Deploy the contracts to the local node: ```bash - railway login + npm run contracts dev:eth ``` -3. Link the repo to the project: +3. Gather the contract address from the terminal and add it [here](https://github.com/blockful-io/external-resolver/blob/main/packages/contracts/script/local/L1ArbitrumResolver.s.sol#L56) so the L1 domain gets resolved by the L2 contract you just deployed. + +4. Start the gateway: ```bash - railway link + npm run gateway dev:eth ``` -4. Deploy the Gateway: +5. Request domain properties through the client: ```bash - railway up + npm run client read ``` -### Contracts - -1. `npm run contracts deploy:db -- --rpc-url ` - ## Architecture ### High-Level Overview From aa2064ea73b1e5865dd6f25c4537e889c537b006 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Mon, 2 Feb 2026 14:54:53 -0300 Subject: [PATCH 4/5] refactor: add zod env parsing --- package-lock.json | 23 ++++++++++++++++++ packages/client/package.json | 3 ++- packages/client/src/env.ts | 34 ++++++++++++++++++++++++++ packages/client/src/read.ts | 42 ++++++++++++++++---------------- packages/client/src/write.ts | 46 +++++++++++++++--------------------- 5 files changed, 98 insertions(+), 50 deletions(-) create mode 100644 packages/client/src/env.ts diff --git a/package-lock.json b/package-lock.json index 72c9d06..e267c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24890,6 +24890,17 @@ "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", "license": "MIT" }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/ccip-server": { "name": "@blockful/ccip-server", "dependencies": { @@ -25693,6 +25704,9 @@ }, "packages/client": { "name": "@blockful/client", + "dependencies": { + "zod": "^4" + }, "devDependencies": { "@apollo/server": "^4.11.2", "@blockful/ccip-server": "*", @@ -25717,6 +25731,15 @@ "@blockful/tsconfig": "*" } }, + "packages/client/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/contracts": { "name": "@blockful/contracts", "hasInstallScript": true, diff --git a/packages/client/package.json b/packages/client/package.json index fb9d423..00f6c45 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -27,7 +27,8 @@ "reflect-metadata": "^0.2.2", "solidity-coverage": "^0.8.12", "typeorm": "^0.3.20", - "viem": "^2.21.42" + "viem": "^2.21.42", + "zod": "^4" }, "peerDependencies": { "@blockful/tsconfig": "*" diff --git a/packages/client/src/env.ts b/packages/client/src/env.ts new file mode 100644 index 0000000..012e3a8 --- /dev/null +++ b/packages/client/src/env.ts @@ -0,0 +1,34 @@ +import { z } from 'zod' +import { config } from 'dotenv' +import { isAddress, getAddress, isHex } from 'viem' + +config({ path: process.env.ENV_FILE || '../.env' }) + +export const readEnvSchema = z.object({ + CHAIN_ID: z.coerce.number().default(31337), + RPC_URL: z.url().default('http://127.0.0.1:8545/'), + GATEWAY_URL: z + .url() + .default('http://127.0.0.1:3000/{sender}/{data}.json') + .transform((val) => [val]), + UNIVERSAL_RESOLVER_ADDRESS: z + .string() + .refine((val) => isAddress(val, { strict: false }), 'Invalid address') + .transform((val) => getAddress(val)) + .optional(), +}) + +export const writeEnvSchema = z + .object({ + RESOLVER_ADDRESS: z + .string() + .refine((val) => isAddress(val, { strict: false }), 'Invalid address') + .transform((val) => getAddress(val)), + L2_RPC_URL: z.url().optional().default('http://127.0.0.1:8545/'), + PRIVATE_KEY: z + .string() + .min(64) + .max(66) + .refine((val) => isHex(val), 'Private key must be a valid hex string'), + }) + .extend(readEnvSchema.shape) diff --git a/packages/client/src/read.ts b/packages/client/src/read.ts index b6c35f9..f57f05f 100644 --- a/packages/client/src/read.ts +++ b/packages/client/src/read.ts @@ -3,59 +3,57 @@ * Blockchain Node and to redirect the request to a Gateway whenever necessary. */ -import { config } from 'dotenv' -import { Hex, createPublicClient, http } from 'viem' +import { createPublicClient, http } from 'viem' import { normalize } from 'viem/ens' -import { getChain } from './client' -config() +import { getChain } from './client' +import { readEnvSchema } from './env' const { - CHAIN_ID: chainId = '31337', - RPC_URL: provider = 'http://127.0.0.1:8545/', - GATEWAY_URL: gateway = 'http://127.0.0.1:3000/{sender}/{data}.json', + CHAIN_ID, + RPC_URL, + GATEWAY_URL: gatewayUrls, UNIVERSAL_RESOLVER_ADDRESS: universalResolverAddress, -} = process.env +} = readEnvSchema.parse(process.env) -const chain = getChain(parseInt(chainId)) +const chain = getChain(CHAIN_ID) console.log(`Connecting to ${chain?.name}.`) const client = createPublicClient({ chain, - transport: http(provider), + transport: http(RPC_URL), }) // eslint-disable-next-line const _ = (async () => { - const name = normalize('meditation.blockful.eth') + const name = normalize('blockful.eth') const twitter = await client.getEnsText({ name, key: 'com.twitter', - universalResolverAddress: universalResolverAddress as Hex, - gatewayUrls: [gateway], + universalResolverAddress, + gatewayUrls, }) const avatar = await client.getEnsAvatar({ name, - universalResolverAddress: universalResolverAddress as Hex, - gatewayUrls: [gateway], + universalResolverAddress, + gatewayUrls, }) - const address = await client.getEnsAddress({ name, - universalResolverAddress: universalResolverAddress as Hex, - gatewayUrls: [gateway], + universalResolverAddress, + gatewayUrls, }) const addressBtc = await client.getEnsAddress({ name, coinType: 1, - universalResolverAddress: universalResolverAddress as Hex, - gatewayUrls: [gateway], + universalResolverAddress, + gatewayUrls, }) const domainName = await client.getEnsName({ address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', - universalResolverAddress: universalResolverAddress as Hex, - gatewayUrls: [gateway], + universalResolverAddress, + gatewayUrls, }) console.log({ diff --git a/packages/client/src/write.ts b/packages/client/src/write.ts index 05858bd..165ccf8 100644 --- a/packages/client/src/write.ts +++ b/packages/client/src/write.ts @@ -3,7 +3,6 @@ * Blockchain Node and to redirect the request to a Gateway whenever necessary. */ -import { config } from 'dotenv' import { Hex, createPublicClient, @@ -26,19 +25,18 @@ import { abi as urAbi } from '@blockful/contracts/out/UniversalResolver.sol/Univ import { abi as scAbi } from '@blockful/contracts/out/SubdomainController.sol/SubdomainController.json' import { MessageData, DomainData } from '@blockful/gateway/src/types' import { getRevertErrorData, getChain, handleDBStorage } from './client' +import { writeEnvSchema } from './env' -config() - -let { - UNIVERSAL_RESOLVER_ADDRESS: universalResolver, - RESOLVER_ADDRESS: resolver, - CHAIN_ID: chainId = '31337', - RPC_URL: provider = 'http://127.0.0.1:8545/', - L2_RPC_URL: providerL2, +const { + CHAIN_ID, + RPC_URL, + UNIVERSAL_RESOLVER_ADDRESS: universalResolverAddress, + RESOLVER_ADDRESS: resolverAddress, + L2_RPC_URL, PRIVATE_KEY: privateKey, -} = process.env +} = writeEnvSchema.parse(process.env) -const chain = getChain(parseInt(chainId)) +const chain = getChain(CHAIN_ID) if (!chain) { throw new Error('Chain not found') } @@ -46,27 +44,16 @@ if (!chain) { const client = createPublicClient({ // chain: addEnsContracts(chain), chain, - transport: http(provider), + transport: http(RPC_URL), }).extend(walletActions) console.log(`Connecting to ${chain?.name}.`) // eslint-disable-next-line const _ = (async () => { - if (!resolver) { - throw new Error('RESOLVER_ADDRESS is required') - } - - if (!universalResolver) { - universalResolver = getChainContractAddress({ - chain: client.chain, - contract: 'ensUniversalResolver', - }) - } - const name = normalize('meditation.blockful.eth') const encodedName = toHex(packetToBytes(name)) const node = namehash(name) - const signer = privateKeyToAccount(privateKey as Hex) + const signer = privateKeyToAccount(privateKey) const duration = 31556952000n const multicallData = [ @@ -79,7 +66,7 @@ const _ = (async () => { owner: signer.address, duration, secret: zeroHash, - resolver, + resolver: resolverAddress, extraData: zeroHash, }, ], @@ -114,7 +101,12 @@ const _ = (async () => { for (const calldata of multicallData) { try { await client.readContract({ - address: universalResolver as Hex, + address: + universalResolverAddress || + getChainContractAddress({ + chain: client.chain, + contract: 'ensUniversalResolver', + }), abi: urAbi, functionName: 'resolve', args: [ @@ -162,7 +154,7 @@ const _ = (async () => { const l2Client = createPublicClient({ chain: getChain(Number(chainId)), - transport: http(providerL2), + transport: http(L2_RPC_URL), }).extend(walletActions) let value = 0n From fe9c0cd8437df7937c83802a8132fe0da5429b66 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Mon, 2 Feb 2026 16:56:37 -0300 Subject: [PATCH 5/5] chore: update deps lock --- package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e267c52..162974a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25704,9 +25704,6 @@ }, "packages/client": { "name": "@blockful/client", - "dependencies": { - "zod": "^4" - }, "devDependencies": { "@apollo/server": "^4.11.2", "@blockful/ccip-server": "*", @@ -25725,7 +25722,8 @@ "reflect-metadata": "^0.2.2", "solidity-coverage": "^0.8.12", "typeorm": "^0.3.20", - "viem": "^2.21.42" + "viem": "^2.21.42", + "zod": "^4" }, "peerDependencies": { "@blockful/tsconfig": "*" @@ -25735,6 +25733,7 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks"