@ensdomains/ensjs 3.0.0-alpha.25 → 3.0.0-alpha.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/contracts/getContractAddress.js +4 -4
  3. package/dist/cjs/functions/batchWrappers.js +41 -8
  4. package/dist/cjs/functions/deleteSubname.js +2 -5
  5. package/dist/cjs/functions/getName.js +2 -2
  6. package/dist/cjs/functions/getOwner.js +4 -4
  7. package/dist/cjs/functions/getProfile.js +117 -87
  8. package/dist/cjs/generated/factories/NameWrapper__factory.js +13 -0
  9. package/dist/cjs/generated/factories/UniversalResolver__factory.js +277 -0
  10. package/dist/cjs/utils/ccip.js +128 -0
  11. package/dist/cjs/utils/singleCall.js +1 -1
  12. package/dist/esm/contracts/getContractAddress.mjs +4 -4
  13. package/dist/esm/functions/batchWrappers.mjs +35 -8
  14. package/dist/esm/functions/deleteSubname.mjs +2 -5
  15. package/dist/esm/functions/getName.mjs +2 -2
  16. package/dist/esm/functions/getOwner.mjs +4 -4
  17. package/dist/esm/functions/getProfile.mjs +117 -87
  18. package/dist/esm/generated/factories/NameWrapper__factory.mjs +13 -0
  19. package/dist/esm/generated/factories/UniversalResolver__factory.mjs +277 -0
  20. package/dist/esm/utils/ccip.mjs +114 -0
  21. package/dist/esm/utils/singleCall.mjs +1 -1
  22. package/dist/types/functions/batchWrappers.d.ts +58 -1
  23. package/dist/types/functions/getProfile.d.ts +6 -0
  24. package/dist/types/generated/NameWrapper.d.ts +9 -1
  25. package/dist/types/generated/UniversalResolver.d.ts +123 -21
  26. package/dist/types/index.d.ts +64 -2
  27. package/dist/types/utils/ccip.d.ts +3 -0
  28. package/package.json +3 -3
  29. package/src/contracts/getContractAddress.ts +4 -4
  30. package/src/functions/batchWrappers.ts +40 -9
  31. package/src/functions/deleteSubname.test.ts +21 -1
  32. package/src/functions/deleteSubname.ts +2 -6
  33. package/src/functions/getName.ts +2 -2
  34. package/src/functions/getOwner.ts +4 -2
  35. package/src/functions/getProfile-ccip.test.ts +29 -0
  36. package/src/functions/getProfile.test.ts +1 -1
  37. package/src/functions/getProfile.ts +160 -100
  38. package/src/functions/setName.test.ts +6 -2
  39. package/src/functions/setRecord.test.ts +3 -3
  40. package/src/functions/setRecords.test.ts +2 -2
  41. package/src/generated/NameWrapper.ts +14 -0
  42. package/src/generated/UniversalResolver.ts +382 -19
  43. package/src/generated/factories/NameWrapper__factory.ts +13 -0
  44. package/src/generated/factories/UniversalResolver__factory.ts +277 -0
  45. package/src/index.ts +1 -1
  46. package/src/utils/ccip.ts +148 -0
  47. package/src/utils/singleCall.ts +1 -1
@@ -20,19 +20,19 @@ const addresses: Record<
20
20
  '1': '0x283af0b28c62c092c9727f1ee09c02ca627eb7f5',
21
21
  '3': '0xa5627AB7Ae47063B533622C34FEBDb52d3281dF8',
22
22
  '4': '0x283af0b28c62c092c9727f1ee09c02ca627eb7f5',
23
- '5': '0x4a16c6Bbee697b66706E7dc0101BfCA1d60cdE76',
23
+ '5': '0x9C51161bA2FB02Cc0a403332B607117685f34831',
24
24
  },
25
25
  Multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
26
26
  NameWrapper: {
27
27
  '1': '0x0000000000000000000000000000000000000000',
28
28
  '3': '0xF82155e2a43Be0871821E9654Fc8Ae894FB8307C',
29
29
  '4': '0x0000000000000000000000000000000000000000',
30
- '5': '0x9c4C40960B53e9A01af429D3f90aEf02Bd0c4c72',
30
+ '5': '0x582224b8d4534F4749EFA4f22eF7241E0C56D4B8',
31
31
  },
32
32
  PublicResolver: {
33
33
  '1': '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41',
34
34
  '3': '0x13F0659Ee6bb7484C884FEeFb7F75C93951ef837',
35
- '5': '0x58e4627848223BF59F8Ce34265c426E7B05E8625',
35
+ '5': '0xE264d5bb84bA3b8061ADC38D3D76e6674aB91852',
36
36
  },
37
37
  ENSRegistryWithFallback: {
38
38
  '1': '0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e',
@@ -49,7 +49,7 @@ const addresses: Record<
49
49
  '1': '0x580AF46E06DaaD47eb5940526FD64d95b815Cb70',
50
50
  '3': '0x74e20bd2a1fe0cdbe45b9a1d89cb7e0a45b36376',
51
51
  '4': '0x74e20bd2a1fe0cdbe45b9a1d89cb7e0a45b36376',
52
- '5': '0x9380F1974D2B7064eA0c0EC251968D8c69f0Ae31',
52
+ '5': '0x687c30Cc44bFA39A1449e86E172BF002E7b3f0b0',
53
53
  },
54
54
  BulkRenewal: {
55
55
  '1': '0xfF252725f6122A92551A5FA9a6b6bf10eb0Be035',
@@ -1,5 +1,6 @@
1
1
  import { ethers } from 'ethers'
2
2
  import { ENSArgs } from '..'
3
+ import ccipLookup from '../utils/ccip'
3
4
  import { hexEncodeName } from '../utils/hexEncodedName'
4
5
 
5
6
  export const universalWrapper = {
@@ -11,16 +12,16 @@ export const universalWrapper = {
11
12
  const universalResolver = await contracts?.getUniversalResolver()!
12
13
  return {
13
14
  to: universalResolver.address,
14
- data: universalResolver.interface.encodeFunctionData('resolve', [
15
- hexEncodeName(name),
16
- data,
17
- ]),
15
+ data: universalResolver.interface.encodeFunctionData(
16
+ 'resolve(bytes,bytes)',
17
+ [hexEncodeName(name), data],
18
+ ),
18
19
  }
19
20
  },
20
21
  decode: async ({ contracts }: ENSArgs<'contracts'>, data: string) => {
21
22
  const universalResolver = await contracts?.getUniversalResolver()!
22
23
  const response = universalResolver.interface.decodeFunctionResult(
23
- 'resolve',
24
+ 'resolve(bytes,bytes)',
24
25
  data,
25
26
  )
26
27
  if (!response || !response[0]) {
@@ -75,7 +76,11 @@ export const multicallWrapper = {
75
76
  ]),
76
77
  }
77
78
  },
78
- async decode({ contracts }: ENSArgs<'contracts'>, data: string) {
79
+ async decode(
80
+ { contracts, provider }: ENSArgs<'contracts' | 'provider'>,
81
+ data: string,
82
+ transactions: ethers.providers.TransactionRequest[],
83
+ ) {
79
84
  if (!data) return
80
85
  const multicall = await contracts?.getMulticall()!
81
86
  try {
@@ -83,9 +88,35 @@ export const multicallWrapper = {
83
88
  'tryAggregate',
84
89
  data,
85
90
  )
86
- return result
87
- } catch (e) {
88
- return e
91
+ const ccipChecked = await Promise.all(
92
+ (result as [boolean, string][]).map(
93
+ async ([success, returnData], i) => {
94
+ let newArr: [boolean, string] = [success, returnData]
95
+ // OffchainLookup(address,string[],bytes,bytes4,bytes)
96
+ if (!success && returnData.startsWith('0x556f1830')) {
97
+ try {
98
+ const newData = await ccipLookup(
99
+ provider!,
100
+ transactions[i],
101
+ returnData,
102
+ )
103
+ if (newData) {
104
+ newArr = [true, newData]
105
+ }
106
+ } catch {}
107
+ }
108
+ return {
109
+ ...newArr,
110
+ success: newArr[0],
111
+ returnData: newArr[1],
112
+ }
113
+ },
114
+ ),
115
+ )
116
+ return ccipChecked
117
+ } catch (e: any) {
118
+ console.error(e)
119
+ return
89
120
  }
90
121
  },
91
122
  }
@@ -29,7 +29,10 @@ describe('deleteSubname', () => {
29
29
  const result = await registry.owner(namehash('test.with-subnames.eth'))
30
30
  expect(result).toBe('0x0000000000000000000000000000000000000000')
31
31
  })
32
+
32
33
  it('should allow deleting a subname on the nameWrapper', async () => {
34
+ const nameWrapper = await ensInstance.contracts!.getNameWrapper()!
35
+
33
36
  const tx = await ensInstance.deleteSubname(
34
37
  'test.wrapped-with-subnames.eth',
35
38
  {
@@ -40,10 +43,27 @@ describe('deleteSubname', () => {
40
43
  expect(tx).toBeTruthy()
41
44
  await tx.wait()
42
45
 
43
- const nameWrapper = await ensInstance.contracts!.getNameWrapper()!
44
46
  const result = await nameWrapper.ownerOf(
45
47
  namehash('test.wrapped-with-subnames.eth'),
46
48
  )
47
49
  expect(result).toBe('0x0000000000000000000000000000000000000000')
48
50
  })
51
+
52
+ it('should not allow deleting 1LD', async () => {
53
+ await expect(
54
+ ensInstance.deleteSubname('eth', {
55
+ contract: 'nameWrapper',
56
+ addressOrIndex: 1,
57
+ }),
58
+ ).rejects.toThrow()
59
+ })
60
+
61
+ it('should not allow deleting 2LD', async () => {
62
+ await expect(
63
+ ensInstance.deleteSubname('test123.eth', {
64
+ contract: 'registry',
65
+ addressOrIndex: 1,
66
+ }),
67
+ ).rejects.toThrow()
68
+ })
49
69
  })
@@ -13,12 +13,8 @@ export default async function (
13
13
  ) {
14
14
  const labels = name.split('.')
15
15
 
16
- if (labels.length !== 3) {
17
- throw new Error('ENS.js currently only supports deleting 2LDs, not TLDs')
18
- }
19
-
20
- if (labels[2] !== 'eth') {
21
- throw new Error('ENS.js currently only supports deleting .eth 2LDs')
16
+ if (labels.length < 3) {
17
+ throw new Error(`${name} is not a valid subname`)
22
18
  }
23
19
 
24
20
  const label = labels.shift() as string
@@ -6,7 +6,7 @@ const raw = async ({ contracts }: ENSArgs<'contracts'>, address: string) => {
6
6
  const reverseNode = `${address.toLowerCase().substring(2)}.addr.reverse`
7
7
  return {
8
8
  to: universalResolver.address,
9
- data: universalResolver.interface.encodeFunctionData('reverse', [
9
+ data: universalResolver.interface.encodeFunctionData('reverse(bytes)', [
10
10
  hexEncodeName(reverseNode),
11
11
  ]),
12
12
  }
@@ -21,7 +21,7 @@ const decode = async (
21
21
  const universalResolver = await contracts?.getUniversalResolver()!
22
22
  try {
23
23
  const result = universalResolver.interface.decodeFunctionResult(
24
- 'reverse',
24
+ 'reverse(bytes)',
25
25
  data,
26
26
  )
27
27
  return {
@@ -100,7 +100,7 @@ const decode = async (
100
100
  name: string,
101
101
  contract?: 'nameWrapper' | 'registry' | 'registrar',
102
102
  ): Promise<Owner | undefined> => {
103
- if (data === null) return
103
+ if (!data) return
104
104
  const labels = name.split('.')
105
105
  if (contract || labels.length === 1) {
106
106
  const singleOwner = singleContractOwnerDecode(data)
@@ -119,7 +119,7 @@ const decode = async (
119
119
  }
120
120
  }
121
121
  const result = await multicallWrapper.decode(data)
122
- if (result === null) return
122
+ if (!result) return
123
123
  const nameWrapper = await contracts?.getNameWrapper()!
124
124
 
125
125
  const decodedData = [result[0][1], result[1][1], result[2]?.[1]].map(
@@ -163,6 +163,7 @@ const decode = async (
163
163
  // this means that the subname is wrapped
164
164
  if (
165
165
  registryOwner === nameWrapper.address &&
166
+ nameWrapperOwner &&
166
167
  ethers.utils.hexStripZeros(nameWrapperOwner) !== '0x'
167
168
  ) {
168
169
  return {
@@ -186,6 +187,7 @@ const decode = async (
186
187
  // and for wrapped names, owner and registrant are the same thing
187
188
  if (
188
189
  registryOwner === nameWrapper.address &&
190
+ nameWrapperOwner &&
189
191
  ethers.utils.hexStripZeros(nameWrapperOwner) !== '0x'
190
192
  ) {
191
193
  return {
@@ -0,0 +1,29 @@
1
+ import dotenv from 'dotenv'
2
+ import { ENS } from '..'
3
+ import setup from '../tests/setup'
4
+
5
+ dotenv.config()
6
+
7
+ let ensInstance: ENS
8
+
9
+ beforeAll(async () => {
10
+ ;({ ensInstance } = await setup(true))
11
+ })
12
+
13
+ jest.setTimeout(20000)
14
+
15
+ describe('getProfile', () => {
16
+ it('should return a profile from a ccip-read name', async () => {
17
+ const result = await ensInstance.getProfile('1.offchainexample.eth', {
18
+ fallback: {
19
+ texts: ['email', 'description'],
20
+ contentHash: true,
21
+ coinTypes: ['0', '60'],
22
+ },
23
+ })
24
+ expect(result).toBeTruthy()
25
+ if (result) {
26
+ expect(result.address).toBe('0x41563129cDbbD0c5D3e1c86cf9563926b243834d')
27
+ }
28
+ })
29
+ })
@@ -129,7 +129,7 @@ describe('getProfile', () => {
129
129
  '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
130
130
  )
131
131
  expect(result.resolverAddress).toBe(
132
- deploymentAddresses.NoMulticallResolver.toLowerCase(),
132
+ deploymentAddresses.NoMulticallResolver,
133
133
  )
134
134
  }
135
135
  })
@@ -1,5 +1,6 @@
1
1
  import { formatsByName } from '@ensdomains/address-encoder'
2
2
  import { ethers } from 'ethers'
3
+ import { hexStripZeros } from 'ethers/lib/utils'
3
4
  import { ENSArgs } from '..'
4
5
  import { decodeContenthash, DecodedContentHash } from '../utils/contentHash'
5
6
  import { hexEncodeName } from '../utils/hexEncodedName'
@@ -18,6 +19,12 @@ type ProfileResponse = {
18
19
  coinTypes?: string[]
19
20
  }
20
21
 
22
+ type FallbackRecords = {
23
+ contentHash?: boolean
24
+ texts?: string[]
25
+ coinTypes?: string[]
26
+ }
27
+
21
28
  type DataItem = {
22
29
  key: string | number
23
30
  type: 'addr' | 'text' | 'contentHash'
@@ -41,19 +48,25 @@ type ResolvedProfile = {
41
48
  reverseResolverAddress?: string
42
49
  }
43
50
 
51
+ type CallObj = {
52
+ key: string
53
+ data: {
54
+ to: string
55
+ data: string
56
+ }
57
+ type: 'addr' | 'text' | 'contentHash'
58
+ }
59
+
44
60
  const makeMulticallData = async (
45
61
  {
46
62
  _getAddr,
47
63
  _getContentHash,
48
64
  _getText,
49
- resolverMulticallWrapper,
50
- }: ENSArgs<
51
- '_getText' | '_getAddr' | '_getContentHash' | 'resolverMulticallWrapper'
52
- >,
65
+ }: ENSArgs<'_getText' | '_getAddr' | '_getContentHash'>,
53
66
  name: string,
54
67
  options: InternalProfileOptions,
55
68
  ) => {
56
- let calls: any[] = []
69
+ let calls: (CallObj | null)[] = []
57
70
  if (options.texts)
58
71
  calls = [
59
72
  ...calls,
@@ -61,7 +74,7 @@ const makeMulticallData = async (
61
74
  options.texts.map(async (x) => ({
62
75
  key: x,
63
76
  data: await _getText.raw(name, x),
64
- type: 'text',
77
+ type: 'text' as const,
65
78
  })),
66
79
  )),
67
80
  ]
@@ -73,7 +86,7 @@ const makeMulticallData = async (
73
86
  options.coinTypes.map(async (x) => ({
74
87
  key: x,
75
88
  data: await _getAddr.raw(name, x, true),
76
- type: 'addr',
89
+ type: 'addr' as const,
77
90
  })),
78
91
  )),
79
92
  ]
@@ -82,21 +95,19 @@ const makeMulticallData = async (
82
95
  calls.push({
83
96
  key: 'contentHash',
84
97
  data: await _getContentHash.raw(name),
85
- type: 'contenthash',
98
+ type: 'contentHash' as const,
86
99
  })
87
100
  }
88
101
 
89
- if (!calls.find((x) => x.key === '60')) {
102
+ if (!calls.find((x) => x!.key === '60')) {
90
103
  calls.push({
91
104
  key: '60',
92
105
  data: await _getAddr.raw(name, '60', true),
93
- type: 'addr',
106
+ type: 'addr' as const,
94
107
  })
95
108
  }
96
109
 
97
- const prRawData = await resolverMulticallWrapper.raw(calls.map((x) => x.data))
98
-
99
- return { data: prRawData.data, calls }
110
+ return { data: calls.map((x) => x!.data.data), calls }
100
111
  }
101
112
 
102
113
  const fetchWithoutResolverMulticall = async (
@@ -116,9 +127,11 @@ const fetchWithoutResolverMulticall = async (
116
127
  data: call.data.data,
117
128
  }))
118
129
 
119
- return (await multicallWrapper(callsWithResolver)).map(
120
- (x: [boolean, string]) => x[1],
121
- )
130
+ const results = await multicallWrapper(callsWithResolver)
131
+
132
+ if (!results || !results.length) return []
133
+
134
+ return results.map((x: [boolean, string]) => x[1])
122
135
  }
123
136
 
124
137
  const formatRecords = async (
@@ -139,7 +152,7 @@ const formatRecords = async (
139
152
  key: calls[i].key,
140
153
  type: calls[i].type,
141
154
  }
142
- if (itemRet.type === 'contenthash') {
155
+ if (itemRet.type === 'contentHash') {
143
156
  ;[decodedFromAbi] = ethers.utils.defaultAbiCoder.decode(
144
157
  ['bytes'],
145
158
  item,
@@ -171,7 +184,7 @@ const formatRecords = async (
171
184
  } catch {
172
185
  return
173
186
  }
174
- case 'contenthash':
187
+ case 'contentHash':
175
188
  try {
176
189
  itemRet = {
177
190
  ...itemRet,
@@ -217,7 +230,7 @@ const formatRecords = async (
217
230
  }
218
231
  } else if (options.contentHash) {
219
232
  const foundRecord = returnedRecords.find(
220
- (item: any) => item.type === 'contenthash',
233
+ (item: any) => item.type === 'contentHash',
221
234
  )
222
235
  returnedResponse.contentHash = foundRecord ? foundRecord.value : null
223
236
  }
@@ -240,7 +253,6 @@ const getDataForName = async (
240
253
  _getAddr,
241
254
  _getContentHash,
242
255
  _getText,
243
- resolverMulticallWrapper,
244
256
  multicallWrapper,
245
257
  }: ENSArgs<
246
258
  | 'contracts'
@@ -252,62 +264,77 @@ const getDataForName = async (
252
264
  >,
253
265
  name: string,
254
266
  options: InternalProfileOptions,
255
- fallbackResolver?: string,
256
267
  specificResolver?: string,
257
268
  ) => {
258
- const universalResolver = await contracts?.getUniversalResolver()
269
+ const universalResolver = await contracts?.getUniversalResolver()!
259
270
 
260
271
  const { data, calls } = await makeMulticallData(
261
- { _getAddr, _getContentHash, _getText, resolverMulticallWrapper },
272
+ { _getAddr, _getContentHash, _getText },
262
273
  name,
263
274
  options,
264
275
  )
265
276
 
266
- let resolvedData: any
267
- let useFallbackResolver = false
268
- try {
269
- if (specificResolver) {
277
+ let recordData: (string | null)[] | undefined
278
+ let resolverAddress: string | undefined = specificResolver
279
+
280
+ if (specificResolver) {
281
+ try {
270
282
  const publicResolver = await contracts?.getPublicResolver(
271
283
  undefined,
272
284
  specificResolver,
273
285
  )
274
- resolvedData = await publicResolver?.callStatic.multicall(
275
- calls.map((x) => x.data),
286
+ recordData = await publicResolver?.callStatic.multicall(data)
287
+ } catch (e: any) {
288
+ console.error('getProfile error:', e)
289
+ recordData = await fetchWithoutResolverMulticall(
290
+ { multicallWrapper },
291
+ calls as CallObj[],
292
+ resolverAddress!,
276
293
  )
277
- } else {
278
- resolvedData = await universalResolver?.resolve(hexEncodeName(name), data)
279
294
  }
280
- } catch {
281
- useFallbackResolver = true
282
- }
283
-
284
- let resolverAddress: string
285
- let recordData: any
286
-
287
- if (useFallbackResolver) {
288
- resolverAddress = specificResolver || fallbackResolver!
289
- recordData = await fetchWithoutResolverMulticall(
290
- { multicallWrapper },
291
- calls,
292
- resolverAddress,
293
- )
294
295
  } else {
295
- resolverAddress = specificResolver || resolvedData['1']
296
- if (specificResolver) {
297
- recordData = resolvedData
298
- } else {
299
- ;[recordData] = await resolverMulticallWrapper.decode(resolvedData['0'])
296
+ const resolvedData = await universalResolver['resolve(bytes,bytes[])'](
297
+ hexEncodeName(name),
298
+ data,
299
+ {
300
+ ccipReadEnabled: true,
301
+ },
302
+ )
303
+ recordData = [...resolvedData['0']]
304
+ resolverAddress = resolvedData['1']
305
+ for (let i = 0; i < recordData.length; i += 1) {
306
+ // error code for reverted call in batch
307
+ // this is expected when using offchain resolvers, so should be ignored
308
+ if (recordData[i]!.startsWith('0x0d1947a9')) {
309
+ calls[i] = null
310
+ recordData[i] = null
311
+ }
312
+ }
313
+ }
314
+ if (
315
+ !resolverAddress ||
316
+ !recordData ||
317
+ hexStripZeros(resolverAddress) === '0x'
318
+ ) {
319
+ return {
320
+ address: null,
321
+ records: {},
322
+ resolverAddress: null,
300
323
  }
301
324
  }
302
325
 
303
- const matchAddress = recordData[calls.findIndex((x) => x.key === '60')]
326
+ const filteredCalls = calls.filter((x) => x) as CallObj[]
327
+ const filteredRecordData = recordData.filter((x) => x)
328
+
329
+ const matchAddress =
330
+ filteredRecordData[filteredCalls.findIndex((x) => x.key === '60')]
304
331
 
305
332
  return {
306
333
  address: matchAddress && (await _getAddr.decode(matchAddress)),
307
334
  records: await formatRecords(
308
335
  { _getAddr, _getContentHash, _getText },
309
- recordData,
310
- calls,
336
+ filteredRecordData,
337
+ filteredCalls,
311
338
  options,
312
339
  ),
313
340
  resolverAddress,
@@ -332,7 +359,6 @@ const graphFetch = async (
332
359
  addr {
333
360
  id
334
361
  }
335
- address
336
362
  }
337
363
  }
338
364
  }
@@ -379,14 +405,7 @@ const graphFetch = async (
379
405
 
380
406
  const returnedRecords: ProfileResponse = {}
381
407
 
382
- if (!resolverResponse) return { isMigrated, createdAt }
383
-
384
- if (!wantedRecords)
385
- return {
386
- isMigrated,
387
- createdAt,
388
- graphResolverAddress: resolverResponse.address || resolverAddress,
389
- }
408
+ if (!resolverResponse || !wantedRecords) return { isMigrated, createdAt }
390
409
 
391
410
  Object.keys(wantedRecords).forEach((key: string) => {
392
411
  const data = wantedRecords[key as keyof ProfileOptions]
@@ -403,7 +422,6 @@ const graphFetch = async (
403
422
  ...returnedRecords,
404
423
  isMigrated,
405
424
  createdAt,
406
- graphResolverAddress: resolverResponse.address || resolverAddress,
407
425
  }
408
426
  }
409
427
 
@@ -415,6 +433,7 @@ type ProfileOptions = {
415
433
 
416
434
  type InputProfileOptions = ProfileOptions & {
417
435
  resolverAddress?: string
436
+ fallback?: FallbackRecords
418
437
  }
419
438
 
420
439
  const getProfileFromName = async (
@@ -438,7 +457,7 @@ const getProfileFromName = async (
438
457
  name: string,
439
458
  options?: InputProfileOptions,
440
459
  ) => {
441
- const { resolverAddress, ..._options } = options || {}
460
+ const { resolverAddress, fallback, ..._options } = options || {}
442
461
  const optsLength = Object.keys(_options).length
443
462
  let usingOptions: InputProfileOptions | undefined
444
463
  if (!optsLength || _options?.texts === true || _options?.coinTypes === true) {
@@ -452,35 +471,69 @@ const getProfileFromName = async (
452
471
  usingOptions,
453
472
  resolverAddress,
454
473
  )
455
- if (!graphResult) return
456
- const {
457
- isMigrated,
458
- createdAt,
459
- graphResolverAddress,
460
- ...wantedRecords
461
- }: {
462
- isMigrated: boolean
463
- createdAt: string
464
- graphResolverAddress?: string
465
- } & InternalProfileOptions = graphResult
466
- if (!graphResolverAddress && !options?.resolverAddress)
467
- return { isMigrated, createdAt, message: "Name doesn't have a resolver" }
468
- const result = await getDataForName(
469
- {
470
- contracts,
471
- _getAddr,
472
- _getContentHash,
473
- _getText,
474
- resolverMulticallWrapper,
475
- multicallWrapper,
476
- },
477
- name,
478
- usingOptions ? wantedRecords : (options as InternalProfileOptions),
479
- graphResolverAddress,
480
- options?.resolverAddress!,
481
- )
482
- if (!result)
483
- return { isMigrated, createdAt, message: "Records fetch didn't complete" }
474
+ let isMigrated: boolean | null = null
475
+ let createdAt: string | null = null
476
+ let result: Awaited<ReturnType<typeof getDataForName>> | null = null
477
+ if (!graphResult) {
478
+ if (!fallback) return
479
+ result = await getDataForName(
480
+ {
481
+ contracts,
482
+ _getAddr,
483
+ _getContentHash,
484
+ _getText,
485
+ resolverMulticallWrapper,
486
+ multicallWrapper,
487
+ },
488
+ name,
489
+ fallback,
490
+ undefined,
491
+ )
492
+ } else {
493
+ const {
494
+ isMigrated: _isMigrated,
495
+ createdAt: _createdAt,
496
+ ...wantedRecords
497
+ }: {
498
+ isMigrated: boolean
499
+ createdAt: string
500
+ } & InternalProfileOptions = graphResult
501
+ isMigrated = _isMigrated
502
+ createdAt = _createdAt
503
+ let recordsWithFallback = usingOptions
504
+ ? wantedRecords
505
+ : (_options as InternalProfileOptions)
506
+ if (
507
+ (Object.keys(recordsWithFallback).length === 0 ||
508
+ (!recordsWithFallback.coinTypes &&
509
+ !recordsWithFallback.texts &&
510
+ Object.keys(recordsWithFallback.contentHash || {}).length === 0)) &&
511
+ fallback
512
+ ) {
513
+ recordsWithFallback = fallback
514
+ }
515
+ result = await getDataForName(
516
+ {
517
+ contracts,
518
+ _getAddr,
519
+ _getContentHash,
520
+ _getText,
521
+ resolverMulticallWrapper,
522
+ multicallWrapper,
523
+ },
524
+ name,
525
+ recordsWithFallback,
526
+ options?.resolverAddress!,
527
+ )
528
+ }
529
+ if (!result?.resolverAddress)
530
+ return {
531
+ isMigrated,
532
+ createdAt,
533
+ message: !result
534
+ ? "Records fetch didn't complete"
535
+ : "Name doesn't have a resolver",
536
+ }
484
537
  return { ...result, isMigrated, createdAt, message: undefined }
485
538
  }
486
539
 
@@ -537,6 +590,13 @@ const getProfileFromAddress = async (
537
590
  }
538
591
  }
539
592
 
593
+ const mapCoinTypes = (coin: string) => {
594
+ if (!Number.isNaN(parseInt(coin))) {
595
+ return coin
596
+ }
597
+ return `${formatsByName[coin.toUpperCase()].coinType}`
598
+ }
599
+
540
600
  export default async function (
541
601
  {
542
602
  contracts,
@@ -560,13 +620,13 @@ export default async function (
560
620
  nameOrAddress: string,
561
621
  options?: InputProfileOptions,
562
622
  ): Promise<ResolvedProfile | undefined> {
563
- if (options && options.coinTypes && typeof options.coinTypes !== 'boolean') {
564
- options.coinTypes = options.coinTypes.map((coin: string) => {
565
- if (!Number.isNaN(parseInt(coin))) {
566
- return coin
567
- }
568
- return `${formatsByName[coin.toUpperCase()].coinType}`
569
- })
623
+ if (options) {
624
+ if (options.coinTypes && typeof options.coinTypes !== 'boolean') {
625
+ options.coinTypes = options.coinTypes.map(mapCoinTypes)
626
+ }
627
+ if (options.fallback && options.fallback.coinTypes) {
628
+ options.fallback.coinTypes = options.fallback.coinTypes.map(mapCoinTypes)
629
+ }
570
630
  }
571
631
 
572
632
  const inputType = parseInputType(nameOrAddress)