-
Notifications
You must be signed in to change notification settings - Fork 16
add records resolution #1974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
add records resolution #1974
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| import type { InterpretedLabel, InterpretedName } from "enssdk"; | ||
| import { beforeAll, describe, expect, it } from "vitest"; | ||
|
|
||
| import { DEVNET_OWNER } from "@ensnode/ensnode-sdk/internal"; | ||
|
|
||
| import { DEVNET_ETH_LABELS } from "@/test/integration/devnet-names"; | ||
| import { | ||
| DomainSubdomainsPaginated, | ||
|
|
@@ -252,3 +254,49 @@ describe("Domain.events filtering (EventsWhereInput)", () => { | |
| expect(events.length).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Domain.records", () => { | ||
| type DomainRecordsResult = { | ||
| domain: { | ||
| records: { | ||
| addresses: Array<{ coinType: number; address: string | null }>; | ||
| texts: Array<{ key: string; value: string | null }>; | ||
| } | null; | ||
| }; | ||
| }; | ||
|
|
||
| const DomainRecords = gql` | ||
| query DomainRecords($name: InterpretedName!, $addresses: [CoinType!], $texts: [String!]) { | ||
| domain(by: { name: $name }) { | ||
| records(selection: { addresses: $addresses, texts: $texts }) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the future i want to move the addresses/texts selections into the fields themselves (which will require some gql query introspection, which we do elsewhere) but good call to not do that immediately |
||
| addresses { coinType address } | ||
| texts { key value } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| it("resolves ETH address for test.eth", async () => { | ||
| const result = await request<DomainRecordsResult>(DomainRecords, { | ||
| name: "test.eth", | ||
| addresses: [60], | ||
| texts: [], | ||
| }); | ||
|
|
||
| expect(result.domain.records?.addresses).toEqual([{ coinType: 60, address: DEVNET_OWNER }]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: await expect().resolves.toMatchObject({
domain: { records: { texts: [], addresses: [{coinType: 60, address: DEVNET_OWNER }] } }
})
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto below, consolidate into a single object match and use the await expect(...).resolves syntax. we should add that preference to our AGENT.md.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, remove this case in favor of the below case which tests both at the same time. |
||
| expect(result.domain.records?.texts).toEqual([]); | ||
| }); | ||
|
|
||
| it("resolves address and text records for example.eth", async () => { | ||
| const result = await request<DomainRecordsResult>(DomainRecords, { | ||
| name: "example.eth", | ||
| addresses: [60], | ||
| texts: ["description"], | ||
| }); | ||
|
|
||
| expect(result.domain.records?.addresses).toEqual([{ coinType: 60, address: DEVNET_OWNER }]); | ||
| expect(result.domain.records?.texts).toEqual([{ key: "description", value: "example.eth" }]); | ||
| }); | ||
|
|
||
|
|
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import { | |
| import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton"; | ||
| import { withSpanAsync } from "@/lib/instrumentation/auto-span"; | ||
| import { builder } from "@/omnigraph-api/builder"; | ||
| import type { context as graphqlContext } from "@/omnigraph-api/context"; | ||
| import { | ||
| orderPaginationBy, | ||
| paginateBy, | ||
|
|
@@ -40,8 +41,12 @@ import { EventRef, EventsWhereInput } from "@/omnigraph-api/schema/event"; | |
| import { LabelRef } from "@/omnigraph-api/schema/label"; | ||
| import { OrderDirection } from "@/omnigraph-api/schema/order-direction"; | ||
| import { PermissionsUserRef } from "@/omnigraph-api/schema/permissions"; | ||
| import type { ResolverRecordsResponseBase } from "@ensnode/ensnode-sdk"; | ||
| import { resolveForward } from "@/lib/resolution/forward-resolution"; | ||
| import { runWithTrace } from "@/lib/tracing/tracing-api"; | ||
| import { RegistrationInterfaceRef } from "@/omnigraph-api/schema/registration"; | ||
| import { RegistryRef } from "@/omnigraph-api/schema/registry"; | ||
| import { ResolvedRecordsRef, ResolveSelectionInput } from "@/omnigraph-api/schema/resolution"; | ||
| import { ResolverRef } from "@/omnigraph-api/schema/resolver"; | ||
|
|
||
| const tracer = trace.getTracer("schema/Domain"); | ||
|
|
@@ -101,6 +106,37 @@ export type ENSv1Domain = Exclude<typeof ENSv1DomainRef.$inferType, ENSv1DomainI | |
| export type ENSv2Domain = Exclude<typeof ENSv2DomainRef.$inferType, ENSv2DomainId>; | ||
| export type Domain = Exclude<typeof DomainInterfaceRef.$inferType, DomainId>; | ||
|
|
||
| /** | ||
| * Returns the canonical interpreted name for a domain, or null if the domain is not canonical. | ||
| * Reuses the canonical path DataLoaders so repeated calls within a request are batched/cached. | ||
| */ | ||
| async function getDomainInterpretedName( | ||
| domain: Domain, | ||
| context: ReturnType<typeof graphqlContext>, | ||
| ): Promise<ReturnType<typeof interpretedLabelsToInterpretedName> | null> { | ||
| const canonicalPath = isENSv1Domain(domain) | ||
| ? await context.loaders.v1CanonicalPath.load(domain.id) | ||
| : await context.loaders.v2CanonicalPath.load(domain.id); | ||
|
|
||
| if (!canonicalPath) return null; | ||
|
|
||
| const domains = await rejectAnyErrors( | ||
| DomainInterfaceRef.getDataloader(context).loadMany(canonicalPath), | ||
| ); | ||
|
|
||
| const labels = canonicalPath.map((domainId: DomainId) => { | ||
| const found = domains.find((d) => d.id === domainId); | ||
| if (!found) { | ||
| throw new Error( | ||
| `Invariant(getDomainInterpretedName): Domain in CanonicalPath not found:\nPath: ${JSON.stringify(canonicalPath)}\nDomainId: ${domainId}`, | ||
| ); | ||
| } | ||
| return found.label.interpreted; | ||
| }); | ||
|
|
||
| return interpretedLabelsToInterpretedName(labels); | ||
| } | ||
|
|
||
| ////////////////////////////////// | ||
| // DomainInterface Implementation | ||
| ////////////////////////////////// | ||
|
|
@@ -137,31 +173,7 @@ DomainInterfaceRef.implement({ | |
| tracing: true, | ||
| type: "InterpretedName", | ||
| nullable: true, | ||
| resolve: async (domain, args, context) => { | ||
| const canonicalPath = isENSv1Domain(domain) | ||
| ? await context.loaders.v1CanonicalPath.load(domain.id) | ||
| : await context.loaders.v2CanonicalPath.load(domain.id); | ||
| if (!canonicalPath) return null; | ||
|
|
||
| // TODO: this could be more efficient if the get*CanonicalPath helpers included the label | ||
| // join for us. | ||
| const domains = await rejectAnyErrors( | ||
| DomainInterfaceRef.getDataloader(context).loadMany(canonicalPath), | ||
| ); | ||
|
|
||
| const labels = canonicalPath.map((domainId) => { | ||
| const found = domains.find((d) => d.id === domainId); | ||
| if (!found) { | ||
| throw new Error( | ||
| `Invariant(Domain.name): Domain in CanonicalPath not found:\nPath: ${JSON.stringify(canonicalPath)}\nDomainId: ${domainId}`, | ||
| ); | ||
| } | ||
|
|
||
| return found.label.interpreted; | ||
| }); | ||
|
|
||
| return interpretedLabelsToInterpretedName(labels); | ||
| }, | ||
| resolve: (domain, args, context) => getDomainInterpretedName(domain, context), | ||
| }), | ||
|
|
||
| /////////////// | ||
|
|
@@ -206,6 +218,41 @@ DomainInterfaceRef.implement({ | |
| resolve: (parent) => getDomainResolver(parent.id), | ||
| }), | ||
|
|
||
| /////////////////// | ||
| // Domain.records | ||
| /////////////////// | ||
| records: t.field({ | ||
| description: | ||
| "Resolve ENS records for this Domain via the ENS protocol. Only canonical domains can be resolved. Returns null if the domain is not canonical.", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. additional: only canonical, normalized names can be resolved; so names with unknown labels can't be resolved actually (which is crazy to me but that's what the contracts say) |
||
| type: ResolvedRecordsRef, | ||
| nullable: true, | ||
| args: { | ||
| selection: t.arg({ | ||
| type: ResolveSelectionInput, | ||
| required: true, | ||
| description: "Which records to resolve.", | ||
| }), | ||
| }, | ||
| resolve: async (domain, { selection }, context) => { | ||
| const name = await getDomainInterpretedName(domain, context); | ||
| if (!name) return null; | ||
|
|
||
| const { result } = await runWithTrace(() => | ||
| resolveForward( | ||
| name, | ||
| { | ||
| name: selection.reverseName ?? undefined, | ||
| texts: selection.texts ?? undefined, | ||
| addresses: selection.addresses ?? undefined, | ||
| }, | ||
| { accelerate: false, canAccelerate: false }, | ||
| ), | ||
|
Comment on lines
+224
to
+249
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify context acceleration wiring and Domain.records schema args.
rg -nP -C3 '\bcanAccelerate\b|records:\s*t\.field|accelerate:\s*t\.arg' --iglob '*.ts'Repository: namehash/ensnode Length of output: 17937 🏁 Script executed: # Check omnigraph context configuration and middleware wiring
cd apps/ensapi
# Find omnigraph context type definition
rg -nP 'type.*Context|interface.*Context' --iglob '*omnigraph*.ts' -B2 -A5
# Check if omnigraph routes/schema uses canAccelerateMiddleware
rg -nP 'canAccelerateMiddleware|omnigraph-api' --iglob '*.ts' -B2 -A2Repository: namehash/ensnode Length of output: 33798 🏁 Script executed: # Check omnigraph context definition and yoga setup
cat -n apps/ensapi/src/omnigraph-api/context.ts
# Check omnigraph yoga handler
cat -n apps/ensapi/src/omnigraph-api/yoga.tsRepository: namehash/ensnode Length of output: 3243 🏁 Script executed: # Check omnigraph router mounting and middleware setup
rg -nP 'omnigraph|canAccelerateMiddleware' apps/ensapi/src/handlers/api/router.ts -B3 -A3
# Check app.ts for middleware chain
rg -nP 'app\.use|middleware|canAccelerateMiddleware' apps/ensapi/src/app.ts -B2 -A2Repository: namehash/ensnode Length of output: 971 🏁 Script executed: # Check omnigraph-api handler definition
cat -n apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.tsRepository: namehash/ensnode Length of output: 1068 🏁 Script executed: # Check if there's any middleware applied globally before omnigraph routes
cat -n apps/ensapi/src/app.ts | head -50
# Check if omnigraph has any acceleration-related code
rg -nP 'accelerate|canAccelerate' apps/ensapi/src/omnigraph-api/ --iglob '*.ts'Repository: namehash/ensnode Length of output: 1990 🏁 Script executed: # Check how graphql-yoga context is supposed to work with Hono
# Look at yoga creation and how context parameter is passed
rg -nP 'createYoga|context' apps/ensapi/src/omnigraph-api/yoga.ts -B5 -A5
# Check if there are other GraphQL resolvers that access Hono context variables
rg -nP 'context\.' apps/ensapi/src/omnigraph-api/schema/ --iglob '*.ts' | head -20Repository: namehash/ensnode Length of output: 1604 🏁 Script executed: # Check graphql-yoga documentation or types for context parameter
# Look for how yoga.fetch uses the second parameter (c.var)
rg -nP 'fetch.*c\.var|context.*function' apps/ensapi/ --iglob '*.ts' -B2 -A2
# Check if there are examples of graphql-yoga receiving context from request
rg -nP 'createYoga.*context|yoga.*context' apps/ensapi/ --iglob '*.ts' -B5 -A5Repository: namehash/ensnode Length of output: 2707 🏁 Script executed: # Verify graphql-yoga context function signature and how it receives request context
# Check if context can be a function that receives request info
cat -n apps/ensapi/src/omnigraph-api/yoga.ts | grep -A20 "createYoga"
# Look at subgraph-api to see if it shows context wiring pattern
cat -n apps/ensapi/src/handlers/subgraph/subgraph-api.ts | grep -B5 -A10 "context"Repository: namehash/ensnode Length of output: 1883 🏁 Script executed: # Check graphql-yoga version and context handling
# Look at package.json for graphql-yoga version
cd apps/ensapi && cat package.json | grep -A2 -B2 "graphql-yoga"
# Look at how subgraph or other yoga instances handle context from serverContext
rg -nP 'context.*serverContext|createYoga.*context' apps/ensapi/ --iglob '*.ts' -B3 -A10Repository: namehash/ensnode Length of output: 267 🏁 Script executed: # Check graphql-yoga version and context patterns
grep -A2 -B2 "graphql-yoga" apps/ensapi/package.json
# Look for context function patterns with parameters in the codebase
rg -nP 'context\s*=.*\(\s*\w+\s*\)' apps/ensapi/src/omnigraph-api/ --iglob '*.ts' -B2 -A5Repository: namehash/ensnode Length of output: 197 🌐 Web query:
💡 Result: In GraphQL Yoga 5.16 (released around early 2026 per changelog), the context function and serverContext work with Hono via the WHATWG Fetch API compatibility, as Hono exposes c.req.raw (a standard Request) and app.fetch. Default context includes: request (Fetch Request), params, query (DocumentNode), operationName, variables (query variables object), extensions. For Hono integration (no official docs, but community examples from 2023+ still valid for v5): import { Hono } from 'hono' import { createYoga, createSchema } from 'graphql-yoga' import { serve } from '@hono/node-server' const schema = createSchema({...}) const yoga = createYoga({ schema }) const app = new Hono app.all('/graphql*', async (c) => yoga.fetch(c.req.raw, { /* serverContext if needed */ })) serve({ fetch: app.fetch, port: 3000 }) Or app.use('/graphql', async (c) => yoga.handle({ request: c.req.raw }, {}) ) // older handle In resolvers, access context.variables (object from client query), context.request (for headers/body), context.fetch (server fetch utility). To add Hono-specific serverContext (e.g., Hono c object), pass as second arg to handle/fetch: yoga.fetch(request, { honoC: c }) Then in user context factory: context: async (initialContext) => { const { honoC } = initialContext; return { honoVars: honoC.env?.VAR || c.req.param('id') } } For Cloudflare Workers/Deno (Hono common): serverContext auto-includes FetchEvent with request, waitUntil, etc. No explicit serverContext needed usually. Examples access context.request.headers.get('auth'), context.variables.userId in resolvers. Use context.fetch(url) for internal server calls. Citations:
🏁 Script executed: # Check if canAccelerateMiddleware could be applied to omnigraph
# Look at isRealtimeMiddleware to see if it's applied to omnigraph
rg -nP 'isRealtimeMiddleware|makeIsRealtimeMiddleware' apps/ensapi/src/ --iglob '*.ts' -B2 -A2
# Check omnigraph-api router to see all middlewares applied
cat -n apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.tsRepository: namehash/ensnode Length of output: 5546 🏁 Script executed: # Final verification: check if context function in yoga can be updated to receive serverContext
# Look at graphql-yoga documentation patterns in the code
rg -nP 'context.*async|context.*=>|serverContext' apps/ensapi/src/omnigraph-api/ --iglob '*.ts' -B3 -A3Repository: namehash/ensnode Length of output: 10109 🏁 Script executed: # Check if omnigraph is expected to support acceleration or if it's REST-only
rg -nP 'omnigraph.*accelerat|accelerat.*omnigraph' apps/ensapi/src/ --iglob '*.ts'
# Check if there are any tests or documentation about Domain.records with accelerate
rg -nP 'Domain.*records|records.*accelerate' apps/ensapi/src/omnigraph-api/ --iglob '*.ts' -B2 -A2Repository: namehash/ensnode Length of output: 836 Expose and honor the
To implement this, wire the acceleration middleware to the Omnigraph API router and update the context factory to receive
Proposed fix records: t.field({
description:
"Resolve ENS records for this Domain via the ENS protocol. Only canonical domains can be resolved. Returns null if the domain is not canonical.",
type: ResolvedRecordsRef,
nullable: true,
args: {
+ accelerate: t.arg.boolean({
+ required: false,
+ defaultValue: true,
+ description: "Whether to use accelerated resolution when available.",
+ }),
selection: t.arg({
type: ResolveSelectionInput,
required: true,
description: "Which records to resolve.",
}),
},
- resolve: async (domain, { selection }, context) => {
+ resolve: async (domain, { accelerate, selection }, context) => {
const name = await getDomainInterpretedName(domain, context);
if (!name) return null;
const { result } = await runWithTrace(() =>
resolveForward(
name,
{
texts: selection.texts ?? undefined,
addresses: selection.addresses ?? undefined,
},
- { accelerate: false, canAccelerate: false },
+ { accelerate: accelerate ?? true, canAccelerate: context.canAccelerate ?? false },
),
);🤖 Prompt for AI Agents |
||
| ); | ||
|
|
||
| return result as ResolverRecordsResponseBase; | ||
| }, | ||
|
Comment on lines
+236
to
+253
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A simple guard before calling import { isNormalizedName } from "enssdk";
resolve: async (domain, { selection }, context) => {
const name = await getDomainInterpretedName(domain, context);
if (!name) return null;
if (!isNormalizedName(name)) return null; // add this guard
const { result } = await runWithTrace(() =>
resolveForward(...)
);
...
}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmmm, make sense |
||
| }), | ||
|
|
||
| /////////////////////// | ||
| // Domain.registration | ||
| /////////////////////// | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,107 @@ | ||||||
| import type { CoinType } from "enssdk"; | ||||||
|
|
||||||
| import { builder } from "@/omnigraph-api/builder"; | ||||||
|
|
||||||
| /////////////////////// | ||||||
| // ResolveSelectionInput | ||||||
| /////////////////////// | ||||||
| export const ResolveSelectionInput = builder.inputType("ResolveSelectionInput", { | ||||||
| description: | ||||||
| "Specifies which ENS records to resolve. At least one field must be set to receive any records.", | ||||||
| fields: (t) => ({ | ||||||
| reverseName: t.boolean({ | ||||||
| description: "Whether to resolve the `name` record (used in Reverse Resolution, ENSIP-19).", | ||||||
| required: false, | ||||||
| }), | ||||||
| texts: t.stringList({ | ||||||
| description: "Text record keys to resolve (e.g. `avatar`, `description`, `com.).", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The description for the
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops, should be |
||||||
| required: false, | ||||||
|
sevenzing marked this conversation as resolved.
|
||||||
| }), | ||||||
| addresses: t.field({ | ||||||
| description: "Coin types to resolve address records for (e.g. `60` for ETH).", | ||||||
| type: ["CoinType"], | ||||||
| required: false, | ||||||
| }), | ||||||
| }), | ||||||
| }); | ||||||
|
|
||||||
| /////////////////////// | ||||||
| // ResolvedTextRecord | ||||||
| /////////////////////// | ||||||
| export const ResolvedTextRecordRef = builder | ||||||
| .objectRef<{ key: string; value: string | null }>("ResolvedTextRecord") | ||||||
| .implement({ | ||||||
| description: "A resolved text record for an ENS name.", | ||||||
| fields: (t) => ({ | ||||||
| key: t.exposeString("key", { | ||||||
| description: "The text record key.", | ||||||
| nullable: false, | ||||||
| }), | ||||||
| value: t.exposeString("value", { | ||||||
| description: "The text record value, or null if not set.", | ||||||
| nullable: true, | ||||||
| }), | ||||||
| }), | ||||||
| }); | ||||||
|
|
||||||
| /////////////////////////// | ||||||
| // ResolvedAddressRecord | ||||||
| /////////////////////////// | ||||||
| export const ResolvedAddressRecordRef = builder | ||||||
| .objectRef<{ coinType: CoinType; address: string | null }>("ResolvedAddressRecord") | ||||||
| .implement({ | ||||||
| description: "A resolved address record for an ENS name.", | ||||||
| fields: (t) => ({ | ||||||
| coinType: t.field({ | ||||||
| description: "The coin type for this address record.", | ||||||
| type: "CoinType", | ||||||
| nullable: false, | ||||||
| resolve: (r) => r.coinType, | ||||||
| }), | ||||||
| address: t.exposeString("address", { | ||||||
| description: "The address value, or null if not set.", | ||||||
| nullable: true, | ||||||
| }), | ||||||
| }), | ||||||
| }); | ||||||
|
|
||||||
| //////////////////// | ||||||
| // ResolvedRecords | ||||||
| //////////////////// | ||||||
| export const ResolvedRecordsRef = builder | ||||||
| .objectRef<{ | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the types used here can likely be inferred (or already exist) within the resolution module likely using |
||||||
| name: string | null | undefined; | ||||||
| texts: Record<string, string | null> | undefined; | ||||||
| addresses: Record<CoinType, string | null> | undefined; | ||||||
| }>("ResolvedRecords") | ||||||
| .implement({ | ||||||
| description: | ||||||
| "Records resolved for a specific ENS name via the ENS protocol. Only selected records are populated.", | ||||||
| fields: (t) => ({ | ||||||
| reverseName: t.string({ | ||||||
| description: | ||||||
| "The `name` record value used in Reverse Resolution (ENSIP-19), or null if not set or not selected.", | ||||||
| nullable: true, | ||||||
| resolve: (r) => r.name ?? null, | ||||||
| }), | ||||||
| texts: t.field({ | ||||||
| description: "Resolved text records for selected keys.", | ||||||
| type: [ResolvedTextRecordRef], | ||||||
| nullable: false, | ||||||
| resolve: (r) => | ||||||
| r.texts ? Object.entries(r.texts).map(([key, value]) => ({ key, value })) : [], | ||||||
| }), | ||||||
| addresses: t.field({ | ||||||
| description: "Resolved address records for selected coin types.", | ||||||
| type: [ResolvedAddressRecordRef], | ||||||
| nullable: false, | ||||||
| resolve: (r) => | ||||||
| r.addresses | ||||||
| ? Object.entries(r.addresses).map(([coinType, address]) => ({ | ||||||
| coinType: Number(coinType) as CoinType, | ||||||
| address, | ||||||
| })) | ||||||
| : [], | ||||||
| }), | ||||||
| }), | ||||||
| }); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you can use the
enssdk#CoinTypetype here