diff --git a/Dockerfile b/Dockerfile index e9c3b32a..0fd773ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1.2.13 +FROM oven/bun:1.2.21 # Install system dependencies RUN apt-get update && apt-get install -y \ diff --git a/alto/alto-config-l1-local.json b/alto/alto-config-l1-local.json index c2b86ee4..49244056 100644 --- a/alto/alto-config-l1-local.json +++ b/alto/alto-config-l1-local.json @@ -13,5 +13,6 @@ "log-level": "info", "public-client-log-level": "error", "wallet-client-log-level": "error", - "polling-interval": 100 + "polling-interval": 100, + "deploy-simulations-contract": false } \ No newline at end of file diff --git a/alto/alto-config-l1.json b/alto/alto-config-l1.json index 865ea6ec..0443942c 100644 --- a/alto/alto-config-l1.json +++ b/alto/alto-config-l1.json @@ -13,5 +13,6 @@ "log-level": "info", "public-client-log-level": "error", "wallet-client-log-level": "error", - "polling-interval": 100 + "polling-interval": 100, + "deploy-simulations-contract": false } diff --git a/alto/alto-config-l2-local.json b/alto/alto-config-l2-local.json index 417d9232..d7aa7da4 100644 --- a/alto/alto-config-l2-local.json +++ b/alto/alto-config-l2-local.json @@ -13,5 +13,6 @@ "log-level": "info", "public-client-log-level": "error", "wallet-client-log-level": "error", - "polling-interval": 100 + "polling-interval": 100, + "deploy-simulations-contract": false } \ No newline at end of file diff --git a/alto/alto-config-l2.json b/alto/alto-config-l2.json index ab4234b4..00b763d5 100644 --- a/alto/alto-config-l2.json +++ b/alto/alto-config-l2.json @@ -13,5 +13,6 @@ "log-level": "info", "public-client-log-level": "error", "wallet-client-log-level": "error", - "polling-interval": 100 + "polling-interval": 100, + "deploy-simulations-contract": false } diff --git a/bun.lockb b/bun.lockb index 52adb525..89c230de 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/contracts/deploy/l2/02_PriceOracle.ts b/contracts/deploy/l2/02_PriceOracle.ts index 947302ec..fe5504a4 100644 --- a/contracts/deploy/l2/02_PriceOracle.ts +++ b/contracts/deploy/l2/02_PriceOracle.ts @@ -33,4 +33,4 @@ export default execute( console.log(` - MockDAI (18 decimals): ${tokenAddresses[1]}`); }, { tags: ["PriceOracle", "registry", "l2"], dependencies: ["MockTokens"] }, -); +); \ No newline at end of file diff --git a/contracts/lib/ens-contracts b/contracts/lib/ens-contracts index c8615398..20e34971 160000 --- a/contracts/lib/ens-contracts +++ b/contracts/lib/ens-contracts @@ -1 +1 @@ -Subproject commit c8615398f2ac9dcdfa831f519b5ee1b080abf499 +Subproject commit 20e34971fd55f9e3b3cf4a5825d52e1504d36493 diff --git a/contracts/package.json b/contracts/package.json index 99d41d6f..cbe98026 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -12,6 +12,10 @@ "clean": "forge clean && hardhat clean", "devnet": "bun ./script/runDevnet.ts", "aakit": "bun ./script/deployAAkit.ts", + "list-names": "bun ./script/listRegisteredNames.ts", + "watch-names": "bun ./script/watchNameRegistrations.ts", + "cors-proxy": "bun ./script/corsProxy.ts", + "mint-tokens": "bun ./script/mintTokensDirect.ts", "devnet:clean": "bun run test && bun ./script/runDevnet.ts", "check:types": "tsc --noEmit" }, diff --git a/contracts/script/README.md b/contracts/script/README.md new file mode 100644 index 00000000..8937c1e2 --- /dev/null +++ b/contracts/script/README.md @@ -0,0 +1,68 @@ +# Scripts Documentation + +Quick reference for the available scripts in this project. + +## šŸ“‹ List Registered Names + +**Script:** `listRegisteredNames.ts` +**Command:** `bun run list-names` +**Purpose:** Lists all registered domain names in a table format +**Shows:** Name, Owner, Duration, Total Cost (USD) +**Use case:** Check which names are registered and who owns them + +## šŸŖ™ Mint Mock Tokens + +**Script:** `mintTokensDirect.ts` +**Command:** `bun run mint-tokens ` +**Purpose:** Mints 1000 MockUSDC and 1000 MockDAI to a specified address +**Use case:** Give test tokens to addresses for testing name registration + +## šŸ‘€ Watch Name Registrations + +**Script:** `watchNameRegistrations.ts` +**Command:** `bun run watch-names` +**Purpose:** Real-time monitoring of new name registrations +**Shows:** New registrations as they happen with owner and cost details +**Use case:** Monitor registration activity during development/testing + +## 🌐 CORS Proxy + +**Script:** `corsProxy.ts` +**Command:** `bun run cors-proxy` +**Purpose:** Adds CORS headers to Alto bundler responses for frontend access +**Port:** 4339 (proxies to Alto L2 on 4338) +**Use case:** Enable frontend to communicate with AA infrastructure + +## šŸš€ Development Environment + +**Script:** `runDevnet.ts` +**Command:** `bun run devnet` +**Purpose:** Sets up the complete development environment with AA Kit +**Shows:** Contract addresses, endpoints, test accounts +**Use case:** Initialize the local development setup + +## šŸ“ Usage Examples + +```bash +# List all registered names +bun run list-names + +# Mint tokens to an address +bun run mint-tokens 0x1234... + +# Watch for new registrations +bun run watch-names + +# Start CORS proxy +bun run cors-proxy + +# Setup devnet +bun run devnet +``` + +## šŸ”§ Prerequisites + +- Running local blockchain (Anvil/Hardhat) +- AA Kit infrastructure (Alto bundlers, mock paymasters) +- Contracts deployed +- Bun package manager diff --git a/contracts/script/corsProxy.ts b/contracts/script/corsProxy.ts new file mode 100644 index 00000000..84532b9b --- /dev/null +++ b/contracts/script/corsProxy.ts @@ -0,0 +1,46 @@ +import express from 'express'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import cors from 'cors'; + +const app = express(); +const PORT = 4339; + +app.use(cors({ + origin: ['http://localhost:3002'], + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], + credentials: true +})); + +app.get('/health', (req, res) => { + res.json({ status: 'ok', service: 'cors-proxy', timestamp: new Date().toISOString() }); +}); + +app.use('/', createProxyMiddleware({ + target: 'http://localhost:4338', + changeOrigin: true, + pathRewrite: { + '^/': '/' + }, + onProxyRes: (proxyRes, req, res) => { + proxyRes.headers['Access-Control-Allow-Origin'] = '*'; + proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'; + proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'; + proxyRes.headers['Access-Control-Allow-Credentials'] = 'true'; + }, + onError: (err, req, res) => { + console.error('Proxy error:', err); + res.status(500).json({ error: 'Proxy error', message: err.message }); + } +})); + +app.listen(PORT, () => { + console.log(`CORS Proxy running on http://localhost:${PORT}`); + console.log(`Proxying requests to Alto L2 at http://localhost:4338`); + console.log(`CORS enabled for frontend at http://localhost:3002`); +}); + +process.on('SIGINT', () => { + console.log('\nShutting down CORS Proxy...'); + process.exit(0); +}); diff --git a/contracts/script/listRegisteredNames.ts b/contracts/script/listRegisteredNames.ts new file mode 100644 index 00000000..4047e866 --- /dev/null +++ b/contracts/script/listRegisteredNames.ts @@ -0,0 +1,87 @@ +import { createPublicClient, http, parseAbiItem, decodeEventLog } from 'viem'; +import { localhost } from 'viem/chains'; + +const RPC_URL = 'http://localhost:8546'; +const ETH_REGISTRAR_ADDRESS = '0xa513e6e4b8f2a923d98304ec87f64353c4d5c853'; + +const client = createPublicClient({ + chain: localhost, + transport: http(RPC_URL), +}); + +const nameRegisteredEvent = parseAbiItem( + 'event NameRegistered(string name, address owner, address subregistry, address resolver, uint64 duration, uint256 tokenId, uint256 baseCost, uint256 premium)' +); + +function shortenAddress(address: string): string { + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +async function listRegisteredNames() { + console.log('Fetching all registered names...\n'); + + try { + const logs = await client.getLogs({ + address: ETH_REGISTRAR_ADDRESS as `0x${string}`, + event: nameRegisteredEvent, + fromBlock: 0n, + toBlock: 'latest', + }); + + console.log(`Found ${logs.length} registered names\n`); + + if (logs.length === 0) { + console.log('No names found'); + return; + } + + const namesData = logs.map((log, index) => { + try { + const decoded = decodeEventLog({ + abi: [nameRegisteredEvent], + data: log.data, + topics: log.topics, + }); + + const { name, owner, duration, baseCost, premium } = decoded.args; + const totalCost = Number(baseCost) + Number(premium); + const durationInDays = Math.floor(Number(duration) / 86400); + + return { + '#': index + 1, + 'Name': name, + 'Owner': shortenAddress(owner), + 'Duration (days)': durationInDays, + 'Total Cost (USD)': (totalCost / 1e8).toFixed(2), + }; + } catch (error) { + return { + '#': index + 1, + 'Name': 'Error decoding', + 'Owner': 'Error', + 'Duration (days)': 'Error', + 'Total Cost (USD)': 'Error', + }; + } + }); + + console.table(namesData); + + console.log('\nSummary:'); + console.log('==========='); + + const uniqueOwners = new Set(namesData.map(item => item.Owner).filter(owner => owner !== 'Error')); + const totalCost = namesData + .filter(item => item['Total Cost (USD)'] !== 'Error') + .reduce((sum, item) => sum + parseFloat(item['Total Cost (USD)']), 0); + + console.log(`Total Names: ${namesData.length}`); + console.log(`Unique Owners: ${uniqueOwners.size}`); + console.log(`Total Value: $${totalCost.toFixed(2)} USD`); + + } catch (error) { + console.error('Error fetching names:', error); + } +} + +listRegisteredNames().catch(console.error); diff --git a/contracts/script/mintTokensDirect.ts b/contracts/script/mintTokensDirect.ts new file mode 100644 index 00000000..5989197a --- /dev/null +++ b/contracts/script/mintTokensDirect.ts @@ -0,0 +1,126 @@ +#!/usr/bin/env bun +import { createPublicClient, createWalletClient, http, parseEther, parseUnits } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +const MOCK_USDC_ADDRESS = '0xe7f1725e7734ce288f8367e1bb143e90bb3f0512'; +const MOCK_DAI_ADDRESS = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'; + +const MOCK_ERC20_ABI = [ + { + inputs: [ + { name: 'to', type: 'address' }, + { name: 'amount', type: 'uint256' } + ], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [{ name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +]; + +async function main() { + const targetWallet = process.argv[2]; + + if (!targetWallet) { + console.error('Usage: bun run script/mintTokensDirect.ts '); + console.error('Example: bun run script/mintTokensDirect.ts 0x1234...'); + process.exit(1); + } + + console.log(`Minting mock tokens to: ${targetWallet}`); + console.log(`MockUSDC: ${MOCK_USDC_ADDRESS}`); + console.log(`MockDAI: ${MOCK_DAI_ADDRESS}`); + + const l2Client = createPublicClient({ + chain: { + id: 31338, + name: 'L2 Local', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { + default: { http: ['http://127.0.0.1:8546'] }, + public: { http: ['http://127.0.0.1:8546'] }, + }, + }, + transport: http('http://127.0.0.1:8546'), + }); + + const deployerAccount = privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'); + + const walletClient = createWalletClient({ + account: deployerAccount, + chain: { + id: 31338, + name: 'L2 Local', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { + default: { http: ['http://127.0.0.1:8546'] }, + public: { http: ['http://127.0.0.1:8546'] }, + }, + }, + transport: http('http://127.0.0.1:8546'), + }); + + try { + const usdcAmount = parseUnits('1000', 6); + console.log(`Minting 1000 USDC...`); + + const usdcMintTx = await walletClient.writeContract({ + address: MOCK_USDC_ADDRESS, + abi: MOCK_ERC20_ABI, + functionName: 'mint', + args: [targetWallet, usdcAmount], + }); + + console.log(`USDC minted! Transaction: ${usdcMintTx}`); + + const daiAmount = parseEther('1000'); + console.log(`Minting 1000 DAI...`); + + const daiMintTx = await walletClient.writeContract({ + address: MOCK_DAI_ADDRESS, + abi: MOCK_ERC20_ABI, + functionName: 'mint', + args: [targetWallet, daiAmount], + }); + + console.log(`DAI minted! Transaction: ${daiMintTx}`); + + console.log('Waiting for transactions to be mined...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + + const usdcBalance = await l2Client.readContract({ + address: MOCK_USDC_ADDRESS, + abi: MOCK_ERC20_ABI, + functionName: 'balanceOf', + args: [targetWallet], + }); + + const daiBalance = await l2Client.readContract({ + address: MOCK_DAI_ADDRESS, + abi: MOCK_ERC20_ABI, + functionName: 'balanceOf', + args: [targetWallet], + }); + + console.log('\nToken minting complete!'); + console.log(`${targetWallet} now has:`); + console.log(` - USDC: ${Number(usdcBalance) / 1e6} USDC`); + console.log(` - DAI: ${Number(daiBalance) / 1e18} DAI`); + console.log('\nYou can now use these tokens to register names from your frontend!'); + console.log(` - Each name registration costs $10 (in either USDC or DAI)`); + console.log(` - Make sure to approve the ETHRegistrar contract to spend your tokens`); + + } catch (error) { + console.error('Error minting tokens:', error); + process.exit(1); + } +} + +main().catch(console.error); \ No newline at end of file diff --git a/contracts/script/runDevnet.ts b/contracts/script/runDevnet.ts index 9bd2ebb4..96c134e0 100644 --- a/contracts/script/runDevnet.ts +++ b/contracts/script/runDevnet.ts @@ -2,10 +2,35 @@ import { toHex } from "viem"; import { createMockRelay } from "./mockRelay.js"; import { setupCrossChainEnvironment } from "./setup.js"; +const banner = [ + "ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—", + "ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘", + "ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘", + "ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘", + "ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘", + "ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•ā•" +]; + +// RGB gradient from #0080bc to darker shades +const rgbColors = [ + '\x1b[38;2;0;128;188m', // #0080bc (your exact color) + '\x1b[38;2;0;110;160m', // Slightly darker + '\x1b[38;2;0;95;140m', // Medium dark + '\x1b[38;2;0;80;120m', // Darker + '\x1b[38;2;0;65;100m', // Much darker + '\x1b[38;2;0;50;80m' // Very dark +]; +console.log('\n'); +banner.forEach((line, i) => { + console.log(rgbColors[i] + line + '\x1b[0m'); +}); +console.log('\n'); +console.log("šŸš€ Starting NameChain Development Environment...\n"); + const env = await setupCrossChainEnvironment({ l1Port: 8545, - l2Port: 8456, - urgPort: 8457, + l2Port: 8546, + urgPort: 8547, saveDeployments: true, }); @@ -22,17 +47,40 @@ createMockRelay({ l2Client: env.l2.client, }); -console.log("\nAvailable Test Accounts:"); -console.log("========================"); +console.log("\nšŸ“‹ Available Test Accounts:"); +console.log("============================"); console.table(env.accounts.map(({ name, address }, i) => ({ name, address }))); -console.log("\nDeployments:"); -console.log("============"); -console.log({ - urg: (({ gateway, ...a }) => a)(env.urg), - l1: dump(env.l1), - l2: dump(env.l2), -}); +console.log("\nšŸ—ļø Deployments:"); +console.log("================="); + +const urgDeployment = (({ gateway, ...a }) => a)(env.urg); +const l1Deployment = dump(env.l1); +const l2Deployment = dump(env.l2); + +console.log("\nšŸ”— URG (Universal Resolver Gateway):"); +console.table(Object.entries(urgDeployment).map(([key, value]) => ({ + Component: key, + Address: typeof value === 'string' ? value : JSON.stringify(value) +}))); + +console.log("\n🌐 L1 (Layer 1):"); +console.table(Object.entries(l1Deployment.contracts).map(([name, address]) => ({ + Contract: name, + Address: address +}))); + +console.log("\n⚔ L2 (Layer 2):"); +console.table(Object.entries(l2Deployment.contracts).map(([name, address]) => ({ + Contract: name, + Address: address +}))); + +console.log("\nšŸ”Œ Endpoints:"); +console.table([ + { Layer: "L1", Endpoint: l1Deployment.endpoint, ChainID: l1Deployment.chain }, + { Layer: "L2", Endpoint: l2Deployment.endpoint, ChainID: l2Deployment.chain } +]); function dump(deployment: typeof env.l1 | typeof env.l2) { const { client, hostPort, contracts } = deployment; diff --git a/contracts/script/watchNameRegistrations.ts b/contracts/script/watchNameRegistrations.ts new file mode 100644 index 00000000..f513e65e --- /dev/null +++ b/contracts/script/watchNameRegistrations.ts @@ -0,0 +1,133 @@ +import { createPublicClient, http, parseAbiItem, decodeEventLog } from 'viem'; +import { localhost } from 'viem/chains'; + +const RPC_URL = 'http://localhost:8546'; +const ETH_REGISTRAR_ADDRESS = '0xa513e6e4b8f2a923d98304ec87f64353c4d5c853'; +const POLL_INTERVAL = 2000; + +const client = createPublicClient({ + chain: localhost, + transport: http(RPC_URL), +}); + +const nameRegisteredEvent = parseAbiItem( + 'event NameRegistered(string name, address owner, address subregistry, address resolver, uint64 duration, uint256 tokenId, uint256 baseCost, uint256 premium)' +); + +const processedEvents = new Set(); + +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + white: '\x1b[37m', + red: '\x1b[31m', +}; + +function log(message: string, color: keyof typeof colors = 'reset') { + const timestamp = new Date().toLocaleTimeString(); + console.log(`${colors[color]}[${timestamp}] ${message}${colors.reset}`); +} + +function displayNewRegistration(name: string, owner: string, baseCost: string, premium: string) { + const totalCost = Number(baseCost) + Number(premium); + const costInUSD = (totalCost / 1e8).toFixed(2); + + log('NEW NAME REGISTERED!', 'green'); + log('======================', 'green'); + log(`Name: ${colors.bright}${name}${colors.reset}`, 'white'); + log(`Owner: ${colors.cyan}${owner}${colors.reset}`, 'white'); + log(`Total Cost: ${colors.yellow}$${costInUSD}${colors.reset}`, 'white'); + + if (Number(premium) > 0) { + log(` └─ Base: $${(Number(baseCost) / 1e8).toFixed(2)} | Premium: $${(Number(premium) / 1e8).toFixed(2)}`, 'white'); + } + log(''); +} + +async function checkForNewRegistrations() { + try { + const logs = await client.getLogs({ + address: ETH_REGISTRAR_ADDRESS as `0x${string}`, + event: nameRegisteredEvent, + fromBlock: 0n, + toBlock: 'latest', + }); + + logs.forEach(log => { + const eventKey = `${log.transactionHash}-${log.logIndex}`; + + if (!processedEvents.has(eventKey)) { + try { + const decoded = decodeEventLog({ + abi: [nameRegisteredEvent], + data: log.data, + topics: log.topics, + }); + + const { name, owner, baseCost, premium } = decoded.args; + + displayNewRegistration( + name, + owner, + baseCost.toString(), + premium.toString() + ); + + processedEvents.add(eventKey); + + } catch (error) { + // Skip invalid logs + } + } + }); + + } catch (error) { + log(`Error checking for new registrations: ${error}`, 'red'); + } +} + +async function watchRegistrations() { + log('Starting Name Registration Monitor...', 'green'); + log(`Watching ETHRegistrar: ${ETH_REGISTRAR_ADDRESS}`, 'blue'); + log(`Polling every ${POLL_INTERVAL / 1000} seconds`, 'blue'); + log('Try registering a new name to see it here!', 'yellow'); + log('Press Ctrl+C to stop monitoring\n', 'yellow'); + + try { + log('Loading existing registrations...', 'cyan'); + const existingLogs = await client.getLogs({ + address: ETH_REGISTRAR_ADDRESS as `0x${string}`, + event: nameRegisteredEvent, + fromBlock: 0n, + toBlock: 'latest', + }); + + log(`Found ${existingLogs.length} existing registrations (will show new ones only)`, 'cyan'); + + existingLogs.forEach(log => { + const eventKey = `${log.transactionHash}-${log.logIndex}`; + processedEvents.add(eventKey); + }); + + log('\nMonitoring for new registrations...', 'green'); + + const pollInterval = setInterval(async () => { + await checkForNewRegistrations(); + }, POLL_INTERVAL); + + process.on('SIGINT', () => { + log('\nStopping monitor...', 'yellow'); + clearInterval(pollInterval); + process.exit(0); + }); + + } catch (error) { + log(`Error setting up monitor: ${error}`, 'red'); + } +} + +watchRegistrations().catch(console.error); diff --git a/contracts/test/utils/ndps.ts b/contracts/test/utils/ndps.ts new file mode 100644 index 00000000..7fb2294f --- /dev/null +++ b/contracts/test/utils/ndps.ts @@ -0,0 +1,68 @@ +type Rounding = "nearest" | "floor" | "ceil"; + +const SECONDS_PER_YEAR_BI = 31_557_600n; // exact (365.25 * 86400) +const ND_PER_DOLLAR_BI = 1_000_000_000n; // nanodollars per dollar + +function divRound(n: bigint, d: bigint, mode: Rounding): bigint { + if (mode === "floor") return n / d; + if (mode === "ceil") return (n + d - 1n) / d; + // nearest, half-up (inputs assumed non-negative) + return (n + d / 2n) / d; +} + +/** + * exact: dollars/year (string, up to 9 decimal places) -> nd/s (bigint) + * examples of valid inputs: "12", "12.3", "12.345678901" + */ +export function dollarsPerYearToNdpsExact( + dollarsPerYear: string, + rounding: Rounding = "nearest", +): bigint { + const s = dollarsPerYear.trim(); + if (!/^\d+(\.\d{0,9})?$/.test(s)) { + throw new Error("use dollars with up to 9 decimal places"); + } + const [dollars, fracRaw = ""] = s.split("."); + const frac = (fracRaw + "000000000").slice(0, 9); // pad to 9 places + const ndPerYear = BigInt(dollars) * ND_PER_DOLLAR_BI + BigInt(frac); // already nanodollars + + return divRound(ndPerYear, SECONDS_PER_YEAR_BI, rounding); +} + +const POW10N: readonly bigint[] = [ + 1n, + 10n, + 100n, + 1_000n, + 10_000n, + 100_000n, + 1_000_000n, + 10_000_000n, + 100_000_000n, + 1_000_000_000n, +]; + +function formatFixed(n: bigint, decimals: number): string { + if (decimals === 0) return n.toString(); + const base = POW10N[decimals]; + const i = n / base; + const f = (n % base).toString().padStart(decimals, "0"); + return `${i}.${f}`; +} + +/** + * exact: nd/s (bigint) -> $/yr as a decimal string with `decimals` places (0..9) + * default 9 decimals gives the exact dollar value since inputs are in nanodollars. + */ +export function ndpsToDollarsPerYearStringExact( + ndps: bigint, + decimals: number = 9, + rounding: Rounding = "nearest", +): string { + if (decimals < 0 || decimals > 9) throw new Error("decimals must be 0..9"); + const ndPerYear = ndps * SECONDS_PER_YEAR_BI; // nanodollars/year (exact) + const scaleDown = 9 - decimals; // convert nd -> dollars with N decimals + const divisor = POW10N[scaleDown]; + const scaled = divRound(ndPerYear, divisor, rounding); // integer of dollars * 10^decimals + return formatFixed(scaled, decimals); +}