diff --git a/.changeset/omnigraph-resolution-api.md b/.changeset/omnigraph-resolution-api.md index 8c6f14748d..6312e6f99d 100644 --- a/.changeset/omnigraph-resolution-api.md +++ b/.changeset/omnigraph-resolution-api.md @@ -5,5 +5,5 @@ Changes related to **Omnigraph**: - add `Domain.resolve { records, trace, acceleration, profile? }` for forward resolution driven by the GraphQL selection set -- add `Account.resolve { primaryName(by: ...), primaryNames(where: ...) }` for reverse (ENSIP-19 primary name) resolution with `@oneOf` inputs (`coinType`/`chain`, `coinTypes`/`chains`) +- add `Account.resolve { primaryName(by: ...), primaryNames(where: ...) }` for reverse (ENSIP-19 primary name) resolution with `@oneOf` inputs (`coinType`/`chainName`, `coinTypes`/`chainNames`) - add `PrimaryNameRecord.resolve { records, ... }` for forward resolution of the resolved primary name diff --git a/apps/ensapi/src/lib/resolution/accelerate-known-onchain-static-resolver.ts b/apps/ensapi/src/lib/resolution/accelerate-known-onchain-static-resolver.ts index 98b8d461c7..0ce3f5f85a 100644 --- a/apps/ensapi/src/lib/resolution/accelerate-known-onchain-static-resolver.ts +++ b/apps/ensapi/src/lib/resolution/accelerate-known-onchain-static-resolver.ts @@ -1,4 +1,4 @@ -import type { AccountId, Node } from "enssdk"; +import type { AccountId, Hex, Node } from "enssdk"; import type { ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; @@ -50,7 +50,7 @@ function resolveOperationWithIndex(op: Operation, records: IndexedRecords): Oper case "addr": { const coinType = op.args[1]; const found = records?.addressRecords.find((r) => r.coinType === coinType); - return { ...op, result: found?.value ?? null }; + return { ...op, result: (found?.value ?? null) as Hex | null }; } case "text": { const key = op.args[1]; diff --git a/apps/ensapi/src/lib/resolution/make-records-response.test.ts b/apps/ensapi/src/lib/resolution/make-records-response.test.ts index e9c31db5c1..6022fc481d 100644 --- a/apps/ensapi/src/lib/resolution/make-records-response.test.ts +++ b/apps/ensapi/src/lib/resolution/make-records-response.test.ts @@ -18,11 +18,11 @@ describe("makeRecordsResponse", () => { it("writes resolved address records keyed by CoinType", () => { const operations = [ - { functionName: "addr", args: [node, 60n], result: "0x123" }, - { functionName: "addr", args: [node, 1001n], result: "0x456" }, + { functionName: "addr", args: [node, 60n], result: "0x1234" as Hex }, + { functionName: "addr", args: [node, 1001n], result: "0x5678" as Hex }, ] satisfies Operation[]; expect(makeRecordsResponse(operations)).toEqual({ - addresses: { 60: "0x123", 1001: "0x456" }, + addresses: { 60: "0x1234", 1001: "0x5678" }, }); }); diff --git a/apps/ensapi/src/lib/resolution/operations.ts b/apps/ensapi/src/lib/resolution/operations.ts index 88e3f1dc27..52c2b12b9b 100644 --- a/apps/ensapi/src/lib/resolution/operations.ts +++ b/apps/ensapi/src/lib/resolution/operations.ts @@ -18,7 +18,7 @@ import type { ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; */ type OperationMap = { name: { args: readonly [Node]; result: InterpretedName | null }; - addr: { args: readonly [Node, bigint]; result: string | null }; + addr: { args: readonly [Node, bigint]; result: Hex | null }; text: { args: readonly [Node, string]; result: string | null }; contenthash: { args: readonly [Node]; result: Hex | null }; pubkey: { args: readonly [Node]; result: { x: Hex; y: Hex } | null }; diff --git a/apps/ensapi/src/omnigraph-api/lib/resolution/account-primary-names-selection.test.ts b/apps/ensapi/src/omnigraph-api/lib/resolution/account-primary-names-selection.test.ts index 75ddd4b0f1..187fb5e356 100644 --- a/apps/ensapi/src/omnigraph-api/lib/resolution/account-primary-names-selection.test.ts +++ b/apps/ensapi/src/omnigraph-api/lib/resolution/account-primary-names-selection.test.ts @@ -23,7 +23,7 @@ const PrimaryNameByInputType = new GraphQLInputObjectType({ name: "PrimaryNameByInput", fields: { coinType: { type: GraphQLInt }, - chain: { type: GraphQLString }, + chainName: { type: GraphQLString }, }, }); @@ -31,7 +31,7 @@ const PrimaryNamesWhereInputType = new GraphQLInputObjectType({ name: "PrimaryNamesWhereInput", fields: { coinTypes: { type: new GraphQLList(GraphQLInt) }, - chains: { type: new GraphQLList(GraphQLString) }, + chainNames: { type: new GraphQLList(GraphQLString) }, }, }); @@ -85,9 +85,9 @@ describe("buildAccountPrimaryNamesSelection", () => { expect(buildAccountPrimaryNamesSelection(info)).toEqual([60, 0]); }); - it("extracts coin type from primaryName(by: { chain: ETHEREUM })", () => { + it("extracts coin type from primaryName(by: { chainName: ETHEREUM })", () => { const info = resolveInfoForAccountResolveSubselection( - 'primaryName(by: { chain: "ETHEREUM" }) { name }', + 'primaryName(by: { chainName: "ETHEREUM" }) { name }', ); expect(buildAccountPrimaryNamesSelection(info)).toEqual([coinNameToTypeMap.eth]); }); @@ -105,14 +105,13 @@ describe("buildAccountPrimaryNamesSelection", () => { one: primaryName(by: { coinType: ${coinNameToTypeMap.btc} }) { name } two: primaryName(by: { coinType: ${coinNameToTypeMap.ltc} }) { name } three: primaryNames(where: { coinTypes: [${coinNameToTypeMap.doge}, ${coinNameToTypeMap.sol}] }) { name } - four: primaryNames(where: { chains: ["DEFAULT", "ETHEREUM", "ARBITRUM_ONE"] }) { name } - five: primaryName(by: { chain: "BASE" }) { name } + four: primaryNames(where: { chainNames: ["ETHEREUM", "ARBITRUM_ONE"] }) { name } + five: primaryName(by: { chainName: "BASE" }) { name } `); expect(buildAccountPrimaryNamesSelection(info)).toEqual([ coinNameToTypeMap.doge, coinNameToTypeMap.sol, - coinNameToTypeMap.default, coinNameToTypeMap.eth, coinNameToTypeMap.arb1, coinNameToTypeMap.btc, diff --git a/apps/ensapi/src/omnigraph-api/lib/resolution/chain-coin-type.ts b/apps/ensapi/src/omnigraph-api/lib/resolution/chain-coin-type.ts index 83f2e248cb..d6621b26f4 100644 --- a/apps/ensapi/src/omnigraph-api/lib/resolution/chain-coin-type.ts +++ b/apps/ensapi/src/omnigraph-api/lib/resolution/chain-coin-type.ts @@ -7,7 +7,6 @@ import type { CoinType } from "enssdk"; * GraphQL `ChainName` enum values. */ export const CHAIN_NAME_ENTRIES = [ - ["default", "DEFAULT"], ["eth", "ETHEREUM"], ["base", "BASE"], ["op", "OPTIMISM"], diff --git a/apps/ensapi/src/omnigraph-api/lib/resolution/primary-name-input.ts b/apps/ensapi/src/omnigraph-api/lib/resolution/primary-name-input.ts index 088ba708e4..9c0e7df02c 100644 --- a/apps/ensapi/src/omnigraph-api/lib/resolution/primary-name-input.ts +++ b/apps/ensapi/src/omnigraph-api/lib/resolution/primary-name-input.ts @@ -11,9 +11,9 @@ import type { */ export const normalizePrimaryNameByInput = (by: PrimaryNameByInputValue): CoinType => { if (by.coinType != null) return by.coinType; - if (by.chain != null) return chainNameToCoinType(by.chain); + if (by.chainName != null) return chainNameToCoinType(by.chainName); // this should never happen as the schema with `@oneOf` prevents it - throw new Error("PrimaryNameByInput must specify exactly one of coinType or chain."); + throw new Error("PrimaryNameByInput must specify exactly one of coinType or chainName."); }; /** @@ -23,7 +23,7 @@ export const normalizeAccountPrimaryNamesWhereInput = ( where: PrimaryNamesWhereInputValue, ): CoinType[] => { if (where.coinTypes != null) return where.coinTypes; - if (where.chains != null) return where.chains.map(chainNameToCoinType); + if (where.chainNames != null) return where.chainNames.map(chainNameToCoinType); // this should never happen as the schema with `@oneOf` prevents it - throw new Error("PrimaryNamesWhereInput must specify exactly one of coinTypes or chains."); + throw new Error("PrimaryNamesWhereInput must specify exactly one of coinTypes or chainNames."); }; diff --git a/apps/ensapi/src/omnigraph-api/lib/resolution/resolve-primary-name-records.ts b/apps/ensapi/src/omnigraph-api/lib/resolution/resolve-primary-name-records.ts index f8e04c462d..55a4573dd6 100644 --- a/apps/ensapi/src/omnigraph-api/lib/resolution/resolve-primary-name-records.ts +++ b/apps/ensapi/src/omnigraph-api/lib/resolution/resolve-primary-name-records.ts @@ -27,7 +27,7 @@ const toPrimaryNameRecord = ( ): PrimaryNameRecordModel => ({ address, coinType, - chain: coinTypeToChainName(coinType), + chainName: coinTypeToChainName(coinType), name, }); diff --git a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts index 55e4efac97..f34d7f4a24 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts @@ -1,9 +1,4 @@ -import { - DEFAULT_EVM_COIN_TYPE, - ETH_COIN_TYPE, - evmChainIdToCoinType, - type InterpretedName, -} from "enssdk"; +import { ETH_COIN_TYPE, evmChainIdToCoinType, type Hex, type InterpretedName } from "enssdk"; import { base } from "viem/chains"; import { beforeAll, describe, expect, it } from "vitest"; @@ -329,10 +324,10 @@ describe("Account.primaryName and Account.primaryNames", () => { type PrimaryNameRecordResult = { coinType: number; - chain: string | null; + chainName: string | null; name: CanonicalNameResult; resolve?: { - records?: { addresses: Array<{ coinType: number; address: string | null }> } | null; + records?: { addresses: Array<{ coinType: number; address: Hex | null }> } | null; } | null; }; @@ -363,7 +358,7 @@ describe("Account.primaryName and Account.primaryNames", () => { resolve { primaryName(by: { coinType: $coinType }) { coinType - chain + chainName name { interpreted beautified } } } @@ -375,37 +370,9 @@ describe("Account.primaryName and Account.primaryNames", () => { query AccountPrimaryNameByChain($address: Address!) { account(by: { address: $address }) { resolve { - primaryName(by: { chain: ETHEREUM }) { + primaryName(by: { chainName: ETHEREUM }) { coinType - chain - name { interpreted beautified } - } - } - } - } - `; - - const AccountPrimaryNameByDefaultChain = gql` - query AccountPrimaryNameByDefaultChain($address: Address!) { - account(by: { address: $address }) { - resolve { - primaryName(by: { chain: DEFAULT }) { - coinType - chain - name { interpreted beautified } - } - } - } - } - `; - - const AccountPrimaryNamesByDefaultChain = gql` - query AccountPrimaryNamesByDefaultChain($address: Address!) { - account(by: { address: $address }) { - resolve { - primaryNames(where: { chains: [DEFAULT] }) { - coinType - chain + chainName name { interpreted beautified } } } @@ -419,7 +386,7 @@ describe("Account.primaryName and Account.primaryNames", () => { resolve { primaryNames(where: { coinTypes: $coinTypes }) { coinType - chain + chainName name { interpreted beautified } } } @@ -431,9 +398,9 @@ describe("Account.primaryName and Account.primaryNames", () => { query AccountPrimaryNamesByChains($address: Address!) { account(by: { address: $address }) { resolve { - primaryNames(where: { chains: [ETHEREUM, BASE] }) { + primaryNames(where: { chainNames: [ETHEREUM, BASE] }) { coinType - chain + chainName name { interpreted beautified } } } @@ -447,7 +414,7 @@ describe("Account.primaryName and Account.primaryNames", () => { resolve { primaryName(by: { coinType: 0 }) { coinType - chain + chainName name { interpreted beautified } resolve { records { @@ -486,13 +453,13 @@ describe("Account.primaryName and Account.primaryNames", () => { ).resolves.toEqual({ account: { resolve: { - primaryName: { coinType: ETH_COIN_TYPE, chain: "ETHEREUM", name: TEST_ETH_NAME }, + primaryName: { coinType: ETH_COIN_TYPE, chainName: "ETHEREUM", name: TEST_ETH_NAME }, }, }, }); }); - it("resolves the same primary name by chain as by coinType", async () => { + it("resolves the same primary name by chainName as by coinType", async () => { await expect( request(AccountPrimaryNameByChain, { address: accounts.owner.address, @@ -500,45 +467,7 @@ describe("Account.primaryName and Account.primaryNames", () => { ).resolves.toEqual({ account: { resolve: { - primaryName: { coinType: ETH_COIN_TYPE, chain: "ETHEREUM", name: TEST_ETH_NAME }, - }, - }, - }); - }); - - it("accepts DEFAULT and maps it to the ENSIP-19 default EVM coin type", async () => { - await expect( - request(AccountPrimaryNameByDefaultChain, { - address: accounts.owner.address, - }), - ).resolves.toEqual({ - account: { - resolve: { - primaryName: { - coinType: DEFAULT_EVM_COIN_TYPE, - chain: "DEFAULT", - name: null, - }, - }, - }, - }); - }); - - it("resolves primary names for DEFAULT", async () => { - await expect( - request(AccountPrimaryNamesByDefaultChain, { - address: accounts.owner.address, - }), - ).resolves.toEqual({ - account: { - resolve: { - primaryNames: [ - { - coinType: DEFAULT_EVM_COIN_TYPE, - chain: "DEFAULT", - name: null, - }, - ], + primaryName: { coinType: ETH_COIN_TYPE, chainName: "ETHEREUM", name: TEST_ETH_NAME }, }, }, }); @@ -553,7 +482,7 @@ describe("Account.primaryName and Account.primaryNames", () => { ).resolves.toEqual({ account: { resolve: { - primaryName: { coinType: ETH_COIN_TYPE, chain: "ETHEREUM", name: null }, + primaryName: { coinType: ETH_COIN_TYPE, chainName: "ETHEREUM", name: null }, }, }, }); @@ -569,15 +498,15 @@ describe("Account.primaryName and Account.primaryNames", () => { account: { resolve: { primaryNames: [ - { coinType: ETH_COIN_TYPE, chain: "ETHEREUM", name: TEST_ETH_NAME }, - { coinType: BASE_COIN_TYPE, chain: "BASE", name: null }, + { coinType: ETH_COIN_TYPE, chainName: "ETHEREUM", name: TEST_ETH_NAME }, + { coinType: BASE_COIN_TYPE, chainName: "BASE", name: null }, ], }, }, }); }); - it("resolves primary names for requested chains", async () => { + it("resolves primary names for requested chainNames", async () => { await expect( request(AccountPrimaryNamesByChains, { address: accounts.owner.address, @@ -586,15 +515,15 @@ describe("Account.primaryName and Account.primaryNames", () => { account: { resolve: { primaryNames: [ - { coinType: ETH_COIN_TYPE, chain: "ETHEREUM", name: TEST_ETH_NAME }, - { coinType: BASE_COIN_TYPE, chain: "BASE", name: null }, + { coinType: ETH_COIN_TYPE, chainName: "ETHEREUM", name: TEST_ETH_NAME }, + { coinType: BASE_COIN_TYPE, chainName: "BASE", name: null }, ], }, }, }); }); - it("returns null name and chain for non-ENSIP-19 coin types", async () => { + it("returns null name and chainName for non-ENSIP-19 coin types", async () => { await expect( request(AccountPrimaryNameNonEnsip19, { address: accounts.owner.address, @@ -604,7 +533,7 @@ describe("Account.primaryName and Account.primaryNames", () => { resolve: { primaryName: { coinType: 0, - chain: null, + chainName: null, name: null, resolve: { records: null, @@ -645,14 +574,14 @@ describe("Account.primaryName and Account.primaryNames", () => { ).rejects.toThrow(); }); - it("rejects empty chains at GraphQL validation", async () => { + it("rejects empty chainNames at GraphQL validation", async () => { await expect( request( gql` - query AccountPrimaryNamesEmptyChains($address: Address!) { + query AccountPrimaryNamesEmptyChainNames($address: Address!) { account(by: { address: $address }) { resolve { - primaryNames(where: { chains: [] }) { coinType } + primaryNames(where: { chainNames: [] }) { coinType } } } } diff --git a/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts index 4e4a2ffd27..df9c6aa712 100644 --- a/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts @@ -515,7 +515,6 @@ describe("Domain.records", () => { version: string | null; abi: { contentType: ContentType; data: string } | null; interfaces: Array<{ interfaceId: string; implementer: string | null }>; - // address is string (not Hex/NormalizedAddress) because non-EVM records may use non-Ethereum formats; matches GraphQL String field addresses: Array<{ coinType: CoinType; address: Hex | null }>; texts: Array<{ key: string; value: string | null }>; } | null; diff --git a/apps/ensapi/src/omnigraph-api/schema/primary-name-record.ts b/apps/ensapi/src/omnigraph-api/schema/primary-name-record.ts index 1148c42791..edcdef198c 100644 --- a/apps/ensapi/src/omnigraph-api/schema/primary-name-record.ts +++ b/apps/ensapi/src/omnigraph-api/schema/primary-name-record.ts @@ -16,7 +16,7 @@ import { ChainName } from "@/omnigraph-api/schema/resolution"; export type PrimaryNameRecordModel = { address: Address; coinType: CoinType; - chain: ChainNameValue | null; + chainName: ChainNameValue | null; name: InterpretedName | null; }; @@ -36,12 +36,12 @@ PrimaryNameRecordRef.implement({ nullable: false, resolve: (r) => r.coinType, }), - chain: t.field({ + chainName: t.field({ description: "The chain corresponding to `coinType`, or null when `coinType` is not represented in `ChainName`.", type: ChainName, nullable: true, - resolve: (r) => r.chain, + resolve: (r) => r.chainName, }), name: t.field({ description: diff --git a/apps/ensapi/src/omnigraph-api/schema/records.ts b/apps/ensapi/src/omnigraph-api/schema/records.ts index 0a4a9ccecc..6b984e4dd8 100644 --- a/apps/ensapi/src/omnigraph-api/schema/records.ts +++ b/apps/ensapi/src/omnigraph-api/schema/records.ts @@ -31,7 +31,7 @@ ResolvedRawTextRecordRef.implement({ /////////////////////////// export const ResolvedAddressRecordRef = builder.objectRef<{ coinType: CoinType; - address: string | null; + address: Hex | null; }>("ResolvedAddressRecord"); ResolvedAddressRecordRef.implement({ @@ -43,9 +43,10 @@ ResolvedAddressRecordRef.implement({ nullable: false, resolve: (r) => r.coinType, }), - address: t.exposeString("address", { + address: t.expose("address", { + type: "Hex", description: - "The raw address record value, or null if not set. May be any address format for the associated coin type and may require validation or preprocessing before use. Only EVM addresses are guaranteed to be NormalizedAddress values; see ENSIP-9 (https://docs.ens.domains/ensip/9) and address-encoder (https://github.com/ensdomains/address-encoder).", + 'The "raw" resolved address record as hex, or null if not set. Decode with ENSIP-9 (https://docs.ens.domains/ensip/9) and address-encoder (https://github.com/ensdomains/address-encoder) for the associated coin type. May be a hex value representing 0 or more bytes. There is no guarantee that an EVM coinType returns an address value of any particular byte length.', nullable: true, }), }), diff --git a/apps/ensapi/src/omnigraph-api/schema/resolution.ts b/apps/ensapi/src/omnigraph-api/schema/resolution.ts index 5722d7eb1a..4ddf1fddaa 100644 --- a/apps/ensapi/src/omnigraph-api/schema/resolution.ts +++ b/apps/ensapi/src/omnigraph-api/schema/resolution.ts @@ -31,7 +31,7 @@ AccelerationStatusRef.implement({ ////////////////// export const ChainName = builder.enumType("ChainName", { description: - "Primary-name chains supported by the Omnigraph API. Use `DEFAULT` for the default EVM chain.\n@see https://github.com/ensdomains/address-encoder/blob/master/docs/supported-cryptocurrencies.md for more details.", + "The names of chains that the Omnigraph API supports identifying by name as a syntactic convenience. The Omnigraph API supports identification of additional chains beyond this list, but those chains must be identified through other mechanisms such as `coinType` or `chainId`.", values: CHAIN_NAME_VALUES, }); @@ -40,14 +40,14 @@ export const ChainName = builder.enumType("ChainName", { /////////////////////// export const PrimaryNameByInput = builder.inputType("PrimaryNameByInput", { description: - "Select a primary name lookup target. Exactly one of `coinType` or `chain` must be provided.", + "Select a primary name lookup target. Exactly one of `coinType` or `chainName` must be provided.", isOneOf: true, fields: (t) => ({ coinType: t.field({ type: "CoinType", description: "The ENSIP-9 coin type to resolve the primary name for.", }), - chain: t.field({ + chainName: t.field({ type: ChainName, description: "A `ChainName` to resolve the primary name for.", }), @@ -58,7 +58,7 @@ export type PrimaryNameByInputValue = typeof PrimaryNameByInput.$inferInput; export const PrimaryNamesWhereInput = builder.inputType("PrimaryNamesWhereInput", { description: - "Filter primary name lookups. Exactly one of `coinTypes` or `chains` must be provided.", + "Filter primary name lookups. Exactly one of `coinTypes` or `chainNames` must be provided.", isOneOf: true, fields: (t) => ({ coinTypes: t.field({ @@ -66,7 +66,7 @@ export const PrimaryNamesWhereInput = builder.inputType("PrimaryNamesWhereInput" description: "Coin types to resolve primary names for.", validate: { minLength: 1 }, }), - chains: t.field({ + chainNames: t.field({ type: [ChainName], description: "`ChainName` values to resolve primary names for.", validate: { minLength: 1 }, diff --git a/apps/ensapi/src/omnigraph-api/schema/reverse-resolve.ts b/apps/ensapi/src/omnigraph-api/schema/reverse-resolve.ts index 091cd48d2b..9f6f74709a 100644 --- a/apps/ensapi/src/omnigraph-api/schema/reverse-resolve.ts +++ b/apps/ensapi/src/omnigraph-api/schema/reverse-resolve.ts @@ -49,14 +49,14 @@ ReverseResolveRef.implement({ }), }), primaryName: t.field({ - description: "The primary name for this Account on a specific coin type or chain.", + description: "The primary name for this Account on a specific coin type or chain name.", type: PrimaryNameRecordRef, nullable: false, args: { by: t.arg({ type: PrimaryNameByInput, required: true, - description: "Select a coin type or chain to resolve a primary name for.", + description: "Select a coin type or chain name to resolve a primary name for.", }), }, resolve: ({ records, accelerate }, { by }) => { @@ -69,14 +69,14 @@ ReverseResolveRef.implement({ }, }), primaryNames: t.field({ - description: "Primary names for this Account on the requested coin types or chains.", + description: "Primary names for this Account on the requested coin types or chain names.", type: [PrimaryNameRecordRef], nullable: false, args: { where: t.arg({ type: PrimaryNamesWhereInput, required: true, - description: "Select coin types or chains to resolve primary names for.", + description: "Select coin types or chain names to resolve primary names for.", }), }, resolve: ({ records, accelerate }, { where }) => { diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index ca8f741064..de2675e9e3 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -341,9 +341,9 @@ query AccountPrimaryNames($address: Address!) { account(by: { address: $address }) { address resolve { - primaryNames(where: { chains: [ETHEREUM, BASE] }) { + primaryNames(where: { chainNames: [ETHEREUM, BASE] }) { coinType - chain + chainName name { interpreted beautified } resolve { records { diff --git a/packages/ensnode-sdk/src/resolution/resolver-records-response.ts b/packages/ensnode-sdk/src/resolution/resolver-records-response.ts index e0d0d77f72..16acb3e449 100644 --- a/packages/ensnode-sdk/src/resolution/resolver-records-response.ts +++ b/packages/ensnode-sdk/src/resolution/resolver-records-response.ts @@ -18,11 +18,10 @@ export type ResolverRecordsResponseBase = { * Address records, keyed by CoinType. * Value is null if no record for the specified CoinType is set. * - * NOTE: ENS resolver address records can store arbitrary string values, - * including non-EVM addresses — always validate the record value against - * the format your application expects. + * Values are the "raw" resolved address record as hex. May be a hex value representing 0 or more bytes. + * There is no guarantee that an EVM coinType returns an address value of any particular byte length. */ - addresses: Record; + addresses: Record; /** * Text records, keyed by key. @@ -82,7 +81,7 @@ export type ResolverRecordsResponseBase = { * // results in the following type * type Response = { * readonly name: Name | null; - * readonly addresses: Record<"60", string | null>; + * readonly addresses: Record<"60", Hex | null>; * readonly texts: Record<"avatar" | "com.twitter", string | null>; * } * ``` @@ -92,7 +91,7 @@ export type ResolverRecordsResponse : K extends "texts" ? Record diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.test.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.test.ts index ddfe16ce05..b7b12c647e 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.test.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.test.ts @@ -31,8 +31,14 @@ describe("interpretAddressRecordValue", () => { expect(interpretAddressRecordValue("0x")).toBeNull(); }); - it("returns as-is for non-EVM address", () => { - expect(interpretAddressRecordValue("someNonEvmAddress")).toBe("someNonEvmAddress"); + it("returns null for non-hex values", () => { + expect(interpretAddressRecordValue("someNonHexString")).toBeNull(); + }); + + it("returns lowercase hex for non-EVM address bytes", () => { + expect(interpretAddressRecordValue(`0x${"05ab".repeat(20).toUpperCase()}`)).toBe( + `0x${"05ab".repeat(20)}`, + ); }); it("returns null for zeroAddress", () => { diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.ts index 6e62c712b1..8f6719b07a 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/interpret-record-values.ts @@ -1,6 +1,6 @@ -import type { InterpretedName, LiteralName } from "enssdk"; -import { isInterpretedName, toNormalizedAddress } from "enssdk"; -import { isAddress, isAddressEqual, zeroAddress } from "viem"; +import type { Hex, InterpretedName, LiteralName } from "enssdk"; +import { isInterpretedName } from "enssdk"; +import { isAddress, isAddressEqual, isHex, zeroAddress } from "viem"; import { hasNullByte } from "../null-bytes"; @@ -29,7 +29,7 @@ export function interpretNameRecordValue(value: LiteralName): InterpretedName | } /** - * Interprets an address record value string and returns null if the value is interpreted as a deletion. + * Interprets an address record value and returns null if the value is interpreted as a deletion. * * The interpreted record value is either: * a) null, representing a non-existent or deletion of the record, or @@ -37,14 +37,13 @@ export function interpretNameRecordValue(value: LiteralName): InterpretedName | * ii. empty string * iii. empty hex (0x) * iv. zeroAddress - * b) an address record value that - * i. does not contain null bytes - * ii. (if is an EVM address) is lowercase + * v. not valid hex + * b) the on-chain record bytes as hex * * @param value - The address record value to interpret. - * @returns The interpreted address string or null if deleted. + * @returns The interpreted address bytes as hex or null if deleted. */ -export function interpretAddressRecordValue(value: string): string | null { +export function interpretAddressRecordValue(value: string): Hex | null { // TODO(null-bytes): store null bytes correctly — for now, interpret as deletion if (hasNullByte(value)) return null; @@ -54,14 +53,14 @@ export function interpretAddressRecordValue(value: string): string | null { // interpret empty bytes as deletion of address record if (value === "0x") return null; - // if it's not an EVM address, return as-is - if (!isAddress(value, { strict: false })) return value; + if (!isHex(value)) return null; + + const hex = value.toLowerCase() as Hex; // interpret zeroAddress as deletion - if (isAddressEqual(value, zeroAddress)) return null; + if (isAddress(hex, { strict: false }) && isAddressEqual(hex, zeroAddress)) return null; - // otherwise normalize and return - return toNormalizedAddress(value); + return hex; } /** diff --git a/packages/enssdk/src/omnigraph/generated/introspection.ts b/packages/enssdk/src/omnigraph/generated/introspection.ts index b51975a7d0..2092a9769d 100644 --- a/packages/enssdk/src/omnigraph/generated/introspection.ts +++ b/packages/enssdk/src/omnigraph/generated/introspection.ts @@ -1144,10 +1144,6 @@ const introspection = { "name": "BASE", "isDeprecated": false }, - { - "name": "DEFAULT", - "isDeprecated": false - }, { "name": "ETHEREUM", "isDeprecated": false @@ -4832,7 +4828,7 @@ const introspection = { "name": "PrimaryNameByInput", "inputFields": [ { - "name": "chain", + "name": "chainName", "type": { "kind": "ENUM", "name": "ChainName" @@ -4853,7 +4849,7 @@ const introspection = { "name": "PrimaryNameRecord", "fields": [ { - "name": "chain", + "name": "chainName", "type": { "kind": "ENUM", "name": "ChainName" @@ -4902,7 +4898,7 @@ const introspection = { "name": "PrimaryNamesWhereInput", "inputFields": [ { - "name": "chains", + "name": "chainNames", "type": { "kind": "LIST", "ofType": { @@ -6128,7 +6124,7 @@ const introspection = { "name": "address", "type": { "kind": "SCALAR", - "name": "String" + "name": "Hex" }, "args": [], "isDeprecated": false diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql index 27d0a5a62d..de0e279676 100644 --- a/packages/enssdk/src/omnigraph/generated/schema.graphql +++ b/packages/enssdk/src/omnigraph/generated/schema.graphql @@ -254,13 +254,11 @@ type CanonicalName { scalar ChainId """ -Primary-name chains supported by the Omnigraph API. Use `DEFAULT` for the default EVM chain. -@see https://github.com/ensdomains/address-encoder/blob/master/docs/supported-cryptocurrencies.md for more details. +The names of chains that the Omnigraph API supports identifying by name as a syntactic convenience. The Omnigraph API supports identification of additional chains beyond this list, but those chains must be identified through other mechanisms such as `coinType` or `chainId`. """ enum ChainName { ARBITRUM_ONE BASE - DEFAULT ETHEREUM LINEA OPTIMISM @@ -1174,11 +1172,11 @@ type PermissionsUserEventsConnectionEdge { scalar PermissionsUserId """ -Select a primary name lookup target. Exactly one of `coinType` or `chain` must be provided. +Select a primary name lookup target. Exactly one of `coinType` or `chainName` must be provided. """ input PrimaryNameByInput @oneOf { """A `ChainName` to resolve the primary name for.""" - chain: ChainName + chainName: ChainName """The ENSIP-9 coin type to resolve the primary name for.""" coinType: CoinType @@ -1189,7 +1187,7 @@ type PrimaryNameRecord { """ The chain corresponding to `coinType`, or null when `coinType` is not represented in `ChainName`. """ - chain: ChainName + chainName: ChainName """The canonical ENSIP-9 coin type for this primary name lookup.""" coinType: CoinType! @@ -1204,11 +1202,11 @@ type PrimaryNameRecord { } """ -Filter primary name lookups. Exactly one of `coinTypes` or `chains` must be provided. +Filter primary name lookups. Exactly one of `coinTypes` or `chainNames` must be provided. """ input PrimaryNamesWhereInput @oneOf { """`ChainName` values to resolve primary names for.""" - chains: [ChainName!] + chainNames: [ChainName!] """Coin types to resolve primary names for.""" coinTypes: [CoinType!] @@ -1487,9 +1485,9 @@ type ResolvedAbiRecord { """A resolved address record for an ENS name.""" type ResolvedAddressRecord { """ - The raw address record value, or null if not set. May be any address format for the associated coin type and may require validation or preprocessing before use. Only EVM addresses are guaranteed to be NormalizedAddress values; see ENSIP-9 (https://docs.ens.domains/ensip/9) and address-encoder (https://github.com/ensdomains/address-encoder). + The "raw" resolved address record as hex, or null if not set. Decode with ENSIP-9 (https://docs.ens.domains/ensip/9) and address-encoder (https://github.com/ensdomains/address-encoder) for the associated coin type. May be a hex value representing 0 or more bytes. There is no guarantee that an EVM coinType returns an address value of any particular byte length. """ - address: String + address: Hex """The coin type for this address record.""" coinType: CoinType! @@ -1677,15 +1675,19 @@ type ReverseResolve { """ acceleration: AccelerationStatus! - """The primary name for this Account on a specific coin type or chain.""" + """ + The primary name for this Account on a specific coin type or chain name. + """ primaryName( - """Select a coin type or chain to resolve a primary name for.""" + """Select a coin type or chain name to resolve a primary name for.""" by: PrimaryNameByInput! ): PrimaryNameRecord! - """Primary names for this Account on the requested coin types or chains.""" + """ + Primary names for this Account on the requested coin types or chain names. + """ primaryNames( - """Select coin types or chains to resolve primary names for.""" + """Select coin types or chain names to resolve primary names for.""" where: PrimaryNamesWhereInput! ): [PrimaryNameRecord!]!