├── .node-version ├── .tool-versions ├── src ├── abi │ ├── factories │ │ ├── index.ts │ │ └── IdRegistry__factory.ts │ ├── index.ts │ ├── common.ts │ ├── idRegistry.abi │ └── IdRegistry.ts ├── log.ts ├── ethereum.ts ├── env.ts ├── index.ts ├── tracing.ts ├── migrations │ └── 20230616130000_initial_migration.ts ├── reserved.ts ├── db.ts ├── signature.ts ├── util.ts ├── app.ts └── transfers.ts ├── .dockerignore ├── .prettierrc.cjs ├── .gitignore ├── jest.config.ts ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── cd.yml ├── tsconfig.json ├── README.md ├── docker-compose.yml ├── .vscode └── settings.json ├── .eslintrc.cjs ├── tests ├── utils.ts ├── transfers.test.ts └── index.test.ts ├── package.json ├── Dockerfile └── .aws └── fname-registry-api.json /.node-version: -------------------------------------------------------------------------------- 1 | 22.2.0 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | # Specify desired versions of all the tools this repository requires, then run: 2 | # 3 | # asdf install 4 | 5 | nodejs 22.2.0 6 | -------------------------------------------------------------------------------- /src/abi/factories/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { IdRegistry__factory } from './IdRegistry__factory.js'; 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.db 3 | README* 4 | jest.config.* 5 | Dockerfile* 6 | node_modules 7 | tests 8 | tmp 9 | .git 10 | .github 11 | .gitignore 12 | .tool-versions 13 | .node-version 14 | .vscode 15 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | import { pino } from 'pino'; 2 | 3 | export const log = pino({ 4 | transport: { 5 | target: 'pino-pretty', 6 | options: { 7 | colorize: true, 8 | }, 9 | }, 10 | }); 11 | 12 | export type Logger = pino.Logger; 13 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | useTabs: false, 8 | parserOptions: { 9 | project: ['tsconfig.json'], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/abi/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { IdRegistry } from './IdRegistry.js'; 5 | export * as factories from './factories/index.js'; 6 | export { IdRegistry__factory } from './factories/IdRegistry__factory.js'; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | yarn-debug.log* 3 | yarn-error.log* 4 | 5 | # Compiled binary addons (https://nodejs.org/api/addons.html) 6 | /build/Release 7 | /build 8 | 9 | # Dependency directories 10 | node_modules/ 11 | 12 | # TypeScript cache 13 | *.tsbuildinfo 14 | 15 | # dotenv environment variables file 16 | .env 17 | 18 | .eslintcache 19 | 20 | /.db 21 | 22 | coverage/ 23 | -------------------------------------------------------------------------------- /src/ethereum.ts: -------------------------------------------------------------------------------- 1 | import { AlchemyProvider } from 'ethers'; 2 | import { OP_ALCHEMY_SECRET, ID_REGISTRY_ADDRESS } from './env.js'; 3 | import { IdRegistry } from './abi/IdRegistry.js'; 4 | import { IdRegistry__factory } from './abi/index.js'; 5 | 6 | export function getIdRegistryContract(): IdRegistry { 7 | const provider = new AlchemyProvider('optimism', OP_ALCHEMY_SECRET); 8 | return IdRegistry__factory.connect(ID_REGISTRY_ADDRESS, provider); 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest/presets/default-esm', 4 | testEnvironment: 'node', 5 | extensionsToTreatAsEsm: ['.ts'], 6 | moduleNameMapper: { 7 | '^(\\.{1,2}/.*)\\.js$': '$1', 8 | }, 9 | transform: { 10 | // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` 11 | // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` 12 | '^.+\\.tsx?$': [ 13 | 'ts-jest', 14 | { 15 | useESM: true, 16 | }, 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | assignees: 9 | - "sds" 10 | 11 | # Maintain dependencies for npm 12 | - package-ecosystem: "npm" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | assignees: 17 | - "sds" 18 | 19 | # Maintain dependencies for Docker images 20 | - package-ecosystem: "docker" 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | assignees: 25 | - "sds" 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "lib": ["esnext"], 7 | "module": "nodenext", 8 | "moduleResolution": "nodenext", 9 | "noImplicitAny": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "build", 14 | "baseUrl": "./src", 15 | "inlineSourceMap": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "es2022", 20 | "noErrorTruncation": true 21 | }, 22 | "include": ["src/**/*.ts", "tests/**/*.ts", "jest.config.ts"], 23 | "exclude": [ 24 | "build", 25 | "node_modules" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fname Registry 2 | 3 | A centralized registry for Farcaster usernames (fnames). Exposes an HTTP API for getting information about recent registrations, etc. 4 | 5 | ## Getting started 6 | 7 | 1. Start Postgres: `docker compose up --detach`. 8 | 2. Create test DB: `echo 'create database registry_test' | PGPASSWORD=password psql -h localhost -p 6543 -U app registry_dev` 9 | 3. Install packages: `yarn install` 10 | 4. Run tests: `yarn test` 11 | 5. Run the server locally: `yarn start` 12 | 13 | ## Deploying 14 | 15 | Once a change is merged it is deployed automatically. Check the GitHub Actions workflow result to confirm whether the deploy was successful. 16 | 17 | ## Updating Node.js version 18 | 19 | 1. Update `.node-version` and `.tool-versions` files 20 | 2. Update `Dockerfile` Node.js base image version to match 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | postgres: 5 | image: 'postgres:15-alpine' 6 | restart: unless-stopped 7 | ports: 8 | - '6543:5432' # Use a port unlikely to be in use so the example "Just Works" 9 | environment: 10 | - POSTGRES_DB=registry_dev 11 | - POSTGRES_USER=app 12 | - POSTGRES_PASSWORD=password 13 | volumes: 14 | - pgdata:/var/lib/postgresql/data 15 | healthcheck: 16 | # Need to specify name/user to avoid `FATAL: role "root" does not exist` errors in logs 17 | test: ['CMD-SHELL', 'env', 'pg_isready', '--dbname', '$$POSTGRES_DB', '-U', '$$POSTGRES_USER'] 18 | interval: 10s 19 | timeout: 10s 20 | retries: 3 21 | networks: 22 | - my_network 23 | 24 | volumes: 25 | pgdata: 26 | 27 | networks: 28 | my_network: 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | ".git": true, 4 | ".eslintcache": true, 5 | "build/{release,app/dist}": true, 6 | "node_modules": true, 7 | "npm-debug.log.*": true, 8 | "test/**/__snapshots__": true, 9 | "yarn.lock": true, 10 | "*.{css,sass,scss}.d.ts": true 11 | }, 12 | "editor.formatOnSave": true, 13 | "[json]": { 14 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 15 | }, 16 | "[html]": { 17 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 18 | }, 19 | "[typescript]": { 20 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 21 | }, 22 | "[typescriptreact]": { 23 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 24 | }, 25 | "[javascript]": { 26 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 27 | }, 28 | "editor.codeActionsOnSave": { 29 | "source.fixAll.eslint": "explicit" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | export const ENVIRONMENT = process.env['ENVIRONMENT'] || 'dev'; 6 | export const SERVICE = process.env['DD_SERVICE'] || 'fname-registry'; 7 | 8 | export const OP_ALCHEMY_SECRET = process.env['OP_ALCHEMY_SECRET'] || ''; 9 | if (OP_ALCHEMY_SECRET === '') { 10 | throw new Error('OP_ALCHEMY_SECRET missing from .env'); 11 | } 12 | 13 | export const WARPCAST_ADDRESS = process.env['WARPCAST_ADDRESS'] || ''; 14 | if (WARPCAST_ADDRESS === '') { 15 | throw new Error('WARPCAST_ADDRESS missing from .env'); 16 | } 17 | 18 | // Address of the ENS CCIP verifier contract 19 | export const CCIP_ADDRESS = process.env['CCIP_ADDRESS'] || ''; 20 | if (WARPCAST_ADDRESS === '') { 21 | throw new Error('CCIP_ADDRESS missing from .env'); 22 | } 23 | 24 | export const ID_REGISTRY_ADDRESS = process.env['ID_REGISTRY_ADDRESS'] || '0x00000000fc6c5f01fc30151999387bb99a9f489b'; 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './tracing.js'; 2 | import { app } from './app.js'; 3 | import { log } from './log.js'; 4 | 5 | const port = process.env.PORT || '3000'; 6 | const server = app.listen(port, () => { 7 | log.info(`⚡️[server]: Server is running at http://localhost:${port}`); 8 | }); 9 | 10 | // Ensure this is larger than the ALB's keep-alive timeout (default 60s). 11 | // See: https://adamcrowder.net/posts/node-express-api-and-aws-alb-502/ 12 | server.keepAliveTimeout = 61000; 13 | 14 | const gracefulShutdown = async () => { 15 | // TODO: Close any open connections/resources to other services (currently none?) 16 | process.exit(); 17 | }; 18 | 19 | for (const signal of ['SIGTERM', 'SIGINT']) { 20 | process.once(signal, async (signalName: string) => { 21 | log.info(`Process received signal ${signalName}`); 22 | process.exitCode = 23 | { 24 | SIGINT: 130, 25 | SIGTERM: 143, 26 | }[signalName] || 1; 27 | await gracefulShutdown(); 28 | }); 29 | } 30 | 31 | process.on('exit', (code) => { 32 | log.info(`Exiting process with code ${code}`); 33 | }); 34 | -------------------------------------------------------------------------------- /src/tracing.ts: -------------------------------------------------------------------------------- 1 | import pkg from 'dd-trace'; 2 | import { ENVIRONMENT, SERVICE } from './env.js'; 3 | 4 | const { tracer } = pkg; 5 | 6 | tracer.init({ 7 | env: ENVIRONMENT, 8 | // Service name that appears in the Datadog UI 9 | service: SERVICE, 10 | // Include trace ID in log messages 11 | logInjection: ENVIRONMENT !== 'test', 12 | // Collect metrics on NodeJS CPU, memory, heap, event loop delay, GC events, etc. 13 | // See https://docs.datadoghq.com/tracing/runtime_metrics/nodejs#data-collected 14 | // for a list of statsd metrics. 15 | runtimeMetrics: ENVIRONMENT !== 'test', 16 | // Log configuration on startup 17 | startupLogs: ENVIRONMENT === 'prod', 18 | }); 19 | 20 | tracer.use('express', { 21 | enabled: true, 22 | service: SERVICE, 23 | blocklist: ['/_health'], 24 | }); 25 | 26 | tracer.use('pg', { 27 | enabled: true, 28 | service: SERVICE, 29 | }); 30 | 31 | tracer.use('http', { 32 | enabled: true, 33 | service: SERVICE, 34 | }); 35 | 36 | tracer.use('http2', { 37 | enabled: true, 38 | service: SERVICE, 39 | }); 40 | 41 | tracer.use('net', { 42 | enabled: true, 43 | service: SERVICE, 44 | }); 45 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | tsconfigRootDir: __dirname, 6 | project: ['./tsconfig.json'], 7 | extraFileExtensions: null, 8 | }, 9 | plugins: ['@typescript-eslint'], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:prettier/recommended', 15 | ], 16 | rules: { 17 | '@typescript-eslint/ban-ts-ignore': 'off', 18 | // Don't require return types on methods/functions 19 | '@typescript-eslint/explicit-function-return-type': 'off', 20 | // Don't prevent us from typing as `any` (for now) 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 23 | // Prevent `await`ing on non-Thenable values 24 | '@typescript-eslint/await-thenable': 'error', 25 | // Prevent using unresolved promises in `if` statements or void functions 26 | '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }], 27 | // Require promise-like statements to be explicitly handled 28 | '@typescript-eslint/no-floating-promises': ['error', { ignoreIIFE: true }], 29 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }], 30 | }, 31 | reportUnusedDisableDirectives: true, 32 | }; 33 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { generateSignature, signer, signerAddress, signerFid } from '../src/signature.js'; 2 | import { createTransfer } from '../src/transfers.js'; 3 | import { currentTimestamp } from '../src/util.js'; 4 | import { Database } from '../src/db.js'; 5 | import { Kysely } from 'kysely'; 6 | import { bytesToHex } from '../src/util.js'; 7 | import { jest } from '@jest/globals'; 8 | 9 | type TestTransferParams = { 10 | username: string; 11 | from?: number; 12 | to: number; 13 | timestamp?: number; 14 | owner?: string; 15 | userSignature?: Uint8Array; 16 | userFid?: number; 17 | }; 18 | 19 | export async function createTestTransfer(db: Kysely, opts: TestTransferParams, idOfOwner?: number) { 20 | opts.timestamp = opts.timestamp ?? currentTimestamp(); 21 | opts.from = opts.from ?? 0; 22 | opts.owner = opts.owner ?? signerAddress; 23 | opts.userSignature = 24 | opts.userSignature ?? (await generateSignature(opts.username, opts.timestamp, opts.owner, signer)); 25 | const userFid = opts.userFid ?? signerFid; 26 | const idRegistry = { idOf: jest.fn().mockReturnValue(Promise.resolve(idOfOwner || 0)) } as any; 27 | return createTransfer( 28 | { 29 | timestamp: opts.timestamp, 30 | username: opts.username, 31 | owner: opts.owner, 32 | from: opts.from, 33 | to: opts.to, 34 | userSignature: bytesToHex(opts.userSignature), 35 | userFid: userFid, 36 | }, 37 | db, 38 | idRegistry as any 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/migrations/20230616130000_initial_migration.ts: -------------------------------------------------------------------------------- 1 | import { Kysely, sql } from 'kysely'; 2 | 3 | export const up = async (db: Kysely) => { 4 | await db.schema 5 | .createTable('transfers') 6 | .addColumn('id', 'bigint', (col) => col.generatedAlwaysAsIdentity().primaryKey()) 7 | .addColumn('createdAt', 'timestamp', (col) => col.notNull().defaultTo(sql`current_timestamp`)) 8 | .addColumn('updatedAt', 'timestamp', (col) => col.notNull().defaultTo(sql`current_timestamp`)) 9 | .addColumn('timestamp', 'integer', (col) => col.notNull()) 10 | .addColumn('username', 'text', (col) => col.notNull()) 11 | .addColumn('owner', 'bytea', (col) => col.notNull()) 12 | .addColumn('from', 'integer', (col) => col.notNull()) 13 | .addColumn('to', 'integer', (col) => col.notNull()) 14 | .addColumn('user_signature', 'bytea', (col) => col.notNull()) 15 | .addColumn('server_signature', 'bytea', (col) => col.notNull()) 16 | .addColumn('user_fid', 'integer') 17 | .execute(); 18 | 19 | await db.schema.createIndex('transfers_from_index').on('transfers').columns(['from']).execute(); 20 | 21 | await db.schema.createIndex('transfers_to_index').on('transfers').columns(['to']).execute(); 22 | 23 | // Cannot have two proofs for the same username at the same time 24 | await db.schema 25 | .createIndex('transfers_username_timestamp_unique') 26 | .on('transfers') 27 | .columns(['username', 'timestamp']) 28 | .execute(); 29 | }; 30 | 31 | export const down = async (db: Kysely) => { 32 | await db.schema.dropTable('transfers').ifExists().execute(); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fname-registry", 3 | "version": "0.1.0", 4 | "description": "fname authority", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "license": "MIT", 8 | "scripts": { 9 | "ts-node": "TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true node --experimental-specifier-resolution=node --loader ts-node/esm", 10 | "start": "npm run ts-node src/index.ts", 11 | "build": "rm -rf ./build && tsc --project ./tsconfig.json", 12 | "test": "NODE_OPTIONS='--no-warnings --experimental-vm-modules' ENVIRONMENT=test jest --detectOpenHandles --forceExit", 13 | "lint": "eslint . --ext .ts", 14 | "abi": "typechain --node16-modules --target ethers-v6 --out-dir src/abi src/abi/*.abi", 15 | "lint:fix": "npm run lint -- --fix" 16 | }, 17 | "engines": { 18 | "node": "22.2.0" 19 | }, 20 | "dependencies": { 21 | "@chainlink/ccip-read-server": "^0.2.1", 22 | "@farcaster/hub-nodejs": "^0.11.12", 23 | "body-parser": "^1.20.2", 24 | "dd-trace": "^5.17.0", 25 | "dotenv": "^16.4.5", 26 | "ethers": "^6.13.0", 27 | "express": "^4.19.2", 28 | "kysely": "^0.27.3", 29 | "kysely-postgres-js": "^2.0.0", 30 | "neverthrow": "^6.2.2", 31 | "pino": "^9.1.0", 32 | "pino-pretty": "^11.2.0", 33 | "postgres": "^3.4.4" 34 | }, 35 | "devDependencies": { 36 | "@typechain/ethers-v6": "^0.5.1", 37 | "@types/express": "^4.17.21", 38 | "@types/jest": "^29.5.12", 39 | "@types/node": "^20.14.2", 40 | "@types/supertest": "^6.0.2", 41 | "@typescript-eslint/eslint-plugin": "^7.12.0", 42 | "@typescript-eslint/parser": "^7.13.0", 43 | "eslint": "^8.57.0", 44 | "eslint-config-prettier": "^9.1.0", 45 | "eslint-plugin-prettier": "^5.1.3", 46 | "jest": "^29.7.0", 47 | "prettier": "^3.3.1", 48 | "supertest": "^7.0.0", 49 | "ts-jest": "^29.1.4", 50 | "ts-node": "^10.9.2", 51 | "typechain": "^8.3.2", 52 | "typescript": "^5.4.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/reserved.ts: -------------------------------------------------------------------------------- 1 | // Usernames that we want only admins to be able to assign to an FID, in order 2 | // to prevent abuse and obvious username squatting. 3 | const RESERVED_USERNAMES = new Set([ 4 | 'adidas', 5 | 'amazon', 6 | 'apple', 7 | 'arbitrum', 8 | 'barackobama', 9 | 'bbc', 10 | 'billgates', 11 | 'binance', 12 | 'bitcoin', 13 | 'blockchain', 14 | 'bmw', 15 | 'cia', 16 | 'cloud', 17 | 'cnn', 18 | 'coinbase', 19 | 'consensys', 20 | 'context', 21 | 'crypto', 22 | 'data', 23 | 'dell', 24 | 'disney', 25 | 'drake', 26 | 'elonmusk', 27 | 'ethereum', 28 | 'f1', 29 | 'facebook', 30 | 'fashion', 31 | 'fbi', 32 | 'fifa', 33 | 'finance', 34 | 'fitness', 35 | 'food', 36 | 'fwb', 37 | 'fwbdao', 38 | 'gallery', 39 | 'games', 40 | 'ge', 41 | 'gem', 42 | 'gm', 43 | 'gnosis', 44 | 'gnosissafe', 45 | 'google', 46 | 'health', 47 | 'hp', 48 | 'hyperverse', 49 | 'ibm', 50 | 'intel', 51 | 'invest', 52 | 'jpmorgan', 53 | 'kraken', 54 | 'kucoin', 55 | 'lebronjames', 56 | 'lionelmessi', 57 | 'luxury', 58 | 'messi', 59 | 'metamask', 60 | 'microsoft', 61 | 'midjourney', 62 | 'mirror', 63 | 'ml', 64 | 'money', 65 | 'nasa', 66 | 'nba', 67 | 'netflix', 68 | 'news', 69 | 'nfl', 70 | 'nike', 71 | 'nounsdao', 72 | 'obsidian', 73 | 'openai', 74 | 'opensea', 75 | 'optimism', 76 | 'oracle', 77 | 'ourzora', 78 | 'pga', 79 | 'poap', 80 | 'polygon', 81 | 'privacy', 82 | 'rainbow', 83 | 'rarible', 84 | 'ronaldo', 85 | 'samsung', 86 | 'sony', 87 | 'starknet', 88 | 'stripe', 89 | 'sudoswap', 90 | 'superrare', 91 | 'sushiswap', 92 | 'syndicate', 93 | 'tesla', 94 | 'tezos', 95 | 'travel', 96 | 'twitter', 97 | 'uber', 98 | 'un', 99 | 'uniswap', 100 | 'urbit', 101 | 'viamirror', 102 | 'vitalik', 103 | 'youtube', 104 | 'zora', 105 | 'zoraco', 106 | ]); 107 | 108 | export function reservedUsername(username: string) { 109 | return RESERVED_USERNAMES.has(username); 110 | } 111 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.5 2 | 3 | # When updating image version, make sure to update below layer as well 4 | FROM node:22.2.0-alpine3.20 as builder 5 | 6 | # Create app directory 7 | WORKDIR /app 8 | 9 | # Dev dependencies for building any local packages 10 | RUN < package.json 62 | 63 | # BuildKit doesn't support the --squash flag, so we emulate it 64 | # copying everything into a single layer. 65 | FROM scratch 66 | COPY --from=app / / 67 | 68 | WORKDIR /app 69 | 70 | CMD ["node", "index.js"] 71 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | # Prevent multiple simultaneous test runs 10 | concurrency: 11 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | timeout-minutes: 10 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install Docker buildx 23 | uses: docker/setup-buildx-action@v3 24 | 25 | - name: Build Docker image 26 | id: docker-image 27 | uses: docker/build-push-action@v5 28 | with: 29 | cache-from: type=gha 30 | cache-to: type=gha,type=inline 31 | context: . 32 | file: Dockerfile 33 | load: true 34 | tags: farcasterxyz/fname-registry:test 35 | 36 | lint: 37 | timeout-minutes: 10 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version-file: '.node-version' 46 | cache: 'yarn' 47 | 48 | - name: Install dependencies 49 | run: | 50 | npm install -g node-gyp 51 | yarn install 52 | 53 | - name: Lint 54 | run: yarn lint 55 | 56 | test: 57 | timeout-minutes: 10 58 | runs-on: ubuntu-latest 59 | 60 | steps: 61 | - uses: actions/checkout@v4 62 | 63 | - name: Start background services 64 | run: docker compose up --detach postgres 65 | shell: bash 66 | 67 | - uses: actions/setup-node@v4 68 | with: 69 | node-version-file: '.node-version' 70 | cache: 'yarn' 71 | 72 | - name: Install dependencies 73 | run: | 74 | npm install -g node-gyp 75 | yarn install 76 | 77 | - name: Create test DB 78 | run: echo 'create database registry_test' | PGPASSWORD=password psql -h localhost -p 6543 -U app registry_dev 79 | shell: bash 80 | 81 | - name: Run tests 82 | env: 83 | OP_ALCHEMY_SECRET: 'dummy' 84 | MAINNET_ALCHEMY_SECRET: 'dummy' 85 | INFURA_PROJECT_ID: 'dummy' 86 | INFURA_PROJECT_SECRET: 'dummy' 87 | ETHERSCAN_API_SECRET: 'dummy' 88 | WARPCAST_ADDRESS: 'dummy' 89 | CCIP_ADDRESS: '0x4ea0be853219be8c9ce27200bdeee36881612ff2' 90 | run: yarn test 91 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { Kysely, CamelCasePlugin, Generated, GeneratedAlways, Migrator, FileMigrationProvider } from 'kysely'; 2 | import { PostgresJSDialect } from 'kysely-postgres-js'; 3 | import postgres from 'postgres'; 4 | import * as path from 'path'; 5 | import { promises as fs } from 'fs'; 6 | import { fileURLToPath } from 'url'; 7 | import { Logger } from './log.js'; 8 | import { err, ok, Result } from 'neverthrow'; 9 | 10 | const POSTGRES_URL = 11 | process.env['ENVIRONMENT'] === 'test' 12 | ? 'postgres://app:password@localhost:6543/registry_test' 13 | : process.env['POSTGRES_URL'] || 'postgres://app:password@localhost:6543/registry_dev'; 14 | 15 | export interface Database { 16 | transfers: TransfersTable; 17 | } 18 | 19 | export interface TransfersTable { 20 | id: GeneratedAlways; 21 | createdAt: Generated; 22 | updatedAt: Generated; 23 | timestamp: number; 24 | username: string; 25 | owner: Uint8Array; 26 | from: number; 27 | to: number; 28 | userSignature: Uint8Array; 29 | serverSignature: Uint8Array; 30 | userFid: number; 31 | } 32 | 33 | export const getDbClient = () => { 34 | return new Kysely({ 35 | dialect: new PostgresJSDialect({ 36 | postgres: postgres(POSTGRES_URL, { 37 | max: 10, 38 | types: { 39 | // BigInts will not exceed Number.MAX_SAFE_INTEGER for our use case. 40 | // Return as JavaScript's `number` type so it's easier to work with. 41 | bigint: { 42 | to: 20, 43 | from: [20], 44 | parse: (x: any) => Number(x), 45 | serialize: (x: any) => x.toString(), 46 | }, 47 | }, 48 | }), 49 | }), 50 | plugins: [new CamelCasePlugin()], 51 | }); 52 | }; 53 | 54 | export const migrateToLatest = async (db: Kysely, log: Logger): Promise> => { 55 | const migrator = new Migrator({ 56 | db, 57 | provider: new FileMigrationProvider({ 58 | fs, 59 | path, 60 | migrationFolder: path.join(path.dirname(fileURLToPath(import.meta.url)), 'migrations'), 61 | }), 62 | }); 63 | 64 | const { error, results } = await migrator.migrateToLatest(); 65 | 66 | results?.forEach((it) => { 67 | if (it.status === 'Success') { 68 | log.info(`migration "${it.migrationName}" was executed successfully`); 69 | } else if (it.status === 'Error') { 70 | log.error(`failed to execute migration "${it.migrationName}"`); 71 | } 72 | }); 73 | 74 | if (error) { 75 | log.error('failed to migrate'); 76 | log.error(error); 77 | return err(error); 78 | } 79 | 80 | return ok(undefined); 81 | }; 82 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | jobs: 7 | publish: 8 | concurrency: fname-registry-publish 9 | timeout-minutes: 30 10 | runs-on: buildjet-2vcpu-ubuntu-2204-arm 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | # Build image with Depot and push it to AWS ECR 16 | - name: Configure AWS credentials 17 | uses: aws-actions/configure-aws-credentials@v4 18 | with: 19 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | mask-aws-account-id: no 21 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 22 | aws-region: us-east-1 23 | 24 | - name: Login to AWS ECR 25 | id: login-ecr 26 | uses: aws-actions/amazon-ecr-login@v2 27 | 28 | - name: Generate Docker image tag for fname-registry 29 | id: image-tag 30 | env: 31 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 32 | ECR_REPOSITORY: farcasterxyz/fname-registry 33 | IMAGE_TAG: ${{ github.sha }} 34 | run: echo "::set-output name=image-tag::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" 35 | shell: bash 36 | 37 | - uses: depot/setup-action@v1 38 | 39 | - uses: depot/build-push-action@v1 40 | with: 41 | buildx-fallback: true 42 | project: ${{ secrets.DEPOT_PROJECT_ID }} 43 | token: ${{ secrets.DEPOT_TOKEN }} 44 | context: . 45 | file: ./Dockerfile 46 | platforms: "linux/arm64" 47 | tags: ${{ steps.image-tag.outputs.image-tag }} 48 | github-token: ${{ secrets.github_token }} 49 | push: true 50 | 51 | - name: Log out of Amazon ECR 52 | if: always() 53 | run: docker logout ${{ steps.login-ecr.outputs.registry }} 54 | shell: bash 55 | 56 | # Deploy the image to ECS one hub at a time to minimize overall downtime 57 | - name: Generate release ID 58 | id: release 59 | run: echo "::set-output name=release::$(date +'%Y-%m-%d-%H-%M')-$(git rev-parse --short HEAD)" 60 | shell: bash 61 | 62 | - name: Update task-definition 63 | id: task-def 64 | uses: aws-actions/amazon-ecs-render-task-definition@v1 65 | with: 66 | task-definition: .aws/fname-registry-api.json 67 | container-name: fname-registry-api 68 | image: ${{ steps.image-tag.outputs.image-tag }} 69 | 70 | - name: Push task-definition to Amazon ECS 71 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1 72 | with: 73 | task-definition: ${{ steps.task-def.outputs.task-definition }} 74 | service: fname-registry-api 75 | cluster: fname-registry-cluster 76 | wait-for-service-stability: true 77 | -------------------------------------------------------------------------------- /src/signature.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { CCIP_ADDRESS, WARPCAST_ADDRESS } from './env.js'; 3 | import * as process from 'process'; 4 | 5 | export const signer = ethers.Wallet.fromPhrase( 6 | process.env.MNEMONIC || 'test test test test test test test test test test test junk' 7 | ); 8 | export const signerAddress = signer.address; 9 | export const signerFid = parseInt(process.env.FID || '14045'); 10 | 11 | type KeyToFid = { 12 | [key: number]: string; 13 | }; 14 | 15 | export const ADMIN_KEYS: KeyToFid = { 16 | [signerFid]: signerAddress, // FName server 17 | 14046: WARPCAST_ADDRESS, // Warpcast backend 18 | }; 19 | 20 | const hub_domain = { 21 | name: 'Farcaster name verification', 22 | version: '1', 23 | chainId: 1, 24 | // TODO: When changing, remember to also update on the backend! 25 | verifyingContract: '0xe3be01d99baa8db9905b33a3ca391238234b79d1', // name registry contract, will be the farcaster ENS CCIP contract later 26 | }; 27 | const ccip_domain = { 28 | name: 'Farcaster name verification', 29 | version: '1', 30 | chainId: 1, 31 | verifyingContract: CCIP_ADDRESS, 32 | }; 33 | const types = { 34 | UserNameProof: [ 35 | { name: 'name', type: 'string' }, 36 | { name: 'timestamp', type: 'uint256' }, 37 | { name: 'owner', type: 'address' }, 38 | ], 39 | }; 40 | 41 | export async function generateSignature(userName: string, timestamp: number, owner: string, signer: ethers.Signer) { 42 | const userNameProof = { 43 | name: userName, 44 | timestamp, 45 | owner: owner, 46 | }; 47 | return Buffer.from((await signer.signTypedData(hub_domain, types, userNameProof)).replace(/^0x/, ''), 'hex'); 48 | } 49 | 50 | export async function generateCCIPSignature(userName: string, timestamp: number, owner: string, signer: ethers.Signer) { 51 | const userNameProof = { 52 | name: userName, 53 | timestamp, 54 | owner: owner, 55 | }; 56 | return Buffer.from((await signer.signTypedData(ccip_domain, types, userNameProof)).replace(/^0x/, ''), 'hex'); 57 | } 58 | 59 | export function verifySignature( 60 | userName: string, 61 | timestamp: number, 62 | owner: string, 63 | signature: string, 64 | signerAddress: string 65 | ) { 66 | const userNameProof = { 67 | name: userName, 68 | timestamp, 69 | owner: owner, 70 | }; 71 | const signer = ethers.verifyTypedData(hub_domain, types, userNameProof, signature); 72 | return signer.toLowerCase() === signerAddress.toLowerCase(); 73 | } 74 | 75 | export function verifyCCIPSignature( 76 | userName: string, 77 | timestamp: number, 78 | owner: string, 79 | signature: string, 80 | signerAddress: string 81 | ) { 82 | const userNameProof = { 83 | name: userName, 84 | timestamp, 85 | owner: owner, 86 | }; 87 | const signer = ethers.verifyTypedData(ccip_domain, types, userNameProof, signature); 88 | return signer.toLowerCase() === signerAddress.toLowerCase(); 89 | } 90 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LogDescription, 3 | ParamType, 4 | Result, 5 | getBytes, 6 | toBeHex, 7 | toQuantity, 8 | concat, 9 | toUtf8Bytes, 10 | BigNumberish, 11 | ZeroHash, 12 | } from 'ethers'; 13 | 14 | export type EventArgBasicValue = string | number | boolean; 15 | type EventArgValue = EventArgBasicValue | EventArgBasicValue[] | EventArgs; 16 | export type EventArgs = { 17 | [key: string]: EventArgValue; 18 | }; 19 | 20 | export const toBytes16Hex = (text: string) => { 21 | return toBeHex(concat([toUtf8Bytes(text), ZeroHash]).slice(0, 16)); 22 | }; 23 | 24 | export const fromBytes32Hex = (bn: BigNumberish) => { 25 | return Buffer.from(getBytes(toQuantity(bn))) 26 | .toString('utf8') 27 | .replace(/\0/g, ''); 28 | }; 29 | 30 | // Alchemy's docs sometimes say the value is a string (e.g. in https://docs.alchemy.com/reference/alchemy-gettransactionreceipts), 31 | // but the type of the resulting TS object is number. This method converts the hex string to a number. 32 | // It will throw an error if the value overflows a JS number 33 | // export const fixAlchemyNumber = (value: any): number => { 34 | // return BigNumberish.from(value).toNumber(); 35 | // }; 36 | 37 | export const contractArgsToObject = (args: Result): Readonly> => { 38 | const cleanObject: Record = {}; 39 | for (const propertyName in args) { 40 | // Exclude keys that are just array indices. We are only interested in the 41 | // non-numeric keys since they are well-named. We cast to `any` since 42 | // isNaN still works with non-numeric values, but is typed to only accept 43 | // numbers for some reason. 44 | if (isNaN(propertyName as any)) { 45 | const arg = args[propertyName]; 46 | cleanObject[propertyName] = arg.__proto__.toHexString ? arg.toHexString() : arg; 47 | } 48 | } 49 | return cleanObject; 50 | }; 51 | 52 | export const contractArgsToObject2 = (logDescription: LogDescription): EventArgs => { 53 | return parseReceiptArgs(logDescription.fragment.inputs, logDescription.args); 54 | }; 55 | 56 | const parseReceiptArgs = (defs: readonly ParamType[], values: Result): EventArgs => { 57 | const results: Record = {}; 58 | defs.forEach((def, index) => { 59 | const value = values[index]; 60 | const result = parseReceiptArg(def, value); 61 | results[def.name] = result; 62 | }); 63 | return results; 64 | }; 65 | 66 | const parseReceiptArg = (def: ParamType, value: Result): EventArgValue => { 67 | if (def.components && def.components.length > 0) { 68 | return parseReceiptArgs(def.components, value); 69 | } else { 70 | return value; 71 | } 72 | }; 73 | 74 | export enum EthereumChain { 75 | Mainnet = 1, 76 | Goerli = 5, 77 | } 78 | 79 | export const SECONDS_PER_HOUR = 60 * 60; 80 | export const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24; 81 | export const SECONDS_PER_YEAR = SECONDS_PER_DAY * 365; 82 | 83 | export const MILLIS_PER_MINUTE = 1000 * 60; 84 | export const MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60; 85 | export const MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; 86 | 87 | export function bytesToHex(value: Uint8Array): string { 88 | return `0x${Buffer.from(value).toString('hex')}`; 89 | } 90 | 91 | export function hexToBytes(value: string): Uint8Array { 92 | return Uint8Array.from(Buffer.from(value.replace(/^0x/, ''), 'hex')); 93 | } 94 | 95 | export function currentTimestamp(): number { 96 | return Math.floor(Date.now() / 1000); 97 | } 98 | 99 | export function decodeDnsName(name: string) { 100 | const dnsname = Buffer.from(name.slice(2), 'hex'); 101 | const labels = []; 102 | let idx = 0; 103 | for (;;) { 104 | const len = dnsname.readUInt8(idx); 105 | if (len === 0) break; 106 | labels.push(dnsname.slice(idx + 1, idx + len + 1).toString('utf8')); 107 | idx += len + 1; 108 | } 109 | return labels; 110 | } 111 | -------------------------------------------------------------------------------- /.aws/fname-registry-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "executionRoleArn": "arn:aws:iam::526236635984:role/fname-registry-ecs-task-execution-role", 3 | "taskRoleArn": "arn:aws:iam::526236635984:role/fname-registry-ecs-task-role", 4 | "containerDefinitions": [ 5 | { 6 | "name": "fname-registry-api", 7 | "logConfiguration": { 8 | "logDriver": "awslogs", 9 | "options": { 10 | "awslogs-group": "/ecs/fname-registry", 11 | "awslogs-region": "us-east-1", 12 | "awslogs-stream-prefix": "api" 13 | } 14 | }, 15 | "portMappings": [ 16 | { 17 | "containerPort": 3000, 18 | "protocol": "tcp" 19 | } 20 | ], 21 | "command": ["node", "index.js"], 22 | "cpu": 0, 23 | "dockerLabels": { 24 | "com.datadoghq.tags.env": "prod", 25 | "com.datadoghq.tags.service": "fname-registry-api" 26 | }, 27 | "linuxParameters": { 28 | "initProcessEnabled": true 29 | }, 30 | "environment": [ 31 | { 32 | "name": "TINI_VERBOSITY", 33 | "value": "3" 34 | }, 35 | { 36 | "name": "ENVIRONMENT", 37 | "value": "prod" 38 | }, 39 | { 40 | "name": "NODE_OPTIONS", 41 | "value": "--enable-source-maps" 42 | }, 43 | { 44 | "name": "DD_ENV", 45 | "value": "prod" 46 | }, 47 | { 48 | "name": "DD_SERVICE", 49 | "value": "fname-registry-api" 50 | }, 51 | { 52 | "name": "WARPCAST_ADDRESS", 53 | "value": "0xABba722926c8302c73e57A25AD8F63753904546f" 54 | }, 55 | { 56 | "name": "CCIP_ADDRESS", 57 | "value": "0x145b9934B42F214C101De04b6115285959BDD4F5" 58 | } 59 | ], 60 | "secrets": [ 61 | { 62 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/FNAME_REGISTRY_MNEMONIC", 63 | "name": "MNEMONIC" 64 | }, 65 | { 66 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/FNAME_REGISTRY_OP_ALCHEMY_SECRET", 67 | "name": "OP_ALCHEMY_SECRET" 68 | }, 69 | { 70 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/FNAME_REGISTRY_MAINNET_ALCHEMY_SECRET", 71 | "name": "MAINNET_ALCHEMY_SECRET" 72 | }, 73 | { 74 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/FNAME_REGISTRY_ETHERSCAN_API_SECRET", 75 | "name": "ETHERSCAN_API_SECRET" 76 | }, 77 | { 78 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/FNAME_REGISTRY_INFURA_PROJECT_ID", 79 | "name": "INFURA_PROJECT_ID" 80 | }, 81 | { 82 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/FNAME_REGISTRY_INFURA_PROJECT_SECRET", 83 | "name": "INFURA_PROJECT_SECRET" 84 | }, 85 | { 86 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/FNAME_REGISTRY_POSTGRES_URL", 87 | "name": "POSTGRES_URL" 88 | } 89 | ], 90 | "image": "public.ecr.aws/alpine:latest", 91 | "essential": true 92 | }, 93 | { 94 | "name": "fname-registry-api-datadog-agent", 95 | "image": "public.ecr.aws/datadog/agent:7", 96 | "environment": [ 97 | { 98 | "name": "DD_ENV", 99 | "value": "prod" 100 | }, 101 | { 102 | "name": "DD_SERVICE", 103 | "value": "fname-registry-api" 104 | }, 105 | { 106 | "name": "ECS_FARGATE", 107 | "value": "true" 108 | } 109 | ], 110 | "secrets": [ 111 | { 112 | "valueFrom": "arn:aws:ssm:us-east-1:526236635984:parameter/DATADOG_AGENT_API_KEY", 113 | "name": "DD_API_KEY" 114 | } 115 | ] 116 | } 117 | ], 118 | "cpu": "1 vCPU", 119 | "memory": "2048", 120 | "runtimePlatform": { 121 | "operatingSystemFamily": "LINUX", 122 | "cpuArchitecture": "ARM64" 123 | }, 124 | "family": "fname-registry-api", 125 | "requiresCompatibilities": ["FARGATE"], 126 | "networkMode": "awsvpc" 127 | } 128 | -------------------------------------------------------------------------------- /src/abi/common.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { 5 | FunctionFragment, 6 | Typed, 7 | EventFragment, 8 | ContractTransaction, 9 | ContractTransactionResponse, 10 | DeferredTopicFilter, 11 | EventLog, 12 | TransactionRequest, 13 | LogDescription, 14 | } from "ethers"; 15 | 16 | export interface TypedDeferredTopicFilter<_TCEvent extends TypedContractEvent> 17 | extends DeferredTopicFilter {} 18 | 19 | export interface TypedContractEvent< 20 | InputTuple extends Array = any, 21 | OutputTuple extends Array = any, 22 | OutputObject = any 23 | > { 24 | (...args: Partial): TypedDeferredTopicFilter< 25 | TypedContractEvent 26 | >; 27 | name: string; 28 | fragment: EventFragment; 29 | getFragment(...args: Partial): EventFragment; 30 | } 31 | 32 | type __TypechainAOutputTuple = T extends TypedContractEvent< 33 | infer _U, 34 | infer W 35 | > 36 | ? W 37 | : never; 38 | type __TypechainOutputObject = T extends TypedContractEvent< 39 | infer _U, 40 | infer _W, 41 | infer V 42 | > 43 | ? V 44 | : never; 45 | 46 | export interface TypedEventLog 47 | extends Omit { 48 | args: __TypechainAOutputTuple & __TypechainOutputObject; 49 | } 50 | 51 | export interface TypedLogDescription 52 | extends Omit { 53 | args: __TypechainAOutputTuple & __TypechainOutputObject; 54 | } 55 | 56 | export type TypedListener = ( 57 | ...listenerArg: [ 58 | ...__TypechainAOutputTuple, 59 | TypedEventLog, 60 | ...undefined[] 61 | ] 62 | ) => void; 63 | 64 | export type MinEthersFactory = { 65 | deploy(...a: ARGS[]): Promise; 66 | }; 67 | 68 | export type GetContractTypeFromFactory = F extends MinEthersFactory< 69 | infer C, 70 | any 71 | > 72 | ? C 73 | : never; 74 | export type GetARGsTypeFromFactory = F extends MinEthersFactory 75 | ? Parameters 76 | : never; 77 | 78 | export type StateMutability = "nonpayable" | "payable" | "view"; 79 | 80 | export type BaseOverrides = Omit; 81 | export type NonPayableOverrides = Omit< 82 | BaseOverrides, 83 | "value" | "blockTag" | "enableCcipRead" 84 | >; 85 | export type PayableOverrides = Omit< 86 | BaseOverrides, 87 | "blockTag" | "enableCcipRead" 88 | >; 89 | export type ViewOverrides = Omit; 90 | export type Overrides = S extends "nonpayable" 91 | ? NonPayableOverrides 92 | : S extends "payable" 93 | ? PayableOverrides 94 | : ViewOverrides; 95 | 96 | export type PostfixOverrides, S extends StateMutability> = 97 | | A 98 | | [...A, Overrides]; 99 | export type ContractMethodArgs< 100 | A extends Array, 101 | S extends StateMutability 102 | > = PostfixOverrides<{ [I in keyof A]-?: A[I] | Typed }, S>; 103 | 104 | export type DefaultReturnType = R extends Array ? R[0] : R; 105 | 106 | // export interface ContractMethod = Array, R = any, D extends R | ContractTransactionResponse = R | ContractTransactionResponse> { 107 | export interface TypedContractMethod< 108 | A extends Array = Array, 109 | R = any, 110 | S extends StateMutability = "payable" 111 | > { 112 | (...args: ContractMethodArgs): S extends "view" 113 | ? Promise> 114 | : Promise; 115 | 116 | name: string; 117 | 118 | fragment: FunctionFragment; 119 | 120 | getFragment(...args: ContractMethodArgs): FunctionFragment; 121 | 122 | populateTransaction( 123 | ...args: ContractMethodArgs 124 | ): Promise; 125 | staticCall(...args: ContractMethodArgs): Promise>; 126 | send(...args: ContractMethodArgs): Promise; 127 | estimateGas(...args: ContractMethodArgs): Promise; 128 | staticCallResult(...args: ContractMethodArgs): Promise; 129 | } 130 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import ccipread from '@chainlink/ccip-read-server'; 2 | import { ZeroAddress } from 'ethers'; 3 | 4 | import { getDbClient, migrateToLatest } from './db.js'; 5 | import './env.js'; 6 | import { log } from './log.js'; 7 | import { generateCCIPSignature, signer, signerAddress } from './signature.js'; 8 | import { 9 | createTransfer, 10 | getCurrentUsername, 11 | getLatestTransfer, 12 | getTransferById, 13 | getTransferHistory, 14 | TransferHistoryFilter, 15 | ValidationError, 16 | } from './transfers.js'; 17 | 18 | import { decodeDnsName } from './util.js'; 19 | import { getIdRegistryContract } from './ethereum.js'; 20 | 21 | export const RESOLVE_ABI = [ 22 | 'function resolve(bytes calldata name, bytes calldata data) external view returns(string name, uint256 timestamp, address owner, bytes memory sig)', 23 | ]; 24 | 25 | const db = getDbClient(); 26 | await migrateToLatest(db, log); 27 | const idContract = getIdRegistryContract(); 28 | 29 | const server = new ccipread.Server(); 30 | 31 | server.add(RESOLVE_ABI, [ 32 | { 33 | type: 'resolve', 34 | func: async ([name, _data], _req) => { 35 | const fname = decodeDnsName(name)[0]; 36 | const transfer = await getLatestTransfer(fname, db); 37 | if (!transfer || transfer.to === 0) { 38 | // If no transfer or the name was unregistered, return empty values 39 | return ['', 0, ZeroAddress, '0x']; 40 | } 41 | const signature = await generateCCIPSignature(transfer.username, transfer.timestamp, transfer.owner, signer); 42 | return [transfer.username, transfer.timestamp, transfer.owner, signature]; 43 | }, 44 | }, 45 | ]); 46 | 47 | export const app = server.makeApp('/ccip/'); 48 | 49 | app.get('/transfers', async (req, res) => { 50 | const filterOpts: TransferHistoryFilter = {}; 51 | if (req.query.from_id) { 52 | filterOpts.fromId = parseInt(req.query.from_id.toString()); 53 | } 54 | // TODO: Remove once hub is fixed 55 | if (req.query.fromId) { 56 | filterOpts.fromId = parseInt(req.query.fromId.toString()); 57 | } 58 | if (req.query.name) { 59 | filterOpts.name = req.query.name.toString().replace(/\0/g, ''); 60 | } 61 | if (req.query.from_ts) { 62 | filterOpts.fromTs = parseInt(req.query.from_ts.toString()); 63 | } 64 | if (req.query.fid) { 65 | filterOpts.fid = parseInt(req.query.fid.toString()); 66 | } 67 | try { 68 | const transfers = await getTransferHistory(filterOpts, db); 69 | res.send({ transfers }); 70 | } catch (e) { 71 | res.status(400).send({ error: 'Unable to get transfers' }).end(); 72 | log.error(e, `Unable to get transfers for request: ${JSON.stringify(req.query)}`); 73 | return; 74 | } 75 | }); 76 | 77 | app.get('/transfers/current', async (req, res) => { 78 | try { 79 | let name: string | undefined; 80 | if (req.query.fid) { 81 | if (Number.isNaN(Number(req.query.fid))) { 82 | res.status(400).send({ error: 'FID is not a number' }).end(); 83 | return; 84 | } 85 | name = await getCurrentUsername(parseInt(req.query.fid.toString()), db); 86 | } else if (req.query.name) { 87 | name = req.query.name.toString(); 88 | } 89 | if (!name || name === '') { 90 | res.status(404).send({ error: 'Could not resolve current name' }).end(); 91 | return; 92 | } 93 | const transfer = await getLatestTransfer(name, db); 94 | if (!transfer || transfer.to === 0) { 95 | res.status(404).send({ error: 'No transfer found' }).end(); 96 | return; 97 | } 98 | res.send({ transfer }); 99 | } catch (e) { 100 | res.status(400).send({ error: 'Unable to get transfer' }).end(); 101 | log.error(e, `Unable to get transfers for query: ${JSON.stringify(req.query)}`); 102 | return; 103 | } 104 | }); 105 | 106 | app.post('/transfers', async (req, res) => { 107 | let tr; 108 | try { 109 | tr = req.body; 110 | const result = await createTransfer( 111 | { 112 | username: tr.name, 113 | from: tr.from, 114 | to: tr.to, 115 | timestamp: tr.timestamp, 116 | owner: tr.owner, 117 | userSignature: tr.signature, 118 | userFid: tr.fid, 119 | }, 120 | db, 121 | idContract 122 | ); 123 | if (!result) { 124 | log.warn({ name: tr.username }, `Unable to create transfer`); 125 | res.status(500).send({ error: 'Unable to create transfer' }).end(); 126 | return; 127 | } 128 | const transfer = await getTransferById(result.id, db); 129 | res.send({ transfer }); 130 | } catch (e: unknown) { 131 | if (e instanceof ValidationError) { 132 | res.status(400).send({ error: 'Validation failure', code: e.code }).end(); 133 | } else { 134 | log.error(e, `Unable to create transfer: ${JSON.stringify(tr)}`); 135 | res 136 | .status(500) 137 | .send({ error: `Unable to validate : ${e}` }) 138 | .end(); 139 | } 140 | } 141 | }); 142 | 143 | app.get('/signer', async (_req, res) => { 144 | res.send({ signer: signerAddress }); 145 | }); 146 | 147 | app.get('/_health', async (_req, res) => { 148 | res.send({ status: 'ok' }); 149 | }); 150 | -------------------------------------------------------------------------------- /src/transfers.ts: -------------------------------------------------------------------------------- 1 | import { Kysely, Selectable } from 'kysely'; 2 | import { Database, TransfersTable } from './db.js'; 3 | import { ADMIN_KEYS, generateSignature, signer, verifySignature } from './signature.js'; 4 | import { reservedUsername } from './reserved.js'; 5 | import { bytesToHex, currentTimestamp, hexToBytes } from './util.js'; 6 | import { bytesCompare, validations } from '@farcaster/hub-nodejs'; 7 | import { log } from './log.js'; 8 | import { IdRegistry } from './abi/index.js'; 9 | import { toNumber } from 'ethers'; 10 | 11 | const PAGE_SIZE = 100; 12 | export const TIMESTAMP_TOLERANCE = 10 * 60; // 5 minute 13 | export const NAME_CHANGE_DELAY = 28 * 24 * 60 * 60; // 28 days in seconds 14 | 15 | type TransferRequest = { 16 | timestamp: number; 17 | username: string; 18 | owner: string; 19 | from: number; 20 | to: number; 21 | userSignature: string; 22 | userFid: number; 23 | }; 24 | 25 | export type TransferHistoryFilter = { 26 | fromTs?: number; 27 | fromId?: number; 28 | name?: string; 29 | fid?: number; 30 | }; 31 | 32 | type ErrorCode = 33 | | 'USERNAME_TAKEN' 34 | | 'USERNAME_RESERVED' 35 | | 'TOO_MANY_NAMES' 36 | | 'UNAUTHORIZED' 37 | | 'USERNAME_NOT_FOUND' 38 | | 'INVALID_SIGNATURE' 39 | | 'INVALID_USERNAME' 40 | | 'INVALID_FID_OWNER' 41 | | 'THROTTLED' 42 | | 'INVALID_TIMESTAMP'; 43 | export class ValidationError extends Error { 44 | public readonly code: ErrorCode; 45 | constructor(code: ErrorCode) { 46 | super(`Validation error: ${code}`); 47 | this.code = code; 48 | } 49 | } 50 | 51 | export async function createTransfer(req: TransferRequest, db: Kysely, idContract: IdRegistry) { 52 | const existing_matching_transfer_id = await validateTransfer(req, db, idContract); 53 | if (existing_matching_transfer_id) { 54 | return { id: existing_matching_transfer_id }; 55 | } 56 | const serverSignature = await generateSignature(req.username, req.timestamp, req.owner, signer); 57 | const transfer = { 58 | ...req, 59 | serverSignature, 60 | owner: hexToBytes(req.owner), 61 | userSignature: hexToBytes(req.userSignature), 62 | }; 63 | return await db.insertInto('transfers').values(transfer).returning('id').executeTakeFirst(); 64 | } 65 | 66 | async function getAndValidateVerifierAddress(req: TransferRequest, idContract: IdRegistry) { 67 | // Admin transfer 68 | if (ADMIN_KEYS[req.userFid]) { 69 | return ADMIN_KEYS[req.userFid]; 70 | } 71 | 72 | // For user transfers, make sure the userFid matches the transfer request if it's present and that the 73 | // owner address actually owns the fid 74 | let userFid = -1; 75 | if (req.from === 0) { 76 | userFid = req.to; 77 | } else if (req.to === 0) { 78 | userFid = req.from; 79 | } 80 | if (req.userFid && userFid !== req.userFid) { 81 | log.warn(`User FID ${req.userFid} does not match FID ${userFid} in transfer request`); 82 | throw new ValidationError('UNAUTHORIZED'); 83 | } 84 | 85 | let ownerFid: bigint; 86 | 87 | try { 88 | ownerFid = await idContract.idOf(req.owner); 89 | } catch (e) { 90 | log.error(e, `Unable to get fid for owner: ${req.owner}`); 91 | throw new ValidationError('INVALID_FID_OWNER'); 92 | } 93 | 94 | if (toNumber(ownerFid) !== userFid) { 95 | log.warn(`Owner for FID ${ownerFid.toString()} does not match owner ${req.owner} in transfer request`); 96 | throw new ValidationError('INVALID_FID_OWNER'); 97 | } 98 | 99 | return req.owner; 100 | } 101 | 102 | export async function validateTransfer(req: TransferRequest, db: Kysely, idContract: IdRegistry) { 103 | const verifierAddress = await getAndValidateVerifierAddress(req, idContract); 104 | if (!verifierAddress) { 105 | throw new ValidationError('UNAUTHORIZED'); 106 | } 107 | 108 | if (reservedUsername(req.username) && !ADMIN_KEYS[req.userFid]) { 109 | throw new ValidationError('USERNAME_RESERVED'); 110 | } 111 | 112 | if (!verifySignature(req.username, req.timestamp, req.owner, req.userSignature, verifierAddress)) { 113 | log.error(`Invalid signature for req ${JSON.stringify(req)}`); 114 | throw new ValidationError('INVALID_SIGNATURE'); 115 | } 116 | 117 | const validationResult = validations.validateFname(req.username); 118 | if (validationResult.isErr()) { 119 | throw new ValidationError('INVALID_USERNAME'); 120 | } 121 | 122 | const existingTransfer = await getLatestTransfer(req.username, db); 123 | 124 | const existingName = await getCurrentUsername(req.to, db); 125 | if (existingName) { 126 | if ( 127 | existingTransfer && 128 | existingName === req.username && 129 | bytesCompare(hexToBytes(existingTransfer.owner), hexToBytes(req.owner)) === 0 130 | ) { 131 | return existingTransfer.id; 132 | } 133 | throw new ValidationError('TOO_MANY_NAMES'); 134 | } 135 | 136 | if (req.timestamp > currentTimestamp() + TIMESTAMP_TOLERANCE) { 137 | log.error(`Timestamp ${req.timestamp} was > ${TIMESTAMP_TOLERANCE}`); 138 | throw new ValidationError('INVALID_TIMESTAMP'); 139 | } 140 | 141 | if (req.timestamp < currentTimestamp() - TIMESTAMP_TOLERANCE) { 142 | log.error(`Timestamp ${req.timestamp} was < ${TIMESTAMP_TOLERANCE}`); 143 | throw new ValidationError('INVALID_TIMESTAMP'); 144 | } 145 | 146 | if (existingTransfer && existingTransfer.timestamp > req.timestamp) { 147 | log.error(`Timestamp ${req.timestamp} < previous transfer timestamp of ${existingTransfer.timestamp}`); 148 | throw new ValidationError('INVALID_TIMESTAMP'); 149 | } 150 | 151 | // Non-admin users can only change their name once every 28 days 152 | if (existingTransfer && !ADMIN_KEYS[req.userFid] && req.timestamp < existingTransfer.timestamp + NAME_CHANGE_DELAY) { 153 | throw new ValidationError('THROTTLED'); 154 | } 155 | 156 | if (req.from === 0) { 157 | // Mint 158 | if (existingTransfer && existingTransfer.to !== 0) { 159 | throw new ValidationError('USERNAME_TAKEN'); 160 | } 161 | } else if (req.to === 0) { 162 | // Burn 163 | if (!existingTransfer || existingTransfer.to === 0) { 164 | throw new ValidationError('USERNAME_NOT_FOUND'); 165 | } 166 | } else { 167 | // Transfer 168 | if (!existingTransfer) { 169 | throw new ValidationError('USERNAME_NOT_FOUND'); 170 | } 171 | } 172 | } 173 | 174 | export async function getLatestTransfer(name: string, db: Kysely) { 175 | return toTransferResponse( 176 | await db 177 | .selectFrom('transfers') 178 | .selectAll() 179 | .where('username', '=', name) 180 | .orderBy('timestamp', 'desc') 181 | .limit(1) 182 | .executeTakeFirst() 183 | ); 184 | } 185 | 186 | export async function getCurrentUsername(fid: number, db: Kysely) { 187 | // fid 0 is the mint/burn address, so it can never have a username 188 | if (fid === 0) { 189 | return undefined; 190 | } 191 | // To get the current username, we need to get the most recent transfer and ensure the fid is the receiver 192 | const transfer = await db 193 | .selectFrom('transfers') 194 | .select(['username', 'from', 'to']) 195 | .where(({ or, eb }) => { 196 | return or([eb('from', '=', fid), eb('to', '=', fid)]); 197 | }) 198 | .orderBy('timestamp', 'desc') 199 | .limit(1) 200 | .executeTakeFirst(); 201 | 202 | // The most recent transfer to the fid is the current username. We have validations that ensure there can only be 203 | // one name per fid 204 | if (transfer && transfer.to === fid) { 205 | return transfer.username; 206 | } else { 207 | return undefined; 208 | } 209 | } 210 | 211 | function toTransferResponse(row: Selectable | undefined) { 212 | if (!row) { 213 | return undefined; 214 | } 215 | return { 216 | id: row.id, 217 | timestamp: row.timestamp, 218 | username: row.username, 219 | owner: bytesToHex(row.owner), 220 | from: row.from, 221 | to: row.to, 222 | user_signature: bytesToHex(row.userSignature), 223 | server_signature: bytesToHex(row.serverSignature), 224 | }; 225 | } 226 | 227 | export async function getTransferById(id: number, db: Kysely) { 228 | const row = await db.selectFrom('transfers').selectAll().where('id', '=', id).executeTakeFirst(); 229 | return toTransferResponse(row); 230 | } 231 | 232 | export async function getTransferHistory(filterOpts: TransferHistoryFilter, db: Kysely) { 233 | let query = db.selectFrom('transfers').selectAll(); 234 | if (filterOpts.fromId) { 235 | query = query.where('id', '>', filterOpts.fromId); 236 | } 237 | if (filterOpts.fromTs) { 238 | query = query.where('timestamp', '>', filterOpts.fromTs); 239 | } 240 | if (filterOpts.name) { 241 | query = query.where('username', '=', filterOpts.name); 242 | } 243 | if (filterOpts.fid) { 244 | const _fid = filterOpts.fid; 245 | query = query.where(({ or, eb }) => { 246 | return or([eb('from', '=', _fid), eb('to', '=', _fid)]); 247 | }); 248 | } 249 | 250 | // If we're filtering by timestamp, we need to order by timestamp first because clients may use that as the high watermark 251 | if (filterOpts.fromTs) { 252 | query = query.orderBy('timestamp'); 253 | } 254 | 255 | const res = await query.orderBy('id').limit(PAGE_SIZE).execute(); 256 | return res.map(toTransferResponse); 257 | } 258 | -------------------------------------------------------------------------------- /tests/transfers.test.ts: -------------------------------------------------------------------------------- 1 | import { getDbClient, migrateToLatest } from '../src/db.js'; 2 | import { log } from '../src/log.js'; 3 | import { sql } from 'kysely'; 4 | import { getCurrentUsername, getLatestTransfer, TIMESTAMP_TOLERANCE } from '../src/transfers.js'; 5 | import { currentTimestamp } from '../src/util.js'; 6 | import { createTestTransfer } from './utils.js'; 7 | import { generateSignature, signer, signerAddress, signerFid } from '../src/signature.js'; 8 | import { ethers } from 'ethers'; 9 | 10 | const db = getDbClient(); 11 | const owner = signerAddress; 12 | const anotherSigner = ethers.Wallet.createRandom(); 13 | 14 | beforeAll(async () => { 15 | await migrateToLatest(db, log); 16 | await sql`TRUNCATE TABLE transfers RESTART IDENTITY`.execute(db); 17 | await createTestTransfer(db, { username: 'test123', to: 1 }); 18 | await createTestTransfer(db, { username: 'test123', from: 1, to: 2, timestamp: currentTimestamp() + 10 }); 19 | await createTestTransfer(db, { username: 'test3', to: 3, timestamp: currentTimestamp() - 2 }); 20 | }); 21 | 22 | describe('transfers', () => { 23 | describe('createTransfer', () => { 24 | test('should throw error if validation fails', async () => { 25 | await expect(createTestTransfer(db, { username: 'test3', to: 4 })).rejects.toThrow('USERNAME_TAKEN'); 26 | }); 27 | }); 28 | 29 | describe('validateTransfer', () => { 30 | test('cannot register an existing name', async () => { 31 | await expect(createTestTransfer(db, { username: 'test3', to: 4 })).rejects.toThrow('USERNAME_TAKEN'); 32 | }); 33 | 34 | test('cannot register a reserved name with a non-admin account', async () => { 35 | await expect( 36 | createTestTransfer( 37 | db, 38 | { 39 | username: 'apple', 40 | owner: '0xd469E0504c20185941E73029C6A400bD2dD28A1A', 41 | from: 0, 42 | to: 123, 43 | userFid: 123, 44 | }, 45 | 123 46 | ) 47 | ).rejects.toThrow('USERNAME_RESERVED'); 48 | }); 49 | 50 | test('can register a reserved name with an admin account', async () => { 51 | expect(await createTestTransfer(db, { username: 'apple', to: 123 })); 52 | }); 53 | 54 | test('same fid cannot register twice', async () => { 55 | await expect(createTestTransfer(db, { username: 'test1234', to: 2 })).rejects.toThrow('TOO_MANY_NAMES'); 56 | }); 57 | 58 | test('cannot unregister a nonexistent name', async () => { 59 | await expect(createTestTransfer(db, { username: 'nonexistent', from: 1, to: 0 })).rejects.toThrow( 60 | 'USERNAME_NOT_FOUND' 61 | ); 62 | }); 63 | 64 | test('cannot transfer a nonexistent name', async () => { 65 | await expect(createTestTransfer(db, { username: 'nonexistent', from: 1, to: 10 })).rejects.toThrow( 66 | 'USERNAME_NOT_FOUND' 67 | ); 68 | }); 69 | 70 | test('cannot register an invalid name', async () => { 71 | await expect( 72 | createTestTransfer(db, { username: 'namethatislongerthan16chars', from: 0, to: 10 }) 73 | ).rejects.toThrow('INVALID_USERNAME'); 74 | await expect(createTestTransfer(db, { username: 'invalidchars!', from: 0, to: 10 })).rejects.toThrow( 75 | 'INVALID_USERNAME' 76 | ); 77 | await expect(createTestTransfer(db, { username: '', from: 0, to: 10 })).rejects.toThrow('INVALID_USERNAME'); 78 | }); 79 | 80 | test('must have a valid timestamp', async () => { 81 | // Timestamp cannot be older than existing transfer 82 | await expect( 83 | createTestTransfer(db, { username: 'test123', from: 1, to: 10, timestamp: currentTimestamp() - 100 }) 84 | ).rejects.toThrow('INVALID_TIMESTAMP'); 85 | 86 | // Timestamp cannot be too far in the future 87 | await expect( 88 | createTestTransfer(db, { 89 | username: 'test123', 90 | from: 2, 91 | to: 10, 92 | timestamp: currentTimestamp() + (TIMESTAMP_TOLERANCE + 10), 93 | }) 94 | ).rejects.toThrow('INVALID_TIMESTAMP'); 95 | 96 | // Timestamp cannot be too far in the past 97 | await expect( 98 | createTestTransfer(db, { 99 | username: 'newusername', 100 | from: 0, 101 | to: 15, 102 | timestamp: currentTimestamp() - (TIMESTAMP_TOLERANCE + 10), 103 | }) 104 | ).rejects.toThrow('INVALID_TIMESTAMP'); 105 | 106 | await expect(createTestTransfer(db, { username: 'newusername', from: 0, to: 15, timestamp: 0 })).rejects.toThrow( 107 | 'INVALID_TIMESTAMP' 108 | ); 109 | }); 110 | 111 | test('fails for an invalid signature', async () => { 112 | const now = currentTimestamp(); 113 | // different name than signed type 114 | await expect( 115 | createTestTransfer(db, { 116 | username: 'differentname', 117 | to: 5, 118 | timestamp: now, 119 | userSignature: await generateSignature('aname', now, owner, signer), 120 | }) 121 | ).rejects.toThrow('INVALID_SIGNATURE'); 122 | 123 | // different timestamp than signed type 124 | await expect( 125 | createTestTransfer(db, { 126 | username: 'aname', 127 | to: 5, 128 | timestamp: now + 1, 129 | userSignature: await generateSignature('aname', now, owner, signer), 130 | }) 131 | ).rejects.toThrow('INVALID_SIGNATURE'); 132 | 133 | // different owner than signed type 134 | await expect( 135 | createTestTransfer(db, { 136 | username: 'aname', 137 | to: 5, 138 | timestamp: now, 139 | owner: anotherSigner.address, 140 | userSignature: await generateSignature('aname', now, owner, signer), 141 | }) 142 | ).rejects.toThrow('INVALID_SIGNATURE'); 143 | }); 144 | 145 | test('only admins can transfer names owned by other fids', async () => { 146 | const now = currentTimestamp(); 147 | 148 | // FID is not an admin, rejected 149 | await expect( 150 | createTestTransfer(db, { 151 | username: 'name', 152 | to: 5, 153 | owner: anotherSigner.address, 154 | userFid: 1, 155 | }) 156 | ).rejects.toThrow('UNAUTHORIZED'); 157 | 158 | // Fid is an admin, but signature doesn't match known public key, rejected 159 | await expect( 160 | createTestTransfer(db, { 161 | username: 'name', 162 | to: 5, 163 | owner: anotherSigner.address, 164 | userFid: signerFid, 165 | userSignature: await generateSignature('name', now, owner, signer), 166 | }) 167 | ).rejects.toThrow('INVALID_SIGNATURE'); 168 | }); 169 | 170 | test('user can transfer name if they own the fid', async () => { 171 | await expect( 172 | createTestTransfer( 173 | db, 174 | { 175 | username: 'anewname', 176 | to: 5, 177 | owner: anotherSigner.address, 178 | userSignature: await generateSignature( 179 | 'anewname', 180 | currentTimestamp(), 181 | anotherSigner.address, 182 | anotherSigner 183 | ), 184 | userFid: 5, 185 | }, 186 | 5 187 | ) 188 | ).resolves.toBeDefined(); 189 | }); 190 | 191 | test('user can only transfer name once in 28 days', async () => { 192 | await expect( 193 | createTestTransfer( 194 | db, 195 | { 196 | username: 'anewname', 197 | to: 5, 198 | owner: anotherSigner.address, 199 | userSignature: await generateSignature( 200 | 'anewname', 201 | currentTimestamp(), 202 | anotherSigner.address, 203 | anotherSigner 204 | ), 205 | userFid: 5, 206 | }, 207 | 5 208 | ) 209 | ).resolves.toBeDefined(); 210 | await expect( 211 | createTestTransfer( 212 | db, 213 | { 214 | username: 'anewname', 215 | from: 5, 216 | to: 0, 217 | owner: anotherSigner.address, 218 | timestamp: currentTimestamp() + 1, 219 | userSignature: await generateSignature( 220 | 'anewname', 221 | currentTimestamp() + 1, 222 | anotherSigner.address, 223 | anotherSigner 224 | ), 225 | userFid: 5, 226 | }, 227 | 5 228 | ) 229 | ).rejects.toThrow('THROTTLED'); 230 | }); 231 | 232 | test('user cannot transfer name if they do not own the fid', async () => { 233 | await expect( 234 | createTestTransfer( 235 | db, 236 | { 237 | username: 'anewname', 238 | to: 5, 239 | owner: anotherSigner.address, 240 | userSignature: await generateSignature( 241 | 'anewname', 242 | currentTimestamp(), 243 | anotherSigner.address, 244 | anotherSigner 245 | ), 246 | userFid: 5, 247 | }, 248 | 1 249 | ) 250 | ).rejects.toThrow('INVALID_FID_OWNER'); 251 | }); 252 | test('fails if userFid does not match transfer fid', async () => { 253 | await expect( 254 | createTestTransfer(db, { 255 | username: 'anewname', 256 | to: 5, 257 | owner: anotherSigner.address, 258 | userFid: 1, 259 | }) 260 | ).rejects.toThrow('UNAUTHORIZED'); 261 | }); 262 | }); 263 | 264 | describe('getLatestTransfer', () => { 265 | test('should return the latest transfer', async () => { 266 | const latest = await getLatestTransfer('test123', db); 267 | expect(latest).toBeDefined(); 268 | expect(latest!.username).toBe('test123'); 269 | expect(latest!.from).toBe(1); 270 | expect(latest!.to).toBe(2); 271 | expect(latest!.user_signature).toBeDefined(); 272 | expect(latest!.server_signature).toBeDefined(); 273 | }); 274 | test('returns undefined if no transfer', async () => { 275 | const latest = await getLatestTransfer('nonexistent', db); 276 | expect(latest).toBeUndefined(); 277 | }); 278 | }); 279 | 280 | describe('getCurrentUsernames', () => { 281 | test('should return the current usernames', async () => { 282 | const username_for_1 = await getCurrentUsername(1, db); 283 | expect(username_for_1).toBeUndefined(); 284 | const username_for_2 = await getCurrentUsername(2, db); 285 | expect(username_for_2).toBe('test123'); 286 | }); 287 | 288 | test('returns undefined for fid 0', async () => { 289 | await createTestTransfer(db, { username: 'test123', from: 2, to: 0, timestamp: currentTimestamp() + 20 }); 290 | await createTestTransfer(db, { username: 'test3', from: 3, to: 0 }); 291 | const username_for_0 = await getCurrentUsername(0, db); 292 | expect(username_for_0).toBeUndefined(); 293 | }); 294 | }); 295 | }); 296 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { app, RESOLVE_ABI } from '../src/app.js'; 3 | import { sql } from 'kysely'; 4 | import { getDbClient, migrateToLatest } from '../src/db.js'; 5 | import { log } from '../src/log.js'; 6 | import { 7 | generateSignature, 8 | signer, 9 | signerAddress, 10 | signerFid, 11 | verifyCCIPSignature, 12 | verifySignature, 13 | } from '../src/signature.js'; 14 | import { bytesToHex, currentTimestamp } from '../src/util.js'; 15 | import { createTestTransfer } from './utils.js'; 16 | import { AbiCoder, ethers, Interface, ZeroAddress } from 'ethers'; 17 | import { CCIP_ADDRESS } from '../src/env.js'; 18 | 19 | const db = getDbClient(); 20 | const anotherSigner = ethers.Wallet.createRandom(); 21 | 22 | beforeAll(async () => { 23 | await migrateToLatest(db, log); 24 | }); 25 | 26 | describe('app', () => { 27 | const now = currentTimestamp(); 28 | 29 | beforeAll(async () => { 30 | await sql`TRUNCATE TABLE transfers RESTART IDENTITY`.execute(db); 31 | await createTestTransfer(db, { username: 'test1', to: 1, timestamp: now }); 32 | await createTestTransfer(db, { username: 'test2', to: 2, timestamp: now }); 33 | await createTestTransfer(db, { username: 'test3', to: 3, timestamp: now + 1 }); 34 | await createTestTransfer(db, { username: 'test3', from: 3, to: 0, timestamp: now + 2 }); 35 | }); 36 | 37 | describe('get transfers', () => { 38 | test('should returns transfers', async () => { 39 | const response = await request(app).get('/transfers'); 40 | expect(response.status).toBe(200); 41 | expect(response.body.transfers).toHaveLength(4); 42 | }); 43 | 44 | test('should returns transfers from an id', async () => { 45 | const response = await request(app).get('/transfers?from_id=2'); 46 | expect(response.status).toBe(200); 47 | expect(response.body.transfers).toHaveLength(2); 48 | }); 49 | 50 | test('should returns transfers from a timestamp', async () => { 51 | const response = await request(app).get(`/transfers?from_ts=${now}`); 52 | expect(response.status).toBe(200); 53 | expect(response.body.transfers).toHaveLength(2); 54 | expect(response.body.transfers[0]).toMatchObject({ timestamp: now + 1 }); 55 | expect(response.body.transfers[1]).toMatchObject({ timestamp: now + 2 }); 56 | }); 57 | 58 | test('should returns transfers for a name', async () => { 59 | const response = await request(app).get('/transfers?name=test2'); 60 | expect(response.status).toBe(200); 61 | expect(response.body.transfers).toHaveLength(1); 62 | expect(response.body.transfers[0]).toMatchObject({ username: 'test2' }); 63 | }); 64 | 65 | test('should returns transfers for an fid', async () => { 66 | const response = await request(app).get('/transfers?fid=3'); 67 | expect(response.status).toBe(200); 68 | // Includes both from and to 69 | expect(response.body.transfers).toHaveLength(2); 70 | expect(response.body.transfers[0]).toMatchObject({ username: 'test3', from: 0, to: 3 }); 71 | expect(response.body.transfers[1]).toMatchObject({ username: 'test3', from: 3, to: 0 }); 72 | }); 73 | 74 | test('combines multiple queries', async () => { 75 | const response = await request(app).get(`/transfers?name=test3&from_ts=${now + 1}`); 76 | expect(response.status).toBe(200); 77 | // Includes both from and to 78 | expect(response.body.transfers).toHaveLength(1); 79 | expect(response.body.transfers[0]).toMatchObject({ username: 'test3', from: 3, to: 0, timestamp: now + 2 }); 80 | }); 81 | 82 | test('does not fail for zero padded names', async () => { 83 | const zeroPaddedName = Buffer.concat([Buffer.from('test2'), Buffer.alloc(3)]).toString('utf-8'); 84 | const response = await request(app).get(`/transfers?name=${zeroPaddedName}`); 85 | expect(response.status).toBe(200); 86 | expect(response.body.transfers).toHaveLength(1); 87 | expect(response.body.transfers[0]).toMatchObject({ username: 'test2' }); 88 | }); 89 | }); 90 | 91 | describe('get current transfer', () => { 92 | test('returns error for unknown name', async () => { 93 | let response = await request(app).get('/transfers/current?name=nonexistent'); 94 | expect(response.status).toBe(404); 95 | 96 | // Name was burned 97 | response = await request(app).get('/transfers/current?name=test3'); 98 | expect(response.status).toBe(404); 99 | }); 100 | test('returns error for unknown fid', async () => { 101 | let response = await request(app).get('/transfers/current?fid=129837123'); 102 | expect(response.status).toBe(404); 103 | 104 | // Name was burned 105 | response = await request(app).get('/transfers/current?fid=3'); 106 | expect(response.status).toBe(404); 107 | }); 108 | test('returns error if no name or fid provided', async () => { 109 | const response = await request(app).get('/transfers/current'); 110 | expect(response.status).toBe(404); 111 | }); 112 | test('returns latest transfer for fid', async () => { 113 | await createTestTransfer(db, { username: 'test-current', from: 0, to: 3, timestamp: now + 3 }); 114 | const response = await request(app).get('/transfers/current?fid=3'); 115 | expect(response.status).toBe(200); 116 | expect(response.body.transfer).toMatchObject({ username: 'test-current', from: 0, to: 3, timestamp: now + 3 }); 117 | }); 118 | test('returns latest transfer for name', async () => { 119 | await createTestTransfer(db, { username: 'test3', from: 0, to: 10, timestamp: now + 3 }); 120 | const response = await request(app).get('/transfers/current?name=test3'); 121 | expect(response.status).toBe(200); 122 | expect(response.body.transfer).toMatchObject({ username: 'test3', from: 0, to: 10, timestamp: now + 3 }); 123 | }); 124 | }); 125 | 126 | describe('create transfer', () => { 127 | const now = currentTimestamp(); 128 | 129 | test('should create a transfer', async () => { 130 | const user_signature = bytesToHex(await generateSignature('test4', now, anotherSigner.address, signer)); 131 | const response = await request(app).post('/transfers').send({ 132 | name: 'test4', 133 | from: 0, 134 | to: 4, 135 | owner: anotherSigner.address, 136 | timestamp: now, 137 | signature: user_signature, 138 | fid: signerFid, 139 | }); 140 | expect(response.status).toBe(200); 141 | const transferRes = response.body.transfer; 142 | expect(transferRes).toMatchObject({ 143 | username: 'test4', 144 | from: 0, 145 | to: 4, 146 | timestamp: now, 147 | owner: anotherSigner.address.toLowerCase(), 148 | }); 149 | expect(verifySignature('test4', now, anotherSigner.address, transferRes.user_signature, signer.address)).toBe( 150 | true 151 | ); 152 | expect(verifySignature('test4', now, anotherSigner.address, transferRes.server_signature, signer.address)).toBe( 153 | true 154 | ); 155 | }); 156 | 157 | test('registering the same name to the same owner and fid twice should not fail', async () => { 158 | const user_signature = bytesToHex(await generateSignature('testreuse', now, signerAddress, signer)); 159 | const transfer_request = { 160 | name: 'testreuse', 161 | from: 0, 162 | to: 5, 163 | owner: signerAddress, 164 | timestamp: now, 165 | signature: user_signature, 166 | fid: signerFid, 167 | }; 168 | const response = await request(app).post('/transfers').send(transfer_request); 169 | expect(response.status).toBe(200); 170 | 171 | // Posting the same request again should not fail 172 | const second_response = await request(app).post('/transfers').send(transfer_request); 173 | expect(second_response.status).toBe(200); 174 | 175 | // Posting the same request with a different owner address should fail 176 | const bad_owner_response = await request(app) 177 | .post('/transfers') 178 | .send({ 179 | ...transfer_request, 180 | owner: anotherSigner.address, 181 | signature: bytesToHex(await generateSignature('testreuse', now, anotherSigner.address, signer)), 182 | }); 183 | expect(bad_owner_response.status).toBe(400); 184 | expect(bad_owner_response.body.code).toBe('TOO_MANY_NAMES'); 185 | }); 186 | 187 | test('should throw error if validation fails', async () => { 188 | const response = await request(app) 189 | .post('/transfers') 190 | .send({ 191 | name: 'nonexistent', 192 | from: 1, 193 | to: 0, 194 | owner: anotherSigner.address, 195 | timestamp: now, 196 | signature: bytesToHex(await generateSignature('nonexistent', now, anotherSigner.address, signer)), 197 | fid: signerFid, 198 | }); 199 | expect(response.status).toBe(400); 200 | expect(response.body.code).toBe('USERNAME_NOT_FOUND'); 201 | }); 202 | 203 | test('should throw error if signature is invalid', async () => { 204 | const response = await request(app) 205 | .post('/transfers') 206 | .send({ 207 | name: 'nonexistent', 208 | from: 1, 209 | to: 0, 210 | owner: anotherSigner.address, 211 | timestamp: now, 212 | signature: bytesToHex(await generateSignature('nonexistent', now, signer.address, signer)), 213 | fid: signerFid, 214 | }); 215 | expect(response.status).toBe(400); 216 | expect(response.body.code).toBe('INVALID_SIGNATURE'); 217 | }); 218 | }); 219 | 220 | describe('signer', () => { 221 | test('returns signer address', async () => { 222 | const response = await request(app).get('/signer'); 223 | expect(response.status).toBe(200); 224 | expect(response.body.signer.toLowerCase()).toEqual(signerAddress.toLowerCase()); 225 | }); 226 | }); 227 | 228 | describe('ccip resolution', () => { 229 | const resolveABI = new Interface(RESOLVE_ABI).getFunction('resolve')!; 230 | 231 | it('should return a valid signature for a ccip lookup of a registered name', async () => { 232 | // Calldata for test1.farcaster.xyz 233 | const callData = 234 | '0x9061b923000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000015057465737431096661726361737465720378797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000243b3b57deeea35aa5de7e8da11d1636463a57da877fa0c0c65b969f7fecb7eb6c93a20c1b00000000000000000000000000000000000000000000000000000000'; 235 | const response = await request(app).get(`/ccip/${CCIP_ADDRESS}/${callData}.json`); 236 | expect(response.status).toBe(200); 237 | const [username, timestamp, owner, signature] = AbiCoder.defaultAbiCoder().decode( 238 | resolveABI.outputs, 239 | response.body.data 240 | ); 241 | expect(username).toBe('test1'); 242 | expect(verifyCCIPSignature(username, timestamp, owner, signature, signer.address)).toBe(true); 243 | // CCIP domain is different from hub domain 244 | expect(verifySignature(username, timestamp, owner, signature, signer.address)).toBe(false); 245 | }); 246 | 247 | it('should return an empty signature for a ccip lookup of an unregistered name', async () => { 248 | // Calldata for alice.farcaster.xyz 249 | const callData = 250 | '0x9061b92300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001505616c696365096661726361737465720378797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000243b3b57de00d4f449060ad2a07ff5ad355ae8da52281e95f6ad10fb923ae7cad9f2c43c2a00000000000000000000000000000000000000000000000000000000'; 251 | const response = await request(app).get(`/ccip/${CCIP_ADDRESS}/${callData}.json`); 252 | expect(response.status).toBe(200); 253 | const [username, timestamp, owner, signature] = AbiCoder.defaultAbiCoder().decode( 254 | resolveABI.outputs, 255 | response.body.data 256 | ); 257 | expect(username).toBe(''); 258 | expect(timestamp.toString()).toEqual('0'); 259 | expect(owner).toBe(ZeroAddress); 260 | expect(signature).toBe('0x'); 261 | }); 262 | }); 263 | }); 264 | -------------------------------------------------------------------------------- /src/abi/factories/IdRegistry__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Interface, type ContractRunner } from 'ethers'; 6 | import type { IdRegistry, IdRegistryInterface } from '../IdRegistry.js'; 7 | 8 | const _abi = [ 9 | { 10 | inputs: [ 11 | { 12 | internalType: 'address', 13 | name: '_owner', 14 | type: 'address', 15 | }, 16 | ], 17 | stateMutability: 'nonpayable', 18 | type: 'constructor', 19 | }, 20 | { 21 | inputs: [], 22 | name: 'HasId', 23 | type: 'error', 24 | }, 25 | { 26 | inputs: [], 27 | name: 'HasNoId', 28 | type: 'error', 29 | }, 30 | { 31 | inputs: [ 32 | { 33 | internalType: 'address', 34 | name: 'account', 35 | type: 'address', 36 | }, 37 | { 38 | internalType: 'uint256', 39 | name: 'currentNonce', 40 | type: 'uint256', 41 | }, 42 | ], 43 | name: 'InvalidAccountNonce', 44 | type: 'error', 45 | }, 46 | { 47 | inputs: [], 48 | name: 'InvalidAddress', 49 | type: 'error', 50 | }, 51 | { 52 | inputs: [], 53 | name: 'InvalidShortString', 54 | type: 'error', 55 | }, 56 | { 57 | inputs: [], 58 | name: 'InvalidSignature', 59 | type: 'error', 60 | }, 61 | { 62 | inputs: [], 63 | name: 'OnlyTrustedCaller', 64 | type: 'error', 65 | }, 66 | { 67 | inputs: [], 68 | name: 'Registrable', 69 | type: 'error', 70 | }, 71 | { 72 | inputs: [], 73 | name: 'Seedable', 74 | type: 'error', 75 | }, 76 | { 77 | inputs: [], 78 | name: 'SignatureExpired', 79 | type: 'error', 80 | }, 81 | { 82 | inputs: [ 83 | { 84 | internalType: 'string', 85 | name: 'str', 86 | type: 'string', 87 | }, 88 | ], 89 | name: 'StringTooLong', 90 | type: 'error', 91 | }, 92 | { 93 | inputs: [], 94 | name: 'Unauthorized', 95 | type: 'error', 96 | }, 97 | { 98 | anonymous: false, 99 | inputs: [ 100 | { 101 | indexed: true, 102 | internalType: 'uint256', 103 | name: 'id', 104 | type: 'uint256', 105 | }, 106 | { 107 | indexed: true, 108 | internalType: 'address', 109 | name: 'recovery', 110 | type: 'address', 111 | }, 112 | ], 113 | name: 'ChangeRecoveryAddress', 114 | type: 'event', 115 | }, 116 | { 117 | anonymous: false, 118 | inputs: [], 119 | name: 'DisableTrustedOnly', 120 | type: 'event', 121 | }, 122 | { 123 | anonymous: false, 124 | inputs: [], 125 | name: 'EIP712DomainChanged', 126 | type: 'event', 127 | }, 128 | { 129 | anonymous: false, 130 | inputs: [ 131 | { 132 | indexed: true, 133 | internalType: 'address', 134 | name: 'previousOwner', 135 | type: 'address', 136 | }, 137 | { 138 | indexed: true, 139 | internalType: 'address', 140 | name: 'newOwner', 141 | type: 'address', 142 | }, 143 | ], 144 | name: 'OwnershipTransferStarted', 145 | type: 'event', 146 | }, 147 | { 148 | anonymous: false, 149 | inputs: [ 150 | { 151 | indexed: true, 152 | internalType: 'address', 153 | name: 'previousOwner', 154 | type: 'address', 155 | }, 156 | { 157 | indexed: true, 158 | internalType: 'address', 159 | name: 'newOwner', 160 | type: 'address', 161 | }, 162 | ], 163 | name: 'OwnershipTransferred', 164 | type: 'event', 165 | }, 166 | { 167 | anonymous: false, 168 | inputs: [ 169 | { 170 | indexed: false, 171 | internalType: 'address', 172 | name: 'account', 173 | type: 'address', 174 | }, 175 | ], 176 | name: 'Paused', 177 | type: 'event', 178 | }, 179 | { 180 | anonymous: false, 181 | inputs: [ 182 | { 183 | indexed: true, 184 | internalType: 'address', 185 | name: 'to', 186 | type: 'address', 187 | }, 188 | { 189 | indexed: true, 190 | internalType: 'uint256', 191 | name: 'id', 192 | type: 'uint256', 193 | }, 194 | { 195 | indexed: false, 196 | internalType: 'address', 197 | name: 'recovery', 198 | type: 'address', 199 | }, 200 | ], 201 | name: 'Register', 202 | type: 'event', 203 | }, 204 | { 205 | anonymous: false, 206 | inputs: [ 207 | { 208 | indexed: true, 209 | internalType: 'address', 210 | name: 'oldCaller', 211 | type: 'address', 212 | }, 213 | { 214 | indexed: true, 215 | internalType: 'address', 216 | name: 'newCaller', 217 | type: 'address', 218 | }, 219 | { 220 | indexed: false, 221 | internalType: 'address', 222 | name: 'owner', 223 | type: 'address', 224 | }, 225 | ], 226 | name: 'SetTrustedCaller', 227 | type: 'event', 228 | }, 229 | { 230 | anonymous: false, 231 | inputs: [ 232 | { 233 | indexed: true, 234 | internalType: 'address', 235 | name: 'from', 236 | type: 'address', 237 | }, 238 | { 239 | indexed: true, 240 | internalType: 'address', 241 | name: 'to', 242 | type: 'address', 243 | }, 244 | { 245 | indexed: true, 246 | internalType: 'uint256', 247 | name: 'id', 248 | type: 'uint256', 249 | }, 250 | ], 251 | name: 'Transfer', 252 | type: 'event', 253 | }, 254 | { 255 | anonymous: false, 256 | inputs: [ 257 | { 258 | indexed: false, 259 | internalType: 'address', 260 | name: 'account', 261 | type: 'address', 262 | }, 263 | ], 264 | name: 'Unpaused', 265 | type: 'event', 266 | }, 267 | { 268 | inputs: [], 269 | name: 'acceptOwnership', 270 | outputs: [], 271 | stateMutability: 'nonpayable', 272 | type: 'function', 273 | }, 274 | { 275 | inputs: [ 276 | { 277 | internalType: 'address', 278 | name: 'recovery', 279 | type: 'address', 280 | }, 281 | ], 282 | name: 'changeRecoveryAddress', 283 | outputs: [], 284 | stateMutability: 'nonpayable', 285 | type: 'function', 286 | }, 287 | { 288 | inputs: [], 289 | name: 'disableTrustedOnly', 290 | outputs: [], 291 | stateMutability: 'nonpayable', 292 | type: 'function', 293 | }, 294 | { 295 | inputs: [], 296 | name: 'eip712Domain', 297 | outputs: [ 298 | { 299 | internalType: 'bytes1', 300 | name: 'fields', 301 | type: 'bytes1', 302 | }, 303 | { 304 | internalType: 'string', 305 | name: 'name', 306 | type: 'string', 307 | }, 308 | { 309 | internalType: 'string', 310 | name: 'version', 311 | type: 'string', 312 | }, 313 | { 314 | internalType: 'uint256', 315 | name: 'chainId', 316 | type: 'uint256', 317 | }, 318 | { 319 | internalType: 'address', 320 | name: 'verifyingContract', 321 | type: 'address', 322 | }, 323 | { 324 | internalType: 'bytes32', 325 | name: 'salt', 326 | type: 'bytes32', 327 | }, 328 | { 329 | internalType: 'uint256[]', 330 | name: 'extensions', 331 | type: 'uint256[]', 332 | }, 333 | ], 334 | stateMutability: 'view', 335 | type: 'function', 336 | }, 337 | { 338 | inputs: [ 339 | { 340 | internalType: 'address', 341 | name: 'owner', 342 | type: 'address', 343 | }, 344 | ], 345 | name: 'idOf', 346 | outputs: [ 347 | { 348 | internalType: 'uint256', 349 | name: 'fid', 350 | type: 'uint256', 351 | }, 352 | ], 353 | stateMutability: 'view', 354 | type: 'function', 355 | }, 356 | { 357 | inputs: [ 358 | { 359 | internalType: 'address', 360 | name: 'owner', 361 | type: 'address', 362 | }, 363 | ], 364 | name: 'nonces', 365 | outputs: [ 366 | { 367 | internalType: 'uint256', 368 | name: '', 369 | type: 'uint256', 370 | }, 371 | ], 372 | stateMutability: 'view', 373 | type: 'function', 374 | }, 375 | { 376 | inputs: [], 377 | name: 'owner', 378 | outputs: [ 379 | { 380 | internalType: 'address', 381 | name: '', 382 | type: 'address', 383 | }, 384 | ], 385 | stateMutability: 'view', 386 | type: 'function', 387 | }, 388 | { 389 | inputs: [], 390 | name: 'pauseRegistration', 391 | outputs: [], 392 | stateMutability: 'nonpayable', 393 | type: 'function', 394 | }, 395 | { 396 | inputs: [], 397 | name: 'paused', 398 | outputs: [ 399 | { 400 | internalType: 'bool', 401 | name: '', 402 | type: 'bool', 403 | }, 404 | ], 405 | stateMutability: 'view', 406 | type: 'function', 407 | }, 408 | { 409 | inputs: [], 410 | name: 'pendingOwner', 411 | outputs: [ 412 | { 413 | internalType: 'address', 414 | name: '', 415 | type: 'address', 416 | }, 417 | ], 418 | stateMutability: 'view', 419 | type: 'function', 420 | }, 421 | { 422 | inputs: [ 423 | { 424 | internalType: 'address', 425 | name: 'from', 426 | type: 'address', 427 | }, 428 | { 429 | internalType: 'address', 430 | name: 'to', 431 | type: 'address', 432 | }, 433 | { 434 | internalType: 'uint256', 435 | name: 'deadline', 436 | type: 'uint256', 437 | }, 438 | { 439 | internalType: 'bytes', 440 | name: 'sig', 441 | type: 'bytes', 442 | }, 443 | ], 444 | name: 'recover', 445 | outputs: [], 446 | stateMutability: 'nonpayable', 447 | type: 'function', 448 | }, 449 | { 450 | inputs: [ 451 | { 452 | internalType: 'address', 453 | name: 'recovery', 454 | type: 'address', 455 | }, 456 | ], 457 | name: 'register', 458 | outputs: [ 459 | { 460 | internalType: 'uint256', 461 | name: 'fid', 462 | type: 'uint256', 463 | }, 464 | ], 465 | stateMutability: 'nonpayable', 466 | type: 'function', 467 | }, 468 | { 469 | inputs: [ 470 | { 471 | internalType: 'address', 472 | name: 'to', 473 | type: 'address', 474 | }, 475 | { 476 | internalType: 'address', 477 | name: 'recovery', 478 | type: 'address', 479 | }, 480 | { 481 | internalType: 'uint256', 482 | name: 'deadline', 483 | type: 'uint256', 484 | }, 485 | { 486 | internalType: 'bytes', 487 | name: 'sig', 488 | type: 'bytes', 489 | }, 490 | ], 491 | name: 'registerFor', 492 | outputs: [ 493 | { 494 | internalType: 'uint256', 495 | name: 'fid', 496 | type: 'uint256', 497 | }, 498 | ], 499 | stateMutability: 'nonpayable', 500 | type: 'function', 501 | }, 502 | { 503 | inputs: [], 504 | name: 'renounceOwnership', 505 | outputs: [], 506 | stateMutability: 'nonpayable', 507 | type: 'function', 508 | }, 509 | { 510 | inputs: [ 511 | { 512 | internalType: 'address', 513 | name: '_trustedCaller', 514 | type: 'address', 515 | }, 516 | ], 517 | name: 'setTrustedCaller', 518 | outputs: [], 519 | stateMutability: 'nonpayable', 520 | type: 'function', 521 | }, 522 | { 523 | inputs: [ 524 | { 525 | internalType: 'address', 526 | name: 'to', 527 | type: 'address', 528 | }, 529 | { 530 | internalType: 'uint256', 531 | name: 'deadline', 532 | type: 'uint256', 533 | }, 534 | { 535 | internalType: 'bytes', 536 | name: 'sig', 537 | type: 'bytes', 538 | }, 539 | ], 540 | name: 'transfer', 541 | outputs: [], 542 | stateMutability: 'nonpayable', 543 | type: 'function', 544 | }, 545 | { 546 | inputs: [ 547 | { 548 | internalType: 'address', 549 | name: 'newOwner', 550 | type: 'address', 551 | }, 552 | ], 553 | name: 'transferOwnership', 554 | outputs: [], 555 | stateMutability: 'nonpayable', 556 | type: 'function', 557 | }, 558 | { 559 | inputs: [], 560 | name: 'trustedCaller', 561 | outputs: [ 562 | { 563 | internalType: 'address', 564 | name: '', 565 | type: 'address', 566 | }, 567 | ], 568 | stateMutability: 'view', 569 | type: 'function', 570 | }, 571 | { 572 | inputs: [ 573 | { 574 | internalType: 'address', 575 | name: 'to', 576 | type: 'address', 577 | }, 578 | { 579 | internalType: 'address', 580 | name: 'recovery', 581 | type: 'address', 582 | }, 583 | ], 584 | name: 'trustedRegister', 585 | outputs: [ 586 | { 587 | internalType: 'uint256', 588 | name: 'fid', 589 | type: 'uint256', 590 | }, 591 | ], 592 | stateMutability: 'nonpayable', 593 | type: 'function', 594 | }, 595 | { 596 | inputs: [], 597 | name: 'unpauseRegistration', 598 | outputs: [], 599 | stateMutability: 'nonpayable', 600 | type: 'function', 601 | }, 602 | ] as const; 603 | 604 | export class IdRegistry__factory { 605 | static readonly abi = _abi; 606 | static createInterface(): IdRegistryInterface { 607 | return new Interface(_abi) as IdRegistryInterface; 608 | } 609 | static connect(address: string, runner?: ContractRunner | null): IdRegistry { 610 | return new Contract(address, _abi, runner) as unknown as IdRegistry; 611 | } 612 | } 613 | -------------------------------------------------------------------------------- /src/abi/idRegistry.abi: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": 4 | [ 5 | { 6 | "internalType": "address", 7 | "name": "_owner", 8 | "type": "address" 9 | } 10 | ], 11 | "stateMutability": "nonpayable", 12 | "type": "constructor" 13 | }, 14 | { 15 | "inputs": 16 | [], 17 | "name": "HasId", 18 | "type": "error" 19 | }, 20 | { 21 | "inputs": 22 | [], 23 | "name": "HasNoId", 24 | "type": "error" 25 | }, 26 | { 27 | "inputs": 28 | [ 29 | { 30 | "internalType": "address", 31 | "name": "account", 32 | "type": "address" 33 | }, 34 | { 35 | "internalType": "uint256", 36 | "name": "currentNonce", 37 | "type": "uint256" 38 | } 39 | ], 40 | "name": "InvalidAccountNonce", 41 | "type": "error" 42 | }, 43 | { 44 | "inputs": 45 | [], 46 | "name": "InvalidAddress", 47 | "type": "error" 48 | }, 49 | { 50 | "inputs": 51 | [], 52 | "name": "InvalidShortString", 53 | "type": "error" 54 | }, 55 | { 56 | "inputs": 57 | [], 58 | "name": "InvalidSignature", 59 | "type": "error" 60 | }, 61 | { 62 | "inputs": 63 | [], 64 | "name": "OnlyTrustedCaller", 65 | "type": "error" 66 | }, 67 | { 68 | "inputs": 69 | [], 70 | "name": "Registrable", 71 | "type": "error" 72 | }, 73 | { 74 | "inputs": 75 | [], 76 | "name": "Seedable", 77 | "type": "error" 78 | }, 79 | { 80 | "inputs": 81 | [], 82 | "name": "SignatureExpired", 83 | "type": "error" 84 | }, 85 | { 86 | "inputs": 87 | [ 88 | { 89 | "internalType": "string", 90 | "name": "str", 91 | "type": "string" 92 | } 93 | ], 94 | "name": "StringTooLong", 95 | "type": "error" 96 | }, 97 | { 98 | "inputs": 99 | [], 100 | "name": "Unauthorized", 101 | "type": "error" 102 | }, 103 | { 104 | "anonymous": false, 105 | "inputs": 106 | [ 107 | { 108 | "indexed": true, 109 | "internalType": "uint256", 110 | "name": "id", 111 | "type": "uint256" 112 | }, 113 | { 114 | "indexed": true, 115 | "internalType": "address", 116 | "name": "recovery", 117 | "type": "address" 118 | } 119 | ], 120 | "name": "ChangeRecoveryAddress", 121 | "type": "event" 122 | }, 123 | { 124 | "anonymous": false, 125 | "inputs": 126 | [], 127 | "name": "DisableTrustedOnly", 128 | "type": "event" 129 | }, 130 | { 131 | "anonymous": false, 132 | "inputs": 133 | [], 134 | "name": "EIP712DomainChanged", 135 | "type": "event" 136 | }, 137 | { 138 | "anonymous": false, 139 | "inputs": 140 | [ 141 | { 142 | "indexed": true, 143 | "internalType": "address", 144 | "name": "previousOwner", 145 | "type": "address" 146 | }, 147 | { 148 | "indexed": true, 149 | "internalType": "address", 150 | "name": "newOwner", 151 | "type": "address" 152 | } 153 | ], 154 | "name": "OwnershipTransferStarted", 155 | "type": "event" 156 | }, 157 | { 158 | "anonymous": false, 159 | "inputs": 160 | [ 161 | { 162 | "indexed": true, 163 | "internalType": "address", 164 | "name": "previousOwner", 165 | "type": "address" 166 | }, 167 | { 168 | "indexed": true, 169 | "internalType": "address", 170 | "name": "newOwner", 171 | "type": "address" 172 | } 173 | ], 174 | "name": "OwnershipTransferred", 175 | "type": "event" 176 | }, 177 | { 178 | "anonymous": false, 179 | "inputs": 180 | [ 181 | { 182 | "indexed": false, 183 | "internalType": "address", 184 | "name": "account", 185 | "type": "address" 186 | } 187 | ], 188 | "name": "Paused", 189 | "type": "event" 190 | }, 191 | { 192 | "anonymous": false, 193 | "inputs": 194 | [ 195 | { 196 | "indexed": true, 197 | "internalType": "address", 198 | "name": "to", 199 | "type": "address" 200 | }, 201 | { 202 | "indexed": true, 203 | "internalType": "uint256", 204 | "name": "id", 205 | "type": "uint256" 206 | }, 207 | { 208 | "indexed": false, 209 | "internalType": "address", 210 | "name": "recovery", 211 | "type": "address" 212 | } 213 | ], 214 | "name": "Register", 215 | "type": "event" 216 | }, 217 | { 218 | "anonymous": false, 219 | "inputs": 220 | [ 221 | { 222 | "indexed": true, 223 | "internalType": "address", 224 | "name": "oldCaller", 225 | "type": "address" 226 | }, 227 | { 228 | "indexed": true, 229 | "internalType": "address", 230 | "name": "newCaller", 231 | "type": "address" 232 | }, 233 | { 234 | "indexed": false, 235 | "internalType": "address", 236 | "name": "owner", 237 | "type": "address" 238 | } 239 | ], 240 | "name": "SetTrustedCaller", 241 | "type": "event" 242 | }, 243 | { 244 | "anonymous": false, 245 | "inputs": 246 | [ 247 | { 248 | "indexed": true, 249 | "internalType": "address", 250 | "name": "from", 251 | "type": "address" 252 | }, 253 | { 254 | "indexed": true, 255 | "internalType": "address", 256 | "name": "to", 257 | "type": "address" 258 | }, 259 | { 260 | "indexed": true, 261 | "internalType": "uint256", 262 | "name": "id", 263 | "type": "uint256" 264 | } 265 | ], 266 | "name": "Transfer", 267 | "type": "event" 268 | }, 269 | { 270 | "anonymous": false, 271 | "inputs": 272 | [ 273 | { 274 | "indexed": false, 275 | "internalType": "address", 276 | "name": "account", 277 | "type": "address" 278 | } 279 | ], 280 | "name": "Unpaused", 281 | "type": "event" 282 | }, 283 | { 284 | "inputs": 285 | [], 286 | "name": "acceptOwnership", 287 | "outputs": 288 | [], 289 | "stateMutability": "nonpayable", 290 | "type": "function" 291 | }, 292 | { 293 | "inputs": 294 | [ 295 | { 296 | "internalType": "address", 297 | "name": "recovery", 298 | "type": "address" 299 | } 300 | ], 301 | "name": "changeRecoveryAddress", 302 | "outputs": 303 | [], 304 | "stateMutability": "nonpayable", 305 | "type": "function" 306 | }, 307 | { 308 | "inputs": 309 | [], 310 | "name": "disableTrustedOnly", 311 | "outputs": 312 | [], 313 | "stateMutability": "nonpayable", 314 | "type": "function" 315 | }, 316 | { 317 | "inputs": 318 | [], 319 | "name": "eip712Domain", 320 | "outputs": 321 | [ 322 | { 323 | "internalType": "bytes1", 324 | "name": "fields", 325 | "type": "bytes1" 326 | }, 327 | { 328 | "internalType": "string", 329 | "name": "name", 330 | "type": "string" 331 | }, 332 | { 333 | "internalType": "string", 334 | "name": "version", 335 | "type": "string" 336 | }, 337 | { 338 | "internalType": "uint256", 339 | "name": "chainId", 340 | "type": "uint256" 341 | }, 342 | { 343 | "internalType": "address", 344 | "name": "verifyingContract", 345 | "type": "address" 346 | }, 347 | { 348 | "internalType": "bytes32", 349 | "name": "salt", 350 | "type": "bytes32" 351 | }, 352 | { 353 | "internalType": "uint256[]", 354 | "name": "extensions", 355 | "type": "uint256[]" 356 | } 357 | ], 358 | "stateMutability": "view", 359 | "type": "function" 360 | }, 361 | { 362 | "inputs": 363 | [ 364 | { 365 | "internalType": "address", 366 | "name": "owner", 367 | "type": "address" 368 | } 369 | ], 370 | "name": "idOf", 371 | "outputs": 372 | [ 373 | { 374 | "internalType": "uint256", 375 | "name": "fid", 376 | "type": "uint256" 377 | } 378 | ], 379 | "stateMutability": "view", 380 | "type": "function" 381 | }, 382 | { 383 | "inputs": 384 | [ 385 | { 386 | "internalType": "address", 387 | "name": "owner", 388 | "type": "address" 389 | } 390 | ], 391 | "name": "nonces", 392 | "outputs": 393 | [ 394 | { 395 | "internalType": "uint256", 396 | "name": "", 397 | "type": "uint256" 398 | } 399 | ], 400 | "stateMutability": "view", 401 | "type": "function" 402 | }, 403 | { 404 | "inputs": 405 | [], 406 | "name": "owner", 407 | "outputs": 408 | [ 409 | { 410 | "internalType": "address", 411 | "name": "", 412 | "type": "address" 413 | } 414 | ], 415 | "stateMutability": "view", 416 | "type": "function" 417 | }, 418 | { 419 | "inputs": 420 | [], 421 | "name": "pauseRegistration", 422 | "outputs": 423 | [], 424 | "stateMutability": "nonpayable", 425 | "type": "function" 426 | }, 427 | { 428 | "inputs": 429 | [], 430 | "name": "paused", 431 | "outputs": 432 | [ 433 | { 434 | "internalType": "bool", 435 | "name": "", 436 | "type": "bool" 437 | } 438 | ], 439 | "stateMutability": "view", 440 | "type": "function" 441 | }, 442 | { 443 | "inputs": 444 | [], 445 | "name": "pendingOwner", 446 | "outputs": 447 | [ 448 | { 449 | "internalType": "address", 450 | "name": "", 451 | "type": "address" 452 | } 453 | ], 454 | "stateMutability": "view", 455 | "type": "function" 456 | }, 457 | { 458 | "inputs": 459 | [ 460 | { 461 | "internalType": "address", 462 | "name": "from", 463 | "type": "address" 464 | }, 465 | { 466 | "internalType": "address", 467 | "name": "to", 468 | "type": "address" 469 | }, 470 | { 471 | "internalType": "uint256", 472 | "name": "deadline", 473 | "type": "uint256" 474 | }, 475 | { 476 | "internalType": "bytes", 477 | "name": "sig", 478 | "type": "bytes" 479 | } 480 | ], 481 | "name": "recover", 482 | "outputs": 483 | [], 484 | "stateMutability": "nonpayable", 485 | "type": "function" 486 | }, 487 | { 488 | "inputs": 489 | [ 490 | { 491 | "internalType": "address", 492 | "name": "recovery", 493 | "type": "address" 494 | } 495 | ], 496 | "name": "register", 497 | "outputs": 498 | [ 499 | { 500 | "internalType": "uint256", 501 | "name": "fid", 502 | "type": "uint256" 503 | } 504 | ], 505 | "stateMutability": "nonpayable", 506 | "type": "function" 507 | }, 508 | { 509 | "inputs": 510 | [ 511 | { 512 | "internalType": "address", 513 | "name": "to", 514 | "type": "address" 515 | }, 516 | { 517 | "internalType": "address", 518 | "name": "recovery", 519 | "type": "address" 520 | }, 521 | { 522 | "internalType": "uint256", 523 | "name": "deadline", 524 | "type": "uint256" 525 | }, 526 | { 527 | "internalType": "bytes", 528 | "name": "sig", 529 | "type": "bytes" 530 | } 531 | ], 532 | "name": "registerFor", 533 | "outputs": 534 | [ 535 | { 536 | "internalType": "uint256", 537 | "name": "fid", 538 | "type": "uint256" 539 | } 540 | ], 541 | "stateMutability": "nonpayable", 542 | "type": "function" 543 | }, 544 | { 545 | "inputs": 546 | [], 547 | "name": "renounceOwnership", 548 | "outputs": 549 | [], 550 | "stateMutability": "nonpayable", 551 | "type": "function" 552 | }, 553 | { 554 | "inputs": 555 | [ 556 | { 557 | "internalType": "address", 558 | "name": "_trustedCaller", 559 | "type": "address" 560 | } 561 | ], 562 | "name": "setTrustedCaller", 563 | "outputs": 564 | [], 565 | "stateMutability": "nonpayable", 566 | "type": "function" 567 | }, 568 | { 569 | "inputs": 570 | [ 571 | { 572 | "internalType": "address", 573 | "name": "to", 574 | "type": "address" 575 | }, 576 | { 577 | "internalType": "uint256", 578 | "name": "deadline", 579 | "type": "uint256" 580 | }, 581 | { 582 | "internalType": "bytes", 583 | "name": "sig", 584 | "type": "bytes" 585 | } 586 | ], 587 | "name": "transfer", 588 | "outputs": 589 | [], 590 | "stateMutability": "nonpayable", 591 | "type": "function" 592 | }, 593 | { 594 | "inputs": 595 | [ 596 | { 597 | "internalType": "address", 598 | "name": "newOwner", 599 | "type": "address" 600 | } 601 | ], 602 | "name": "transferOwnership", 603 | "outputs": 604 | [], 605 | "stateMutability": "nonpayable", 606 | "type": "function" 607 | }, 608 | { 609 | "inputs": 610 | [], 611 | "name": "trustedCaller", 612 | "outputs": 613 | [ 614 | { 615 | "internalType": "address", 616 | "name": "", 617 | "type": "address" 618 | } 619 | ], 620 | "stateMutability": "view", 621 | "type": "function" 622 | }, 623 | { 624 | "inputs": 625 | [ 626 | { 627 | "internalType": "address", 628 | "name": "to", 629 | "type": "address" 630 | }, 631 | { 632 | "internalType": "address", 633 | "name": "recovery", 634 | "type": "address" 635 | } 636 | ], 637 | "name": "trustedRegister", 638 | "outputs": 639 | [ 640 | { 641 | "internalType": "uint256", 642 | "name": "fid", 643 | "type": "uint256" 644 | } 645 | ], 646 | "stateMutability": "nonpayable", 647 | "type": "function" 648 | }, 649 | { 650 | "inputs": 651 | [], 652 | "name": "unpauseRegistration", 653 | "outputs": 654 | [], 655 | "stateMutability": "nonpayable", 656 | "type": "function" 657 | } 658 | ] -------------------------------------------------------------------------------- /src/abi/IdRegistry.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { 5 | BaseContract, 6 | BigNumberish, 7 | BytesLike, 8 | FunctionFragment, 9 | Result, 10 | Interface, 11 | EventFragment, 12 | AddressLike, 13 | ContractRunner, 14 | ContractMethod, 15 | Listener, 16 | } from 'ethers'; 17 | import type { 18 | TypedContractEvent, 19 | TypedDeferredTopicFilter, 20 | TypedEventLog, 21 | TypedLogDescription, 22 | TypedListener, 23 | TypedContractMethod, 24 | } from './common.js'; 25 | 26 | export interface IdRegistryInterface extends Interface { 27 | getFunction( 28 | nameOrSignature: 29 | | 'acceptOwnership' 30 | | 'changeRecoveryAddress' 31 | | 'disableTrustedOnly' 32 | | 'eip712Domain' 33 | | 'idOf' 34 | | 'nonces' 35 | | 'owner' 36 | | 'pauseRegistration' 37 | | 'paused' 38 | | 'pendingOwner' 39 | | 'recover' 40 | | 'register' 41 | | 'registerFor' 42 | | 'renounceOwnership' 43 | | 'setTrustedCaller' 44 | | 'transfer' 45 | | 'transferOwnership' 46 | | 'trustedCaller' 47 | | 'trustedRegister' 48 | | 'unpauseRegistration' 49 | ): FunctionFragment; 50 | 51 | getEvent( 52 | nameOrSignatureOrTopic: 53 | | 'ChangeRecoveryAddress' 54 | | 'DisableTrustedOnly' 55 | | 'EIP712DomainChanged' 56 | | 'OwnershipTransferStarted' 57 | | 'OwnershipTransferred' 58 | | 'Paused' 59 | | 'Register' 60 | | 'SetTrustedCaller' 61 | | 'Transfer' 62 | | 'Unpaused' 63 | ): EventFragment; 64 | 65 | encodeFunctionData(functionFragment: 'acceptOwnership', values?: undefined): string; 66 | encodeFunctionData(functionFragment: 'changeRecoveryAddress', values: [AddressLike]): string; 67 | encodeFunctionData(functionFragment: 'disableTrustedOnly', values?: undefined): string; 68 | encodeFunctionData(functionFragment: 'eip712Domain', values?: undefined): string; 69 | encodeFunctionData(functionFragment: 'idOf', values: [AddressLike]): string; 70 | encodeFunctionData(functionFragment: 'nonces', values: [AddressLike]): string; 71 | encodeFunctionData(functionFragment: 'owner', values?: undefined): string; 72 | encodeFunctionData(functionFragment: 'pauseRegistration', values?: undefined): string; 73 | encodeFunctionData(functionFragment: 'paused', values?: undefined): string; 74 | encodeFunctionData(functionFragment: 'pendingOwner', values?: undefined): string; 75 | encodeFunctionData(functionFragment: 'recover', values: [AddressLike, AddressLike, BigNumberish, BytesLike]): string; 76 | encodeFunctionData(functionFragment: 'register', values: [AddressLike]): string; 77 | encodeFunctionData( 78 | functionFragment: 'registerFor', 79 | values: [AddressLike, AddressLike, BigNumberish, BytesLike] 80 | ): string; 81 | encodeFunctionData(functionFragment: 'renounceOwnership', values?: undefined): string; 82 | encodeFunctionData(functionFragment: 'setTrustedCaller', values: [AddressLike]): string; 83 | encodeFunctionData(functionFragment: 'transfer', values: [AddressLike, BigNumberish, BytesLike]): string; 84 | encodeFunctionData(functionFragment: 'transferOwnership', values: [AddressLike]): string; 85 | encodeFunctionData(functionFragment: 'trustedCaller', values?: undefined): string; 86 | encodeFunctionData(functionFragment: 'trustedRegister', values: [AddressLike, AddressLike]): string; 87 | encodeFunctionData(functionFragment: 'unpauseRegistration', values?: undefined): string; 88 | 89 | decodeFunctionResult(functionFragment: 'acceptOwnership', data: BytesLike): Result; 90 | decodeFunctionResult(functionFragment: 'changeRecoveryAddress', data: BytesLike): Result; 91 | decodeFunctionResult(functionFragment: 'disableTrustedOnly', data: BytesLike): Result; 92 | decodeFunctionResult(functionFragment: 'eip712Domain', data: BytesLike): Result; 93 | decodeFunctionResult(functionFragment: 'idOf', data: BytesLike): Result; 94 | decodeFunctionResult(functionFragment: 'nonces', data: BytesLike): Result; 95 | decodeFunctionResult(functionFragment: 'owner', data: BytesLike): Result; 96 | decodeFunctionResult(functionFragment: 'pauseRegistration', data: BytesLike): Result; 97 | decodeFunctionResult(functionFragment: 'paused', data: BytesLike): Result; 98 | decodeFunctionResult(functionFragment: 'pendingOwner', data: BytesLike): Result; 99 | decodeFunctionResult(functionFragment: 'recover', data: BytesLike): Result; 100 | decodeFunctionResult(functionFragment: 'register', data: BytesLike): Result; 101 | decodeFunctionResult(functionFragment: 'registerFor', data: BytesLike): Result; 102 | decodeFunctionResult(functionFragment: 'renounceOwnership', data: BytesLike): Result; 103 | decodeFunctionResult(functionFragment: 'setTrustedCaller', data: BytesLike): Result; 104 | decodeFunctionResult(functionFragment: 'transfer', data: BytesLike): Result; 105 | decodeFunctionResult(functionFragment: 'transferOwnership', data: BytesLike): Result; 106 | decodeFunctionResult(functionFragment: 'trustedCaller', data: BytesLike): Result; 107 | decodeFunctionResult(functionFragment: 'trustedRegister', data: BytesLike): Result; 108 | decodeFunctionResult(functionFragment: 'unpauseRegistration', data: BytesLike): Result; 109 | } 110 | 111 | export namespace ChangeRecoveryAddressEvent { 112 | export type InputTuple = [id: BigNumberish, recovery: AddressLike]; 113 | export type OutputTuple = [id: bigint, recovery: string]; 114 | export interface OutputObject { 115 | id: bigint; 116 | recovery: string; 117 | } 118 | export type Event = TypedContractEvent; 119 | export type Filter = TypedDeferredTopicFilter; 120 | export type Log = TypedEventLog; 121 | export type LogDescription = TypedLogDescription; 122 | } 123 | 124 | export namespace DisableTrustedOnlyEvent { 125 | export type InputTuple = []; 126 | export type OutputTuple = []; 127 | export interface OutputObject {} 128 | export type Event = TypedContractEvent; 129 | export type Filter = TypedDeferredTopicFilter; 130 | export type Log = TypedEventLog; 131 | export type LogDescription = TypedLogDescription; 132 | } 133 | 134 | export namespace EIP712DomainChangedEvent { 135 | export type InputTuple = []; 136 | export type OutputTuple = []; 137 | export interface OutputObject {} 138 | export type Event = TypedContractEvent; 139 | export type Filter = TypedDeferredTopicFilter; 140 | export type Log = TypedEventLog; 141 | export type LogDescription = TypedLogDescription; 142 | } 143 | 144 | export namespace OwnershipTransferStartedEvent { 145 | export type InputTuple = [previousOwner: AddressLike, newOwner: AddressLike]; 146 | export type OutputTuple = [previousOwner: string, newOwner: string]; 147 | export interface OutputObject { 148 | previousOwner: string; 149 | newOwner: string; 150 | } 151 | export type Event = TypedContractEvent; 152 | export type Filter = TypedDeferredTopicFilter; 153 | export type Log = TypedEventLog; 154 | export type LogDescription = TypedLogDescription; 155 | } 156 | 157 | export namespace OwnershipTransferredEvent { 158 | export type InputTuple = [previousOwner: AddressLike, newOwner: AddressLike]; 159 | export type OutputTuple = [previousOwner: string, newOwner: string]; 160 | export interface OutputObject { 161 | previousOwner: string; 162 | newOwner: string; 163 | } 164 | export type Event = TypedContractEvent; 165 | export type Filter = TypedDeferredTopicFilter; 166 | export type Log = TypedEventLog; 167 | export type LogDescription = TypedLogDescription; 168 | } 169 | 170 | export namespace PausedEvent { 171 | export type InputTuple = [account: AddressLike]; 172 | export type OutputTuple = [account: string]; 173 | export interface OutputObject { 174 | account: string; 175 | } 176 | export type Event = TypedContractEvent; 177 | export type Filter = TypedDeferredTopicFilter; 178 | export type Log = TypedEventLog; 179 | export type LogDescription = TypedLogDescription; 180 | } 181 | 182 | export namespace RegisterEvent { 183 | export type InputTuple = [to: AddressLike, id: BigNumberish, recovery: AddressLike]; 184 | export type OutputTuple = [to: string, id: bigint, recovery: string]; 185 | export interface OutputObject { 186 | to: string; 187 | id: bigint; 188 | recovery: string; 189 | } 190 | export type Event = TypedContractEvent; 191 | export type Filter = TypedDeferredTopicFilter; 192 | export type Log = TypedEventLog; 193 | export type LogDescription = TypedLogDescription; 194 | } 195 | 196 | export namespace SetTrustedCallerEvent { 197 | export type InputTuple = [oldCaller: AddressLike, newCaller: AddressLike, owner: AddressLike]; 198 | export type OutputTuple = [oldCaller: string, newCaller: string, owner: string]; 199 | export interface OutputObject { 200 | oldCaller: string; 201 | newCaller: string; 202 | owner: string; 203 | } 204 | export type Event = TypedContractEvent; 205 | export type Filter = TypedDeferredTopicFilter; 206 | export type Log = TypedEventLog; 207 | export type LogDescription = TypedLogDescription; 208 | } 209 | 210 | export namespace TransferEvent { 211 | export type InputTuple = [from: AddressLike, to: AddressLike, id: BigNumberish]; 212 | export type OutputTuple = [from: string, to: string, id: bigint]; 213 | export interface OutputObject { 214 | from: string; 215 | to: string; 216 | id: bigint; 217 | } 218 | export type Event = TypedContractEvent; 219 | export type Filter = TypedDeferredTopicFilter; 220 | export type Log = TypedEventLog; 221 | export type LogDescription = TypedLogDescription; 222 | } 223 | 224 | export namespace UnpausedEvent { 225 | export type InputTuple = [account: AddressLike]; 226 | export type OutputTuple = [account: string]; 227 | export interface OutputObject { 228 | account: string; 229 | } 230 | export type Event = TypedContractEvent; 231 | export type Filter = TypedDeferredTopicFilter; 232 | export type Log = TypedEventLog; 233 | export type LogDescription = TypedLogDescription; 234 | } 235 | 236 | export interface IdRegistry extends BaseContract { 237 | connect(runner?: ContractRunner | null): IdRegistry; 238 | waitForDeployment(): Promise; 239 | 240 | interface: IdRegistryInterface; 241 | 242 | queryFilter( 243 | event: TCEvent, 244 | fromBlockOrBlockhash?: string | number | undefined, 245 | toBlock?: string | number | undefined 246 | ): Promise>>; 247 | queryFilter( 248 | filter: TypedDeferredTopicFilter, 249 | fromBlockOrBlockhash?: string | number | undefined, 250 | toBlock?: string | number | undefined 251 | ): Promise>>; 252 | 253 | on(event: TCEvent, listener: TypedListener): Promise; 254 | on( 255 | filter: TypedDeferredTopicFilter, 256 | listener: TypedListener 257 | ): Promise; 258 | 259 | once(event: TCEvent, listener: TypedListener): Promise; 260 | once( 261 | filter: TypedDeferredTopicFilter, 262 | listener: TypedListener 263 | ): Promise; 264 | 265 | listeners(event: TCEvent): Promise>>; 266 | listeners(eventName?: string): Promise>; 267 | removeAllListeners(event?: TCEvent): Promise; 268 | 269 | acceptOwnership: TypedContractMethod<[], [void], 'nonpayable'>; 270 | 271 | changeRecoveryAddress: TypedContractMethod<[recovery: AddressLike], [void], 'nonpayable'>; 272 | 273 | disableTrustedOnly: TypedContractMethod<[], [void], 'nonpayable'>; 274 | 275 | eip712Domain: TypedContractMethod< 276 | [], 277 | [ 278 | [string, string, string, bigint, string, string, bigint[]] & { 279 | fields: string; 280 | name: string; 281 | version: string; 282 | chainId: bigint; 283 | verifyingContract: string; 284 | salt: string; 285 | extensions: bigint[]; 286 | } 287 | ], 288 | 'view' 289 | >; 290 | 291 | idOf: TypedContractMethod<[owner: AddressLike], [bigint], 'view'>; 292 | 293 | nonces: TypedContractMethod<[owner: AddressLike], [bigint], 'view'>; 294 | 295 | owner: TypedContractMethod<[], [string], 'view'>; 296 | 297 | pauseRegistration: TypedContractMethod<[], [void], 'nonpayable'>; 298 | 299 | paused: TypedContractMethod<[], [boolean], 'view'>; 300 | 301 | pendingOwner: TypedContractMethod<[], [string], 'view'>; 302 | 303 | recover: TypedContractMethod< 304 | [from: AddressLike, to: AddressLike, deadline: BigNumberish, sig: BytesLike], 305 | [void], 306 | 'nonpayable' 307 | >; 308 | 309 | register: TypedContractMethod<[recovery: AddressLike], [bigint], 'nonpayable'>; 310 | 311 | registerFor: TypedContractMethod< 312 | [to: AddressLike, recovery: AddressLike, deadline: BigNumberish, sig: BytesLike], 313 | [bigint], 314 | 'nonpayable' 315 | >; 316 | 317 | renounceOwnership: TypedContractMethod<[], [void], 'nonpayable'>; 318 | 319 | setTrustedCaller: TypedContractMethod<[_trustedCaller: AddressLike], [void], 'nonpayable'>; 320 | 321 | transfer: TypedContractMethod<[to: AddressLike, deadline: BigNumberish, sig: BytesLike], [void], 'nonpayable'>; 322 | 323 | transferOwnership: TypedContractMethod<[newOwner: AddressLike], [void], 'nonpayable'>; 324 | 325 | trustedCaller: TypedContractMethod<[], [string], 'view'>; 326 | 327 | trustedRegister: TypedContractMethod<[to: AddressLike, recovery: AddressLike], [bigint], 'nonpayable'>; 328 | 329 | unpauseRegistration: TypedContractMethod<[], [void], 'nonpayable'>; 330 | 331 | getFunction(key: string | FunctionFragment): T; 332 | 333 | getFunction(nameOrSignature: 'acceptOwnership'): TypedContractMethod<[], [void], 'nonpayable'>; 334 | getFunction( 335 | nameOrSignature: 'changeRecoveryAddress' 336 | ): TypedContractMethod<[recovery: AddressLike], [void], 'nonpayable'>; 337 | getFunction(nameOrSignature: 'disableTrustedOnly'): TypedContractMethod<[], [void], 'nonpayable'>; 338 | getFunction(nameOrSignature: 'eip712Domain'): TypedContractMethod< 339 | [], 340 | [ 341 | [string, string, string, bigint, string, string, bigint[]] & { 342 | fields: string; 343 | name: string; 344 | version: string; 345 | chainId: bigint; 346 | verifyingContract: string; 347 | salt: string; 348 | extensions: bigint[]; 349 | } 350 | ], 351 | 'view' 352 | >; 353 | getFunction(nameOrSignature: 'idOf'): TypedContractMethod<[owner: AddressLike], [bigint], 'view'>; 354 | getFunction(nameOrSignature: 'nonces'): TypedContractMethod<[owner: AddressLike], [bigint], 'view'>; 355 | getFunction(nameOrSignature: 'owner'): TypedContractMethod<[], [string], 'view'>; 356 | getFunction(nameOrSignature: 'pauseRegistration'): TypedContractMethod<[], [void], 'nonpayable'>; 357 | getFunction(nameOrSignature: 'paused'): TypedContractMethod<[], [boolean], 'view'>; 358 | getFunction(nameOrSignature: 'pendingOwner'): TypedContractMethod<[], [string], 'view'>; 359 | getFunction( 360 | nameOrSignature: 'recover' 361 | ): TypedContractMethod< 362 | [from: AddressLike, to: AddressLike, deadline: BigNumberish, sig: BytesLike], 363 | [void], 364 | 'nonpayable' 365 | >; 366 | getFunction(nameOrSignature: 'register'): TypedContractMethod<[recovery: AddressLike], [bigint], 'nonpayable'>; 367 | getFunction( 368 | nameOrSignature: 'registerFor' 369 | ): TypedContractMethod< 370 | [to: AddressLike, recovery: AddressLike, deadline: BigNumberish, sig: BytesLike], 371 | [bigint], 372 | 'nonpayable' 373 | >; 374 | getFunction(nameOrSignature: 'renounceOwnership'): TypedContractMethod<[], [void], 'nonpayable'>; 375 | getFunction( 376 | nameOrSignature: 'setTrustedCaller' 377 | ): TypedContractMethod<[_trustedCaller: AddressLike], [void], 'nonpayable'>; 378 | getFunction( 379 | nameOrSignature: 'transfer' 380 | ): TypedContractMethod<[to: AddressLike, deadline: BigNumberish, sig: BytesLike], [void], 'nonpayable'>; 381 | getFunction(nameOrSignature: 'transferOwnership'): TypedContractMethod<[newOwner: AddressLike], [void], 'nonpayable'>; 382 | getFunction(nameOrSignature: 'trustedCaller'): TypedContractMethod<[], [string], 'view'>; 383 | getFunction( 384 | nameOrSignature: 'trustedRegister' 385 | ): TypedContractMethod<[to: AddressLike, recovery: AddressLike], [bigint], 'nonpayable'>; 386 | getFunction(nameOrSignature: 'unpauseRegistration'): TypedContractMethod<[], [void], 'nonpayable'>; 387 | 388 | getEvent( 389 | key: 'ChangeRecoveryAddress' 390 | ): TypedContractEvent< 391 | ChangeRecoveryAddressEvent.InputTuple, 392 | ChangeRecoveryAddressEvent.OutputTuple, 393 | ChangeRecoveryAddressEvent.OutputObject 394 | >; 395 | getEvent( 396 | key: 'DisableTrustedOnly' 397 | ): TypedContractEvent< 398 | DisableTrustedOnlyEvent.InputTuple, 399 | DisableTrustedOnlyEvent.OutputTuple, 400 | DisableTrustedOnlyEvent.OutputObject 401 | >; 402 | getEvent( 403 | key: 'EIP712DomainChanged' 404 | ): TypedContractEvent< 405 | EIP712DomainChangedEvent.InputTuple, 406 | EIP712DomainChangedEvent.OutputTuple, 407 | EIP712DomainChangedEvent.OutputObject 408 | >; 409 | getEvent( 410 | key: 'OwnershipTransferStarted' 411 | ): TypedContractEvent< 412 | OwnershipTransferStartedEvent.InputTuple, 413 | OwnershipTransferStartedEvent.OutputTuple, 414 | OwnershipTransferStartedEvent.OutputObject 415 | >; 416 | getEvent( 417 | key: 'OwnershipTransferred' 418 | ): TypedContractEvent< 419 | OwnershipTransferredEvent.InputTuple, 420 | OwnershipTransferredEvent.OutputTuple, 421 | OwnershipTransferredEvent.OutputObject 422 | >; 423 | getEvent( 424 | key: 'Paused' 425 | ): TypedContractEvent; 426 | getEvent( 427 | key: 'Register' 428 | ): TypedContractEvent; 429 | getEvent( 430 | key: 'SetTrustedCaller' 431 | ): TypedContractEvent< 432 | SetTrustedCallerEvent.InputTuple, 433 | SetTrustedCallerEvent.OutputTuple, 434 | SetTrustedCallerEvent.OutputObject 435 | >; 436 | getEvent( 437 | key: 'Transfer' 438 | ): TypedContractEvent; 439 | getEvent( 440 | key: 'Unpaused' 441 | ): TypedContractEvent; 442 | 443 | filters: { 444 | 'ChangeRecoveryAddress(uint256,address)': TypedContractEvent< 445 | ChangeRecoveryAddressEvent.InputTuple, 446 | ChangeRecoveryAddressEvent.OutputTuple, 447 | ChangeRecoveryAddressEvent.OutputObject 448 | >; 449 | ChangeRecoveryAddress: TypedContractEvent< 450 | ChangeRecoveryAddressEvent.InputTuple, 451 | ChangeRecoveryAddressEvent.OutputTuple, 452 | ChangeRecoveryAddressEvent.OutputObject 453 | >; 454 | 455 | 'DisableTrustedOnly()': TypedContractEvent< 456 | DisableTrustedOnlyEvent.InputTuple, 457 | DisableTrustedOnlyEvent.OutputTuple, 458 | DisableTrustedOnlyEvent.OutputObject 459 | >; 460 | DisableTrustedOnly: TypedContractEvent< 461 | DisableTrustedOnlyEvent.InputTuple, 462 | DisableTrustedOnlyEvent.OutputTuple, 463 | DisableTrustedOnlyEvent.OutputObject 464 | >; 465 | 466 | 'EIP712DomainChanged()': TypedContractEvent< 467 | EIP712DomainChangedEvent.InputTuple, 468 | EIP712DomainChangedEvent.OutputTuple, 469 | EIP712DomainChangedEvent.OutputObject 470 | >; 471 | EIP712DomainChanged: TypedContractEvent< 472 | EIP712DomainChangedEvent.InputTuple, 473 | EIP712DomainChangedEvent.OutputTuple, 474 | EIP712DomainChangedEvent.OutputObject 475 | >; 476 | 477 | 'OwnershipTransferStarted(address,address)': TypedContractEvent< 478 | OwnershipTransferStartedEvent.InputTuple, 479 | OwnershipTransferStartedEvent.OutputTuple, 480 | OwnershipTransferStartedEvent.OutputObject 481 | >; 482 | OwnershipTransferStarted: TypedContractEvent< 483 | OwnershipTransferStartedEvent.InputTuple, 484 | OwnershipTransferStartedEvent.OutputTuple, 485 | OwnershipTransferStartedEvent.OutputObject 486 | >; 487 | 488 | 'OwnershipTransferred(address,address)': TypedContractEvent< 489 | OwnershipTransferredEvent.InputTuple, 490 | OwnershipTransferredEvent.OutputTuple, 491 | OwnershipTransferredEvent.OutputObject 492 | >; 493 | OwnershipTransferred: TypedContractEvent< 494 | OwnershipTransferredEvent.InputTuple, 495 | OwnershipTransferredEvent.OutputTuple, 496 | OwnershipTransferredEvent.OutputObject 497 | >; 498 | 499 | 'Paused(address)': TypedContractEvent; 500 | Paused: TypedContractEvent; 501 | 502 | 'Register(address,uint256,address)': TypedContractEvent< 503 | RegisterEvent.InputTuple, 504 | RegisterEvent.OutputTuple, 505 | RegisterEvent.OutputObject 506 | >; 507 | Register: TypedContractEvent; 508 | 509 | 'SetTrustedCaller(address,address,address)': TypedContractEvent< 510 | SetTrustedCallerEvent.InputTuple, 511 | SetTrustedCallerEvent.OutputTuple, 512 | SetTrustedCallerEvent.OutputObject 513 | >; 514 | SetTrustedCaller: TypedContractEvent< 515 | SetTrustedCallerEvent.InputTuple, 516 | SetTrustedCallerEvent.OutputTuple, 517 | SetTrustedCallerEvent.OutputObject 518 | >; 519 | 520 | 'Transfer(address,address,uint256)': TypedContractEvent< 521 | TransferEvent.InputTuple, 522 | TransferEvent.OutputTuple, 523 | TransferEvent.OutputObject 524 | >; 525 | Transfer: TypedContractEvent; 526 | 527 | 'Unpaused(address)': TypedContractEvent< 528 | UnpausedEvent.InputTuple, 529 | UnpausedEvent.OutputTuple, 530 | UnpausedEvent.OutputObject 531 | >; 532 | Unpaused: TypedContractEvent; 533 | }; 534 | } 535 | --------------------------------------------------------------------------------