diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95dc4047..a3aadad4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,12 +3,12 @@ name: Release on: release: types: [published] - jobs: release: name: Release permissions: id-token: write + contents: write runs-on: ubuntu-latest strategy: matrix: @@ -47,7 +47,7 @@ jobs: NPM_CONFIG_PROVENANCE: true run: | pnpm config set //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} - pnpm -F @ensdomains/ensjs publish + pnpm -F @ensdomains/ensjs publish --no-git-checks - name: Push changes run: git push diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a58d2d2c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.18.2 \ No newline at end of file diff --git a/packages/ens-test-env/src/manager.js b/packages/ens-test-env/src/manager.js index bb599660..568910b5 100644 --- a/packages/ens-test-env/src/manager.js +++ b/packages/ens-test-env/src/manager.js @@ -157,6 +157,7 @@ const awaitCommand = async (name, command) => { deploy.stdout.pipe(outPrepender).pipe(process.stdout) } deploy.stderr.pipe(errPrepender).pipe(process.stderr) + deploy.stderr.on('data', cleanup.bind(null, { exit: true })) return new Promise((resolve) => deploy.on('exit', () => resolve())) } @@ -188,6 +189,7 @@ export const main = async (_config, _options, justKill) => { const compose = await getCompose() try { + console.log('Starting anvil...') await compose.upOne('anvil', opts) } catch (e) { console.error('e: ', e) @@ -279,11 +281,62 @@ export const main = async (_config, _options, justKill) => { if (options.graph) { try { + console.log('Starting graph-node...') + await compose.upOne('graph-node', opts) + + await waitOn({ resources: ['http://localhost:8040'] }) + + const latestBlock = await rpcFetch('eth_getBlockByNumber', ['latest', false]) + const latestBlockNumber = parseInt(latestBlock.result.number, 16) + if (Number.isNaN(latestBlockNumber)) { + console.error('Failed to fetch latest block number') + return cleanup(undefined, 0) + } + console.log('latest block number:', latestBlockNumber) + + let indexArray = [] + const getCurrentIndex = async () => + fetch('http://localhost:8000/subgraphs/name/graphprotocol/ens', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: ` + { + _meta { + block { + number + } + } + } + `, + variables: {}, + }), + }) + .then((res) => res.json()) + .then((res) => { + if (res.errors) return 0 + return res.data._meta.block.number + }) + .catch(() => 0) + do { + const index = await getCurrentIndex() + console.log('subgraph index:', index) + indexArray.push(await getCurrentIndex()) + if (indexArray.length > 10) indexArray.shift() + await new Promise((resolve) => setTimeout(resolve, 1000)) + if (indexArray.every((i) => i === indexArray[0]) && indexArray.length === 10) { + console.error('Subgraph failed to launch properly') + return cleanup(undefined, 0) + } + } while ( + indexArray[indexArray.length - 1] < latestBlockNumber + ) + console.log('Starting remaining docker containers...') await compose.upAll(opts) } catch {} - await waitOn({ resources: ['http://localhost:8040'] }) - if (options.save) { const internalHashes = [ { @@ -336,48 +389,12 @@ export const main = async (_config, _options, justKill) => { 'http-get://localhost:8000/subgraphs/name/graphprotocol/ens', ], }) - await new Promise((resolve) => setTimeout(resolve, 100)) } } + await new Promise((resolve) => setTimeout(resolve, 100)) + if (!options.save && cmdsToRun.length > 0 && options.scripts) { - if (options.graph) { - let indexArray = [] - const getCurrentIndex = async () => - fetch('http://localhost:8000/subgraphs/name/graphprotocol/ens', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: ` - { - _meta { - block { - number - } - } - } - `, - variables: {}, - }), - }) - .then((res) => res.json()) - .then((res) => { - if (res.errors) return 0 - return res.data._meta.block.number - }) - .catch(() => 0) - do { - indexArray.push(await getCurrentIndex()) - if (indexArray.length > 10) indexArray.shift() - await new Promise((resolve) => setTimeout(resolve, 100)) - } while ( - !indexArray.every((i) => i === indexArray[0]) || - indexArray.length < 2 || - indexArray[0] === 0 - ) - } /** * @type {import('concurrently').ConcurrentlyResult['result']} **/ diff --git a/packages/ensjs/deploy/00_register_concurrently.cjs b/packages/ensjs/deploy/00_register_concurrently.cjs new file mode 100644 index 00000000..3a285157 --- /dev/null +++ b/packages/ensjs/deploy/00_register_concurrently.cjs @@ -0,0 +1,221 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable no-await-in-loop */ +// eslint-disable-next-line @typescript-eslint/naming-convention +const { BigNumber } = require('ethers') +const { ethers } = require('hardhat') +const { + makeNameGenerator: makeWrappedNameGenerator, +} = require('../utils/wrappedNameGenerator.cjs') +const { + makeNameGenerator: makeLegacyNameGenerator, +} = require('../utils/legacyNameGenerator.cjs') +const { makeNonceManager } = require('../utils/nonceManager.cjs') +const { encodeFuses } = require('../dist/cjs/utils/fuses') +const { MAX_DATE_INT } = require('../dist/cjs/utils/consts') + +const DURATION = 31556000 + +/** + * @type {{ + * label: string + * namedOwner: string + * namedAddr?: string + * type: 'wrapped' | 'legacy' + * data?: any[] + * reverseRecord?: boolean + * fuses?: number + * subnames?: { + * label: string + * namedOwner: string + * fuses?: number + * expiry?: number + * }[] + * duration?: number | BigNumber + * }[]} + */ + +const names = [ + ...Array.from({ length: 2 }, (_, index) => ({ + label: `concurrent-legacy-name-${index}`, + type: 'legacy', + namedOwner: 'owner4', + reverseRecord: true, + duration: DURATION, + })), + ...Array.from({ length: 2 }, (_, index) => ({ + label: `concurrent-wrapped-name-${index}`, + type: 'wrapped', + namedOwner: 'owner4', + fuses: encodeFuses({ + input: { + child: { + named: ['CANNOT_UNWRAP'], + }, + }, + }), + duration: DURATION, + subnames: [ + { + label: `xyz`, + namedOwner: 'owner4', + type: 'wrapped', + expiry: MAX_DATE_INT, + fuses: encodeFuses({ + input: { + parent: { + named: ['PARENT_CANNOT_CONTROL'], + }, + child: { + named: ['CANNOT_UNWRAP'], + }, + }, + }), + }, + ], + })), +] + +/** + * @type {import('hardhat-deploy/types').DeployFunction} + */ +const func = async function (hre) { + const { network } = hre + + const nonceManager = await makeNonceManager(hre) + const wrappedNameGenerator = await makeWrappedNameGenerator(hre, nonceManager) + const legacyNameGenerator = await makeLegacyNameGenerator(hre, nonceManager) + + await network.provider.send('evm_setAutomine', [false]) + + // Commit + const commitTxs = await Promise.all( + names.map( + ({ + label, + type, + namedOwner, + namedAddr = namedOwner, + data = [], + reverseRecord = false, + fuses = 0, + duration = 31536000, + }) => { + console.log(`Committing commitment for ${label}.eth...`) + if (type === 'legacy') + return legacyNameGenerator.commit({ + label, + namedOwner, + namedAddr, + }) + return wrappedNameGenerator.commit({ + label, + namedOwner, + data, + reverseRecord, + fuses, + duration, + }) + }, + ), + ) + + network.provider.send('evm_mine') + await Promise.all( + commitTxs.map(async (tx) => { + return tx.wait() + }), + ) + + const oldTimestamp = (await ethers.provider.getBlock('latest')).timestamp + await network.provider.send('evm_setNextBlockTimestamp', [oldTimestamp + 60]) + await network.provider.send('evm_increaseTime', [300]) + await network.provider.send('evm_mine') + + // Register + const registerTxs = await Promise.all( + names.map( + ({ + label, + type, + namedOwner, + namedAddr = namedOwner, + data = [], + reverseRecord = false, + fuses = 0, + duration = 31536000, + }) => { + if (type === 'legacy') + return legacyNameGenerator.register({ + label, + namedOwner, + namedAddr, + duration, + }) + return wrappedNameGenerator.register({ + label, + namedOwner, + data, + reverseRecord, + fuses, + duration, + }) + }, + ), + ) + + await network.provider.send('evm_mine') + await Promise.all( + registerTxs.map(async (tx) => { + return tx.wait() + }), + ) + + await network.provider.send('evm_setAutomine', [true]) + + // Create subnames + for (const { + label, + namedOwner, + type, + subnames, + } of names) { + if (!subnames) continue + console.log(`Setting subnames for ${label}.eth...`) + for (const { + label: subnameLabel, + namedOwner: namedSubnameOwner, + fuses: subnameFuses = 0, + expiry: subnameExpiry = BigNumber.from(2).pow(64).sub(1), + } of subnames) { + let setSubnameTx + if (type === 'legacy') + setSubnameTx = await legacyNameGenerator.subname({ + label, + namedOwner, + subnameLabel, + namedSubnameOwner, + }) + else + setSubnameTx = await wrappedNameGenerator.subname({ + label, + namedOwner, + subnameLabel, + namedSubnameOwner, + subnameFuses, + subnameExpiry, + }) + console.log(` - ${subnameLabel} (tx: ${setSubnameTx.hash})...`) + await setSubnameTx.wait() + } + } + + await network.provider.send('evm_mine') + return true +} + +func.id = 'register-concurrent-names' +func.tags = ['register-concurrent-names'] +func.dependencies = ['ETHRegistrarController', 'register-wrapped-names'] +func.runAtTheEnd = true + +module.exports = func diff --git a/packages/ensjs/deploy/00_register_legacy.cjs b/packages/ensjs/deploy/00_register_legacy.cjs index e0df8870..1296788a 100644 --- a/packages/ensjs/deploy/00_register_legacy.cjs +++ b/packages/ensjs/deploy/00_register_legacy.cjs @@ -4,6 +4,7 @@ const cbor = require('cbor') const { ethers } = require('hardhat') const pako = require('pako') const { labelhash, namehash, toBytes } = require('viem') +const { makeNameGenerator } = require('../utils/legacyNameGenerator.cjs') const dummyABI = [ { @@ -366,6 +367,22 @@ const names = [ namedOwner: 'owner2', namedAddr: 'owner2', })), + ...Array.from({ length: 2}, (_, i) => ({ + label: `nonconcurrent-legacy-name-${i}`, + namedOwner: 'owner4', + namedAddr: 'owner4', + duration: 31536000 / 2, + subnames: [ + { + label: `test`, + namedOwner: 'owner4', + }, + { + label: `xyz`, + namedOwner: 'owner4', + } + ] + })) ] /** @@ -375,11 +392,12 @@ const func = async function (hre) { const { getNamedAccounts, network } = hre const allNamedAccts = await getNamedAccounts() - const controller = await ethers.getContract('LegacyETHRegistrarController') const publicResolver = await ethers.getContract('LegacyPublicResolver') await network.provider.send('anvil_setBlockTimestampInterval', [60]) + const nameGenerator = await makeNameGenerator(hre) + for (const { label, namedOwner, @@ -388,22 +406,13 @@ const func = async function (hre) { subnames, duration = 31536000, } of names) { - const secret = - '0x0000000000000000000000000000000000000000000000000000000000000000' const registrant = allNamedAccts[namedOwner] - const resolver = publicResolver.address - const addr = allNamedAccts[namedAddr] - - const commitment = await controller.makeCommitmentWithConfig( + const commitTx = await nameGenerator.commit({ label, - registrant, - secret, - resolver, - addr, - ) + namedOwner, + namedAddr, + }) - const _controller = controller.connect(await ethers.getSigner(registrant)) - const commitTx = await _controller.commit(commitment) console.log( `Committing commitment for ${label}.eth (tx: ${commitTx.hash})...`, ) @@ -411,20 +420,14 @@ const func = async function (hre) { await network.provider.send('evm_mine') - const price = await controller.rentPrice(label, duration) - - const registerTx = await _controller.registerWithConfig( + const registerTx = await nameGenerator.register({ label, - registrant, + namedOwner, + namedAddr, duration, - secret, - resolver, - addr, - { - value: price, - }, - ) + }) console.log(`Registering name ${label}.eth (tx: ${registerTx.hash})...`) + await registerTx.wait() if (records) { @@ -493,20 +496,16 @@ const func = async function (hre) { if (subnames) { console.log(`Setting subnames for ${label}.eth...`) - const registry = await ethers.getContract('ENSRegistry') for (const { label: subnameLabel, - namedOwner: subnameOwner, + namedOwner: namedSubnameOwner, } of subnames) { - const owner = allNamedAccts[subnameOwner] - const _registry = registry.connect(await ethers.getSigner(registrant)) - const setSubnameTx = await _registry.setSubnodeRecord( - namehash(`${label}.eth`), - labelhash(subnameLabel), - owner, - resolver, - '0', - ) + const setSubnameTx = await nameGenerator.subname({ + label, + namedOwner, + subnameLabel, + namedSubnameOwner, + }) console.log(` - ${subnameLabel} (tx: ${setSubnameTx.hash})...`) await setSubnameTx.wait() } diff --git a/packages/ensjs/deploy/00_register_wrapped.cjs b/packages/ensjs/deploy/00_register_wrapped.cjs index 629d3ded..68559f0a 100644 --- a/packages/ensjs/deploy/00_register_wrapped.cjs +++ b/packages/ensjs/deploy/00_register_wrapped.cjs @@ -3,9 +3,9 @@ // eslint-disable-next-line @typescript-eslint/naming-convention const { BigNumber } = require('ethers') const { ethers } = require('hardhat') -const { namehash } = require('viem/ens') const { MAX_DATE_INT } = require('../dist/cjs/utils/consts') const { encodeFuses } = require('../dist/cjs/utils/fuses') +const { makeNameGenerator } = require('../utils/wrappedNameGenerator.cjs') /** * @type {{ @@ -98,17 +98,44 @@ const names = [ }, ], }, + ...Array.from({ length: 2}, (_, index) => ({ + label: `nonconcurrent-wrapped-name-${index}`, + namedOwner: 'owner4', + fuses: encodeFuses({ + input: { + child: { + named: ['CANNOT_UNWRAP'], + }, + }, + }), + duration: 31556000 * 2, + subnames: [ + { + label: `xyz`, + namedOwner: 'owner4', + expiry: MAX_DATE_INT, + fuses: encodeFuses({ + input: { + parent: { + named: ['PARENT_CANNOT_CONTROL'], + }, + child: { + named: ['CANNOT_UNWRAP'], + }, + }, + }), + }, + ], + + })) ] /** * @type {import('hardhat-deploy/types').DeployFunction} */ const func = async function (hre) { - const { getNamedAccounts, network } = hre - const allNamedAccts = await getNamedAccounts() - - const controller = await ethers.getContract('ETHRegistrarController') - const publicResolver = await ethers.getContract('PublicResolver') + const { network } = hre + const nameGenerator = await makeNameGenerator(hre) await network.provider.send('anvil_setBlockTimestampInterval', [60]) @@ -121,24 +148,15 @@ const func = async function (hre) { subnames, duration = 31536000, } of names) { - const secret = - '0x0000000000000000000000000000000000000000000000000000000000000000' - const owner = allNamedAccts[namedOwner] - const resolver = publicResolver.address - - const commitment = await controller.makeCommitment( + const commitTx = await nameGenerator.commit({ label, - owner, - duration, - secret, - resolver, + namedOwner, data, reverseRecord, fuses, - ) + duration + }) - const _controller = controller.connect(await ethers.getSigner(owner)) - const commitTx = await controller.commit(commitment) console.log( `Committing commitment for ${label}.eth (tx: ${commitTx.hash})...`, ) @@ -146,44 +164,34 @@ const func = async function (hre) { await network.provider.send('evm_mine') - const [price] = await controller.rentPrice(label, duration) - - const registerTx = await _controller.register( + const registerTx = await nameGenerator.register({ label, - owner, - duration, - secret, - resolver, + namedOwner, data, reverseRecord, fuses, - { - value: price, - }, - ) + duration, + }) + console.log(`Registering name ${label}.eth (tx: ${registerTx.hash})...`) await registerTx.wait() if (subnames) { console.log(`Setting subnames for ${label}.eth...`) - const nameWrapper = await ethers.getContract('NameWrapper') for (const { label: subnameLabel, namedOwner: namedSubnameOwner, fuses: subnameFuses = 0, expiry: subnameExpiry = BigNumber.from(2).pow(64).sub(1), } of subnames) { - const subnameOwner = allNamedAccts[namedSubnameOwner] - const _nameWrapper = nameWrapper.connect(await ethers.getSigner(owner)) - const setSubnameTx = await _nameWrapper.setSubnodeRecord( - namehash(`${label}.eth`), + const setSubnameTx = await nameGenerator.subname({ + label, + namedOwner, subnameLabel, - subnameOwner, - resolver, - '0', + namedSubnameOwner, subnameFuses, subnameExpiry, - ) + }) console.log(` - ${subnameLabel} (tx: ${setSubnameTx.hash})...`) await setSubnameTx.wait() } diff --git a/packages/ensjs/deploy/01_delete_names.cjs b/packages/ensjs/deploy/01_delete_names.cjs index 644dba00..73402c68 100644 --- a/packages/ensjs/deploy/01_delete_names.cjs +++ b/packages/ensjs/deploy/01_delete_names.cjs @@ -55,11 +55,19 @@ const func = async function (hre) { await deleteName(name1) await deleteName(name2) + for (const name of [name1, name2]) { + const owner = await registry.owner(namehash(name)) + if (owner !== EMPTY_ADDRESS) { + throw new Error(`Failed to delete name ${name}`) + } + } + return true } func.id = 'delete-names' func.tags = ['delete-names'] +func.dependencies = ['register-unwrapped-names'] func.runAtTheEnd = true module.exports = func diff --git a/packages/ensjs/hardhat.config.cjs b/packages/ensjs/hardhat.config.cjs index 49311e93..ea955f29 100644 --- a/packages/ensjs/hardhat.config.cjs +++ b/packages/ensjs/hardhat.config.cjs @@ -59,6 +59,7 @@ const config = { }, owner2: 2, owner3: 3, + owner4: 4 }, external: { contracts: [ diff --git a/packages/ensjs/package.json b/packages/ensjs/package.json index e33c41a4..e8912ad0 100644 --- a/packages/ensjs/package.json +++ b/packages/ensjs/package.json @@ -106,7 +106,8 @@ "dns-packet": "^5.3.1", "graphql": "^16.3.0", "graphql-request": "6.1.0", - "pako": "^2.1.0" + "pako": "^2.1.0", + "ts-pattern": "^5.4.0" }, "devDependencies": { "@ensdomains/buffer": "^0.0.13", diff --git a/packages/ensjs/src/contracts/consts.ts b/packages/ensjs/src/contracts/consts.ts index 65426c25..af67f61f 100644 --- a/packages/ensjs/src/contracts/consts.ts +++ b/packages/ensjs/src/contracts/consts.ts @@ -102,19 +102,19 @@ export const addresses = { address: '0x283af0b28c62c092c9727f1ee09c02ca627eb7f5', }, ensEthRegistrarController: { - address: '0x179Be112b24Ad4cFC392eF8924DfA08C20Ad8583', + address: '0xF404D2F84BC1735f7D9948F032D61F5fFfD9D3C3', }, ensNameWrapper: { address: '0xab50971078225D365994dc1Edcb9b7FD72Bb4862', }, ensPublicResolver: { - address: '0x9010A27463717360cAD99CEA8bD39b8705CCA238', + address: '0x5a692ffe769A9B3D0e61F7446F5cAED650044C36', }, ensRegistry: { address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', }, ensReverseRegistrar: { - address: '0x132AC0B116a73add4225029D1951A9A707Ef673f', + address: '0x65EE0b0B030a76c95a7ff046C0e0c8f7A2d1B004', }, ensUniversalResolver: { address: '0xa6ac935d4971e3cd133b950ae053becd16fe7f3b', @@ -134,19 +134,19 @@ export const addresses = { address: '0xe62E4b6cE018Ad6e916fcC24545e20a33b9d8653', }, ensEthRegistrarController: { - address: '0xFED6a969AaA60E4961FCD3EBF1A2e8913ac65B72', + address: '0x4477cAc137F3353Ca35060E01E5aEb777a1Ca01B', }, ensNameWrapper: { address: '0x0635513f179D50A207757E05759CbD106d7dFcE8', }, ensPublicResolver: { - address: '0x8FADE66B79cC9f707aB26799354482EB93a5B7dD', + address: '0x8948458626811dd0c23EB25Cc74291247077cC51', }, ensRegistry: { address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', }, ensReverseRegistrar: { - address: '0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6', + address: '0xCF75B92126B02C9811d8c632144288a3eb84afC8', }, ensUniversalResolver: { address: '0xc8af999e38273d658be1b921b88a9ddf005769cc', diff --git a/packages/ensjs/src/errors/version.ts b/packages/ensjs/src/errors/version.ts index bf1c6054..bc2da28e 100644 --- a/packages/ensjs/src/errors/version.ts +++ b/packages/ensjs/src/errors/version.ts @@ -1 +1 @@ -export const version = 'v4.0.0' +export const version = 'v4.0.2-alpha.5' diff --git a/packages/ensjs/src/functions/subgraph/filters.ts b/packages/ensjs/src/functions/subgraph/filters.ts index 9fa8d765..21bb78d0 100644 --- a/packages/ensjs/src/functions/subgraph/filters.ts +++ b/packages/ensjs/src/functions/subgraph/filters.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { match, P } from 'ts-pattern' import type { InputMaybe, Scalars } from './types.js' +import type { Name } from './utils.js' +import { GRACE_PERIOD_SECONDS } from '../../utils/consts.js' export type BlockChangedFilter = { number_gte: Scalars['Int'] @@ -700,3 +703,146 @@ export type ResolverEventFilter = { and?: InputMaybe>> or?: InputMaybe>> } + +export const getExpiryDateOrderFilter = ({ + orderDirection, + lastDomain, +}: { + orderDirection: 'asc' | 'desc' + lastDomain: Name +}): DomainFilter => { + let lastExpiryDate = lastDomain.expiryDate?.value + ? lastDomain.expiryDate.value / 1000 + : 0 + if (lastDomain.parentName === 'eth') lastExpiryDate += GRACE_PERIOD_SECONDS + + return match({ + lastExpiryDate, + orderDirection, + }) + .with( + { + lastExpiryDate: P.number.lte(0), + orderDirection: 'asc', + }, + () => + ({ + and: [{ expiryDate: null }, { id_gt: lastDomain.id }], + } as DomainFilter), + ) + .with( + { + lastExpiryDate: P.number, + orderDirection: 'asc', + }, + () => + ({ + or: [ + { + and: [ + { + expiryDate_gte: `${lastExpiryDate}`, + }, + { id_gt: lastDomain.id }, + ], + }, + { + expiryDate_gt: `${lastExpiryDate}`, + }, + { + expiryDate: null, + }, + ], + } as DomainFilter), + ) + .with( + { + lastExpiryDate: P.number.lte(0), + orderDirection: 'desc', + }, + () => + ({ + or: [ + { + and: [{ expiryDate: null }, { [`id_lt`]: lastDomain.id }], + }, + { + [`expiryDate_gt`]: 0, + }, + ], + } as DomainFilter), + ) + .with( + { + lastExpiryDate: P.number, + orderDirection: 'desc', + }, + () => + ({ + or: [ + { + and: [ + { expiryDate_lte: `${lastExpiryDate}` }, + { id_lt: lastDomain.id }, + ], + }, + { + expiryDate_lt: `${lastExpiryDate}`, + }, + ], + } as DomainFilter), + ) + .exhaustive() +} + +export const getCreatedAtOrderFilter = ({ + orderDirection, + lastDomain, +}: { + orderDirection: 'asc' | 'desc' + lastDomain: Name +}): DomainFilter => + match({ + orderDirection, + }) + .with( + { + orderDirection: 'asc', + }, + () => + ({ + or: [ + { + and: [ + { + createdAt_gte: `${lastDomain.createdAt.value / 1000}`, + id_gt: lastDomain.id, + }, + ], + }, + { + createdAt_gt: `${lastDomain.createdAt.value / 1000}`, + }, + ], + } as DomainFilter), + ) + .with( + { + orderDirection: 'desc', + }, + () => + ({ + or: [ + { + and: [ + { createdAt_lte: `${lastDomain.createdAt.value / 1000}` }, + { id_lt: lastDomain.id }, + ], + }, + { + createdAt_lt: `${lastDomain.createdAt.value / 1000}`, + }, + ], + } as DomainFilter), + ) + .exhaustive() diff --git a/packages/ensjs/src/functions/subgraph/getNamesForAddress.test.ts b/packages/ensjs/src/functions/subgraph/getNamesForAddress.test.ts index 198b17de..43698101 100644 --- a/packages/ensjs/src/functions/subgraph/getNamesForAddress.test.ts +++ b/packages/ensjs/src/functions/subgraph/getNamesForAddress.test.ts @@ -3,10 +3,11 @@ import type { Address } from 'viem' import { beforeAll, describe, expect, it } from 'vitest' import { publicClient, walletClient } from '../../test/addTestContracts.js' -import { EMPTY_ADDRESS } from '../../utils/consts.js' -import getNamesForAddress, { - type NameWithRelation, -} from './getNamesForAddress.js' +import { GRACE_PERIOD_SECONDS } from '../../utils/consts.js' +import getNamesForAddress from './getNamesForAddress.js' +import getOwner from '../public/getOwner.js' +import getExpiry from '../public/getExpiry.js' +import getWrapperData from '../public/getWrapperData.js' let accounts: Address[] @@ -14,6 +15,39 @@ beforeAll(async () => { accounts = await walletClient.getAddresses() }) +const user4 = '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65' +let expiry: bigint + +describe('check that concurrent names all have the same expiry date', () => { + it.each([ + ...Array.from({ length: 2 }, (_, i) => `concurrent-legacy-name-${i}.eth`), + ...Array.from({ length: 4 }, (_, i) => { + const index = Math.floor(i / 2) + const isSubname = i % 2 === 1 + return isSubname + ? `xyz.concurrent-wrapped-name-${index}.eth` + : `concurrent-wrapped-name-${index}.eth` + }), + ])('%s', async (name) => { + const ownerData = await getOwner(publicClient, { name }) + + const owner = ownerData?.registrant ?? ownerData?.owner + expect(owner).toEqual(user4) + const expiryData = await getExpiry(publicClient, { name }) + const expiryValue = expiryData?.expiry?.value ?? 0n + if (!expiry) expiry = expiryValue + + const wrapperData = await getWrapperData(publicClient, { name }) + const wrappedExpiryValue = + (wrapperData?.expiry?.value || 0n) - BigInt(GRACE_PERIOD_SECONDS) + const expectedExpiry = + ownerData?.ownershipLevel === 'nameWrapper' + ? wrappedExpiryValue + : expiryValue + expect(expectedExpiry).toEqual(expiry) + }) +}) + it('returns with default values', async () => { const result = await getNamesForAddress(publicClient, { address: accounts[1], @@ -35,279 +69,462 @@ it('has registration date on .eth names', async () => { } } }) -describe('filter', () => { - it('filters by owner', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - filter: { - owner: true, - registrant: false, - resolvedAddress: false, - wrappedOwner: false, - }, - }) - if (!result.length) throw new Error('No names found') - for (const name of result) { - expect(name.owner).toBe(accounts[1]) - expect(name.relation.owner).toBe(true) - } - }) - it('filters by registrant', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - filter: { - owner: false, - registrant: true, - resolvedAddress: false, - wrappedOwner: false, - }, - }) - if (!result.length) throw new Error('No names found') - for (const name of result) { - expect(name.registrant).toBe(accounts[1]) - expect(name.relation.registrant).toBe(true) - } - }) - it('filters by resolved address', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - filter: { - owner: false, - registrant: false, - resolvedAddress: true, - wrappedOwner: false, - }, - }) - if (!result.length) throw new Error('No names found') - for (const name of result) { - expect(name.resolvedAddress).toBe(accounts[1]) - expect(name.relation.resolvedAddress).toBe(true) - } - }) - it('filters by wrapped owner', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - filter: { - owner: false, - registrant: false, - resolvedAddress: false, - wrappedOwner: true, - }, - }) - if (!result.length) throw new Error('No names found') - for (const name of result) { - expect(name.wrappedOwner).toBe(accounts[1]) - expect(name.relation.wrappedOwner).toBe(true) - } - }) - it('allows including expired names', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - filter: { - owner: true, - registrant: true, - resolvedAddress: true, - wrappedOwner: true, - allowExpired: true, - }, - }) - if (!result.length) throw new Error('No names found') - const expiredNames = result.filter( - (x) => x.expiryDate?.date && x.expiryDate.date < new Date(), - ) - expect(expiredNames.length).toBeGreaterThan(0) - }) - it('allows including reverse record', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[2], - filter: { - owner: true, - registrant: true, - resolvedAddress: true, - wrappedOwner: true, - allowReverseRecord: true, - }, - }) - if (!result.length) throw new Error('No names found') - const reverseRecordNames = result.filter( - (x) => x.parentName === 'addr.reverse', - ) - expect(reverseRecordNames.length).toBeGreaterThan(0) - }) - it('does not include deleted names by default', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - pageSize: 1000, - }) - if (!result.length) throw new Error('No names found') - const deletedNames = result.filter( - (x) => x.parentName === 'deletable.eth' && x.owner === EMPTY_ADDRESS, - ) - expect(deletedNames.length).toBe(0) - }) - it('allows including deleted names', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - pageSize: 1000, - filter: { - owner: true, - registrant: true, - resolvedAddress: true, - wrappedOwner: true, - allowDeleted: true, - }, - }) - if (!result.length) throw new Error('No names found') - const deletedNames = result.filter( - (x) => x.parentName === 'deletable.eth' && x.owner === EMPTY_ADDRESS, - ) - expect(deletedNames.length).toBe(1) - }) - it('filters by search string', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - pageSize: 1000, - filter: { - owner: true, - registrant: true, - resolvedAddress: true, - wrappedOwner: true, - searchString: 'test123', - }, - }) - if (!result.length) throw new Error('No names found') - for (const name of result) { - expect(name.labelName).toContain('test123') - } +it('should get ascending names by expiry date correctly, including names with the same expiry date', async () => { + const fullResults = await getNamesForAddress(publicClient, { + address: accounts[4], + orderBy: 'expiryDate', + orderDirection: 'asc', + pageSize: 100, }) - it('filters by search string - name', async () => { - const result = await getNamesForAddress(publicClient, { - address: accounts[2], - pageSize: 1000, - filter: { - owner: true, - registrant: true, - resolvedAddress: true, - wrappedOwner: true, - searchString: 'wrapped-with-subnames', - searchType: 'name', - }, + const expectedNames = fullResults.map((item) => item.name) + + const names = [] + let previousPage + do { + // eslint-disable-next-line no-await-in-loop + const currentPage = await getNamesForAddress(publicClient, { + address: accounts[4], + orderBy: 'expiryDate', + orderDirection: 'asc', + pageSize: 3, + previousPage, }) + names.push(...currentPage.map((item) => item.name)) + previousPage = currentPage + } while (previousPage.length) - if (!result.length) throw new Error('No names found') - const subnames = result.filter( - (x) => x.parentName === 'wrapped-with-subnames.eth', - ) - expect(subnames.length).toBeGreaterThan(0) - }) + expect(names).toEqual(expectedNames) }) -describe.each([ - { - orderBy: 'name', - orderDirection: 'asc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => - (a.name || '').localeCompare(b.name || ''), - }, - { - orderBy: 'name', - orderDirection: 'desc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => - (b.name || '').localeCompare(a.name || ''), - }, - { - orderBy: 'labelName', - orderDirection: 'asc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => { - const aLabelName = a.labelName || '' - const bLabelName = b.labelName || '' - return aLabelName.localeCompare(bLabelName) - }, - }, - { - orderBy: 'labelName', - orderDirection: 'desc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => { - const aLabelName = a.labelName || '' - const bLabelName = b.labelName || '' - return bLabelName.localeCompare(aLabelName) - }, - }, - { - orderBy: 'expiryDate', - orderDirection: 'asc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => { - const aExpiryDate = a.expiryDate?.value || Infinity - const bExpiryDate = b.expiryDate?.value || Infinity - return aExpiryDate - bExpiryDate - }, - }, - { +it('should get descending names by expiry date correctly, including names with the same expiry date', async () => { + const fullResults = await getNamesForAddress(publicClient, { + address: accounts[4], orderBy: 'expiryDate', orderDirection: 'desc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => { - const aExpiryDate = a.expiryDate?.value || Infinity - const bExpiryDate = b.expiryDate?.value || Infinity - return bExpiryDate - aExpiryDate - }, - }, - { + pageSize: 100, + }) + const expectedNames = fullResults.map((item) => item.name) + + const names = [] + // initial result + let previousPage + do { + // eslint-disable-next-line no-await-in-loop + const currentPage = await getNamesForAddress(publicClient, { + address: accounts[4], + orderBy: 'expiryDate', + orderDirection: 'desc', + pageSize: 3, + previousPage, + }) + names.push(...currentPage.map((item) => item.name)) + previousPage = currentPage + } while (previousPage.length) + expect(names).toEqual(expectedNames) +}) + +it('should get ascending names by creation date correctly, including names with the same expiry date', async () => { + const fullResults = await getNamesForAddress(publicClient, { + address: accounts[4], orderBy: 'createdAt', orderDirection: 'asc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => { - const aCreatedAt = a.createdAt.value - const bCreatedAt = b.createdAt.value - return aCreatedAt - bCreatedAt - }, - }, - { + pageSize: 100, + }) + const expectedNames = fullResults.map((item) => item.name) + + const names = [] + let previousPage + do { + // eslint-disable-next-line no-await-in-loop + const currentPage = await getNamesForAddress(publicClient, { + address: accounts[4], + orderBy: 'createdAt', + orderDirection: 'asc', + pageSize: 3, + previousPage, + }) + names.push(...currentPage.map((item) => item.name)) + previousPage = currentPage + } while (previousPage.length) + + expect(names).toEqual(expectedNames) +}) + +it('should get descending names by creation date correctly, including names with the same expiry date', async () => { + const fullResults = await getNamesForAddress(publicClient, { + address: accounts[4], orderBy: 'createdAt', orderDirection: 'desc', - compareFn: (a: NameWithRelation, b: NameWithRelation) => { - const aCreatedAt = a.createdAt.value - const bCreatedAt = b.createdAt.value - return bCreatedAt - aCreatedAt - }, - }, -])( - 'filters by $orderBy $orderDirection', - ({ orderBy, orderDirection, compareFn }) => { - it('is consistent between full result and paginated results', async () => { - const fullResult = await getNamesForAddress(publicClient, { - address: accounts[1], - orderBy: orderBy as any, - orderDirection: orderDirection as any, - pageSize: 1000, - }) - if (!fullResult.length) throw new Error('No names found') - const paginatedResults = [] - let lastResult: NameWithRelation[] = [] - do { - const result = await getNamesForAddress(publicClient, { - address: accounts[1], - orderBy: orderBy as any, - orderDirection: orderDirection as any, - previousPage: lastResult, - pageSize: 5, - }) - paginatedResults.push(...result) - lastResult = result - } while (lastResult.length) + pageSize: 100, + }) + const expectedNames = fullResults.map((item) => item.name) - expect(paginatedResults.length).toBe(fullResult.length) - expect(paginatedResults).toStrictEqual(fullResult) + const names = [] + // initial result + let previousPage + do { + // eslint-disable-next-line no-await-in-loop + const currentPage = await getNamesForAddress(publicClient, { + address: accounts[4], + orderBy: 'createdAt', + orderDirection: 'desc', + pageSize: 3, + previousPage, }) - it('is sorted correctly', async () => { - const fullResult = await getNamesForAddress(publicClient, { - address: accounts[1], - orderBy: orderBy as any, - orderDirection: orderDirection as any, - pageSize: 1000, - }) - if (!fullResult.length) throw new Error('No names found') - const sortedResult = [...fullResult].sort(compareFn) - expect(fullResult).toEqual(sortedResult) - }) - }, -) + names.push(...currentPage.map((item) => item.name)) + previousPage = currentPage + } while (previousPage.length) + expect(names).toEqual(expectedNames) +}) + +// describe('filter', () => { +// it('filters by owner', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// filter: { +// owner: true, +// registrant: false, +// resolvedAddress: false, +// wrappedOwner: false, +// }, +// }) +// if (!result.length) throw new Error('No names found') +// for (const name of result) { +// expect(name.owner).toBe(accounts[1]) +// expect(name.relation.owner).toBe(true) +// } +// }) +// it('filters by registrant', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// filter: { +// owner: false, +// registrant: true, +// resolvedAddress: false, +// wrappedOwner: false, +// }, +// }) +// if (!result.length) throw new Error('No names found') +// for (const name of result) { +// expect(name.registrant).toBe(accounts[1]) +// expect(name.relation.registrant).toBe(true) +// } +// }) +// it('filters by resolved address', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// filter: { +// owner: false, +// registrant: false, +// resolvedAddress: true, +// wrappedOwner: false, +// }, +// }) +// if (!result.length) throw new Error('No names found') +// for (const name of result) { +// expect(name.resolvedAddress).toBe(accounts[1]) +// expect(name.relation.resolvedAddress).toBe(true) +// } +// }) +// it('filters by wrapped owner', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// filter: { +// owner: false, +// registrant: false, +// resolvedAddress: false, +// wrappedOwner: true, +// }, +// }) +// if (!result.length) throw new Error('No names found') +// for (const name of result) { +// expect(name.wrappedOwner).toBe(accounts[1]) +// expect(name.relation.wrappedOwner).toBe(true) +// } +// }) +// it('allows including expired names', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// filter: { +// owner: true, +// registrant: true, +// resolvedAddress: true, +// wrappedOwner: true, +// allowExpired: true, +// }, +// }) +// if (!result.length) throw new Error('No names found') +// const expiredNames = result.filter( +// (x) => x.expiryDate?.date && x.expiryDate.date < new Date(), +// ) +// expect(expiredNames.length).toBeGreaterThan(0) +// }) +// it('allows including reverse record', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[2], +// filter: { +// owner: true, +// registrant: true, +// resolvedAddress: true, +// wrappedOwner: true, +// allowReverseRecord: true, +// }, +// }) +// if (!result.length) throw new Error('No names found') +// const reverseRecordNames = result.filter( +// (x) => x.parentName === 'addr.reverse', +// ) +// expect(reverseRecordNames.length).toBeGreaterThan(0) +// }) +// it('does not include deleted names by default', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// pageSize: 1000, +// }) +// if (!result.length) throw new Error('No names found') +// const deletedNames = result.filter( +// (x) => x.parentName === 'deletable.eth' && x.owner === EMPTY_ADDRESS, +// ) +// expect(deletedNames.length).toBe(0) +// }) +// it('allows including deleted names', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// pageSize: 1000, +// filter: { +// owner: true, +// registrant: true, +// resolvedAddress: true, +// wrappedOwner: true, +// allowDeleted: true, +// }, +// }) +// if (!result.length) throw new Error('No names found') +// const deletedNames = result.filter( +// (x) => x.parentName === 'deletable.eth' && x.owner === EMPTY_ADDRESS, +// ) +// expect(deletedNames.length).toBe(1) +// }) +// it('filters by search string', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// pageSize: 1000, +// filter: { +// owner: true, +// registrant: true, +// resolvedAddress: true, +// wrappedOwner: true, +// searchString: 'test123', +// }, +// }) + +// if (!result.length) throw new Error('No names found') +// for (const name of result) { +// expect(name.labelName).toContain('test123') +// } +// }) +// it('filters by search string - name', async () => { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[2], +// pageSize: 1000, +// filter: { +// owner: true, +// registrant: true, +// resolvedAddress: true, +// wrappedOwner: true, +// searchString: 'wrapped-with-subnames', +// searchType: 'name', +// }, +// }) + +// if (!result.length) throw new Error('No names found') +// const subnames = result.filter( +// (x) => x.parentName === 'wrapped-with-subnames.eth', +// ) +// expect(subnames.length).toBeGreaterThan(0) +// }) +// }) + +// describe.each([ +// { +// orderBy: 'name', +// orderDirection: 'asc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => +// (a.name || '').localeCompare(b.name || ''), +// }, +// { +// orderBy: 'name', +// orderDirection: 'desc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => +// (b.name || '').localeCompare(a.name || ''), +// }, +// { +// orderBy: 'labelName', +// orderDirection: 'asc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => { +// const aLabelName = a.labelName || '' +// const bLabelName = b.labelName || '' +// return aLabelName.localeCompare(bLabelName) +// }, +// }, +// { +// orderBy: 'labelName', +// orderDirection: 'desc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => { +// const aLabelName = a.labelName || '' +// const bLabelName = b.labelName || '' +// return bLabelName.localeCompare(aLabelName) +// }, +// }, +// { +// orderBy: 'expiryDate', +// orderDirection: 'asc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => { +// const aExpiryDate = a.expiryDate?.value || Infinity +// const bExpiryDate = b.expiryDate?.value || Infinity +// return aExpiryDate - bExpiryDate +// }, +// }, +// { +// orderBy: 'expiryDate', +// orderDirection: 'desc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => { +// const aExpiryDate = a.expiryDate?.value || Infinity +// const bExpiryDate = b.expiryDate?.value || Infinity +// return bExpiryDate - aExpiryDate +// }, +// }, +// { +// orderBy: 'createdAt', +// orderDirection: 'asc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => { +// const aCreatedAt = a.createdAt.value +// const bCreatedAt = b.createdAt.value +// return aCreatedAt - bCreatedAt +// }, +// }, +// { +// orderBy: 'createdAt', +// orderDirection: 'desc', +// compareFn: (a: NameWithRelation, b: NameWithRelation) => { +// const aCreatedAt = a.createdAt.value +// const bCreatedAt = b.createdAt.value +// return bCreatedAt - aCreatedAt +// }, +// }, +// ])( +// 'filters by $orderBy $orderDirection', +// ({ orderBy, orderDirection, compareFn }) => { +// it('is consistent between full result and paginated results', async () => { +// const fullResult = await getNamesForAddress(publicClient, { +// address: accounts[1], +// orderBy: orderBy as any, +// orderDirection: orderDirection as any, +// pageSize: 1000, +// }) +// if (!fullResult.length) throw new Error('No names found') +// const paginatedResults = [] +// let lastResult: NameWithRelation[] = [] +// do { +// const result = await getNamesForAddress(publicClient, { +// address: accounts[1], +// orderBy: orderBy as any, +// orderDirection: orderDirection as any, +// previousPage: lastResult, +// pageSize: 5, +// }) +// paginatedResults.push(...result) +// lastResult = result +// } while (lastResult.length) + +// expect(paginatedResults.length).toBe(fullResult.length) +// expect(paginatedResults).toStrictEqual(fullResult) +// }) +// it('is sorted correctly', async () => { +// const fullResult = await getNamesForAddress(publicClient, { +// address: accounts[1], +// orderBy: orderBy as any, +// orderDirection: orderDirection as any, +// pageSize: 1000, +// }) +// if (!fullResult.length) throw new Error('No names found') +// const sortedResult = [...fullResult].sort(compareFn) +// expect(fullResult).toEqual(sortedResult) +// }) +// /// ///////////////// +// // it('more than 1 page (30 expiry names)', async () => { +// // const fullResult = await getNamesForAddress(publicClient, { +// // address: accounts[1], +// // orderBy: orderBy as any, +// // orderDirection: orderDirection as any, +// // pageSize: 20, +// // }) +// // console.log(fullResult) +// // if (!fullResult.length) throw new Error('No names found') +// // const sortedResult = [...fullResult].sort(compareFn) +// // expect(fullResult).toEqual(sortedResult) +// // }) +// // it('more than 1 page (30 same expiry names)', async () => { +// // const fullResult = await getNamesForAddress(publicClient, { +// // address: accounts[1], +// // orderBy: orderBy as any, +// // orderDirection: orderDirection as any, +// // pageSize: 20, +// // }) +// // console.log(fullResult) +// // if (!fullResult.length) throw new Error('No names found') +// // const sortedResult = [...fullResult].sort(compareFn) +// // expect(fullResult).toEqual(sortedResult) +// // }) +// // it('more than 1 page (30 non-expiry names)', async () => { +// // const fullResult = await getNamesForAddress(publicClient, { +// // address: accounts[1], +// // orderBy: orderBy as any, +// // orderDirection: orderDirection as any, +// // pageSize: 20, +// // }) +// // console.log(fullResult) +// // if (!fullResult.length) throw new Error('No names found') +// // const sortedResult = [...fullResult].sort(compareFn) +// // expect(fullResult).toEqual(sortedResult) +// // }) +// // it('mix names (30 expiry, 10 non-expiry)', async () => { +// // const fullResult = await getNamesForAddress(publicClient, { +// // address: accounts[1], +// // orderBy: orderBy as any, +// // orderDirection: orderDirection as any, +// // pageSize: 20, +// // }) +// // console.log(fullResult) +// // if (!fullResult.length) throw new Error('No names found') +// // const sortedResult = [...fullResult].sort(compareFn) +// // expect(fullResult).toEqual(sortedResult) +// // }) +// // it('mix names (30 same expiry, 10 expiry)', async () => { +// // const fullResult = await getNamesForAddress(publicClient, { +// // address: accounts[1], +// // orderBy: orderBy as any, +// // orderDirection: orderDirection as any, +// // pageSize: 20, +// // }) +// // console.log(fullResult) +// // if (!fullResult.length) throw new Error('No names found') +// // const sortedResult = [...fullResult].sort(compareFn) +// // expect(fullResult).toEqual(sortedResult) +// // }) +// // it('mix names (30 same expiry , 10 no-expiry)', async () => { +// // const fullResult = await getNamesForAddress(publicClient, { +// // address: accounts[1], +// // orderBy: orderBy as any, +// // orderDirection: orderDirection as any, +// // pageSize: 20, +// // }) +// // console.log(fullResult) +// // if (!fullResult.length) throw new Error('No names found') +// // const sortedResult = [...fullResult].sort(compareFn) +// // expect(fullResult).toEqual(sortedResult) +// // }) +// /// ////////////////// +// }, +// ) diff --git a/packages/ensjs/src/functions/subgraph/getNamesForAddress.ts b/packages/ensjs/src/functions/subgraph/getNamesForAddress.ts index 3054e2a3..444eea3d 100644 --- a/packages/ensjs/src/functions/subgraph/getNamesForAddress.ts +++ b/packages/ensjs/src/functions/subgraph/getNamesForAddress.ts @@ -7,9 +7,13 @@ import { InvalidFilterKeyError, InvalidOrderByError, } from '../../errors/subgraph.js' -import { EMPTY_ADDRESS, GRACE_PERIOD_SECONDS } from '../../utils/consts.js' +import { EMPTY_ADDRESS } from '../../utils/consts.js' import { createSubgraphClient } from './client.js' -import type { DomainFilter } from './filters.js' +import { + getExpiryDateOrderFilter, + type DomainFilter, + getCreatedAtOrderFilter, +} from './filters.js' import { domainDetailsFragment, registrationDetailsFragment, @@ -92,34 +96,12 @@ const getOrderByFilter = ({ >): DomainFilter => { const lastDomain = previousPage[previousPage.length - 1] const operator = orderDirection === 'asc' ? 'gt' : 'lt' - switch (orderBy) { case 'expiryDate': { - let lastExpiryDate = lastDomain.expiryDate?.value - ? lastDomain.expiryDate.value / 1000 - : 0 - if (lastDomain.parentName === 'eth') { - lastExpiryDate += GRACE_PERIOD_SECONDS - } - - if (orderDirection === 'asc' && lastExpiryDate === 0) { - return { - and: [{ expiryDate: null }, { [`id_${operator}`]: lastDomain.id }], - } - } - if (orderDirection === 'desc' && lastExpiryDate !== 0) { - return { - [`expiryDate_${operator}`]: `${lastExpiryDate}`, - } - } - return { - or: [ - { - [`expiryDate_${operator}`]: `${lastExpiryDate}`, - }, - { expiryDate: null }, - ], - } + return getExpiryDateOrderFilter({ + orderDirection, + lastDomain, + }) } case 'name': { return { @@ -132,9 +114,7 @@ const getOrderByFilter = ({ } } case 'createdAt': { - return { - [`createdAt_${operator}`]: `${lastDomain.createdAt.value / 1000}`, - } + return getCreatedAtOrderFilter({ lastDomain, orderDirection }) } default: throw new InvalidOrderByError({ @@ -195,6 +175,7 @@ const getNamesForAddress = async ( searchType, ...filters } = filter + const ownerWhereFilters: DomainFilter[] = Object.entries(filters).reduce( (prev, [key, value]) => { if (value) { diff --git a/packages/ensjs/src/functions/subgraph/getSubnames.ts b/packages/ensjs/src/functions/subgraph/getSubnames.ts index a53b1a3e..8315531e 100644 --- a/packages/ensjs/src/functions/subgraph/getSubnames.ts +++ b/packages/ensjs/src/functions/subgraph/getSubnames.ts @@ -2,10 +2,14 @@ import { gql } from 'graphql-request' import type { ClientWithEns } from '../../contracts/consts.js' import { InvalidOrderByError } from '../../errors/subgraph.js' -import { EMPTY_ADDRESS, GRACE_PERIOD_SECONDS } from '../../utils/consts.js' +import { EMPTY_ADDRESS } from '../../utils/consts.js' import { namehash } from '../../utils/normalise.js' import { createSubgraphClient } from './client.js' -import type { DomainFilter } from './filters.js' +import { + getExpiryDateOrderFilter, + type DomainFilter, + getCreatedAtOrderFilter, +} from './filters.js' import { domainDetailsWithoutParentFragment, registrationDetailsFragment, @@ -44,46 +48,21 @@ type SubgraphResult = { } const getOrderByFilter = ({ - name, orderBy, orderDirection, previousPage, }: Required< - Pick< - GetSubnamesParameters, - 'name' | 'orderBy' | 'orderDirection' | 'previousPage' - > + Pick >): DomainFilter => { const lastDomain = previousPage[previousPage.length - 1] const operator = orderDirection === 'asc' ? 'gt' : 'lt' switch (orderBy) { case 'expiryDate': { - let lastExpiryDate = lastDomain.expiryDate?.value - ? lastDomain.expiryDate.value / 1000 - : 0 - if (name === 'eth' && lastExpiryDate) { - lastExpiryDate += GRACE_PERIOD_SECONDS - } - - if (orderDirection === 'asc' && lastExpiryDate === 0) { - return { - and: [{ expiryDate: null }, { [`id_${operator}`]: lastDomain.id }], - } - } - if (orderDirection === 'desc' && lastExpiryDate !== 0) { - return { - [`expiryDate_${operator}`]: `${lastExpiryDate}`, - } - } - return { - or: [ - { - [`expiryDate_${operator}`]: `${lastExpiryDate}`, - }, - { expiryDate: null }, - ], - } + return getExpiryDateOrderFilter({ + lastDomain, + orderDirection, + }) } case 'name': { return { @@ -96,9 +75,7 @@ const getOrderByFilter = ({ } } case 'createdAt': { - return { - [`createdAt_${operator}`]: `${lastDomain.createdAt.value / 1000}`, - } + return getCreatedAtOrderFilter({ lastDomain, orderDirection }) } default: throw new InvalidOrderByError({ @@ -146,7 +123,6 @@ const getSubnames = async ( if (previousPage?.length) { whereFilters.push( getOrderByFilter({ - name, orderBy, orderDirection, previousPage, diff --git a/packages/ensjs/utils/legacyNameGenerator.cjs b/packages/ensjs/utils/legacyNameGenerator.cjs new file mode 100644 index 00000000..d10eb355 --- /dev/null +++ b/packages/ensjs/utils/legacyNameGenerator.cjs @@ -0,0 +1,73 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const { namehash, labelhash } = require('viem') + +const makeNameGenerator = async (hre, optionalNonceManager) => { + const { getNamedAccounts } = hre + const allNamedAccts = await getNamedAccounts() + const controller = await ethers.getContract('LegacyETHRegistrarController') + const publicResolver = await ethers.getContract('LegacyPublicResolver') + const registry = await ethers.getContract('ENSRegistry') + const nonceManager = optionalNonceManager ?? { getNonce: () => undefined} + + return { + commit: async ({ label, namedOwner, namedAddr }) => { + const secret = + '0x0000000000000000000000000000000000000000000000000000000000000000' + const registrant = allNamedAccts[namedOwner] + const resolver = publicResolver.address + const addr = allNamedAccts[namedAddr] + + const commitment = await controller.makeCommitmentWithConfig( + label, + registrant, + secret, + resolver, + addr, + ) + + const _controller = controller.connect(await ethers.getSigner(registrant)) + return _controller.commit(commitment, {nonce: nonceManager.getNonce(namedOwner)}) + }, + register: async ({ label, namedOwner, namedAddr, duration = 31536000 }) => { + const secret = + '0x0000000000000000000000000000000000000000000000000000000000000000' + const registrant = allNamedAccts[namedOwner] + const resolver = publicResolver.address + const addr = allNamedAccts[namedAddr] + const price = await controller.rentPrice(label, duration) + const _controller = controller.connect(await ethers.getSigner(registrant)) + return _controller.registerWithConfig( + label, + registrant, + duration, + secret, + resolver, + addr, + { + value: price, + nonce: nonceManager.getNonce(namedOwner), + }, + ) + }, + subname: async ({ label, namedOwner, subnameLabel, namedSubnameOwner }) => { + console.log(`Setting subnames for ${label}.eth...`) + const resolver = publicResolver.address + const registrant = allNamedAccts[namedOwner] + const owner = allNamedAccts[namedSubnameOwner] + const _registry = registry.connect(await ethers.getSigner(registrant)) + return _registry.setSubnodeRecord( + namehash(`${label}.eth`), + labelhash(subnameLabel), + owner, + resolver, + '0', + ) + }, + setSubnameRecords: async () => {}, + configure: async () => {}, + } +} + +module.exports = { + makeNameGenerator, +} \ No newline at end of file diff --git a/packages/ensjs/utils/nonceManager.cjs b/packages/ensjs/utils/nonceManager.cjs new file mode 100644 index 00000000..a022bb62 --- /dev/null +++ b/packages/ensjs/utils/nonceManager.cjs @@ -0,0 +1,26 @@ + /* eslint-disable import/no-extraneous-dependencies */ + const { hexToNumber, numberToHex} = require('viem') + + const makeNonceManager = async (href) => { + const { getNamedAccounts, network } = href + const names = ['owner', 'owner2', 'owner3', 'owner4'] + const allNamedAccts = await getNamedAccounts() + const startingNonces = await Promise.all(names.map((name) => network.provider.send('eth_getTransactionCount', [allNamedAccts[name], "latest"]).then(hexToNumber))) + const nonceMap = Object.fromEntries(names.map((name, i) => [name, startingNonces[i]])) + + console.log('Nonce manager initialized', nonceMap) + + return { + getNonce: (name) => { + const nonce = nonceMap[name] + nonceMap[name]++ + return nonce + } + } + + +} + +module.exports = { + makeNonceManager +} \ No newline at end of file diff --git a/packages/ensjs/utils/wrappedNameGenerator.cjs b/packages/ensjs/utils/wrappedNameGenerator.cjs new file mode 100644 index 00000000..35f229ea --- /dev/null +++ b/packages/ensjs/utils/wrappedNameGenerator.cjs @@ -0,0 +1,101 @@ +/* eslint-disable import/no-extraneous-dependencies */ + +const { BigNumber } = require('ethers') +const { namehash } = require('viem/ens') + +const makeNameGenerator = async (hre, optionalNonceManager) => { + const { getNamedAccounts } = hre + const allNamedAccts = await getNamedAccounts() + const controller = await ethers.getContract('ETHRegistrarController') + const publicResolver = await ethers.getContract('PublicResolver') + const nameWrapper = await ethers.getContract('NameWrapper') + const nonceManager = optionalNonceManager ?? { getNonce: () => undefined } + + return { + commit: async ({ + label, + namedOwner, + data = [], + reverseRecord = false, + fuses = 0, + duration = 31536000, + }) => { + const secret = + '0x0000000000000000000000000000000000000000000000000000000000000000' + const owner = allNamedAccts[namedOwner] + const resolver = publicResolver.address + const commitment = await controller.makeCommitment( + label, + owner, + duration, + secret, + resolver, + data, + reverseRecord, + fuses, + ) + + const _controller = controller.connect(await ethers.getSigner(owner)) + return _controller.commit(commitment, { + nonce: nonceManager.getNonce(namedOwner), + }) + }, + register: async ({ + label, + namedOwner, + data = [], + reverseRecord = false, + fuses = 0, + duration = 31536000, + }) => { + const secret = + '0x0000000000000000000000000000000000000000000000000000000000000000' + const owner = allNamedAccts[namedOwner] + const resolver = publicResolver.address + const [price] = await controller.rentPrice(label, duration) + + const priceWithBuffer = BigNumber.from(price).mul(105).div(100) + const _controller = controller.connect(await ethers.getSigner(owner)) + return _controller.register( + label, + owner, + duration, + secret, + resolver, + data, + reverseRecord, + fuses, + { + value: priceWithBuffer, + nonce: nonceManager.getNonce(namedOwner), + }, + ) + }, + subname: async ({ + label, + namedOwner, + subnameLabel, + namedSubnameOwner, + subnameFuses = 0, + subnameExpiry = BigNumber.from(2).pow(64).sub(1), + }) => { + const resolver = publicResolver.address + const owner = allNamedAccts[namedOwner] + const subnameOwner = allNamedAccts[namedSubnameOwner] + const _nameWrapper = nameWrapper.connect(await ethers.getSigner(owner)) + return _nameWrapper.setSubnodeRecord( + namehash(`${label}.eth`), + subnameLabel, + subnameOwner, + resolver, + '0', + subnameFuses, + subnameExpiry, + ) + }, + } +} + +module.exports = { + makeNameGenerator, +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c53c3c65..2f7f87d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: pako: specifier: ^2.1.0 version: 2.1.0 + ts-pattern: + specifier: ^5.4.0 + version: 5.4.0 devDependencies: '@ensdomains/buffer': specifier: ^0.0.13 @@ -6912,6 +6915,9 @@ packages: '@swc/wasm': optional: true + ts-pattern@5.4.0: + resolution: {integrity: sha512-hgfOMfjlrARCnYtGD/xEAkFHDXuSyuqjzFSltyQCbN689uNvoQL20TVN2XFcLMjfNuwSsQGU+xtH6MrjIwhwUg==} + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -16616,6 +16622,8 @@ snapshots: optionalDependencies: '@swc/core': 1.7.5 + ts-pattern@5.4.0: {} + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29