├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.yml ├── LICENSE ├── README.md ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── evm │ ├── constants.ts │ ├── fetchers │ │ ├── registrar.fetchers.ts │ │ ├── registry.fetchers.ts │ │ └── root.fetchers.ts │ ├── index.ts │ ├── parsers.ts │ ├── types │ │ ├── Address.ts │ │ ├── AddressAndDomain.ts │ │ ├── EnumValues.ts │ │ ├── EvmChainData.ts │ │ └── NameRecordHeader.ts │ └── utils.ts ├── index.ts ├── parsers.interface.ts ├── parsers.ts └── svm │ ├── constants.ts │ ├── index.ts │ ├── name-record-handler.ts │ ├── parsers.ts │ ├── state │ ├── main-domain.ts │ ├── name-record-header.ts │ └── nft-record.ts │ ├── types │ ├── records.ts │ └── tag.ts │ └── utils.ts ├── tests ├── tld-parser-monad.spec.ts ├── tld-parser.error.ts └── tld-parser.spec.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /.eslintrc.js 2 | /dist 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | mocha: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:import/errors', 11 | 'plugin:import/warnings', 12 | 'plugin:import/typescript', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | sourceType: 'module', 17 | ecmaVersion: 8, 18 | }, 19 | plugins: ['@typescript-eslint'], 20 | rules: { 21 | '@typescript-eslint/no-unused-vars': ['error'], 22 | 'import/first': ['error'], 23 | 'import/no-commonjs': ['error'], 24 | 'import/order': [ 25 | 'error', 26 | { 27 | groups: [ 28 | ['internal', 'external', 'builtin'], 29 | ['index', 'sibling', 'parent'], 30 | ], 31 | 'newlines-between': 'always', 32 | }, 33 | ], 34 | 'linebreak-style': ['error', 'unix'], 35 | 'no-console': [0], 36 | 'no-trailing-spaces': ['error'], 37 | 'no-undef': 'off', 38 | 'no-unused-vars': 'off', 39 | quotes: [ 40 | 'error', 41 | 'single', 42 | { avoidEscape: true, allowTemplateLiterals: true }, 43 | ], 44 | 'require-await': ['error'], 45 | semi: ['error', 'always'], 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE & OS specific 2 | .DS_Store 3 | .idea 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Dependencies 10 | node_modules/ 11 | dist/ 12 | .yarn 13 | 14 | # Coverage 15 | coverage 16 | .nyc_output 17 | 18 | # Generated docs 19 | doc 20 | 21 | # VIM swap files 22 | *.sw* 23 | 24 | # Flow 25 | module.flow.js 26 | 27 | # TypeScript 28 | declarations 29 | tests_local/ 30 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | arrowParens: "avoid" 2 | bracketSpacing: true 3 | semi: true 4 | singleQuote: true 5 | tabWidth: 4 6 | trailingComma: "all" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 onsol-labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLD Parser 2 | A library to parse top-level domain (TLD) names on the Solana blockchain via the Alternative Name Service (ANS) and on EVM-compatible chains (e.g., Monad). This library provides tools to interact with domain names, retrieve ownership details, and manage domain records across supported blockchains. 3 | 4 | 5 | 6 | ## Overview 7 | The TLD Parser supports two primary implementations: 8 | * Solana (SVM): Parses domains on the Solana Virtual Machine using the ANS system. 9 | * EVM: Parses domains on Ethereum Virtual Machine-compatible chains (currently Monad). 10 | * The library operates on mainnet for both chains, with devnet values available in the constants.ts file for Solana. 11 | 12 | ## Supported Chains 13 | * Solana (SVM) 14 | * Eclipse (SVM) 15 | * Monad Testnet (EVM) 16 | 17 | ## Installation 18 | Add @onsol/tld-parser to package.json or `yarn add @onsol/tld-parser` 19 | 20 | ## Usage Examples 21 | Below are examples demonstrating key functions for both Solana and EVM implementations. These mirror the test cases provided. 22 | ### Solana (SVM) Examples 23 | ```javascript 24 | 25 | import { Connection, PublicKey } from '@solana/web3.js'; 26 | import { TldParser, getDomainKey, Record, NameRecordHeader } from '@onsol/tldparser'; 27 | 28 | // Constants 29 | const RPC_URL = 'https://mainnet.rpcpool.com/'; 30 | const OWNER = new PublicKey('2EGGxj2qbNAJNgLCPKca8sxZYetyTjnoRspTPjzN2D67'); 31 | const TLD = 'poor'; 32 | const DOMAIN = 'miester.poor'; 33 | 34 | // Initialize 35 | const connection = new Connection(RPC_URL); 36 | const parser = new TldParser(connection); 37 | 38 | // Get all domains owned by a user 39 | const ownerDomains = await parser.getAllUserDomains(OWNER); 40 | // => [PublicKey("6iE5btnTaan1eqfnwChLdVAyFERdn5uCVnp5GiXVg1aB")] 41 | 42 | // Get domains for a specific TLD 43 | const tldDomains = await parser.getAllUserDomainsFromTld(OWNER, TLD); 44 | // => [PublicKey("6iE5btnTaan1eqfnwChLdVAyFERdn5uCVnp5GiXVg1aB")] 45 | 46 | // Get domain owner 47 | const domainOwner = await parser.getOwnerFromDomainTld(DOMAIN); 48 | // => PublicKey("2EGGxj2qbNAJNgLCPKca8sxZYetyTjnoRspTPjzN2D67") 49 | 50 | // Get NameRecordHeader for a domain 51 | const nameRecord = await parser.getNameRecordFromDomainTld(DOMAIN); 52 | // => NameRecordHeader { parentName, owner, nclass, expiresAt, isValid, data } 53 | 54 | // Get TLD from parent account 55 | const parentAccount = new PublicKey('8err4ThuTiZo9LbozHAvMrzXUmyPWj9urnMo38vC6FdQ'); 56 | const tld = await parser.getTldFromParentAccount(parentAccount); 57 | // => ".poor" 58 | 59 | // Reverse lookup domain from name account 60 | const nameAccount = new PublicKey('6iE5btnTaan1eqfnwChLdVAyFERdn5uCVnp5GiXVg1aB'); 61 | const parentOwner = new PublicKey('ANgPRMKQHgH5Snx2K3VHCvHqFmrABcjTZUrqZBzDCtfA'); 62 | const domainName = await parser.reverseLookupNameAccount(nameAccount, parentOwner); 63 | // => "miester" 64 | 65 | // Get DNS record (e.g., IPFS) 66 | const recordPubkey = (await getDomainKey(Record.IPFS + '.' + DOMAIN, true)).pubkey; 67 | const dnsRecord = await NameRecordHeader.fromAccountAddress(connection, recordPubkey); 68 | // => ipfs://... 69 | 70 | // Get all TLDs 71 | const allTlds = await getAllTlds(connection); 72 | // => [{ tld: '.bonk', parentAccount: "2j6gC6MMrnw4JJpAKR5FyyUFdxxvdZdG2sg4FrqfyWi5" }, ...] 73 | ``` 74 | ### EVM (Monad) Examples 75 | ```javascript 76 | 77 | import { TldParser, NetworkWithRpc } from '@onsol/tldparser'; 78 | import { getAddress } from 'ethers'; 79 | 80 | // Constants 81 | const RPC_URL = 'https://testnet-rpc.monad.xyz'; 82 | const PUBLIC_KEY = getAddress('0x94Bfb92da83B27B39370550CA038Af96d182462f'); 83 | const settings = new NetworkWithRpc('monad', 10143, RPC_URL); 84 | 85 | // Initialize 86 | const parser = new TldParser(settings, 'monad'); 87 | 88 | // Get all user domains 89 | const allDomains = await parser.getAllUserDomains(PUBLIC_KEY); 90 | // => [NameRecord { domain_name: "miester", tld: ".mon", ... }] 91 | 92 | // Get domains for a specific TLD 93 | const tldDomains = await parser.getAllUserDomainsFromTld(PUBLIC_KEY, '.mon'); 94 | // => [NameRecord { domain_name: "miester", tld: ".mon", ... }] 95 | 96 | // Get domain owner 97 | const owner = await parser.getOwnerFromDomainTld('miester.mon'); 98 | // => "0x94Bfb92da83B27B39370550CA038Af96d182462f" 99 | 100 | // Get name record 101 | const nameRecord = await parser.getNameRecordFromDomainTld('miester.mon'); 102 | // => NameRecord { created_at, domain_name, expires_at, main_domain_address, tld, transferrable } 103 | ``` 104 | 105 | ## API Reference 106 | ### Core Methods (Available on Both Chains) 107 | * `getAllUserDomains(userAccount)`: Retrieves all domains owned by a user. 108 | 109 | * `getAllUserDomainsFromTld(userAccount, tld)`: Retrieves domains for a specific TLD. 110 | 111 | * `getOwnerFromDomainTld(domainTld)`: Retrieves the owner of a domain. 112 | 113 | * `getNameRecordFromDomainTld(domainTld)`: Retrieves detailed record data for a domain. 114 | 115 | * `getMainDomain(userAddress)`: Retrieves the user's main domain. 116 | 117 | ### Solana-Specific Methods 118 | * `getTldFromParentAccount(parentAccount)`: Retrieves the TLD from a parent account key. 119 | 120 | * `reverseLookupNameAccount(nameAccount, parentAccountOwner)`: Performs a reverse lookup to get the domain name. 121 | 122 | * `getParsedAllUserDomains(userAccount)`: Retrieves all domains (including NFTs) with parsed names. 123 | 124 | * `getParsedAllUserDomainsFromTld(userAccount, tld)`: Retrieves parsed domains for a specific TLD. 125 | 126 | ## States 127 | ### Solana: NameRecordHeader 128 | Represents the state of an ANS account on Solana: 129 | * `parentName: PublicKey`: Parent name account key. 130 | * `owner: PublicKey | undefined`: Owner of the name account (undefined if expired). 131 | * `nclass: PublicKey`: Class of the name account (e.g., main domain, DNS) or PublicKey.default. 132 | * `expiresAt: Date`: Expiration date (0 for non-expirable domains). 133 | * `isValid: boolean`: Validity status for expirable domains. 134 | * `data: Buffer | undefined`: Additional data stored in the account. 135 | 136 | ### EVM: NameRecord 137 | Represents domain data on EVM chains: 138 | * `created_at: string`: Creation timestamp. 139 | * `domain_name: string`: Domain name (e.g., "miester"). 140 | * `expires_at: string`: Expiration timestamp. 141 | * `main_domain_address: string`: Owner address. 142 | * `tld: string`: Top-level domain (e.g., ".mon"). 143 | * `transferrable: boolean`: Whether the domain can be transferred. 144 | 145 | ## Notes 146 | Backwards Compatibility: The TldParser class ensures compatibility with previous versions while supporting multi-chain expansion. 147 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | verbose: true, 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest', 7 | }, 8 | rootDir: 'tests/', 9 | testRegex: '(.*\\.spec)\\.(jsx?|tsx?)$', 10 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 11 | testTimeout: 60000, 12 | testRunner: 'jasmine2', 13 | }; 14 | export default config; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@onsol/tldparser", 3 | "version": "1.0.6", 4 | "description": "TLD House (Solana and EVM) Javascript API", 5 | "keywords": [ 6 | "api", 7 | "wallet", 8 | "blockchain", 9 | "tld house" 10 | ], 11 | "license": "MIT", 12 | "author": "CryptoMiester ", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/onsol-labs/tld-parser.git" 16 | }, 17 | "bugs": { 18 | "url": "http://github.com/onsol-labs/tld-parser.git/issues" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "browserslist": [ 24 | "defaults" 25 | ], 26 | "files": [ 27 | "/dist" 28 | ], 29 | "main": "./dist/cjs/index.js", 30 | "module": "./dist/esm/index.js", 31 | "types": "./dist/types/index.d.ts", 32 | "exports": { 33 | ".": { 34 | "import": "./dist/esm/index.js", 35 | "require": "./dist/cjs/index.js", 36 | "types": "./dist/types/index.d.ts" 37 | } 38 | }, 39 | "scripts": { 40 | "prepublish": "npm run build", 41 | "clean": "rm -rf dist", 42 | "build": "npm run clean && npm run build:cjs; npm run build:esm", 43 | "build:cjs": "tsc --project tsconfig.cjs.json", 44 | "build:cjs:watch": "concurrently \"tsc --project tsconfig.cjs.json --watch\"", 45 | "build:esm": "tsc --project tsconfig.esm.json", 46 | "build:esm:watch": "concurrently \"tsc --project tsconfig.esm.json --watch\"", 47 | "lint": "set -ex; npm run pretty; eslint . --ext .js,.ts", 48 | "lint:fix": "npm run pretty:fix && eslint . --fix --ext .js,.ts", 49 | "pretty": "prettier --check '{,{src,test}/**/}*.{j,t}s'", 50 | "pretty:fix": "prettier --write '{,{src,test}/**/}*.{j,t}s'", 51 | "test": "jest --config jest.config.ts" 52 | }, 53 | "dependencies": { 54 | "@ethersproject/sha2": "^5.7.0", 55 | "@metaplex-foundation/beet-solana": "^0.4.0", 56 | "async": "^3.2.6" 57 | }, 58 | "devDependencies": { 59 | "@jest/types": "^29.3.1", 60 | "@solana/web3.js": "^1.95.3", 61 | "@types/async": "^3.2.24", 62 | "@types/bn.js": "^5.1.0", 63 | "@types/bs58": "^4.0.1", 64 | "@types/jest": "^29.2.3", 65 | "@typescript-eslint/eslint-plugin": "^8.29.0", 66 | "@typescript-eslint/parser": "^8.29.0", 67 | "bn.js": "^5.2.1", 68 | "borsh": "^2.0.0", 69 | "buffer": "6.0.3", 70 | "eslint": "^9.24.0", 71 | "eslint-config-prettier": "^10.1.1", 72 | "eslint-plugin-import": "^2.26.0", 73 | "eslint-plugin-prettier": "^5.2.6", 74 | "ethers": "^6.13.4", 75 | "jest": "^29.3.1", 76 | "jest-jasmine2": "^29.3.1", 77 | "prettier": "^3.5.3", 78 | "ts-jest": "^29.0.3", 79 | "ts-node": "^10.9.1", 80 | "typescript": "^5.8.3" 81 | }, 82 | "peerDependencies": { 83 | "@solana/web3.js": "^1.95.3", 84 | "bn.js": "^5.2.1", 85 | "borsh": "^2.0.0", 86 | "buffer": "6.0.1" 87 | }, 88 | "engines": { 89 | "node": ">=14" 90 | }, 91 | "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" 92 | } 93 | -------------------------------------------------------------------------------- /src/evm/constants.ts: -------------------------------------------------------------------------------- 1 | import { EVM_CHAINS, EvmChainConfig } from './types/EvmChainData'; 2 | 3 | export const EVM_CHAIN_CONFIGS: EvmChainConfig = { 4 | [EVM_CHAINS.MONAD]: { 5 | chainId: 10143, 6 | rootContractAddress: '0xE6d461c863987F2a1096eA3476137F30f75B3d46', 7 | registryContractAddress: '0xa4338eadf4D2e0851eFb225b0Eab90bE47A095F1', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/evm/fetchers/registrar.fetchers.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | Contract, 5 | ensNormalize, 6 | namehash as ensNamehash, 7 | Provider, 8 | Typed, 9 | } from 'ethers'; 10 | 11 | import { Address } from '../types/Address'; 12 | import { EvmChainData } from '../types/EvmChainData'; 13 | import { labelhashFromLabel } from '../utils'; 14 | 15 | type NameData = { 16 | name: string; 17 | expiry: number; 18 | frozen: boolean; 19 | }; 20 | export type UserNft = NameData & { id: bigint; url?: string }; 21 | 22 | type ScData = { 23 | name: string; 24 | owner: Address; 25 | tldNode: string; 26 | symbol: string; 27 | baseUrl: string; 28 | gracePeriod: number; 29 | tldFrozen: boolean; 30 | defaultTTL: number; 31 | }; 32 | 33 | async function getNameData(params: { 34 | name: string; 35 | config: EvmChainData; 36 | provider: Provider; 37 | registrarAddress: Address | undefined; 38 | }): Promise { 39 | const { name, config, provider, registrarAddress } = params; 40 | if (!provider) throw Error('No provider'); 41 | if (!config) throw Error('Not connected to SmartContract'); 42 | if (!registrarAddress) throw Error('No registrar address'); 43 | 44 | const contract = new Contract( 45 | registrarAddress, 46 | ['function nameData(string) view returns ((string, uint256, bool))'], 47 | provider, 48 | ); 49 | const nameData = await contract.nameData(name); 50 | 51 | return nameData; 52 | } 53 | 54 | async function getScData(params: { 55 | config: EvmChainData; 56 | provider: Provider; 57 | registrarAddress: Address | undefined; 58 | }): Promise { 59 | const { config, provider, registrarAddress } = params; 60 | 61 | if (!provider) throw Error('No provider'); 62 | if (!config) throw Error('Not connected to SmartContract'); 63 | if (!registrarAddress) throw Error('No registrar address'); 64 | 65 | const contract = new Contract( 66 | registrarAddress, 67 | [ 68 | 'function name() view returns (string)', 69 | 'function owner() view returns (address)', 70 | 'function tldNode() view returns (string)', 71 | 'function symbol() view returns (string)', 72 | 'function baseUri() view returns (string)', 73 | 'function gracePeriod() view returns (uint256)', 74 | 'function allFrozen() view returns (bool)', 75 | 'function defaultTTL() view returns (uint256)', 76 | ], 77 | provider, 78 | ); 79 | 80 | const name = (await contract.name()) as unknown as string; 81 | const owner = (await contract.owner()) as unknown as string; 82 | const tldNode = (await contract.tldNode()) as unknown as string; 83 | const symbol = (await contract.symbol()) as unknown as string; 84 | const baseUrl = (await contract.baseUri()) as unknown as string; 85 | const gracePeriod = (await contract.gracePeriod()) as unknown as bigint; 86 | const tldFrozen = (await contract.allFrozen()) as unknown as boolean; 87 | const defaultTTL = (await contract.defaultTTL()) as unknown as bigint; 88 | 89 | return { 90 | name, 91 | owner: owner as Address, 92 | tldNode, 93 | symbol, 94 | baseUrl, 95 | gracePeriod: parseInt(gracePeriod.toString()), 96 | tldFrozen, 97 | defaultTTL: parseInt(defaultTTL.toString()), 98 | }; 99 | } 100 | 101 | async function getUsersNfts(params: { 102 | config: EvmChainData; 103 | provider: Provider; 104 | registrarAddress: Address | undefined; 105 | userAddress: Address | undefined; 106 | withTokenUrl?: boolean; 107 | }): Promise { 108 | const { config, provider, registrarAddress, userAddress, withTokenUrl } = 109 | params; 110 | 111 | if (!provider) throw Error('No provider'); 112 | if (!config) throw Error('Not connected to SmartContract'); 113 | if (!registrarAddress) throw Error('No registrar address'); 114 | if (!userAddress) throw Error('No user address'); 115 | 116 | const contract = new Contract( 117 | registrarAddress, 118 | [ 119 | 'function getUserNfts(address) view returns (uint256[])', 120 | 'function nameData(uint256) view returns ((string, uint256, bool))', 121 | 'function tokenURI(uint256) view returns (string)', 122 | ], 123 | provider, 124 | ); 125 | 126 | const nfts = await contract.getUserNfts(userAddress); 127 | 128 | const nftData = await Promise.all( 129 | nfts.map(async tokenId => { 130 | const tokenDataRaw = (await contract.nameData( 131 | Typed.uint256(tokenId), 132 | )) as [unknown, unknown, unknown]; 133 | 134 | const tokenUrl = 135 | withTokenUrl && 136 | ((await contract.tokenURI(tokenId)) as unknown as string); 137 | return { 138 | name: tokenDataRaw[0] as string, 139 | expiry: parseInt(tokenDataRaw[1] as string), 140 | frozen: tokenDataRaw[2] as boolean, 141 | url: tokenUrl, 142 | id: tokenId, 143 | }; 144 | }), 145 | ); 146 | 147 | return nftData; 148 | } 149 | 150 | async function getUserNftData(params: { 151 | config: EvmChainData; 152 | provider: Provider; 153 | registrarAddress: Address | undefined; 154 | domain: string; 155 | }) { 156 | const { config, provider, registrarAddress, domain } = params; 157 | if (!provider) throw Error('No provider'); 158 | if (!config) throw Error('Not connected to SmartContract'); 159 | if (!registrarAddress) throw Error('No registrar address'); 160 | 161 | const contract = new Contract( 162 | registrarAddress, 163 | [ 164 | 'function nameData(uint256) view returns ((string, uint256, bool))', 165 | 'function tokenURI(uint256) view returns (string)', 166 | ], 167 | provider, 168 | ); 169 | 170 | // Step 1 - convert full domain to only the name of the domain (e.g. domain.eth -> domain) 171 | const normalized = ensNormalize(domain); 172 | const label = normalized.split('.')[0]; 173 | const labelHash = labelhashFromLabel(label); 174 | 175 | // Step 2 - convert node to tokenId 176 | const tokenId = Typed.uint256(labelHash); 177 | 178 | // Step 3 - get the token data 179 | const tokenDataRaw = (await contract.nameData(tokenId)) as [ 180 | unknown, 181 | unknown, 182 | unknown, 183 | ]; 184 | const tokenUrl = (await contract.tokenURI(tokenId)) as unknown as string; 185 | 186 | return { 187 | name: tokenDataRaw[0] as string, 188 | expiry: BigInt(tokenDataRaw[1] as string), 189 | frozen: tokenDataRaw[2] as boolean, 190 | id: tokenId, 191 | url: tokenUrl, 192 | }; 193 | } 194 | 195 | async function getMainDomainRaw(params: { 196 | provider: Provider; 197 | address: Address; 198 | rootAddress: Address; 199 | }): Promise { 200 | const { provider, address, rootAddress } = params; 201 | 202 | if (!provider) throw Error('No provider'); 203 | if (!address) throw Error('No address provided'); 204 | if (!rootAddress) throw Error('No root address'); 205 | 206 | try { 207 | const reverseNode = ensNamehash( 208 | address.substring(2).toLowerCase() + '.addr.reverse', 209 | ); 210 | const resolverAddress = '0x741b2C8254495EbB84440A768bE0B5bACA62F6e8'; 211 | 212 | const resolverContract = new Contract( 213 | resolverAddress, 214 | ['function name(bytes32) view returns (string)'], 215 | provider, 216 | ); 217 | 218 | const mainDomain = await resolverContract.name(reverseNode); 219 | 220 | return mainDomain || null; 221 | } catch (error) { 222 | console.error('Error fetching primary ANS domain:', error); 223 | return null; 224 | } 225 | } 226 | 227 | export const registrarFetchers = { 228 | getNameData, 229 | getScData, 230 | getUsersNfts, 231 | getUserNftData, 232 | getMainDomainRaw, 233 | }; 234 | -------------------------------------------------------------------------------- /src/evm/fetchers/registry.fetchers.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Contract, Provider } from 'ethers'; 4 | 5 | import { Address } from '../types/Address'; 6 | import { EvmChainData } from '../types/EvmChainData'; 7 | 8 | type RecordData = { 9 | owner: Address; 10 | resolver: Address; 11 | ttl: bigint; 12 | }; 13 | 14 | async function getDomainOwner(params: { 15 | node: string; 16 | config: EvmChainData; 17 | provider: Provider; 18 | registryAddress: Address | undefined; 19 | }): Promise
{ 20 | const { node, config, provider, registryAddress } = params; 21 | if (!provider) throw Error('No provider'); 22 | if (!config) throw Error('Not connected to SmartContract'); 23 | if (!registryAddress) throw Error('No registrar address'); 24 | 25 | const contract = new Contract( 26 | registryAddress, 27 | ['function owner(bytes32) view returns (address)'], 28 | provider, 29 | ); 30 | const owner = await contract.owner(node); 31 | 32 | return owner; 33 | } 34 | 35 | async function getRecordData(params: { 36 | node: string; 37 | config: EvmChainData; 38 | provider: Provider; 39 | registryAddress: Address | undefined; 40 | }): Promise { 41 | const { node, config, provider, registryAddress } = params; 42 | if (!provider) throw Error('No provider'); 43 | if (!config) throw Error('Not connected to SmartContract'); 44 | if (!registryAddress) throw Error('No registrar address'); 45 | 46 | const contract = new Contract( 47 | registryAddress, 48 | [ 49 | 'function owner(bytes32) view returns (address)', 50 | 'function resolver(bytes32) view returns (address)', 51 | 'function ttl(bytes32) view returns (uint256)', 52 | ], 53 | provider, 54 | ); 55 | const owner = await contract.owner(node); 56 | const resolver = await contract.resolver(node); 57 | const ttl = await contract.ttl(node); 58 | 59 | return { 60 | owner, 61 | resolver, 62 | ttl: BigInt(ttl.toString()), 63 | }; 64 | } 65 | 66 | export const registryFetchers = { 67 | getDomainOwner, 68 | getRecordData, 69 | }; 70 | -------------------------------------------------------------------------------- /src/evm/fetchers/root.fetchers.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Contract, Provider } from 'ethers'; 4 | 5 | import { Address } from '../types/Address'; 6 | import { EvmChainData } from '../types/EvmChainData'; 7 | 8 | export type TLD = { 9 | controller: Address; 10 | registrar: Address; 11 | tld: string; 12 | name: string; 13 | symbol: string; 14 | locked: boolean; 15 | node: string; 16 | label: string; 17 | }; 18 | 19 | export type PriceSchema = { 20 | for1: number; 21 | for2: number; 22 | for3: number; 23 | for4: number; 24 | for5plus: number; 25 | }; 26 | 27 | export type SplitSchema = { 28 | percentage: number; 29 | recipient: Address; 30 | }; 31 | 32 | async function getRegistryAddress(params: { 33 | config: EvmChainData; 34 | provider: Provider; 35 | }): Promise
{ 36 | const { provider, config } = params; 37 | 38 | if (!provider) throw Error('No provider'); 39 | if (!config) throw Error('Not connected to SmartContract'); 40 | 41 | const contract = new Contract( 42 | config.rootContractAddress, 43 | ['function registry() view returns (address)'], 44 | provider, 45 | ); 46 | 47 | const address = await contract.registry(); 48 | 49 | return address; 50 | } 51 | 52 | async function getTldData(params: { 53 | config: EvmChainData; 54 | provider: Provider; 55 | tldLabel: string; 56 | }): Promise { 57 | const { config, tldLabel, provider } = params; 58 | 59 | if (!config) throw Error('Not connected to SmartContract'); 60 | if (!provider) throw Error('No provider'); 61 | 62 | const contract = new Contract( 63 | config.rootContractAddress, 64 | [ 65 | 'function getTld(bytes32) view returns ((address, address, string, string, string, bool, bytes32, bytes32))', 66 | ], 67 | provider, 68 | ); 69 | 70 | const tldDataRaw = await contract.getTld(tldLabel); 71 | 72 | const [controller, registrar, tld, name, symbol, locked, node, label] = 73 | tldDataRaw; 74 | 75 | return { 76 | controller: controller as Address, 77 | registrar: registrar as Address, 78 | tld: tld as string, 79 | name: name as string, 80 | symbol: symbol as string, 81 | locked: (locked as string) === 'true', 82 | node: node as string, 83 | label: label as string, 84 | }; 85 | } 86 | 87 | async function getTlds(params: { 88 | config: EvmChainData; 89 | provider: Provider; 90 | }): Promise { 91 | const { provider, config } = params; 92 | 93 | if (!provider) throw Error('No provider'); 94 | if (!config) throw Error('Not connected to SmartContract'); 95 | 96 | const contract = new Contract( 97 | config.rootContractAddress, 98 | [ 99 | 'function listTlds() view returns ((address, address, string, string, string, bool, bytes32, bytes32)[])', 100 | ], 101 | provider, 102 | ); 103 | 104 | const tldsRaw = (await contract.listTlds()) as [ 105 | unknown, // controller 106 | unknown, // registrar 107 | unknown, // tld 108 | unknown, // name 109 | unknown, // symbol 110 | unknown, // locked 111 | unknown, // node 112 | unknown, // label 113 | ][]; 114 | 115 | const tlds = tldsRaw.map((tldRaw): TLD => { 116 | const [controller, registrar, tld, name, symbol, locked, node, label] = 117 | tldRaw; 118 | 119 | return { 120 | controller: controller as Address, 121 | registrar: registrar as Address, 122 | tld: tld as string, 123 | name: name as string, 124 | symbol: symbol as string, 125 | locked: (locked as string) === 'true', 126 | node: node as string, 127 | label: label as string, 128 | }; 129 | }); 130 | 131 | return tlds; 132 | } 133 | 134 | export const rootFetchers = { 135 | getRegistryAddress, 136 | getTlds, 137 | getTldData, 138 | }; 139 | -------------------------------------------------------------------------------- /src/evm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './parsers'; 3 | export * from './types/Address'; 4 | export * from './types/AddressAndDomain'; 5 | export * from './types/EnumValues'; 6 | export * from './types/EvmChainData'; 7 | export * from './types/NameRecordHeader'; 8 | export * from './utils'; 9 | -------------------------------------------------------------------------------- /src/evm/parsers.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { ensNormalize, EnsPlugin, JsonRpcProvider, Provider } from 'ethers'; 3 | 4 | import { ITldParser } from '../parsers.interface'; 5 | import { MainDomain, NameAccountAndDomain, NameRecordHeader } from '../svm'; 6 | import { registrarFetchers } from './fetchers/registrar.fetchers'; 7 | import { registryFetchers } from './fetchers/registry.fetchers'; 8 | import { rootFetchers, TLD } from './fetchers/root.fetchers'; 9 | import { Address, isValidAddress } from './types/Address'; 10 | import { AddressAndDomain } from './types/AddressAndDomain'; 11 | import { EvmChainData } from './types/EvmChainData'; 12 | import { 13 | configOfEvmChainId, 14 | labelhashFromLabel, 15 | ansNamehash, 16 | NetworkWithRpc, 17 | } from './utils'; 18 | import { NameRecord } from './types/NameRecordHeader'; 19 | 20 | export class TldParserEvm implements ITldParser { 21 | connection: Provider; 22 | private config: EvmChainData; 23 | 24 | constructor(settings?: Connection | NetworkWithRpc) { 25 | if (settings instanceof NetworkWithRpc) { 26 | const chainId = parseInt(settings.chainId.toString()); 27 | const config = configOfEvmChainId(chainId); 28 | this.config = config; 29 | 30 | settings.attachPlugin( 31 | new EnsPlugin(config.registryContractAddress, chainId), 32 | ); 33 | if (settings.provider) { 34 | this.connection = settings.provider; 35 | } else { 36 | this.connection = new JsonRpcProvider( 37 | settings.rpcUrl, 38 | settings, 39 | { 40 | staticNetwork: true, 41 | }, 42 | ); 43 | } 44 | } else { 45 | throw new Error('Method not implemented.'); 46 | } 47 | } 48 | 49 | async getAllUserDomains(userAccount: string): Promise { 50 | const isValidAddr = isValidAddress(userAccount as string); 51 | if (!isValidAddr) { 52 | throw new Error(`Invalid address for EVM chain: ${userAccount}`); 53 | } 54 | 55 | const tlds = await rootFetchers.getTlds({ 56 | config: this.config, 57 | provider: this.connection, 58 | }); 59 | 60 | const domains = ( 61 | await Promise.all( 62 | tlds.map(tld => { 63 | return this.getUserNftFromTld({ 64 | userAccount: userAccount as string, 65 | tld, 66 | }); 67 | }), 68 | ) 69 | ).flat(); 70 | 71 | return domains.map(domain => { 72 | return { 73 | created_at: '0', 74 | domain_name: domain.nft.name, 75 | expires_at: domain.nft.expiry.toString(), 76 | main_domain_address: '', 77 | tld: domain.tld.tld, 78 | transferrable: !domain.nft.frozen, 79 | }; 80 | }); 81 | } 82 | 83 | async getAllUserDomainsFromTld( 84 | userAccount: string, 85 | tld: string, 86 | ): Promise { 87 | const isValidAddr = isValidAddress(userAccount as string); 88 | if (!isValidAddr) { 89 | throw new Error(`Invalid address for EVM chain: ${userAccount}`); 90 | } 91 | 92 | const tldLabel = labelhashFromLabel(tld); 93 | 94 | const tldData = await rootFetchers.getTldData({ 95 | config: this.config, 96 | provider: this.connection, 97 | tldLabel, 98 | }); 99 | 100 | const domains = await this.getUserNftFromTld({ 101 | userAccount: userAccount as string, 102 | tld: tldData, 103 | }); 104 | 105 | return domains.map(domain => { 106 | return { 107 | created_at: '0', 108 | domain_name: domain.nft.name, 109 | expires_at: domain.nft.expiry.toString(), 110 | main_domain_address: '', 111 | tld: domain.tld.tld, 112 | transferrable: !domain.nft.frozen, 113 | }; 114 | }); 115 | } 116 | 117 | async getOwnerFromDomainTld( 118 | domainTld: string, 119 | ): Promise { 120 | const node = ansNamehash(domainTld); 121 | const owner = await registryFetchers.getDomainOwner({ 122 | config: this.config, 123 | provider: this.connection, 124 | registryAddress: this.config.registryContractAddress as Address, 125 | node, 126 | }); 127 | 128 | return owner; 129 | } 130 | 131 | async getNameRecordFromDomainTld( 132 | domainTld: string, 133 | ): Promise { 134 | const normalized = ensNormalize(domainTld); 135 | const node = ansNamehash(normalized); 136 | const recordData = await registryFetchers.getRecordData({ 137 | config: this.config, 138 | provider: this.connection, 139 | registryAddress: this.config.registryContractAddress as Address, 140 | node, 141 | }); 142 | 143 | const tld = await this.getTldFromFullDomain(domainTld); 144 | const tldData = await rootFetchers.getTldData({ 145 | config: this.config, 146 | provider: this.connection, 147 | tldLabel: labelhashFromLabel(tld), 148 | }); 149 | 150 | const nftData = await registrarFetchers.getUserNftData({ 151 | config: this.config, 152 | provider: this.connection, 153 | registrarAddress: tldData.registrar as Address, 154 | domain: domainTld, 155 | }); 156 | 157 | return { 158 | created_at: (nftData.expiry - recordData.ttl).toString(), 159 | domain_name: nftData.name, 160 | expires_at: nftData.expiry.toString(), 161 | main_domain_address: recordData.owner, 162 | tld: `${tldData.tld}`, 163 | transferrable: !nftData.frozen, 164 | }; 165 | } 166 | 167 | getTldFromParentAccount( 168 | parentAccount: PublicKey | string, 169 | ): Promise { 170 | throw new Error('Method not implemented.'); 171 | } 172 | 173 | reverseLookupNameAccount( 174 | nameAccount: PublicKey | string, 175 | parentAccountOwner: PublicKey | string, 176 | ): Promise { 177 | throw new Error('Method not implemented.'); 178 | } 179 | 180 | async getMainDomain( 181 | userAddress: PublicKey | string, 182 | ): Promise { 183 | const isValidAddr = isValidAddress(userAddress as string); 184 | if (!isValidAddr) { 185 | throw new Error(`Invalid address for EVM chain: ${userAddress}`); 186 | } 187 | 188 | const mainDomain = await registrarFetchers.getMainDomainRaw({ 189 | provider: this.connection, 190 | address: userAddress as Address, 191 | rootAddress: this.config.rootContractAddress as Address, 192 | }); 193 | 194 | if (!mainDomain) { 195 | throw new Error(`No main domain found for: ${userAddress}`); 196 | } 197 | return (await this.getNameRecordFromDomainTld( 198 | mainDomain, 199 | )) as NameRecord; 200 | } 201 | 202 | getParsedAllUserDomainsFromTldUnwrapped( 203 | userAccount: PublicKey | string, 204 | tld: string, 205 | ): Promise { 206 | throw new Error('Method not implemented.'); 207 | } 208 | 209 | async getParsedAllUserDomainsFromTld( 210 | userAccount: PublicKey | string, 211 | tld: string, 212 | ): Promise { 213 | const isValidAddr = isValidAddress(userAccount as string); 214 | if (!isValidAddr) { 215 | throw new Error(`Invalid address for EVM chain: ${userAccount}`); 216 | } 217 | 218 | const tldLabel = labelhashFromLabel(tld); 219 | 220 | const tldData = await rootFetchers.getTldData({ 221 | config: this.config, 222 | provider: this.connection, 223 | tldLabel, 224 | }); 225 | 226 | const domains = await this.getUserNftFromTld({ 227 | userAccount: userAccount as string, 228 | tld: tldData, 229 | }); 230 | 231 | return domains.map(domain => { 232 | return { 233 | address: domain.nft.name, 234 | domain: domain.tld.tld, 235 | }; 236 | }); 237 | } 238 | 239 | getParsedAllUserDomainsUnwrapped( 240 | userAccount: PublicKey | string, 241 | ): Promise { 242 | throw new Error('Method not implemented.'); 243 | } 244 | 245 | async getParsedAllUserDomains( 246 | userAccount: PublicKey | string, 247 | ): Promise { 248 | const isValidAddr = isValidAddress(userAccount as string); 249 | if (!isValidAddr) { 250 | throw new Error(`Invalid address for EVM chain: ${userAccount}`); 251 | } 252 | 253 | const tlds = await rootFetchers.getTlds({ 254 | config: this.config, 255 | provider: this.connection, 256 | }); 257 | 258 | const domains = ( 259 | await Promise.all( 260 | tlds.map(tld => { 261 | return this.getUserNftFromTld({ 262 | userAccount: userAccount as string, 263 | tld, 264 | }); 265 | }), 266 | ) 267 | ).flat(); 268 | 269 | return domains.map(domain => { 270 | return { 271 | address: domain.nft.name, 272 | domain: domain.tld.tld, 273 | }; 274 | }); 275 | } 276 | 277 | private async getUserNftFromTld(data: { userAccount: string; tld: TLD }) { 278 | const { tld, userAccount } = data; 279 | const registrar = tld.registrar; 280 | const nftData = await registrarFetchers.getUsersNfts({ 281 | config: this.config, 282 | provider: this.connection, 283 | registrarAddress: registrar as Address, 284 | userAddress: userAccount as Address, 285 | }); 286 | 287 | return nftData.map(nft => { 288 | return { 289 | nft, 290 | tld, 291 | }; 292 | }); 293 | } 294 | 295 | private async getBaseRegistry(chainId: number): Promise { 296 | const config = configOfEvmChainId(chainId); 297 | 298 | const data = await rootFetchers.getRegistryAddress({ 299 | config, 300 | provider: this.connection, 301 | }); 302 | 303 | return data; 304 | } 305 | 306 | private async getTldFromFullDomain(domain: string) { 307 | // Considering there can be unlimited subdomains, the last part after a dot is the tld 308 | const parts = domain.split('.'); 309 | const tld = parts[parts.length - 1]; 310 | return '.' + tld; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/evm/types/Address.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { isAddress } from 'ethers'; 4 | 5 | export type Address = `0x${string}`; 6 | 7 | export function isValidAddress(address: string): boolean { 8 | return isAddress(address); 9 | } 10 | -------------------------------------------------------------------------------- /src/evm/types/AddressAndDomain.ts: -------------------------------------------------------------------------------- 1 | import { Address } from './Address'; 2 | 3 | export type AddressAndDomain = { 4 | address: Address; 5 | domain: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/evm/types/EnumValues.ts: -------------------------------------------------------------------------------- 1 | export type EnumValues = T[keyof T]; 2 | -------------------------------------------------------------------------------- /src/evm/types/EvmChainData.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { EnumValues } from './EnumValues'; 4 | 5 | export const EVM_CHAINS = { 6 | SEPOLIA: 'sepolia', 7 | AMOY: 'amoy', 8 | MONAD: 'monad', 9 | } as const; 10 | 11 | export type EvmChainType = EnumValues; 12 | 13 | export type EvmChainData = { 14 | chainId: number; 15 | rootContractAddress: string; 16 | registryContractAddress: string; 17 | }; 18 | 19 | export type EvmChainConfig = { [key in EvmChainType]?: EvmChainData }; 20 | -------------------------------------------------------------------------------- /src/evm/types/NameRecordHeader.ts: -------------------------------------------------------------------------------- 1 | export type NameRecord = { 2 | created_at: string; 3 | domain_name: string; 4 | expires_at: string; 5 | main_domain_address: any; 6 | tld: string; 7 | transferrable: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /src/evm/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | concat, 3 | ensNormalize, 4 | hexlify, 5 | keccak256, 6 | Network, 7 | Provider, 8 | toUtf8Bytes, 9 | ZeroHash, 10 | } from 'ethers'; 11 | import { EVM_CHAIN_CONFIGS } from './constants'; 12 | import { EvmChainData } from './types/EvmChainData'; 13 | 14 | export class NetworkWithRpc extends Network { 15 | public rpcUrl: string; 16 | public provider: Provider | undefined; 17 | 18 | constructor( 19 | name: string, 20 | chainId: number, 21 | rpcUrl: string, 22 | provider?: Provider, 23 | ) { 24 | super(name, chainId); 25 | this.rpcUrl = rpcUrl; 26 | this.provider = provider; 27 | } 28 | } 29 | 30 | export function getValues>(obj: T): [T[keyof T]] { 31 | return Object.values(obj) as [(typeof obj)[keyof T]]; 32 | } 33 | 34 | export function configOfEvmChainId( 35 | chainId: number | undefined, 36 | ): EvmChainData | undefined { 37 | if (chainId === undefined) return undefined; 38 | 39 | const config = Object.values(EVM_CHAIN_CONFIGS).find(chainData => { 40 | return chainData.chainId === chainId; 41 | }); 42 | 43 | if (config === undefined) { 44 | throw new Error(`ChainId ${chainId} is not currently supported`); 45 | } 46 | 47 | return config; 48 | } 49 | 50 | export function labelhashFromLabel(label: string): string { 51 | const labelhash = keccak256(toUtf8Bytes(label)); 52 | return labelhash; 53 | } 54 | 55 | export function namehashFromDomain(domain: string): string { 56 | const label = ansNamehash(domain); 57 | return label; 58 | } 59 | 60 | export function ansNamehash(name: string): string { 61 | let result: string | Uint8Array = ZeroHash; 62 | 63 | const comps = ansNameSplit(name); 64 | while (comps.length) { 65 | result = keccak256( 66 | concat([result, keccak256(comps.pop())]), 67 | ); 68 | } 69 | 70 | return hexlify(result); 71 | } 72 | 73 | function ansNameSplit(name: string): Array { 74 | const bytes = toUtf8Bytes(ensNormalize(name)); 75 | const comps: Array = []; 76 | 77 | if (name.length === 0) { 78 | return comps; 79 | } 80 | 81 | let last = 0; 82 | for (let i = 0; i < bytes.length; i++) { 83 | const d = bytes[i]; 84 | 85 | // A separator (i.e. "."); copy this component including the dot 86 | if (d === 0x2e) { 87 | comps.push(bytes.slice(last, i)); 88 | last = i + 1; 89 | } 90 | } 91 | 92 | comps.push(bytes.slice(last - 1)); 93 | return comps; 94 | } 95 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parsers'; 2 | export * from './parsers.interface'; 3 | export * from './svm'; 4 | export * from './evm'; 5 | -------------------------------------------------------------------------------- /src/parsers.interface.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { Provider } from 'ethers'; 3 | import { AddressAndDomain } from './evm/types/AddressAndDomain'; 4 | import { NameAccountAndDomain } from './svm/name-record-handler'; 5 | import { MainDomain } from './svm/state/main-domain'; 6 | import { NameRecordHeader } from './svm/state/name-record-header'; 7 | import { NameRecord } from 'evm/types/NameRecordHeader'; 8 | 9 | export interface ITldParser { 10 | connection: Connection | Provider; 11 | /** 12 | * retrieves all nameAccounts for any user. 13 | * 14 | * @param userAccount user publickey or string 15 | */ 16 | getAllUserDomains( 17 | userAccount: PublicKey | string, 18 | ): Promise; 19 | /** 20 | * retrieves all nameaccounts for any user in a specific tld. 21 | * 22 | * @param userAccount user publickey or string 23 | * @param tld tld to be retrieved from 24 | */ 25 | getAllUserDomainsFromTld( 26 | userAccount: PublicKey | string, 27 | tld: string, 28 | ): Promise; 29 | 30 | /** 31 | * retrieves owner of a specific Name Account from domain.tld. 32 | * 33 | * @param domainTld full string of domain and tld e.g. "miester.poor" 34 | */ 35 | getOwnerFromDomainTld( 36 | domainTld: string, 37 | ): Promise; 38 | 39 | /** 40 | * retrieves domainTld data a domain from domain.tld. 41 | * 42 | * @param domainTld full string of domain and tld e.g. "miester.poor" 43 | */ 44 | getNameRecordFromDomainTld( 45 | domainTld: string, 46 | ): Promise; 47 | 48 | /** 49 | * retrieves tld from parent name via TldHouse account. 50 | * 51 | * @param parentAccount parent publickey or string 52 | */ 53 | getTldFromParentAccount(parentAccount: PublicKey | string): Promise; 54 | 55 | /** 56 | * retrieves domain from name account via tldParent account. 57 | * 58 | * @param nameAccount name publickey or string 59 | * @param parentAccountOwner parent Owner or string (TldHouse) 60 | */ 61 | reverseLookupNameAccount( 62 | nameAccount: PublicKey | string, 63 | parentAccountOwner: PublicKey | string, 64 | ): Promise; 65 | 66 | /** 67 | * retrieves main domain name account and its domain tld from user address. 68 | * 69 | * @param userAddress user publickey or string 70 | */ 71 | getMainDomain( 72 | userAddress: PublicKey | string, 73 | ): Promise; 74 | /** 75 | * retrieves all parsed domains as strings with name accounts in an array for user in a specific TLD. 76 | * in alphabetical order 77 | * 78 | * @param userAccount user publickey or string 79 | * @param tld tld to be retrieved from 80 | */ 81 | getParsedAllUserDomainsFromTldUnwrapped( 82 | userAccount: PublicKey | string, 83 | tld: string, 84 | ): Promise; 85 | 86 | /** 87 | * retrieves all parsed domains and name accounts including NFTs in an array for any user in a specific TLD. 88 | * in alphabetical order 89 | * 90 | * @param userAccount user publickey or string 91 | * @param tld tld to be retrieved from 92 | */ 93 | getParsedAllUserDomainsFromTld( 94 | userAccount: PublicKey | string, 95 | tld: string, 96 | ): Promise; 97 | 98 | /** 99 | * retrieves all parsed domains and name accounts for user. 100 | * in alphabetical order 101 | * 102 | * @param userAccount user publickey or string 103 | * @param tld tld to be retrieved from 104 | */ 105 | getParsedAllUserDomainsUnwrapped( 106 | userAccount: PublicKey | string, 107 | ): Promise; 108 | 109 | /** 110 | * retrieves all parsed domains and name accounts including NFTs for user. 111 | * in alphabetical order 112 | * 113 | * @param userAccount user publickey or string 114 | * @param tld tld to be retrieved from 115 | */ 116 | getParsedAllUserDomains( 117 | userAccount: PublicKey | string, 118 | ): Promise; 119 | } 120 | -------------------------------------------------------------------------------- /src/parsers.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { Provider } from 'ethers'; 3 | import { TldParserEvm } from './evm/parsers'; 4 | import { AddressAndDomain } from './evm/types/AddressAndDomain'; 5 | import { NetworkWithRpc } from './evm/utils'; 6 | import { ITldParser } from './parsers.interface'; 7 | import { NameAccountAndDomain } from './svm/name-record-handler'; 8 | import { TldParserSvm } from './svm/parsers'; 9 | import { MainDomain } from './svm/state/main-domain'; 10 | import { NameRecordHeader } from './svm/state/name-record-header'; 11 | import { NameRecord } from 'evm'; 12 | 13 | /** 14 | * TldParser class 15 | * 16 | * This class has been improved to maintain compatibility with previous versions. 17 | * The methods present in this class are provided for backwards compatibility 18 | * and to facilitate easy migration to v1 in future builds. 19 | * 20 | * The TldParser for multiple chains will be implemented, and Solana integration will remain unchanged without any breaking modifications. 21 | */ 22 | export class TldParser implements ITldParser { 23 | connection: Connection | Provider; 24 | 25 | constructor(connection: Connection | NetworkWithRpc, chain?: string) { 26 | if (new.target === TldParser) { 27 | return TldParser.createParser(connection, chain); 28 | } 29 | } 30 | 31 | private static createParser( 32 | connection: Connection | NetworkWithRpc, 33 | chain?: string, 34 | ): ITldParser { 35 | switch (chain?.toLowerCase()) { 36 | case 'yona': 37 | case 'eclipse': 38 | case 'termina': 39 | case 'solana': 40 | case undefined: 41 | return new TldParserSvm(connection as Connection); 42 | case 'monad': 43 | return new TldParserEvm(connection as NetworkWithRpc); 44 | default: 45 | throw new Error(`Unsupported TldParser chain: ${chain}`); 46 | } 47 | } 48 | 49 | /** 50 | * retrieves all nameAccounts for any user. 51 | * 52 | * @param userAccount user publickey or string 53 | */ 54 | async getAllUserDomains( 55 | userAccount: PublicKey | string, 56 | ): Promise { 57 | throw new Error('Method not implemented.'); 58 | } 59 | 60 | /** 61 | * retrieves all nameaccounts for any user in a specific tld. 62 | * 63 | * @param userAccount user publickey or string 64 | * @param tld tld to be retrieved from 65 | */ 66 | async getAllUserDomainsFromTld( 67 | userAccount: PublicKey | string, 68 | tld: string, 69 | ): Promise { 70 | throw new Error('Method not implemented.'); 71 | } 72 | 73 | /** 74 | * retrieves owner of a specific Name Account from domain.tld. 75 | * 76 | * @param domainTld full string of domain and tld e.g. "miester.poor" 77 | */ 78 | async getOwnerFromDomainTld( 79 | domainTld: string, 80 | ): Promise { 81 | throw new Error('Method not implemented.'); 82 | } 83 | 84 | /** 85 | * retrieves domainTld data a domain from domain.tld. 86 | * 87 | * @param domainTld full string of domain and tld e.g. "miester.poor" 88 | */ 89 | async getNameRecordFromDomainTld( 90 | domainTld: string, 91 | ): Promise { 92 | throw new Error('Method not implemented.'); 93 | } 94 | 95 | /** 96 | * retrieves tld from parent name via TldHouse account. 97 | * 98 | * @param parentAccount parent publickey or string 99 | */ 100 | async getTldFromParentAccount( 101 | parentAccount: PublicKey | string, 102 | ): Promise { 103 | throw new Error('Method not implemented.'); 104 | } 105 | 106 | /** 107 | * retrieves domain from name account via tldParent account. 108 | * 109 | * @param nameAccount name publickey or string 110 | * @param parentAccountOwner parent Owner or string (TldHouse) 111 | */ 112 | async reverseLookupNameAccount( 113 | nameAccount: PublicKey | string, 114 | parentAccountOwner: PublicKey | string, 115 | ): Promise { 116 | throw new Error('Method not implemented.'); 117 | } 118 | 119 | /** 120 | * retrieves main domain name account and its domain tld from user address. 121 | * 122 | * @param userAddress user publickey or string 123 | */ 124 | async getMainDomain( 125 | userAddress: PublicKey | string, 126 | ): Promise { 127 | throw new Error('Method not implemented.'); 128 | } 129 | 130 | /** 131 | * retrieves all parsed domains as strings with name accounts in an array for user in a specific TLD. 132 | * in alphabetical order 133 | * 134 | * @param userAccount user publickey or string 135 | * @param tld tld to be retrieved from 136 | */ 137 | async getParsedAllUserDomainsFromTldUnwrapped( 138 | userAccount: PublicKey | string, 139 | tld: string, 140 | concurrency: number = 10, 141 | ): Promise { 142 | throw new Error('Method not implemented.'); 143 | } 144 | 145 | /** 146 | * retrieves all parsed domains and name accounts including NFTs in an array for any user in a specific TLD. 147 | * in alphabetical order 148 | * 149 | * @param userAccount user publickey or string 150 | * @param tld tld to be retrieved from 151 | */ 152 | async getParsedAllUserDomainsFromTld( 153 | userAccount: PublicKey | string, 154 | tld: string, 155 | concurrency: number = 10, 156 | ): Promise { 157 | throw new Error('Method not implemented.'); 158 | } 159 | 160 | /** 161 | * retrieves all parsed domains and name accounts for user. 162 | * in alphabetical order 163 | * 164 | * @param userAccount user publickey or string 165 | */ 166 | async getParsedAllUserDomainsUnwrapped( 167 | userAccount: PublicKey | string, 168 | concurrency: number = 10, 169 | ): Promise { 170 | throw new Error('Method not implemented.'); 171 | } 172 | 173 | /** 174 | * retrieves all parsed domains and name accounts including NFTs for user. 175 | * in alphabetical order 176 | * 177 | * @param userAccount user publickey or string 178 | */ 179 | async getParsedAllUserDomains( 180 | userAccount: PublicKey | string, 181 | concurrency: number = 10, 182 | ): Promise { 183 | throw new Error('Method not implemented.'); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/svm/constants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | export const MAIN_DOMAIN_PREFIX = 'main_domain'; 4 | // MAINNET 5 | export const ORIGIN_TLD = 'ANS'; 6 | export const ANS_PROGRAM_ID = new PublicKey( 7 | 'ALTNSZ46uaAUU7XUV6awvdorLGqAsPwa9shm7h4uP2FK', 8 | ); 9 | export const TLD_HOUSE_PROGRAM_ID = new PublicKey( 10 | 'TLDHkysf5pCnKsVA4gXpNvmy7psXLPEu4LAdDJthT9S', 11 | ); 12 | 13 | export const NAME_HOUSE_PROGRAM_ID = new PublicKey( 14 | 'NH3uX6FtVE2fNREAioP7hm5RaozotZxeL6khU1EHx51', 15 | ); 16 | 17 | export const SPL_TOKEN_PROGRAM_ID = new PublicKey( 18 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 19 | ); 20 | export const TOKEN_METADATA_PROGRAM_ID = new PublicKey( 21 | 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', 22 | ); 23 | 24 | export const ROOT_ANS_PUBLIC_KEY = new PublicKey( 25 | "3mX9b4AZaQehNoQGfckVcmgmA6bkBoFcbLj9RMmMyNcU", 26 | ); 27 | 28 | export const NFT_RECORD_PREFIX = 'nft_record'; 29 | export const TLD_HOUSE_PREFIX = 'tld_house'; 30 | export const NAME_HOUSE_PREFIX = 'name_house'; 31 | export const MULTIPLE_ACCOUNT_INFO_MAX = 100; 32 | -------------------------------------------------------------------------------- /src/svm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './constants'; 3 | export * from './parsers'; 4 | export * from './state/name-record-header'; 5 | export * from './state/main-domain'; 6 | export * from './state/nft-record'; 7 | export * from './name-record-handler'; 8 | export * from './types/records'; 9 | export * from './types/tag'; 10 | -------------------------------------------------------------------------------- /src/svm/name-record-handler.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | import { 4 | getHashedName, 5 | getNameAccountKeyWithBump, 6 | getOriginNameAccountKey, 7 | } from './utils'; 8 | 9 | /** 10 | * This function can be used to compute the public key of a domain or subdomain and multi-level subdomain. 11 | * @param domainTld The domain to compute the public key for (e.g `vlad.poor`, `ipfs.miester.poor`, 'ipfs.super.miester.poor') 12 | * @returns 13 | */ 14 | export const getDomainKey = async (domainTld: string, record = false) => { 15 | const domainTldSplit = domainTld.split('.'); 16 | if (domainTldSplit.length === 3) { 17 | // handles subdomains 18 | const tld = '.' + domainTldSplit[2]; 19 | const domain = domainTldSplit[1]; 20 | const subDomain = domainTldSplit[0]; 21 | // parent key 22 | const { pubkey: parentKey } = await _getNameAccount(tld); 23 | // domain key 24 | const { pubkey: domainKey } = await _getNameAccount(domain, parentKey); 25 | // Sub domain 26 | const prefix = Buffer.from([record ? 1 : 0]).toString(); 27 | const sub = prefix.concat(subDomain); 28 | const result = await _getNameAccount(sub, domainKey); 29 | return { ...result, isSub: true, parent: domainKey }; 30 | } else if (domainTldSplit.length === 4 && record) { 31 | // handles four-level subdomain 32 | const tld = '.' + domainTldSplit[3]; 33 | const domain = domainTldSplit[2]; 34 | const subDomain = domainTldSplit[1]; 35 | const multiLevelSubDomain = domainTldSplit[0]; 36 | // parent key 37 | const { pubkey: parentKey } = await _getNameAccount(tld); 38 | // domain key 39 | const { pubkey: domainKey } = await _getNameAccount(domain, parentKey); 40 | // Sub domain has to be added when we create subdomains for users which are not records 41 | const { pubkey: subKey } = await _getNameAccount( 42 | '\0'.concat(subDomain), 43 | domainKey, 44 | ); 45 | // Sub record 46 | const recordPrefix = Buffer.from([1]).toString(); 47 | const result = await _getNameAccount( 48 | recordPrefix.concat(multiLevelSubDomain), 49 | subKey, 50 | ); 51 | return { ...result, isSub: true, parent: domainKey, isSubRecord: true }; 52 | } else if (domainTldSplit.length > 4) { 53 | throw new Error('Invalid derivation input'); 54 | } 55 | // just a regular domainTld 56 | const tldName = '.' + domainTldSplit[1]; 57 | const { pubkey: parentKeyDomainAccount } = await _getNameAccount(tldName); 58 | const domain = domainTldSplit[0]; 59 | const result = await _getNameAccount(domain, parentKeyDomainAccount); 60 | return { ...result, isSub: false, parent: undefined }; 61 | }; 62 | 63 | const _getNameAccount = async (name: string, parent?: PublicKey) => { 64 | if (!parent) { 65 | parent = await getOriginNameAccountKey(); 66 | } 67 | let hashed = await getHashedName(name); 68 | let [pubkey] = await getNameAccountKeyWithBump(hashed, undefined, parent); 69 | return { pubkey, hashed }; 70 | }; 71 | 72 | export type NameAccountAndDomain = { nameAccount: PublicKey; domain: string }; 73 | -------------------------------------------------------------------------------- /src/svm/parsers.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Connection } from '@solana/web3.js'; 2 | import { BN } from 'bn.js'; 3 | 4 | import { MainDomain } from './state/main-domain'; 5 | import { NameRecordHeader } from './state/name-record-header'; 6 | import { 7 | delay, 8 | findMainDomain, 9 | findNameHouse, 10 | findOwnedNameAccountsForUser, 11 | findTldHouse, 12 | getAllTld, 13 | getHashedName, 14 | getNameAccountKeyWithBump, 15 | getNameOwner, 16 | getOriginNameAccountKey, 17 | getOwnedDomains, 18 | getParsedAllDomainsNftAccountsByOwner, 19 | getUserNfts, 20 | performReverseLookupBatched, 21 | } from './utils'; 22 | import { MULTIPLE_ACCOUNT_INFO_MAX } from './constants'; 23 | import { NameAccountAndDomain, getDomainKey } from './name-record-handler'; 24 | import { ITldParser } from '../parsers.interface'; 25 | import async from 'async'; 26 | 27 | export class TldParserSvm implements ITldParser { 28 | connection: Connection; 29 | 30 | constructor(connection: Connection) { 31 | if (connection instanceof Connection) { 32 | this.connection = connection; 33 | } else { 34 | throw new Error('Method not implemented.'); 35 | } 36 | } 37 | 38 | /** 39 | * retrieves all nameAccounts for any user. 40 | * 41 | * @param userAccount user publickey or string 42 | */ 43 | async getAllUserDomains( 44 | userAccount: PublicKey | string, 45 | ): Promise { 46 | if (typeof userAccount == 'string') { 47 | userAccount = new PublicKey(userAccount); 48 | } 49 | const allDomains = await findOwnedNameAccountsForUser( 50 | this.connection, 51 | userAccount, 52 | undefined, 53 | ); 54 | return allDomains.map((a: any) => a.pubkey); 55 | } 56 | 57 | /** 58 | * retrieves all nameaccounts for any user in a specific tld. 59 | * 60 | * @param userAccount user publickey or string 61 | * @param tld tld to be retrieved from 62 | */ 63 | async getAllUserDomainsFromTld( 64 | userAccount: PublicKey | string, 65 | tld: string, 66 | ): Promise { 67 | const tldName = '.' + tld; 68 | 69 | const nameOriginTldKey = await getOriginNameAccountKey(); 70 | const parentHashedName = await getHashedName(tldName); 71 | const [parentAccountKey] = getNameAccountKeyWithBump( 72 | parentHashedName, 73 | undefined, 74 | nameOriginTldKey, 75 | ); 76 | if (typeof userAccount == 'string') { 77 | userAccount = new PublicKey(userAccount); 78 | } 79 | const allDomains = await findOwnedNameAccountsForUser( 80 | this.connection, 81 | userAccount, 82 | parentAccountKey, 83 | ); 84 | return allDomains.map((a: any) => a.pubkey); 85 | } 86 | 87 | /** 88 | * retrieves owner of a specific Name Account from domain.tld. 89 | * 90 | * @param domainTld full string of domain and tld e.g. "miester.poor" 91 | */ 92 | async getOwnerFromDomainTld( 93 | domainTld: string, 94 | ): Promise { 95 | const domainTldSplit = domainTld.split('.'); 96 | const domain = domainTldSplit[0]; 97 | const tldName = '.' + domainTldSplit[1]; 98 | 99 | const nameOriginTldKey = await getOriginNameAccountKey(); 100 | const parentHashedName = await getHashedName(tldName); 101 | const [parentAccountKey] = getNameAccountKeyWithBump( 102 | parentHashedName, 103 | undefined, 104 | nameOriginTldKey, 105 | ); 106 | 107 | const domainHashedName = await getHashedName(domain); 108 | const [domainAccountKey] = getNameAccountKeyWithBump( 109 | domainHashedName, 110 | undefined, 111 | parentAccountKey, 112 | ); 113 | 114 | const [tldHouse] = findTldHouse(tldName); 115 | 116 | const nameOwner = await getNameOwner( 117 | this.connection, 118 | domainAccountKey, 119 | tldHouse, 120 | ); 121 | return nameOwner; 122 | } 123 | 124 | /** 125 | * retrieves domainTld data a domain from domain.tld. 126 | * 127 | * @param domainTld full string of domain and tld e.g. "miester.poor" 128 | */ 129 | async getNameRecordFromDomainTld( 130 | domainTld: string, 131 | ): Promise { 132 | const domainTldSplit = domainTld.split('.'); 133 | const domain = domainTldSplit[0]; 134 | const tldName = '.' + domainTldSplit[1]; 135 | 136 | const nameOriginTldKey = await getOriginNameAccountKey(); 137 | const parentHashedName = await getHashedName(tldName); 138 | const [parentAccountKey] = getNameAccountKeyWithBump( 139 | parentHashedName, 140 | undefined, 141 | nameOriginTldKey, 142 | ); 143 | 144 | const domainHashedName = await getHashedName(domain); 145 | const [domainAccountKey] = getNameAccountKeyWithBump( 146 | domainHashedName, 147 | undefined, 148 | parentAccountKey, 149 | ); 150 | const nameRecord = await NameRecordHeader.fromAccountAddress( 151 | this.connection, 152 | domainAccountKey, 153 | ); 154 | return nameRecord; 155 | } 156 | 157 | /** 158 | * retrieves tld from parent name via TldHouse account. 159 | * 160 | * @param parentAccount parent publickey or string 161 | */ 162 | async getTldFromParentAccount( 163 | parentAccount: PublicKey | string, 164 | ): Promise { 165 | if (typeof parentAccount == 'string') { 166 | parentAccount = new PublicKey(parentAccount); 167 | } 168 | const parentNameAccount = await NameRecordHeader.fromAccountAddress( 169 | this.connection, 170 | parentAccount, 171 | ); 172 | 173 | const tldHouseData = await this.connection.getAccountInfo( 174 | parentNameAccount?.owner!, 175 | ); 176 | const tldStart = 8 + 32 + 32 + 32; 177 | const tldBuffer = tldHouseData?.data?.subarray(tldStart); 178 | const nameLength = new BN(tldBuffer?.subarray(0, 4), 'le').toNumber(); 179 | const tld = tldBuffer 180 | .subarray(4, 4 + nameLength) 181 | .toString() 182 | .replace(/\0.*$/g, ''); 183 | return tld; 184 | } 185 | 186 | /** 187 | * retrieves domain from name account via tldParent account. 188 | * 189 | * @param nameAccount name publickey or string 190 | * @param parentAccountOwner parent Owner or string (TldHouse) 191 | */ 192 | async reverseLookupNameAccount( 193 | nameAccount: PublicKey | string, 194 | parentAccountOwner: PublicKey | string, 195 | ): Promise { 196 | if (typeof nameAccount == 'string') { 197 | nameAccount = new PublicKey(nameAccount); 198 | } 199 | if (typeof parentAccountOwner == 'string') { 200 | parentAccountOwner = new PublicKey(parentAccountOwner); 201 | } 202 | 203 | const reverseLookupHashedName = await getHashedName( 204 | nameAccount.toString(), 205 | ); 206 | const [reverseLookupAccount] = getNameAccountKeyWithBump( 207 | reverseLookupHashedName, 208 | parentAccountOwner, 209 | undefined, 210 | ); 211 | 212 | const reverseLookUpResult = await NameRecordHeader.fromAccountAddress( 213 | this.connection, 214 | reverseLookupAccount, 215 | ); 216 | const domain = reverseLookUpResult?.data?.toString(); 217 | return domain; 218 | } 219 | 220 | /** 221 | * retrieves main domain name account and its domain tld from user address. 222 | * 223 | * @param userAddress user publickey or string 224 | */ 225 | async getMainDomain(userAddress: PublicKey | string): Promise { 226 | if (typeof userAddress == 'string') { 227 | userAddress = new PublicKey(userAddress); 228 | } 229 | 230 | const [mainDomainAddress] = findMainDomain(userAddress); 231 | const mainDomain = await MainDomain.fromAccountAddress( 232 | this.connection, 233 | mainDomainAddress, 234 | ); 235 | return mainDomain; 236 | } 237 | 238 | /** 239 | * retrieves all parsed domains as strings with name accounts in an array for user in a specific TLD. 240 | * in alphabetical order 241 | * 242 | * @param userAccount user publickey or string 243 | * @param tld tld to be retrieved from 244 | */ 245 | async getParsedAllUserDomainsFromTldUnwrapped( 246 | userAccount: PublicKey | string, 247 | tld: string, 248 | concurrency: number = 10, 249 | ): Promise { 250 | const tldName = '.' + tld; 251 | 252 | const nameOriginTldKey = await getOriginNameAccountKey(); 253 | const parentHashedName = await getHashedName(tldName); 254 | const [tldHouse] = findTldHouse(tldName); 255 | const [parentAccountKey] = getNameAccountKeyWithBump( 256 | parentHashedName, 257 | undefined, 258 | nameOriginTldKey, 259 | ); 260 | if (typeof userAccount == 'string') { 261 | userAccount = new PublicKey(userAccount); 262 | } 263 | const allDomains = await findOwnedNameAccountsForUser( 264 | this.connection, 265 | userAccount, 266 | parentAccountKey, 267 | ); 268 | const allDomainsPubkeys = allDomains.map((a: any) => a.pubkey); 269 | const batches: PublicKey[][] = []; 270 | for ( 271 | let i = 0; 272 | i < allDomainsPubkeys.length; 273 | i += MULTIPLE_ACCOUNT_INFO_MAX 274 | ) { 275 | const end = Math.min( 276 | i + MULTIPLE_ACCOUNT_INFO_MAX, 277 | allDomainsPubkeys.length, 278 | ); 279 | batches.push(allDomainsPubkeys.slice(i, end)); 280 | } 281 | 282 | const results = await async.mapLimit( 283 | batches, 284 | concurrency, 285 | async (batch: PublicKey[]) => { 286 | const batchReverseLookup = await performReverseLookupBatched( 287 | this.connection, 288 | batch, 289 | tldHouse, 290 | ); 291 | const domainsWithTldsAndNameAccounts = batchReverseLookup.map( 292 | (domain, index) => ({ 293 | nameAccount: batch[index], 294 | domain: domain + tldName, 295 | }), 296 | ); 297 | if (domainsWithTldsAndNameAccounts.length > 0) { 298 | domainsWithTldsAndNameAccounts.sort((a, b) => 299 | a.domain.localeCompare(b.domain, undefined, { 300 | numeric: true, 301 | sensitivity: 'base', 302 | }), 303 | ); 304 | } 305 | return domainsWithTldsAndNameAccounts; 306 | }, 307 | ); 308 | 309 | const parsedNameAccountsAndDomains = results.flat(); 310 | return parsedNameAccountsAndDomains; 311 | } 312 | 313 | /** 314 | * retrieves all parsed domains and name accounts including NFTs in an array for any user in a specific TLD. 315 | * in alphabetical order 316 | * 317 | * @param userAccount user publickey or string 318 | * @param tld tld to be retrieved from 319 | */ 320 | async getParsedAllUserDomainsFromTld( 321 | userAccount: PublicKey | string, 322 | tld: string, 323 | concurrency: number = 5, 324 | ): Promise { 325 | const tldName = '.' + tld; 326 | 327 | const nameOriginTldKey = await getOriginNameAccountKey(); 328 | const parentHashedName = await getHashedName(tldName); 329 | const [tldHouse] = findTldHouse(tldName); 330 | const [parentAccountKey] = getNameAccountKeyWithBump( 331 | parentHashedName, 332 | undefined, 333 | nameOriginTldKey, 334 | ); 335 | if (typeof userAccount == 'string') { 336 | userAccount = new PublicKey(userAccount); 337 | } 338 | const allDomains = await findOwnedNameAccountsForUser( 339 | this.connection, 340 | userAccount, 341 | parentAccountKey, 342 | ); 343 | const allDomainsPubkeys = allDomains.map((a: any) => a.pubkey); 344 | let parsedNameAccountsAndDomains: NameAccountAndDomain[] = []; 345 | 346 | const allNFTDomains = await getParsedAllDomainsNftAccountsByOwner( 347 | userAccount, 348 | this.connection, 349 | findNameHouse(tldHouse)[0], 350 | ); 351 | const nftDomainsWithTlds = allNFTDomains.map(domain => { 352 | return domain + tldName; 353 | }); 354 | const domainsWithTldsAndNameAccounts = await Promise.all( 355 | nftDomainsWithTlds.map(async domain => { 356 | return { 357 | nameAccount: (await getDomainKey(domain)).pubkey, 358 | domain, 359 | }; 360 | }), 361 | ); 362 | 363 | parsedNameAccountsAndDomains.push(...domainsWithTldsAndNameAccounts); 364 | 365 | const batches: PublicKey[][] = []; 366 | for ( 367 | let i = 0; 368 | i < allDomainsPubkeys.length; 369 | i += MULTIPLE_ACCOUNT_INFO_MAX 370 | ) { 371 | const end = Math.min( 372 | i + MULTIPLE_ACCOUNT_INFO_MAX, 373 | allDomainsPubkeys.length, 374 | ); 375 | batches.push(allDomainsPubkeys.slice(i, end)); 376 | } 377 | const batchResults = await async.mapLimit( 378 | batches, 379 | concurrency, 380 | async (batch: PublicKey[]) => { 381 | const batchReverseLookup = await performReverseLookupBatched( 382 | this.connection, 383 | batch, 384 | tldHouse, 385 | ); 386 | return batchReverseLookup.map((domain, index) => ({ 387 | nameAccount: batch[index], 388 | domain: domain + tldName, 389 | })); 390 | }, 391 | ); 392 | parsedNameAccountsAndDomains.push(...batchResults.flat()); 393 | if (parsedNameAccountsAndDomains.length > 0) { 394 | parsedNameAccountsAndDomains.sort((a, b) => 395 | a.domain.localeCompare(b.domain, undefined, { 396 | numeric: true, 397 | sensitivity: 'base', 398 | }), 399 | ); 400 | } 401 | return parsedNameAccountsAndDomains; 402 | } 403 | 404 | /** 405 | * retrieves all parsed domains and name accounts for user. 406 | * in alphabetical order 407 | * 408 | * @param userAccount user publickey or string 409 | * @param tld tld to be retrieved from 410 | */ 411 | async getParsedAllUserDomainsUnwrapped( 412 | userAccount: PublicKey | string, 413 | concurrency: number = 10, 414 | ): Promise { 415 | const allTlds = await getAllTld(this.connection); 416 | let parsedNameAccountsAndDomains: NameAccountAndDomain[] = []; 417 | 418 | if (typeof userAccount == 'string') { 419 | userAccount = new PublicKey(userAccount); 420 | } 421 | const allDomainsRaw = await findOwnedNameAccountsForUser( 422 | this.connection, 423 | userAccount, 424 | undefined, 425 | ); 426 | const tldResults = await async.mapLimit( 427 | allTlds, 428 | concurrency, 429 | async ({ parentAccount, tld }) => { 430 | let tldName = tld.toString(); 431 | const [tldHouse] = findTldHouse(tldName); 432 | const allDomains = allDomainsRaw.filter( 433 | a => 434 | a.data.parentName.toString() === 435 | parentAccount.toString(), 436 | ); 437 | const allDomainsPubkeys = allDomains.map((a: any) => a.pubkey); 438 | 439 | const batches: PublicKey[][] = []; 440 | for ( 441 | let i = 0; 442 | i < allDomainsPubkeys.length; 443 | i += MULTIPLE_ACCOUNT_INFO_MAX 444 | ) { 445 | const end = Math.min( 446 | i + MULTIPLE_ACCOUNT_INFO_MAX, 447 | allDomainsPubkeys.length, 448 | ); 449 | batches.push(allDomainsPubkeys.slice(i, end)); 450 | } 451 | // Sequentially process batches for each TLD 452 | let tldDomains: NameAccountAndDomain[] = []; 453 | for (const batch of batches) { 454 | const batchReverseLookup = 455 | await performReverseLookupBatched( 456 | this.connection, 457 | batch, 458 | tldHouse, 459 | ); 460 | const domainsWithTldsAndNameAccounts = 461 | batchReverseLookup.map((domain, index) => ({ 462 | nameAccount: batch[index], 463 | domain: domain + tldName, 464 | })); 465 | tldDomains.push(...domainsWithTldsAndNameAccounts); 466 | } 467 | return tldDomains; 468 | }, 469 | ); 470 | parsedNameAccountsAndDomains = tldResults.flat(); 471 | if (parsedNameAccountsAndDomains.length > 0) { 472 | parsedNameAccountsAndDomains.sort((a, b) => 473 | a.domain.localeCompare(b.domain, undefined, { 474 | numeric: true, 475 | sensitivity: 'base', 476 | }), 477 | ); 478 | } 479 | return parsedNameAccountsAndDomains; 480 | } 481 | 482 | /** 483 | * retrieves all parsed domains and name accounts including NFTs for user. 484 | * in alphabetical order 485 | * 486 | * @param userAccount user publickey or string 487 | * @param tld tld to be retrieved from 488 | */ 489 | async getParsedAllUserDomains( 490 | userAccount: PublicKey | string, 491 | concurrency: number = 10, 492 | ): Promise { 493 | const allTlds = await getAllTld(this.connection); 494 | let parsedNameAccountsAndDomains: NameAccountAndDomain[] = []; 495 | 496 | if (typeof userAccount == 'string') { 497 | userAccount = new PublicKey(userAccount); 498 | } 499 | const allDomainsRaw = await findOwnedNameAccountsForUser( 500 | this.connection, 501 | userAccount, 502 | undefined, 503 | ); 504 | const userNfts = await getUserNfts(userAccount, this.connection); 505 | const tldResults = await async.mapLimit( 506 | allTlds, 507 | concurrency, 508 | async ({ parentAccount, tld }) => { 509 | let tldName = tld.toString(); 510 | const [tldHouse] = findTldHouse(tldName); 511 | const allDomains = allDomainsRaw.filter( 512 | a => 513 | a.data.parentName.toString() === 514 | parentAccount.toString(), 515 | ); 516 | const allDomainsPubkeys = allDomains.map((a: any) => a.pubkey); 517 | const allNFTDomains = await getOwnedDomains( 518 | userNfts, 519 | this.connection, 520 | findNameHouse(tldHouse)[0], 521 | ); 522 | 523 | const nftDomainsWithTlds = allNFTDomains.map( 524 | domain => domain + tldName, 525 | ); 526 | const domainsWithTldsAndNameAccounts = await Promise.all( 527 | nftDomainsWithTlds.map(async domain => { 528 | return { 529 | nameAccount: (await getDomainKey(domain)).pubkey, 530 | domain, 531 | }; 532 | }), 533 | ); 534 | 535 | let tldDomains: NameAccountAndDomain[] = []; 536 | tldDomains.push(...domainsWithTldsAndNameAccounts); 537 | 538 | const batches: PublicKey[][] = []; 539 | for ( 540 | let i = 0; 541 | i < allDomainsPubkeys.length; 542 | i += MULTIPLE_ACCOUNT_INFO_MAX 543 | ) { 544 | const end = Math.min( 545 | i + MULTIPLE_ACCOUNT_INFO_MAX, 546 | allDomainsPubkeys.length, 547 | ); 548 | batches.push(allDomainsPubkeys.slice(i, end)); 549 | } 550 | // Sequentially process batches for each TLD 551 | for (const batch of batches) { 552 | const batchReverseLookup = 553 | await performReverseLookupBatched( 554 | this.connection, 555 | batch, 556 | tldHouse, 557 | ); 558 | const domainsWithTldsAndNameAccounts = 559 | batchReverseLookup.map((domain, index) => ({ 560 | nameAccount: batch[index], 561 | domain: domain + tldName, 562 | })); 563 | tldDomains.push(...domainsWithTldsAndNameAccounts); 564 | } 565 | return tldDomains; 566 | }, 567 | ); 568 | parsedNameAccountsAndDomains = tldResults.flat(); 569 | if (parsedNameAccountsAndDomains.length > 0) { 570 | parsedNameAccountsAndDomains.sort((a, b) => 571 | a.domain.localeCompare(b.domain, undefined, { 572 | numeric: true, 573 | sensitivity: 'base', 574 | }), 575 | ); 576 | } 577 | return parsedNameAccountsAndDomains; 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /src/svm/state/main-domain.ts: -------------------------------------------------------------------------------- 1 | import * as web3 from '@solana/web3.js'; 2 | import * as beetSolana from '@metaplex-foundation/beet-solana'; 3 | import * as beet from '@metaplex-foundation/beet'; 4 | 5 | /** 6 | * Arguments used to create {@link MainDomain} 7 | * @category Accounts 8 | * @category generated 9 | */ 10 | export type MainDomainArgs = { 11 | nameAccount: web3.PublicKey; 12 | tld: string; 13 | domain: string; 14 | }; 15 | 16 | export const mainDomainDiscriminator = [109, 239, 227, 199, 98, 226, 66, 175]; 17 | /** 18 | * Holds the data for the {@link MainDomain} Account and provides de/serialization 19 | * functionality for that data 20 | * 21 | * @category Accounts 22 | * @category generated 23 | */ 24 | export class MainDomain implements MainDomainArgs { 25 | private constructor( 26 | readonly nameAccount: web3.PublicKey, 27 | readonly tld: string, 28 | readonly domain: string, 29 | ) {} 30 | 31 | /** 32 | * Creates a {@link MainDomain} instance from the provided args. 33 | */ 34 | static fromArgs(args: MainDomainArgs) { 35 | return new MainDomain(args.nameAccount, args.tld, args.domain); 36 | } 37 | 38 | /** 39 | * Deserializes the {@link MainDomain} from the data of the provided {@link web3.AccountInfo}. 40 | * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. 41 | */ 42 | static fromAccountInfo( 43 | accountInfo: web3.AccountInfo, 44 | offset = 0, 45 | ): [MainDomain, number] { 46 | return MainDomain.deserialize(accountInfo.data, offset); 47 | } 48 | 49 | /** 50 | * Retrieves the account info from the provided address and deserializes 51 | * the {@link MainDomain} from its data. 52 | * 53 | * @throws Error if no account info is found at the address or if deserialization fails 54 | */ 55 | static async fromAccountAddress( 56 | connection: web3.Connection, 57 | address: web3.PublicKey, 58 | commitmentOrConfig?: web3.Commitment | web3.GetAccountInfoConfig, 59 | ): Promise { 60 | const accountInfo = await connection.getAccountInfo( 61 | address, 62 | commitmentOrConfig, 63 | ); 64 | if (accountInfo == null) { 65 | throw new Error(`Unable to find MainDomain account at ${address}`); 66 | } 67 | return MainDomain.fromAccountInfo(accountInfo, 0)[0]; 68 | } 69 | 70 | /** 71 | * Provides a {@link web3.Connection.getProgramAccounts} config builder, 72 | * to fetch accounts matching filters that can be specified via that builder. 73 | * 74 | * @param programId - the program that owns the accounts we are filtering 75 | */ 76 | static gpaBuilder( 77 | programId: web3.PublicKey = new web3.PublicKey( 78 | 'TLDHkysf5pCnKsVA4gXpNvmy7psXLPEu4LAdDJthT9S', 79 | ), 80 | ) { 81 | return beetSolana.GpaBuilder.fromStruct(programId, mainDomainBeet); 82 | } 83 | 84 | /** 85 | * Deserializes the {@link MainDomain} from the provided data Buffer. 86 | * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. 87 | */ 88 | static deserialize(buf: Buffer, offset = 0): [MainDomain, number] { 89 | return mainDomainBeet.deserialize(buf, offset); 90 | } 91 | 92 | /** 93 | * Serializes the {@link MainDomain} into a Buffer. 94 | * @returns a tuple of the created Buffer and the offset up to which the buffer was written to store it. 95 | */ 96 | serialize(): [Buffer, number] { 97 | return mainDomainBeet.serialize({ 98 | accountDiscriminator: mainDomainDiscriminator, 99 | ...this, 100 | }); 101 | } 102 | 103 | /** 104 | * Returns the byteSize of a {@link Buffer} holding the serialized data of 105 | * {@link MainDomain} for the provided args. 106 | * 107 | * @param args need to be provided since the byte size for this account 108 | * depends on them 109 | */ 110 | static byteSize(args: MainDomainArgs) { 111 | const instance = MainDomain.fromArgs(args); 112 | return mainDomainBeet.toFixedFromValue({ 113 | accountDiscriminator: mainDomainDiscriminator, 114 | ...instance, 115 | }).byteSize; 116 | } 117 | 118 | /** 119 | * Fetches the minimum balance needed to exempt an account holding 120 | * {@link MainDomain} data from rent 121 | * 122 | * @param args need to be provided since the byte size for this account 123 | * depends on them 124 | * @param connection used to retrieve the rent exemption information 125 | */ 126 | static async getMinimumBalanceForRentExemption( 127 | args: MainDomainArgs, 128 | connection: web3.Connection, 129 | commitment?: web3.Commitment, 130 | ): Promise { 131 | return await connection.getMinimumBalanceForRentExemption( 132 | MainDomain.byteSize(args), 133 | commitment, 134 | ); 135 | } 136 | 137 | /** 138 | * Returns a readable version of {@link MainDomain} properties 139 | * and can be used to convert to JSON and/or logging 140 | */ 141 | pretty() { 142 | return { 143 | nameAccount: this.nameAccount.toBase58(), 144 | tld: this.tld, 145 | domain: this.domain, 146 | }; 147 | } 148 | } 149 | 150 | /** 151 | * @category Accounts 152 | * @category generated 153 | */ 154 | export const mainDomainBeet = new beet.FixableBeetStruct< 155 | MainDomain, 156 | MainDomainArgs & { 157 | accountDiscriminator: number[] /* size: 8 */; 158 | } 159 | >( 160 | [ 161 | ['accountDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], 162 | ['nameAccount', beetSolana.publicKey], 163 | ['tld', beet.utf8String], 164 | ['domain', beet.utf8String], 165 | ], 166 | MainDomain.fromArgs, 167 | 'MainDomain', 168 | ); 169 | -------------------------------------------------------------------------------- /src/svm/state/name-record-header.ts: -------------------------------------------------------------------------------- 1 | import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'; 2 | import { deserialize, Schema } from 'borsh'; 3 | import { ROOT_ANS_PUBLIC_KEY } from '../constants'; 4 | 5 | /** 6 | * Holds the data for the {@link NameRecordHeader} Account and provides de/serialization 7 | * functionality for that data 8 | */ 9 | export class NameRecordHeader { 10 | // only for normal domains, tld name record might not be working. 11 | static async create( 12 | obj: { 13 | parentName: Uint8Array; 14 | owner: Uint8Array; 15 | nclass: Uint8Array; 16 | expiresAt: Uint8Array; 17 | createdAt: Uint8Array; 18 | nonTransferable: Uint8Array; 19 | }, 20 | connection: Connection, 21 | parentNameRecord?: NameRecordHeader, 22 | ): Promise { 23 | const instance = new NameRecordHeader(obj); 24 | if (!parentNameRecord) { 25 | await instance.initializeParentNameRecordHeader(connection); 26 | } else { 27 | instance.updateGracePeriod(parentNameRecord); 28 | } 29 | return instance; 30 | } 31 | 32 | async initializeParentNameRecordHeader( 33 | connection: Connection, 34 | ): Promise { 35 | if (this.parentName.toString() === PublicKey.default.toString()) { 36 | this.isValid = true; 37 | return; 38 | } 39 | const parentNameRecordHeader = await NameRecordHeader.fromAccountAddress( 40 | connection, 41 | this.parentName, 42 | ); 43 | this.updateGracePeriod(parentNameRecordHeader); 44 | } 45 | 46 | updateGracePeriod(parentNameRecord: NameRecordHeader | undefined): void { 47 | const currentTime = Date.now(); 48 | const defaultGracePeriod = 50 * 24 * 60 * 60 * 1000; 49 | const gracePeriod = 50 | parentNameRecord?.expiresAt.getTime() || defaultGracePeriod; 51 | 52 | this.isValid = 53 | this.expiresAt.getTime() === 0 || 54 | this.expiresAt.getTime() + gracePeriod > currentTime; 55 | 56 | if (!this.isValid) { 57 | this.owner = undefined; 58 | } 59 | } 60 | 61 | constructor(obj: { 62 | parentName: Uint8Array; 63 | owner: Uint8Array; 64 | nclass: Uint8Array; 65 | expiresAt: Uint8Array; 66 | createdAt: Uint8Array; 67 | nonTransferable: Uint8Array; 68 | }) { 69 | this.parentName = new PublicKey(obj.parentName); 70 | this.nclass = new PublicKey(obj.nclass); 71 | 72 | // Convert expiresAt bytes to number using DataView 73 | const expiresAtArrayBuffer = new ArrayBuffer(obj.expiresAt.length); 74 | const expiresAtViewUint8Array = new Uint8Array(expiresAtArrayBuffer); 75 | expiresAtViewUint8Array.set(obj.expiresAt); 76 | const expiresAtView = new DataView(expiresAtArrayBuffer); 77 | const expiresAtTimestamp = Number(expiresAtView.getBigUint64(0, true)); 78 | this.expiresAt = new Date(expiresAtTimestamp * 1000); 79 | 80 | // Convert createdAt bytes to number using DataView 81 | const createdAtArrayBuffer = new ArrayBuffer(obj.createdAt.length); 82 | const createdAtViewUint8Array = new Uint8Array(createdAtArrayBuffer); 83 | createdAtViewUint8Array.set(obj.createdAt); 84 | const createdAtView = new DataView(createdAtArrayBuffer); 85 | const createdAtTimestamp = Number(createdAtView.getBigUint64(0, true)); 86 | this.createdAt = new Date(createdAtTimestamp * 1000); 87 | 88 | this.nonTransferable = obj.nonTransferable[0] === 1; 89 | 90 | // grace period = 45 days * 24 hours * 60 minutes * 60 seconds * 1000 millie seconds 91 | const gracePeriod = 45 * 24 * 60 * 60 * 1000; 92 | this.isValid = 93 | expiresAtTimestamp === 0 94 | ? true 95 | : this.expiresAt > new Date(Date.now() + gracePeriod); 96 | 97 | this.owner = this.isValid ? new PublicKey(obj.owner) : undefined; 98 | } 99 | 100 | parentName: PublicKey; 101 | owner: PublicKey | undefined; 102 | nclass: PublicKey; 103 | expiresAt: Date; 104 | createdAt: Date; 105 | nonTransferable: boolean; 106 | isValid: boolean; 107 | data: Buffer | undefined; 108 | 109 | static DISCRIMINATOR = [68, 72, 88, 44, 15, 167, 103, 243]; 110 | static HASH_PREFIX = 'ALT Name Service'; 111 | 112 | /** 113 | * NameRecordHeader Schema across all alt name service accounts 114 | */ 115 | static schema: Schema = { 116 | struct: { 117 | discriminator: { array: { type: "u8", len: 8 } }, 118 | parentName: { array: { type: "u8", len: 32 } }, 119 | owner: { array: { type: "u8", len: 32 } }, 120 | nclass: { array: { type: "u8", len: 32 } }, 121 | expiresAt: { array: { type: "u8", len: 8 } }, 122 | createdAt: { array: { type: "u8", len: 8 } }, 123 | nonTransferable: { array: { type: "u8", len: 1 } }, 124 | padding: { array: { type: "u8", len: 79 } }, 125 | }, 126 | }; 127 | 128 | /** 129 | * Returns the minimum size of a {@link Buffer} holding the serialized data of 130 | * {@link NameRecordHeader} 131 | */ 132 | static get byteSize() { 133 | return 8 + 32 + 32 + 32 + 8 + 8 + 1 + 79; 134 | } 135 | 136 | /** 137 | * Retrieves the account info from the provided address and deserializes 138 | * the {@link NameRecordHeader} from its data. 139 | */ 140 | public static async fromAccountAddress( 141 | connection: Connection, 142 | nameAccountKey: PublicKey, 143 | ): Promise { 144 | const nameAccount = await connection.getAccountInfo( 145 | nameAccountKey, 146 | 'confirmed', 147 | ); 148 | if (!nameAccount) { 149 | return undefined; 150 | } 151 | 152 | const decodedData = deserialize( 153 | this.schema, 154 | Uint8Array.from(nameAccount.data), 155 | ) as { 156 | discriminator: number[]; 157 | parentName: Uint8Array; 158 | owner: Uint8Array; 159 | nclass: Uint8Array; 160 | expiresAt: Uint8Array; 161 | createdAt: Uint8Array; 162 | nonTransferable: Uint8Array; 163 | }; 164 | const res = new NameRecordHeader(decodedData); 165 | res.data = nameAccount.data?.subarray(this.byteSize); 166 | 167 | if (res.parentName.toString() !== ROOT_ANS_PUBLIC_KEY.toString()) { 168 | await res.initializeParentNameRecordHeader(connection); 169 | } else { 170 | res.isValid = true; 171 | } 172 | 173 | return res; 174 | } 175 | 176 | /** 177 | * Retrieves the account infos from the multiple name accounts 178 | * the {@link NameRecordHeader} from its data. 179 | */ 180 | public static async fromMultipileAccountAddresses( 181 | connection: Connection, 182 | nameAccountKey: PublicKey[], 183 | ): Promise { 184 | let nameRecordAccountInfos = 185 | await connection.getMultipleAccountsInfo(nameAccountKey); 186 | 187 | let nameRecords: NameRecordHeader[] = []; 188 | 189 | nameRecordAccountInfos.forEach(value => { 190 | if (!value) { 191 | nameRecords.push(undefined); 192 | return; 193 | } 194 | let nameRecordData = this.fromAccountInfo(value); 195 | if (!nameRecordData) { 196 | nameRecords.push(undefined); 197 | return; 198 | } 199 | nameRecords.push(nameRecordData); 200 | }); 201 | 202 | return nameRecords; 203 | } 204 | 205 | /** 206 | * Retrieves the account info from the provided data and deserializes 207 | * the {@link NameRecordHeader} from its data. 208 | */ 209 | public static fromAccountInfo( 210 | nameAccountAccountInfo: AccountInfo, 211 | ): NameRecordHeader { 212 | 213 | const decodedData = deserialize( 214 | this.schema, 215 | Uint8Array.from(nameAccountAccountInfo.data), 216 | ) as { 217 | discriminator: number[]; 218 | parentName: Uint8Array; 219 | owner: Uint8Array; 220 | nclass: Uint8Array; 221 | expiresAt: Uint8Array; 222 | createdAt: Uint8Array; 223 | nonTransferable: Uint8Array; 224 | }; 225 | 226 | const res = new NameRecordHeader(decodedData); 227 | res.data = nameAccountAccountInfo.data?.subarray(this.byteSize); 228 | return res; 229 | } 230 | 231 | /** 232 | * Returns a readable version of {@link NameRecordHeader} properties 233 | * and can be used to convert to JSON and/or logging 234 | */ 235 | pretty() { 236 | const indexOf0 = this.data.indexOf(0x00); 237 | return { 238 | parentName: this.parentName.toBase58(), 239 | owner: this.owner?.toBase58(), 240 | nclass: this.nclass.toBase58(), 241 | expiresAt: this.expiresAt, 242 | createdAt: this.createdAt, 243 | nonTransferable: this.nonTransferable, 244 | isValid: this.isValid, 245 | data: this.isValid 246 | ? this.data.subarray(0, indexOf0).toString() 247 | : undefined, 248 | }; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/svm/state/nft-record.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was GENERATED using the solita package. 3 | * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. 4 | * 5 | * See: https://github.com/metaplex-foundation/solita 6 | */ 7 | 8 | import * as beet from '@metaplex-foundation/beet'; 9 | import * as beetSolana from '@metaplex-foundation/beet-solana'; 10 | import * as web3 from '@solana/web3.js'; 11 | 12 | import { Tag, tagBeet } from '../types/tag'; 13 | 14 | /** 15 | * Arguments used to create {@link NftRecord} 16 | * @category Accounts 17 | * @category generated 18 | */ 19 | export type NftRecordArgs = { 20 | tag: Tag; 21 | bump: number; 22 | nameAccount: web3.PublicKey; 23 | owner: web3.PublicKey; 24 | nftMintAccount: web3.PublicKey; 25 | tldHouse: web3.PublicKey; 26 | }; 27 | 28 | export const nftRecordDiscriminator = [174, 190, 114, 100, 177, 14, 90, 254]; 29 | /** 30 | * Holds the data for the {@link NftRecord} Account and provides de/serialization 31 | * functionality for that data 32 | * 33 | * @category Accounts 34 | * @category generated 35 | */ 36 | export class NftRecord implements NftRecordArgs { 37 | private constructor( 38 | readonly tag: Tag, 39 | readonly bump: number, 40 | readonly nameAccount: web3.PublicKey, 41 | readonly owner: web3.PublicKey, 42 | readonly nftMintAccount: web3.PublicKey, 43 | readonly tldHouse: web3.PublicKey, 44 | ) {} 45 | 46 | /** 47 | * Creates a {@link NftRecord} instance from the provided args. 48 | */ 49 | static fromArgs(args: NftRecordArgs) { 50 | return new NftRecord( 51 | args.tag, 52 | args.bump, 53 | args.nameAccount, 54 | args.owner, 55 | args.nftMintAccount, 56 | args.tldHouse, 57 | ); 58 | } 59 | 60 | /** 61 | * Deserializes the {@link NftRecord} from the data of the provided {@link web3.AccountInfo}. 62 | * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. 63 | */ 64 | static fromAccountInfo( 65 | accountInfo: web3.AccountInfo, 66 | offset = 0, 67 | ): [NftRecord, number] { 68 | return NftRecord.deserialize(accountInfo.data, offset); 69 | } 70 | 71 | /** 72 | * Retrieves the account info from the provided address and deserializes 73 | * the {@link NftRecord} from its data. 74 | * 75 | * @throws Error if no account info is found at the address or if deserialization fails 76 | */ 77 | static async fromAccountAddress( 78 | connection: web3.Connection, 79 | address: web3.PublicKey, 80 | ): Promise { 81 | const accountInfo = await connection.getAccountInfo(address); 82 | if (accountInfo == null) { 83 | throw new Error(`Unable to find NftRecord account at ${address}`); 84 | } 85 | return NftRecord.fromAccountInfo(accountInfo, 0)[0]; 86 | } 87 | 88 | /** 89 | * Deserializes the {@link NftRecord} from the provided data Buffer. 90 | * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. 91 | */ 92 | static deserialize(buf: Buffer, offset = 0): [NftRecord, number] { 93 | return nftRecordBeet.deserialize(buf, offset); 94 | } 95 | 96 | /** 97 | * Serializes the {@link NftRecord} into a Buffer. 98 | * @returns a tuple of the created Buffer and the offset up to which the buffer was written to store it. 99 | */ 100 | serialize(): [Buffer, number] { 101 | return nftRecordBeet.serialize({ 102 | accountDiscriminator: nftRecordDiscriminator, 103 | ...this, 104 | }); 105 | } 106 | 107 | /** 108 | * Returns the byteSize of a {@link Buffer} holding the serialized data of 109 | * {@link NftRecord} 110 | */ 111 | static get byteSize() { 112 | return nftRecordBeet.byteSize; 113 | } 114 | 115 | /** 116 | * Fetches the minimum balance needed to exempt an account holding 117 | * {@link NftRecord} data from rent 118 | * 119 | * @param connection used to retrieve the rent exemption information 120 | */ 121 | static async getMinimumBalanceForRentExemption( 122 | connection: web3.Connection, 123 | commitment?: web3.Commitment, 124 | ): Promise { 125 | return await connection.getMinimumBalanceForRentExemption( 126 | NftRecord.byteSize, 127 | commitment, 128 | ); 129 | } 130 | 131 | /** 132 | * Determines if the provided {@link Buffer} has the correct byte size to 133 | * hold {@link NftRecord} data. 134 | */ 135 | static hasCorrectByteSize(buf: Buffer, offset = 0) { 136 | return buf.byteLength - offset === NftRecord.byteSize; 137 | } 138 | 139 | /** 140 | * Returns a readable version of {@link NftRecord} properties 141 | * and can be used to convert to JSON and/or logging 142 | */ 143 | pretty() { 144 | return { 145 | tag: 'Tag.' + Tag[this.tag], 146 | bump: this.bump, 147 | nameAccount: this.nameAccount.toBase58(), 148 | owner: this.owner.toBase58(), 149 | nftMintAccount: this.nftMintAccount.toBase58(), 150 | tldHouse: this.tldHouse.toBase58(), 151 | }; 152 | } 153 | } 154 | 155 | /** 156 | * @category Accounts 157 | * @category generated 158 | */ 159 | export const nftRecordBeet = new beet.BeetStruct< 160 | NftRecord, 161 | NftRecordArgs & { 162 | accountDiscriminator: number[] /* size: 8 */; 163 | } 164 | >( 165 | [ 166 | ['accountDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], 167 | ['tag', tagBeet], 168 | ['bump', beet.u8], 169 | ['nameAccount', beetSolana.publicKey], 170 | ['owner', beetSolana.publicKey], 171 | ['nftMintAccount', beetSolana.publicKey], 172 | ['tldHouse', beetSolana.publicKey], 173 | ], 174 | NftRecord.fromArgs, 175 | 'NftRecord', 176 | ); 177 | -------------------------------------------------------------------------------- /src/svm/types/records.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List of ANS Records 3 | */ 4 | export enum Record { 5 | IPFS = 'IPFS', 6 | ARWV = 'ARWV', 7 | SOL = 'SOL', 8 | ETH = 'ETH', 9 | BTC = 'BTC', 10 | LTC = 'LTC', 11 | DOGE = 'DOGE', 12 | Email = 'email', 13 | Url = 'url', 14 | Discord = 'discord', 15 | Github = 'github', 16 | Reddit = 'reddit', 17 | Twitter = 'twitter', 18 | Telegram = 'telegram', 19 | Pic = 'pic', 20 | SHDW = 'SHDW', 21 | POINT = 'POINT', 22 | } 23 | -------------------------------------------------------------------------------- /src/svm/types/tag.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was GENERATED using the solita package. 3 | * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. 4 | * 5 | * See: https://github.com/metaplex-foundation/solita 6 | */ 7 | 8 | import * as beet from '@metaplex-foundation/beet'; 9 | /** 10 | * @category enums 11 | * @category generated 12 | */ 13 | export enum Tag { 14 | Uninitialized, 15 | ActiveRecord, 16 | InactiveRecord, 17 | } 18 | 19 | /** 20 | * @category userTypes 21 | * @category generated 22 | */ 23 | export const tagBeet = beet.fixedScalarEnum(Tag) as beet.FixedSizeBeet< 24 | Tag, 25 | Tag 26 | >; 27 | -------------------------------------------------------------------------------- /src/svm/utils.ts: -------------------------------------------------------------------------------- 1 | import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'; 2 | import { BN } from 'bn.js'; 3 | import { sha256 } from '@ethersproject/sha2'; 4 | 5 | import { 6 | ANS_PROGRAM_ID, 7 | MAIN_DOMAIN_PREFIX, 8 | NAME_HOUSE_PREFIX, 9 | NAME_HOUSE_PROGRAM_ID, 10 | NFT_RECORD_PREFIX, 11 | ORIGIN_TLD, 12 | SPL_TOKEN_PROGRAM_ID, 13 | TLD_HOUSE_PREFIX, 14 | TLD_HOUSE_PROGRAM_ID, 15 | TOKEN_METADATA_PROGRAM_ID, 16 | } from './constants'; 17 | import { NameRecordHeader } from './state/name-record-header'; 18 | import { Tag } from './types/tag'; 19 | import { NftRecord } from './state/nft-record'; 20 | import { MainDomain } from './state/main-domain'; 21 | 22 | /** 23 | * retrieves raw name account 24 | * 25 | * @param hashedName hashed name of the name account 26 | * @param nameClass defaults to pubkey::default() 27 | * @param parentName defaults to pubkey::default() 28 | */ 29 | export function getNameAccountKeyWithBump( 30 | hashedName: Buffer, 31 | nameClass?: PublicKey, 32 | parentName?: PublicKey, 33 | ): [PublicKey, number] { 34 | const seeds = [ 35 | hashedName, 36 | nameClass ? nameClass.toBuffer() : Buffer.alloc(32), 37 | parentName ? parentName.toBuffer() : Buffer.alloc(32), 38 | ]; 39 | 40 | return PublicKey.findProgramAddressSync(seeds, ANS_PROGRAM_ID); 41 | } 42 | 43 | /** 44 | * retrieves owner of the name account 45 | * 46 | * @param connection sol connection 47 | * @param nameAccountKey nameAccount to get owner of. 48 | */ 49 | export async function getNameOwner( 50 | connection: Connection, 51 | nameAccountKey: PublicKey, 52 | tldHouse?: PublicKey, 53 | ): Promise { 54 | const nameAccount = await NameRecordHeader.fromAccountAddress( 55 | connection, 56 | nameAccountKey, 57 | ); 58 | const owner = nameAccount.owner; 59 | if (!nameAccount.isValid) return undefined; 60 | if (!tldHouse) return owner; 61 | const [nameHouse] = findNameHouse(tldHouse); 62 | const [nftRecord] = findNftRecord(nameAccountKey, nameHouse); 63 | if (owner?.toBase58() !== nftRecord.toBase58()) return owner; 64 | return await getMintOwner(connection, nftRecord); 65 | } 66 | 67 | /** 68 | * computes hashed name 69 | * 70 | * @param name any string or domain name. 71 | */ 72 | export async function getHashedName(name: string): Promise { 73 | const input = NameRecordHeader.HASH_PREFIX + name; 74 | const str = sha256(Buffer.from(input, 'utf8')).slice(2); 75 | return Buffer.from(str, 'hex'); 76 | } 77 | 78 | /** 79 | * A constant in tld house. 80 | * 81 | * get origin name account should always equal to 3mX9b4AZaQehNoQGfckVcmgmA6bkBoFcbLj9RMmMyNcU 82 | * 83 | * @param originTld 84 | */ 85 | export async function getOriginNameAccountKey( 86 | originTld: string = ORIGIN_TLD, 87 | ): Promise { 88 | const hashed_name = await getHashedName(originTld); 89 | const [nameAccountKey] = getNameAccountKeyWithBump( 90 | hashed_name, 91 | undefined, 92 | undefined, 93 | ); 94 | return nameAccountKey; 95 | } 96 | 97 | /** 98 | * finds list of all name accounts for a particular user. 99 | * 100 | * @param connection sol connection 101 | * @param userAccount user's public key 102 | * @param parentAccount nameAccount's parentName 103 | */ 104 | export async function findOwnedNameAccountsForUser( 105 | connection: Connection, 106 | userAccount: PublicKey, 107 | parentAccount: PublicKey | undefined, 108 | ) { 109 | const filters: any = [ 110 | { 111 | memcmp: { 112 | offset: 40, 113 | bytes: userAccount.toBase58(), 114 | encoding: 'base58', 115 | }, 116 | }, 117 | ]; 118 | 119 | if (parentAccount) { 120 | filters.push({ 121 | memcmp: { 122 | offset: 8, 123 | bytes: parentAccount.toBase58(), 124 | encoding: 'base58', 125 | }, 126 | }); 127 | } 128 | 129 | const accounts = await connection.getProgramAccounts(ANS_PROGRAM_ID, { 130 | filters: filters, 131 | }); 132 | 133 | return accounts.map(a => { 134 | return { 135 | pubkey: a.pubkey, 136 | data: NameRecordHeader.fromAccountInfo(a.account), 137 | }; 138 | }); 139 | } 140 | 141 | export function findMainDomain(user: PublicKey) { 142 | return PublicKey.findProgramAddressSync( 143 | [Buffer.from(MAIN_DOMAIN_PREFIX), user.toBuffer()], 144 | TLD_HOUSE_PROGRAM_ID, 145 | ); 146 | } 147 | 148 | /** 149 | * finds list of all tld house accounts live. 150 | * 151 | * @param connection sol connection 152 | */ 153 | export async function getAllTld(connection: Connection): Promise< 154 | Array<{ 155 | tld: String; 156 | parentAccount: PublicKey; 157 | }> 158 | > { 159 | const filters: any = [ 160 | { 161 | memcmp: { 162 | offset: 0, 163 | bytes: 'iQgos3SdaVE', 164 | }, 165 | }, 166 | ]; 167 | 168 | const accounts = await connection.getProgramAccounts(TLD_HOUSE_PROGRAM_ID, { 169 | filters: filters, 170 | }); 171 | 172 | const tldsAndParentAccounts: { 173 | tld: String; 174 | parentAccount: PublicKey; 175 | }[] = []; 176 | 177 | accounts.map(({ account }) => { 178 | const parentAccount = getParentAccountFromTldHouseAccountInfo(account); 179 | const tld = getTldFromTldHouseAccountInfo(account); 180 | tldsAndParentAccounts.push({ tld, parentAccount }); 181 | }); 182 | return tldsAndParentAccounts; 183 | } 184 | 185 | export function getTldFromTldHouseAccountInfo( 186 | tldHouseData: AccountInfo, 187 | ) { 188 | const tldStart = 8 + 32 + 32 + 32; 189 | const tldBuffer = tldHouseData?.data?.subarray(tldStart); 190 | const nameLength = new BN(tldBuffer?.subarray(0, 4), 'le').toNumber(); 191 | return tldBuffer 192 | .subarray(4, 4 + nameLength) 193 | .toString() 194 | .replace(/\0.*$/g, ''); 195 | } 196 | 197 | export function getParentAccountFromTldHouseAccountInfo( 198 | tldHouseData: AccountInfo, 199 | ) { 200 | const parentAccountStart = 8 + 32 + 32; 201 | const parentAccountBuffer = tldHouseData?.data?.subarray( 202 | parentAccountStart, 203 | parentAccountStart + 32, 204 | ); 205 | 206 | return new PublicKey(parentAccountBuffer); 207 | } 208 | 209 | /** 210 | * finds list of all domains in parent account from tld. 211 | * 212 | * @param connection sol connection 213 | * @param parentAccount nameAccount's parentName 214 | */ 215 | export async function findAllDomainsForTld( 216 | connection: Connection, 217 | parentAccount: PublicKey, 218 | ): Promise { 219 | const filters: any = [ 220 | { 221 | memcmp: { 222 | offset: 8, 223 | bytes: parentAccount.toBase58(), 224 | encoding: 'base58', 225 | }, 226 | }, 227 | ]; 228 | 229 | const accounts = await connection.getProgramAccounts(ANS_PROGRAM_ID, { 230 | filters: filters, 231 | }); 232 | return accounts.map((a: any) => a.pubkey); 233 | } 234 | 235 | export async function getMintOwner( 236 | connection: Connection, 237 | nftRecord: PublicKey, 238 | ) { 239 | try { 240 | const nftRecordData = await NftRecord.fromAccountAddress( 241 | connection, 242 | nftRecord, 243 | ); 244 | if (nftRecordData.tag !== Tag.ActiveRecord) return; 245 | const largestAccounts = await connection.getTokenLargestAccounts( 246 | nftRecordData.nftMintAccount, 247 | ); 248 | const largestAccountInfo = await connection.getParsedAccountInfo( 249 | largestAccounts.value[0].address, 250 | ); 251 | if (!largestAccountInfo.value.data) return; 252 | // @ts-ignore 253 | return new PublicKey(largestAccountInfo.value.data.parsed.info.owner); 254 | } catch { 255 | return undefined; 256 | } 257 | } 258 | 259 | export function findNftRecord( 260 | nameAccount: PublicKey, 261 | nameHouseAccount: PublicKey, 262 | ) { 263 | return PublicKey.findProgramAddressSync( 264 | [ 265 | Buffer.from(NFT_RECORD_PREFIX), 266 | nameHouseAccount.toBuffer(), 267 | nameAccount.toBuffer(), 268 | ], 269 | NAME_HOUSE_PROGRAM_ID, 270 | ); 271 | } 272 | 273 | export function findTldHouse(tldString: string) { 274 | tldString = tldString.toLowerCase(); 275 | return PublicKey.findProgramAddressSync( 276 | [Buffer.from(TLD_HOUSE_PREFIX), Buffer.from(tldString)], 277 | TLD_HOUSE_PROGRAM_ID, 278 | ); 279 | } 280 | 281 | export function findNameHouse(tldHouse: PublicKey) { 282 | return PublicKey.findProgramAddressSync( 283 | [Buffer.from(NAME_HOUSE_PREFIX), tldHouse.toBuffer()], 284 | NAME_HOUSE_PROGRAM_ID, 285 | ); 286 | } 287 | 288 | export const findMetadataAddress = (mint: PublicKey): PublicKey => { 289 | return PublicKey.findProgramAddressSync( 290 | [ 291 | Buffer.from('metadata'), 292 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 293 | mint.toBuffer(), 294 | ], 295 | TOKEN_METADATA_PROGRAM_ID, 296 | )[0]; 297 | }; 298 | 299 | export async function performReverseLookupBatched( 300 | connection: Connection, 301 | nameAccounts: PublicKey[], 302 | tldHouse: PublicKey, 303 | ): Promise<(string | undefined)[]> { 304 | const promises = nameAccounts.map(async nameAccount => { 305 | const reverseLookupHashedName = await getHashedName( 306 | nameAccount.toBase58(), 307 | ); 308 | const [reverseLookUpAccount] = getNameAccountKeyWithBump( 309 | reverseLookupHashedName, 310 | tldHouse, 311 | undefined, 312 | ); 313 | return reverseLookUpAccount; 314 | }); 315 | const reverseLookUpAccounts: PublicKey[] = await Promise.all(promises); 316 | const reverseLookupAccountInfos = await connection.getMultipleAccountsInfo( 317 | reverseLookUpAccounts, 318 | ); 319 | 320 | return reverseLookupAccountInfos.map(reverseLookupAccountInfo => { 321 | const domain = reverseLookupAccountInfo?.data 322 | .subarray( 323 | NameRecordHeader.byteSize, 324 | reverseLookupAccountInfo?.data.length, 325 | ) 326 | .toString(); 327 | return domain; 328 | }); 329 | } 330 | 331 | export function delay(ms: number): Promise { 332 | return new Promise(resolve => setTimeout(resolve, ms)); 333 | } 334 | 335 | export async function getUserNfts(owner: PublicKey, connection: Connection) { 336 | const { value: splAccounts } = 337 | await connection.getParsedTokenAccountsByOwner(owner, { 338 | programId: SPL_TOKEN_PROGRAM_ID, 339 | }); 340 | 341 | const nftAccounts = splAccounts 342 | .filter(t => { 343 | const amount = t.account?.data?.parsed?.info?.tokenAmount?.uiAmount; 344 | const decimals = 345 | t.account?.data?.parsed?.info?.tokenAmount?.decimals; 346 | return decimals === 0 && amount === 1; 347 | }) 348 | .map(t => { 349 | const address = t.account?.data?.parsed?.info?.mint; 350 | return address; 351 | }); 352 | return nftAccounts; 353 | } 354 | 355 | export const getParsedAllDomainsNftAccountsByOwner = async ( 356 | owner: PublicKey, 357 | connection: Connection, 358 | expectedCreator: PublicKey, 359 | ) => { 360 | const nftAccounts = await getUserNfts(owner, connection); 361 | const ownerDomains = await getOwnedDomains( 362 | nftAccounts, 363 | connection, 364 | expectedCreator, 365 | ); 366 | 367 | return ownerDomains; 368 | }; 369 | 370 | export const getOwnedDomains = async ( 371 | nftAddresses: string[], 372 | connection: Connection, 373 | expectedCreator: PublicKey, 374 | ) => { 375 | const ownedDomains: string[] = []; 376 | const verifiedCreatorByteOffset = 326; 377 | const verifiedCreatorVerfiiedByteOffset = 326; 378 | 379 | if (nftAddresses.length > 100) { 380 | while (nftAddresses.length > 0) { 381 | let nftMetadataKeys = nftAddresses 382 | .splice(0, 100) 383 | .map((mint: string) => 384 | findMetadataAddress(new PublicKey(mint)), 385 | ); 386 | const nftsMetadata = 387 | await connection.getMultipleAccountsInfo(nftMetadataKeys); 388 | for (const nftMetadata of nftsMetadata) { 389 | if (nftMetadata) { 390 | const verifiedCreatorAddress = new PublicKey( 391 | nftMetadata.data.subarray( 392 | verifiedCreatorByteOffset, 393 | verifiedCreatorByteOffset + 32, 394 | ), 395 | ); 396 | const isVerified = Boolean( 397 | nftMetadata.data.subarray( 398 | verifiedCreatorVerfiiedByteOffset, 399 | verifiedCreatorVerfiiedByteOffset + 1, 400 | ), 401 | ); 402 | if ( 403 | isVerified && 404 | verifiedCreatorAddress.toString() === 405 | expectedCreator.toString() 406 | ) { 407 | const domainName = nftMetadata.data 408 | .subarray(66, 101) 409 | .toString() 410 | .replace(/\u0000/g, ''); 411 | ownedDomains.push(domainName); 412 | } 413 | } 414 | } 415 | } 416 | } else { 417 | let nftMetadataKeys = nftAddresses.map((mint: string) => 418 | findMetadataAddress(new PublicKey(mint)), 419 | ); 420 | const nftsMetadata = 421 | await connection.getMultipleAccountsInfo(nftMetadataKeys); 422 | for (const nftMetadata of nftsMetadata) { 423 | if (nftMetadata) { 424 | const verifiedCreatorAddress = new PublicKey( 425 | nftMetadata.data.subarray( 426 | verifiedCreatorByteOffset, 427 | verifiedCreatorByteOffset + 32, 428 | ), 429 | ); 430 | const isVerified = Boolean( 431 | nftMetadata.data.subarray( 432 | verifiedCreatorVerfiiedByteOffset, 433 | verifiedCreatorVerfiiedByteOffset + 1, 434 | ), 435 | ); 436 | if ( 437 | isVerified && 438 | verifiedCreatorAddress.toString() === 439 | expectedCreator.toString() 440 | ) { 441 | const domainName = nftMetadata.data 442 | .subarray(66, 101) 443 | .toString() 444 | .replace(/\u0000/g, ''); 445 | ownedDomains.push(domainName); 446 | } 447 | } 448 | } 449 | } 450 | 451 | return ownedDomains; 452 | }; 453 | 454 | export function splitDomainTld(domain: string) { 455 | const parts = domain.split('.'); 456 | let tld = '', 457 | domainName = '', 458 | subdomain = ''; 459 | 460 | if (parts.length === 1) { 461 | domainName = parts[0]; 462 | } else { 463 | tld = '.' + parts[parts.length - 1]; 464 | domainName = parts[parts.length - 2]; 465 | subdomain = parts.slice(0, parts.length - 2).join('.'); 466 | } 467 | 468 | return [tld, domainName, subdomain]; 469 | } 470 | 471 | export function findMintAddress( 472 | nameAccount: PublicKey, 473 | nameHouseAccount: PublicKey, 474 | ) { 475 | return PublicKey.findProgramAddressSync( 476 | [ 477 | Buffer.from(NAME_HOUSE_PREFIX), 478 | nameHouseAccount.toBuffer(), 479 | nameAccount.toBuffer(), 480 | ], 481 | NAME_HOUSE_PROGRAM_ID, 482 | ); 483 | } 484 | 485 | export function findRenewableMintAddress( 486 | nameAccount: PublicKey, 487 | nameHouseAccount: PublicKey, 488 | expiresAtBuffer: Buffer, 489 | ) { 490 | return PublicKey.findProgramAddressSync( 491 | [ 492 | Buffer.from(NAME_HOUSE_PREFIX), 493 | nameHouseAccount.toBuffer(), 494 | nameAccount.toBuffer(), 495 | expiresAtBuffer, 496 | ], 497 | NAME_HOUSE_PROGRAM_ID, 498 | ); 499 | } 500 | 501 | /** 502 | * retrieves owner of the name account 503 | * 504 | * @param connection sol connection 505 | * @param nameAccountKey nameAccount to get owner of. 506 | */ 507 | export async function getDomainMintAccountKey( 508 | connection: Connection, 509 | nameAccountKey: PublicKey, 510 | tldHouse: PublicKey, 511 | ): Promise { 512 | const nameAccount = await NameRecordHeader.fromAccountAddress( 513 | connection, 514 | nameAccountKey, 515 | ); 516 | const expiryDate = nameAccount.expiresAt; 517 | const secondSinceEpoch = new Date(0); 518 | let mintAccount: PublicKey | undefined; 519 | const [nameHouse] = findNameHouse(tldHouse); 520 | if (expiryDate === secondSinceEpoch) { 521 | [mintAccount] = findMintAddress(nameAccountKey, nameHouse); 522 | } else { 523 | [mintAccount] = findRenewableMintAddress( 524 | nameAccountKey, 525 | nameHouse, 526 | dateToU64Buffer(expiryDate), 527 | ); 528 | } 529 | return mintAccount; 530 | } 531 | 532 | export async function getMintAccountFromDomainTld( 533 | connection: Connection, 534 | domainTld: string, 535 | ): Promise { 536 | const domainTldSplit = domainTld.split('.'); 537 | const domain = domainTldSplit[0]; 538 | const tldName = '.' + domainTldSplit[1]; 539 | 540 | const nameOriginTldKey = await getOriginNameAccountKey(); 541 | const parentHashedName = await getHashedName(tldName); 542 | const [parentAccountKey] = getNameAccountKeyWithBump( 543 | parentHashedName, 544 | undefined, 545 | nameOriginTldKey, 546 | ); 547 | 548 | const domainHashedName = await getHashedName(domain); 549 | const [domainAccountKey] = getNameAccountKeyWithBump( 550 | domainHashedName, 551 | undefined, 552 | parentAccountKey, 553 | ); 554 | 555 | const [tldHouse] = findTldHouse(tldName); 556 | return await getDomainMintAccountKey( 557 | connection, 558 | domainAccountKey, 559 | tldHouse, 560 | ); 561 | } 562 | 563 | function dateToU64Buffer(expiryDate: Date): Buffer { 564 | const secondsSinceEpoch = Math.floor(expiryDate.getTime() / 1000); 565 | const buffer = Buffer.alloc(8); 566 | buffer.writeBigUInt64BE(BigInt(secondsSinceEpoch)); 567 | return buffer; 568 | } 569 | 570 | export const getMultipleMainDomainsChecked = async ( 571 | connection: Connection, 572 | pubkeys: string[], 573 | ): Promise< 574 | { 575 | pubkey: string; 576 | mainDomain: string | undefined; 577 | nameAccount: PublicKey | undefined; 578 | }[] 579 | > => { 580 | const mainDomainKeys = pubkeys.map( 581 | pubkey => findMainDomain(new PublicKey(pubkey))[0], 582 | ); 583 | 584 | // fetch main domain accounts 585 | const mainDomainAccounts = 586 | await connection.getMultipleAccountsInfo(mainDomainKeys); 587 | 588 | const mainDomainWithNameAccounts = pubkeys.map((pubkey, index) => { 589 | const mainDomainAccount = mainDomainAccounts[index]; 590 | if (!!mainDomainAccount?.data) { 591 | const mainDomainData = 592 | MainDomain.fromAccountInfo(mainDomainAccount)[0]; 593 | const nameAccount = mainDomainData.nameAccount; 594 | return { 595 | pubkey: pubkey.toString(), 596 | nameAccount, 597 | mainDomain: mainDomainData.domain + mainDomainData.tld, 598 | }; 599 | } 600 | return { 601 | pubkey: pubkey.toString(), 602 | nameAccount: undefined, 603 | mainDomain: undefined, 604 | }; 605 | }); 606 | // fetch name accounts 607 | const nameAccounts = mainDomainWithNameAccounts.map( 608 | item => item.nameAccount, 609 | ); 610 | const filteredNameAccountsWithIndex = nameAccounts 611 | .map((pk, idx) => (pk ? { pk, idx } : undefined)) 612 | .filter((x): x is { pk: PublicKey; idx: number } => !!x); 613 | const filteredNameAccounts = filteredNameAccountsWithIndex.map(x => x.pk); 614 | const nameAccountsInfo = 615 | await connection.getMultipleAccountsInfo(filteredNameAccounts); 616 | 617 | // map infoByOriginalIdx 618 | const infoByOriginalIdx = new Array | null>( 619 | nameAccounts.length, 620 | ).fill(null); 621 | filteredNameAccountsWithIndex.forEach((entry, i) => { 622 | infoByOriginalIdx[entry.idx] = nameAccountsInfo[i]; 623 | }); 624 | 625 | const result = await Promise.all( 626 | mainDomainWithNameAccounts.map(async (item, index) => { 627 | const nameAccount = item.nameAccount; 628 | const info = infoByOriginalIdx[index]; 629 | if (!nameAccount || !info) { 630 | return { ...item }; 631 | } 632 | const nameAccountData = NameRecordHeader.fromAccountInfo(info); 633 | const tld = item.mainDomain?.split('.')[1]; 634 | const [tldHouseKey] = findTldHouse(tld); 635 | const [nameHouseKey] = findNameHouse(tldHouseKey); 636 | const [nftRecordKey] = findNftRecord(nameAccount, nameHouseKey); 637 | // check if the main domain owner is the nftrecord onchain 638 | const isNftRecordOwner = 639 | nameAccountData.owner?.toString() === nftRecordKey.toString(); 640 | if (isNftRecordOwner) { 641 | // check if the nft owner is the main domain owner 642 | const mintOwner = await getMintOwner(connection, nftRecordKey); 643 | if (mintOwner?.toString() != pubkeys[index]) { 644 | return { 645 | pubkey: pubkeys[index].toString(), 646 | nameAccount, 647 | mainDomain: undefined, 648 | }; 649 | } 650 | return { ...item }; 651 | } 652 | // check if the name record owner is the main domain owner 653 | if (nameAccountData.owner?.toString() != pubkeys[index]) { 654 | return { 655 | pubkey: pubkeys[index].toString(), 656 | nameAccount, 657 | mainDomain: undefined, 658 | }; 659 | } 660 | // check if the name record expires at is valid 661 | const expiresAtIsValid = nameAccountData.expiresAt.getTime() != 0; 662 | if ( 663 | expiresAtIsValid && 664 | nameAccountData.expiresAt.getTime() < Date.now() 665 | ) { 666 | return { 667 | pubkey: pubkeys[index].toString(), 668 | nameAccount, 669 | mainDomain: undefined, 670 | }; 671 | } 672 | return { ...item }; 673 | }), 674 | ); 675 | return result; 676 | }; 677 | 678 | export const getMainDomainChecked = async ( 679 | connection: Connection, 680 | pubkey: string, 681 | ): Promise<{ 682 | pubkey: string; 683 | mainDomain: string | undefined; 684 | nameAccount: PublicKey | undefined; 685 | }> => { 686 | const mainDomainKey = findMainDomain(new PublicKey(pubkey))[0]; 687 | const mainDomainAccount = await connection.getAccountInfo(mainDomainKey); 688 | 689 | if (!mainDomainAccount?.data) { 690 | return { pubkey, nameAccount: undefined, mainDomain: undefined }; 691 | } 692 | 693 | const mainDomainData = MainDomain.fromAccountInfo(mainDomainAccount)[0]; 694 | const nameAccount = mainDomainData.nameAccount; 695 | 696 | const nameAccountInfo = await connection.getAccountInfo(nameAccount); 697 | if (!nameAccountInfo) { 698 | return { pubkey, nameAccount, mainDomain: undefined }; 699 | } 700 | 701 | const nameAccountData = NameRecordHeader.fromAccountInfo(nameAccountInfo); 702 | const tld = mainDomainData.tld; 703 | const [tldHouseKey] = findTldHouse(tld); 704 | const [nameHouseKey] = findNameHouse(tldHouseKey); 705 | const [nftRecordKey] = findNftRecord(nameAccount, nameHouseKey); 706 | 707 | // NFT owner check 708 | if (nameAccountData.owner?.toString() === nftRecordKey.toString()) { 709 | const mintOwner = await getMintOwner(connection, nftRecordKey); 710 | if (mintOwner?.toString() !== pubkey) { 711 | return { pubkey, nameAccount, mainDomain: undefined }; 712 | } 713 | return { 714 | pubkey, 715 | nameAccount, 716 | mainDomain: mainDomainData.domain + mainDomainData.tld, 717 | }; 718 | } 719 | 720 | // Name record owner check 721 | if (nameAccountData.owner?.toString() !== pubkey) { 722 | return { pubkey, nameAccount, mainDomain: undefined }; 723 | } 724 | 725 | // Expiry check 726 | const expiresAtIsValid = nameAccountData.expiresAt.getTime() !== 0; 727 | if (expiresAtIsValid && nameAccountData.expiresAt.getTime() < Date.now()) { 728 | return { pubkey, nameAccount, mainDomain: undefined }; 729 | } 730 | 731 | return { 732 | pubkey, 733 | nameAccount, 734 | mainDomain: mainDomainData.domain + mainDomainData.tld, 735 | }; 736 | }; 737 | -------------------------------------------------------------------------------- /tests/tld-parser-monad.spec.ts: -------------------------------------------------------------------------------- 1 | import { getAddress } from 'ethers'; 2 | import { NameRecord, TldParser } from '../src'; 3 | import { 4 | labelhashFromLabel, 5 | namehashFromDomain, 6 | NetworkWithRpc, 7 | } from '../src/evm/utils'; 8 | 9 | const PUBLIC_KEY = getAddress('0x94Bfb92da83B27B39370550CA038Af96d182462f'); 10 | const RPC_URL = 'https://testnet-rpc.monad.xyz'; 11 | 12 | describe('tldParser EVM tests', () => { 13 | it('should correctly parse a domain label', async () => { 14 | const label = labelhashFromLabel('.mon'); 15 | expect(label).toEqual( 16 | '0x6e9db97d4be98844b20b5962d628f9e1f4b67c49bbfa2e4a2ffcef594a76ebd7', 17 | ); 18 | }); 19 | 20 | it('should correctly parse a domain namehash', async () => { 21 | const namehash = namehashFromDomain('ans.nad'); 22 | expect(namehash).toEqual( 23 | '0x4a8791bf2b67a8f8920d3ad2b5a54f15fdfcf51bb355fac27533fcf0020a7b4f', 24 | ); 25 | }); 26 | 27 | it('should perform fetching of all user domains', async () => { 28 | const settings = new NetworkWithRpc('monad', 10143, RPC_URL); 29 | const parser = new TldParser(settings, 'monad'); 30 | 31 | const ownedDomainsReceived = await parser.getAllUserDomains(PUBLIC_KEY); 32 | expect(ownedDomainsReceived).toHaveLength(13); 33 | }); 34 | 35 | it('should perform fetching of all user domains from a specific domain', async () => { 36 | const settings = new NetworkWithRpc('monad', 10143, RPC_URL); 37 | const parser = new TldParser(settings, 'monad'); 38 | 39 | const ownedDomainsReceived = await parser.getAllUserDomainsFromTld( 40 | PUBLIC_KEY, 41 | '.mon', 42 | ); 43 | expect(ownedDomainsReceived).toHaveLength(4); 44 | }); 45 | 46 | it('should perform fetching of owner from domain.tld', async () => { 47 | const settings = new NetworkWithRpc('monad', 10143, RPC_URL); 48 | const parser = new TldParser(settings, 'monad'); 49 | 50 | const ownedDomainsReceived = await parser.getOwnerFromDomainTld( 51 | 'miester.mon', 52 | ); 53 | expect(ownedDomainsReceived).toEqual(PUBLIC_KEY); 54 | }); 55 | 56 | it('should perform fetching of name record from domain.tld', async () => { 57 | const settings = new NetworkWithRpc('monad', 10143, RPC_URL); 58 | const parser = new TldParser(settings, 'monad'); 59 | const domainName = 'miester.mon'; 60 | const ownedDomainsReceived = await parser.getNameRecordFromDomainTld( 61 | domainName, 62 | ); 63 | const expectedNameRecord = { 64 | created_at: '1772642402', 65 | domain_name: 'miester', 66 | expires_at: '1772642702', 67 | main_domain_address: PUBLIC_KEY, 68 | tld: '.mon', 69 | transferrable: true, 70 | }; 71 | expect(ownedDomainsReceived).toEqual(expectedNameRecord); 72 | }); 73 | 74 | it('should perform fetching of main domain from useraccount', async () => { 75 | const settings = new NetworkWithRpc('monad', 10143, RPC_URL); 76 | const parser = new TldParser(settings, 'monad'); 77 | const domainName = PUBLIC_KEY; 78 | 79 | const ownedDomainsReceived = await parser.getMainDomain(domainName) ; 80 | const expectedNameRecord = { 81 | created_at: '1772642402', 82 | domain_name: 'miester', 83 | expires_at: '1772642702', 84 | main_domain_address: PUBLIC_KEY, 85 | tld: '.mon', 86 | transferrable: true, 87 | }; 88 | expect(ownedDomainsReceived).toEqual(expectedNameRecord); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/tld-parser.error.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onsol-labs/tld-parser/802e0592a9b7c5cd0458222da16d3d07842b884c/tests/tld-parser.error.ts -------------------------------------------------------------------------------- /tests/tld-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | 3 | import { MainDomain, MainDomainArgs, NameRecordHeader, Record, TldParser, getDomainKey, getMainDomainChecked, getMultipleMainDomainsChecked } from '../src'; 4 | 5 | const RPC_URL = ''; 6 | const connection = new Connection(RPC_URL); 7 | const owner = new PublicKey( 8 | '2EGGxj2qbNAJNgLCPKca8sxZYetyTjnoRspTPjzN2D67', 9 | ); 10 | const parentAccount = new PublicKey('8err4ThuTiZo9LbozHAvMrzXUmyPWj9urnMo38vC6FdQ'); 11 | const nameAccount = new PublicKey('6iE5btnTaan1eqfnwChLdVAyFERdn5uCVnp5GiXVg1aB'); 12 | const parentAccountOwner = new PublicKey('ANgPRMKQHgH5Snx2K3VHCvHqFmrABcjTZUrqZBzDCtfA'); 13 | 14 | describe('tldParser SVM tests', () => { 15 | it('should perform fetching of a pending expiry domain', async () => { 16 | const parser = new TldParser(connection, "solana"); 17 | const domanTld = 'canwenot.abc'; 18 | const ownerRecieved = await parser.getOwnerFromDomainTld(domanTld); 19 | expect(ownerRecieved).toStrictEqual(undefined); 20 | }); 21 | 22 | it('should perform fetching of an owner an nft domain (expired)', async () => { 23 | const parser = new TldParser(connection); 24 | const domanTld = 'legendary.abc'; 25 | const ownerRecieved = await parser.getOwnerFromDomainTld(domanTld); 26 | expect(ownerRecieved).toStrictEqual(undefined); 27 | }); 28 | 29 | it('should perform fetching of an owner an nft domain', async () => { 30 | const parser = new TldParser(connection); 31 | const domanTld = 'miester.abc'; 32 | const ownerRecieved = await parser.getOwnerFromDomainTld(domanTld); 33 | expect(ownerRecieved).toStrictEqual(owner); 34 | }); 35 | 36 | it('should perform retrieval of all user domains', async () => { 37 | const parser = new TldParser(connection); 38 | const allDomainsReceived = await parser.getAllUserDomains(owner); 39 | expect(allDomainsReceived).toHaveLength(79); 40 | }); 41 | 42 | it('should perform retrieval of all user domains for poor tld', async () => { 43 | const parser = new TldParser(connection); 44 | const tld = 'poor'; 45 | const ownedDomainsReceived = await parser.getAllUserDomainsFromTld(owner, tld); 46 | expect(ownedDomainsReceived).toHaveLength(2); 47 | }); 48 | 49 | it('should perform lookup of owner of the domainTld', async () => { 50 | const parser = new TldParser(connection); 51 | const domanTld = 'miester.poor'; 52 | const ownerRecieved = await parser.getOwnerFromDomainTld(domanTld); 53 | expect(ownerRecieved).toStrictEqual(owner); 54 | }); 55 | 56 | it('should perform lookup of nameRecord of the domainTld', async () => { 57 | const parser = new TldParser(connection); 58 | const domanTld = 'miester.poor'; 59 | const nameRecordRecieved = await parser.getNameRecordFromDomainTld(domanTld); 60 | const emptyBuffer = Buffer.alloc(0, 0); 61 | const zeroU64 = Buffer.alloc(8, 0); 62 | const nameRecord = new NameRecordHeader({ 63 | expiresAt: Uint8Array.from(zeroU64), 64 | createdAt: Uint8Array.from(zeroU64), 65 | nonTransferable: Uint8Array.from([0]), 66 | nclass: PublicKey.default.toBuffer(), 67 | owner: new PublicKey("2EGGxj2qbNAJNgLCPKca8sxZYetyTjnoRspTPjzN2D67").toBuffer(), 68 | parentName: parentAccount.toBuffer() 69 | }); 70 | nameRecord.isValid = true; 71 | nameRecord.data = emptyBuffer; 72 | 73 | expect(nameRecordRecieved).toStrictEqual(nameRecord); 74 | }); 75 | 76 | it('should perform lookup of tld from parentAccount', async () => { 77 | const parser = new TldParser(connection); 78 | const tld = await parser.getTldFromParentAccount(parentAccount); 79 | expect(tld).toStrictEqual(expect.stringContaining('poor')); 80 | }); 81 | 82 | it('should perform reverse lookup of domain from nameAccount and parent name owner', async () => { 83 | const parser = new TldParser(connection); 84 | const domain = await parser.reverseLookupNameAccount(nameAccount, parentAccountOwner); 85 | expect(domain).toStrictEqual(expect.stringContaining('miester')); 86 | }); 87 | 88 | it('should perform fetching of dns record of domain', async () => { 89 | let domain = 'miester.poor'; 90 | let multiRecordPubkeys = [ 91 | (await getDomainKey(Record.Url + '.' + domain, true)).pubkey, 92 | (await getDomainKey(Record.IPFS + '.' + domain, true)).pubkey, 93 | (await getDomainKey(Record.ARWV + '.' + domain, true)).pubkey, 94 | (await getDomainKey(Record.SHDW + '.' + domain, true)).pubkey, 95 | ]; 96 | const nameRecords = await NameRecordHeader.fromMultipileAccountAddresses(connection, multiRecordPubkeys); 97 | expect(nameRecords).toHaveLength(4); 98 | }); 99 | 100 | it('should perform fetching of main domain', async () => { 101 | const parser = new TldParser(connection); 102 | const mainDomain = await parser.getMainDomain(owner); 103 | const expectedNameAccount = new PublicKey('DNw14GYVbAFJVun3CeTb447SbmHHi5syMLyzXezQfd3u'); 104 | const expectedMainDomainArgs: MainDomainArgs = { 105 | domain: 'miester', 106 | tld: '.bonk', 107 | nameAccount: expectedNameAccount 108 | }; 109 | const expectedMainDomain = MainDomain.fromArgs(expectedMainDomainArgs); 110 | expect(mainDomain).toMatchObject(expectedMainDomain); 111 | }); 112 | 113 | it('should perform retrieval parsed domains of all user domains (nfts included)', async () => { 114 | const parser = new TldParser(connection); 115 | const allDomainsReceived = await parser.getParsedAllUserDomains(owner, 100); 116 | expect(allDomainsReceived).toHaveLength(88); 117 | }); 118 | 119 | it('should perform retrieval parsed domains of all user domains', async () => { 120 | const parser = new TldParser(connection); 121 | const allDomainsReceived = await parser.getParsedAllUserDomainsUnwrapped(owner); 122 | expect(allDomainsReceived).toHaveLength(71); 123 | }); 124 | 125 | it('should perform retrieval parsed domains of all user domains in a particular tld', async () => { 126 | const parser = new TldParser(connection); 127 | const allDomainsReceived = await parser.getParsedAllUserDomainsFromTldUnwrapped(owner, 'abc'); 128 | expect(allDomainsReceived).toHaveLength(5); 129 | }); 130 | 131 | it('should perform retrieval parsed nft domains of all user domains in a particular tld', async () => { 132 | const parser = new TldParser(connection); 133 | const allDomainsReceived = await parser.getParsedAllUserDomainsFromTld(owner, 'abc'); 134 | // console.log(allDomainsReceived) 135 | expect(allDomainsReceived).toHaveLength(15); 136 | }); 137 | 138 | it('should perform retrieval of user main domain checked', async () => { 139 | const mainDomain = await getMainDomainChecked( 140 | connection, 141 | owner.toString() 142 | ) 143 | expect(mainDomain).toBeDefined(); 144 | }) 145 | 146 | it('should perform retrieval of multiple user main domains checked', async () => { 147 | const mainDomain = await getMultipleMainDomainsChecked( 148 | connection, 149 | [ 150 | owner.toString(), 151 | PublicKey.default.toString() 152 | ] 153 | ) 154 | // console.log(mainDomain) 155 | expect(mainDomain).toBeDefined(); 156 | }) 157 | }); -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "target": "ESNext", 6 | "module": "CommonJS", 7 | "outDir": "dist/cjs", 8 | "declarationDir": "dist/types", 9 | "allowSyntheticDefaultImports": true // Enable synthetic default imports 10 | }, 11 | "include": ["src/**/*.ts"], 12 | "exclude": [ 13 | "src/**/*.spec.ts", 14 | "src/**/*.test.ts", 15 | "tests/**/*.test.ts", 16 | "tests/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "outDir": "dist/esm", 8 | "declarationDir": "dist/types", 9 | "allowSyntheticDefaultImports": true // Enable synthetic default imports 10 | }, 11 | "include": ["src/**/*.ts"], 12 | "exclude": [ 13 | "src/**/*.spec.ts", 14 | "src/**/*.test.ts", 15 | "tests/**/*.test.ts", 16 | "tests/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ESNext", 5 | "DOM" 6 | ], 7 | "module": "NodeNext", 8 | "target": "ESNext", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./src", 14 | // sources 15 | "sourceMap": true, 16 | "declarationMap": true, 17 | "inlineSources": true, 18 | "skipLibCheck": true, 19 | "rootDir": "./src", 20 | }, 21 | "include": [ 22 | "src/**/*.ts" 23 | ], 24 | "exclude": [ 25 | "tests", 26 | "dist", 27 | "node_modules", 28 | "script", 29 | ] 30 | } --------------------------------------------------------------------------------