@w3lib/evm-batch-call 0.1.0 → 0.3.0

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 (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +194 -12
  3. package/dist/aggregate.d.ts +5 -2
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js.map +1 -1
  6. package/dist/call.d.ts +8 -2
  7. package/dist/call.d.ts.map +1 -1
  8. package/dist/call.js +25 -1
  9. package/dist/call.js.map +1 -1
  10. package/dist/chains.d.ts +13 -0
  11. package/dist/chains.d.ts.map +1 -0
  12. package/dist/chains.js +35 -0
  13. package/dist/chains.js.map +1 -0
  14. package/dist/detect.d.ts +23 -0
  15. package/dist/detect.d.ts.map +1 -0
  16. package/dist/detect.js +178 -0
  17. package/dist/detect.js.map +1 -0
  18. package/dist/erc20.d.ts +83 -7
  19. package/dist/erc20.d.ts.map +1 -1
  20. package/dist/erc20.js +36 -5
  21. package/dist/erc20.js.map +1 -1
  22. package/dist/fallback.d.ts +25 -0
  23. package/dist/fallback.d.ts.map +1 -1
  24. package/dist/fallback.js +123 -21
  25. package/dist/fallback.js.map +1 -1
  26. package/dist/index.d.ts +10 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/nft.d.ts +71 -0
  31. package/dist/nft.d.ts.map +1 -0
  32. package/dist/nft.js +194 -0
  33. package/dist/nft.js.map +1 -0
  34. package/dist/owner.d.ts +136 -0
  35. package/dist/owner.d.ts.map +1 -0
  36. package/dist/owner.js +501 -0
  37. package/dist/owner.js.map +1 -0
  38. package/dist/provider.d.ts +112 -12
  39. package/dist/provider.d.ts.map +1 -1
  40. package/dist/provider.js +343 -48
  41. package/dist/provider.js.map +1 -1
  42. package/dist/scan.d.ts +59 -0
  43. package/dist/scan.d.ts.map +1 -0
  44. package/dist/scan.js +245 -0
  45. package/dist/scan.js.map +1 -0
  46. package/dist/shortcuts.d.ts +113 -0
  47. package/dist/shortcuts.d.ts.map +1 -0
  48. package/dist/shortcuts.js +193 -0
  49. package/dist/shortcuts.js.map +1 -0
  50. package/dist/source.d.ts +23 -1
  51. package/dist/source.d.ts.map +1 -1
  52. package/dist/source.js +110 -3
  53. package/dist/source.js.map +1 -1
  54. package/dist/tron.d.ts +6 -0
  55. package/dist/tron.d.ts.map +1 -1
  56. package/dist/tron.js +16 -1
  57. package/dist/tron.js.map +1 -1
  58. package/dist/util.d.ts +6 -0
  59. package/dist/util.d.ts.map +1 -0
  60. package/dist/util.js +20 -0
  61. package/dist/util.js.map +1 -0
  62. package/package.json +3 -2
  63. package/src/aggregate.ts +5 -2
  64. package/src/call.ts +32 -3
  65. package/src/chains.ts +46 -0
  66. package/src/detect.ts +193 -0
  67. package/src/erc20.ts +111 -9
  68. package/src/fallback.ts +127 -23
  69. package/src/index.ts +90 -4
  70. package/src/nft.ts +298 -0
  71. package/src/owner.ts +651 -0
  72. package/src/provider.ts +458 -54
  73. package/src/scan.ts +293 -0
  74. package/src/shortcuts.ts +278 -0
  75. package/src/source.ts +118 -3
  76. package/src/tron.ts +21 -2
  77. package/src/util.ts +20 -0
package/src/nft.ts ADDED
@@ -0,0 +1,298 @@
1
+ import type { CallOverrides } from './aggregate.js'
2
+ import { asStringOrBytes32, type Call } from './call.js'
3
+ import { getCachedTokenMeta, setCachedTokenMeta } from './erc20.js'
4
+ import type { Provider } from './provider.js'
5
+ import { isTronChain } from './source.js'
6
+ import { toTronAddress } from './tron.js'
7
+
8
+ export const ERC721_ABI = [
9
+ 'function name() view returns (string)',
10
+ 'function symbol() view returns (string)',
11
+ 'function totalSupply() view returns (uint256)',
12
+ 'function balanceOf(address owner) view returns (uint256)',
13
+ 'function ownerOf(uint256 tokenId) view returns (address)',
14
+ 'function tokenURI(uint256 tokenId) view returns (string)',
15
+ 'function supportsInterface(bytes4 interfaceId) view returns (bool)',
16
+ ] as const
17
+
18
+ export const ERC1155_ABI = [
19
+ 'function balanceOf(address account, uint256 id) view returns (uint256)',
20
+ 'function uri(uint256 id) view returns (string)',
21
+ 'function supportsInterface(bytes4 interfaceId) view returns (bool)',
22
+ ] as const
23
+
24
+ /** ERC165 接口 ID */
25
+ const ERC721_INTERFACE_ID = '0x80ac58cd'
26
+ const ERC1155_INTERFACE_ID = '0xd9b67a26'
27
+
28
+ export type NftStandard = 'ERC721' | 'ERC1155'
29
+
30
+ /** 某个集合里的一个 NFT */
31
+ export interface NftItem {
32
+ contract: string
33
+ tokenId: bigint | string | number
34
+ }
35
+
36
+ export interface NftOwner {
37
+ contract: string
38
+ /** 十进制字符串 */
39
+ tokenId: string
40
+ /** 持有人地址(Tron 链上为 T 开头的地址);不存在 / 已销毁 / 非 ERC721 时为 null */
41
+ owner: string | null
42
+ success: boolean
43
+ }
44
+
45
+ export interface NftBalance {
46
+ contract: string
47
+ /** 持有数量(十进制字符串) */
48
+ balance: string
49
+ success: boolean
50
+ }
51
+
52
+ export interface Erc1155Balance {
53
+ contract: string
54
+ tokenId: string
55
+ balance: string
56
+ success: boolean
57
+ }
58
+
59
+ export interface NftTokenUri {
60
+ contract: string
61
+ tokenId: string
62
+ /** ERC721 tokenURI 或 ERC1155 uri(已替换 {id},配置了 ipfsGateway 时 ipfs:// 已转换);读取失败为 null */
63
+ uri: string | null
64
+ success: boolean
65
+ }
66
+
67
+ export interface NftTokenUriOptions extends CallOverrides {
68
+ /** 把 ipfs://xxx 转成 `${ipfsGateway}xxx`,如 'https://ipfs.io/ipfs/'。默认不转换 */
69
+ ipfsGateway?: string
70
+ }
71
+
72
+ /** nftCollections() / getNftCollections() 可选的字段 */
73
+ export type NftCollectionField = 'standard' | 'name' | 'symbol' | 'totalSupply'
74
+
75
+ export const DEFAULT_NFT_COLLECTION_FIELDS = ['standard', 'name', 'symbol'] as const satisfies readonly NftCollectionField[]
76
+
77
+ export type DefaultNftCollectionField = (typeof DEFAULT_NFT_COLLECTION_FIELDS)[number]
78
+
79
+ interface NftCollectionFieldTypes {
80
+ /** 通过 ERC165 supportsInterface 识别;都不支持时为 null */
81
+ standard: NftStandard | null
82
+ name: string | null
83
+ symbol: string | null
84
+ /** 十进制字符串;需要合约实现 totalSupply(ERC721Enumerable 等) */
85
+ totalSupply: string | null
86
+ }
87
+
88
+ export type NftCollection<F extends NftCollectionField = DefaultNftCollectionField> = {
89
+ address: string
90
+ /** 请求的字段都读到时为 true */
91
+ success: boolean
92
+ } & { [K in F]: NftCollectionFieldTypes[K] }
93
+
94
+ export interface NftCollectionsOptions<F extends NftCollectionField = DefaultNftCollectionField> extends CallOverrides {
95
+ /** 要返回的字段,默认 ['standard', 'name', 'symbol'] */
96
+ fields?: readonly F[]
97
+ }
98
+
99
+ function tokenIdString(tokenId: NftItem['tokenId']): string {
100
+ return BigInt(tokenId).toString()
101
+ }
102
+
103
+ /** 把 tokenId 规范成 BigInt;非法 tokenId 返回 null(对应项失败,不影响整批) */
104
+ function parseTokenId(tokenId: NftItem['tokenId']): bigint | null {
105
+ try {
106
+ return BigInt(tokenId)
107
+ } catch {
108
+ return null
109
+ }
110
+ }
111
+
112
+ /** ERC1155 uri 规范:{id} 替换为 64 位小写十六进制(不带 0x) */
113
+ function expandUri(uri: string, tokenId: bigint, gateway?: string): string {
114
+ const expanded = uri.replace(/\{id\}/g, tokenId.toString(16).padStart(64, '0'))
115
+ if (gateway && expanded.startsWith('ipfs://')) {
116
+ return gateway + expanded.slice('ipfs://'.length).replace(/^ipfs\//, '')
117
+ }
118
+ return expanded
119
+ }
120
+
121
+ export async function nftOwners(provider: Provider, items: readonly NftItem[], overrides?: CallOverrides): Promise<NftOwner[]> {
122
+ // Tron 上返回 T 开头的地址,方便与 tronWeb.defaultAddress.base58 等直接比较
123
+ const tron = isTronChain(await provider.getChainId())
124
+ const ids = items.map((item) => parseTokenId(item.tokenId))
125
+ const calls = items.flatMap((item, i) => (ids[i] === null ? [] : [provider.contract(item.contract, ERC721_ABI as unknown as string[]).ownerOf(ids[i])]))
126
+ const results = calls.length ? await provider.tryAll<string>(calls, overrides) : []
127
+ let j = 0
128
+ return items.map((item, i) => {
129
+ const raw = ids[i] === null ? null : (results[j++] ?? null)
130
+ const owner = raw !== null && tron ? toTronAddress(raw) : raw
131
+ return { contract: item.contract, tokenId: ids[i] === null ? String(item.tokenId) : tokenIdString(item.tokenId), owner, success: owner !== null }
132
+ })
133
+ }
134
+
135
+ export async function nftBalances(
136
+ provider: Provider,
137
+ owner: string,
138
+ collections: readonly string[],
139
+ overrides?: CallOverrides,
140
+ ): Promise<NftBalance[]> {
141
+ const calls = collections.map((contract) => provider.contract(contract, ERC721_ABI as unknown as string[]).balanceOf(owner))
142
+ const results = calls.length ? await provider.tryAll<bigint>(calls, overrides) : []
143
+ return collections.map((contract, i) => {
144
+ const balance = results[i] ?? null
145
+ return { contract, balance: balance === null ? '0' : balance.toString(), success: balance !== null }
146
+ })
147
+ }
148
+
149
+ export async function erc1155Balances(
150
+ provider: Provider,
151
+ owner: string,
152
+ items: readonly NftItem[],
153
+ overrides?: CallOverrides,
154
+ ): Promise<Erc1155Balance[]> {
155
+ const ids = items.map((item) => parseTokenId(item.tokenId))
156
+ const calls = items.flatMap((item, i) =>
157
+ ids[i] === null ? [] : [provider.contract(item.contract, ERC1155_ABI as unknown as string[]).balanceOf(owner, ids[i])],
158
+ )
159
+ const results = calls.length ? await provider.tryAll<bigint>(calls, overrides) : []
160
+ let j = 0
161
+ return items.map((item, i) => {
162
+ const balance = ids[i] === null ? null : (results[j++] ?? null)
163
+ return {
164
+ contract: item.contract,
165
+ tokenId: ids[i] === null ? String(item.tokenId) : tokenIdString(item.tokenId),
166
+ balance: balance === null ? '0' : balance.toString(),
167
+ success: balance !== null,
168
+ }
169
+ })
170
+ }
171
+
172
+ export async function nftTokenUris(provider: Provider, items: readonly NftItem[], options: NftTokenUriOptions = {}): Promise<NftTokenUri[]> {
173
+ const { ipfsGateway, ...overrides } = options
174
+ const chainId = await provider.getChainId()
175
+ const ids = items.map((item) => parseTokenId(item.tokenId))
176
+
177
+ // 集合标准已知时只发对应的调用;未知时 tokenURI 和 uri 都发,并在同一批里用 ERC165 识别标准(每个集合一次)、缓存下来
178
+ const calls: Call[] = []
179
+ const detect = new Map<string, { erc721: number; erc1155: number }>()
180
+ const plan = items.map((item, i) => {
181
+ const id = ids[i]
182
+ if (id === null || id === undefined) {
183
+ return { tokenUri: -1, uri: -1 }
184
+ }
185
+ const standard = getCachedTokenMeta(chainId, item.contract).standard
186
+ const key = item.contract.toLowerCase()
187
+ if (standard === undefined && !detect.has(key)) {
188
+ const nft = provider.contract(item.contract, ERC721_ABI as unknown as string[])
189
+ detect.set(key, {
190
+ erc721: calls.push(nft.supportsInterface(ERC721_INTERFACE_ID)) - 1,
191
+ erc1155: calls.push(nft.supportsInterface(ERC1155_INTERFACE_ID)) - 1,
192
+ })
193
+ }
194
+ const tokenUri =
195
+ standard === 'ERC1155' ? -1 : calls.push(provider.contract(item.contract, ERC721_ABI as unknown as string[]).tokenURI(id)) - 1
196
+ const uri = standard === 'ERC721' ? -1 : calls.push(provider.contract(item.contract, ERC1155_ABI as unknown as string[]).uri(id)) - 1
197
+ return { tokenUri, uri }
198
+ })
199
+ const results = calls.length ? await provider.tryAll(calls, overrides) : []
200
+
201
+ const detected = new Map<string, NftStandard | null>()
202
+ for (const [key, index] of detect) {
203
+ const is721 = (results[index.erc721] as boolean | null) ?? null
204
+ const is1155 = (results[index.erc1155] as boolean | null) ?? null
205
+ const standard: NftStandard | null = is721 ? 'ERC721' : is1155 ? 'ERC1155' : null
206
+ detected.set(key, standard)
207
+ // 两个调用都成功(返回 false)才缓存“不是 NFT”,失败可能只是暂时的
208
+ if (standard !== null || (is721 !== null && is1155 !== null)) {
209
+ setCachedTokenMeta(chainId, key, { standard })
210
+ }
211
+ }
212
+
213
+ return items.map((item, i) => {
214
+ const id = ids[i]
215
+ const { tokenUri, uri } = plan[i] as { tokenUri: number; uri: number }
216
+ const fromTokenUri = tokenUri === -1 ? null : ((results[tokenUri] as string | null) ?? null)
217
+ const fromUri = uri === -1 ? null : ((results[uri] as string | null) ?? null)
218
+ // 标准识别出来时以对应的方法为准;否则取成功的那个(空字符串也是有效结果,如尚未设置 baseURI)
219
+ const standard = detected.get(item.contract.toLowerCase())
220
+ const raw = standard === 'ERC1155' ? fromUri : standard === 'ERC721' ? fromTokenUri : (fromTokenUri ?? fromUri)
221
+ return {
222
+ contract: item.contract,
223
+ tokenId: id === null || id === undefined ? String(item.tokenId) : id.toString(),
224
+ uri: raw === null || id === null || id === undefined ? null : expandUri(raw, id, ipfsGateway),
225
+ success: raw !== null,
226
+ }
227
+ })
228
+ }
229
+
230
+ export async function nftCollections<F extends NftCollectionField = DefaultNftCollectionField>(
231
+ provider: Provider,
232
+ collections: readonly string[],
233
+ options: NftCollectionsOptions<F> = {},
234
+ ): Promise<NftCollection<F>[]> {
235
+ const { fields = DEFAULT_NFT_COLLECTION_FIELDS as unknown as readonly F[], ...overrides } = options
236
+ const wanted = new Set<NftCollectionField>(fields)
237
+ const chainId = await provider.getChainId()
238
+
239
+ const calls: Call[] = []
240
+ const plan = collections.map((address) => {
241
+ const meta = getCachedTokenMeta(chainId, address)
242
+ const nft = provider.contract(address, ERC721_ABI as unknown as string[])
243
+ const index: Partial<Record<'erc721' | 'erc1155' | 'name' | 'symbol' | 'totalSupply', number>> = {}
244
+ if (wanted.has('standard') && meta.standard === undefined) {
245
+ index.erc721 = calls.push(nft.supportsInterface(ERC721_INTERFACE_ID)) - 1
246
+ index.erc1155 = calls.push(nft.supportsInterface(ERC1155_INTERFACE_ID)) - 1
247
+ }
248
+ if (wanted.has('name') && meta.name === undefined) {
249
+ index.name = calls.push(asStringOrBytes32(nft.name())) - 1
250
+ }
251
+ if (wanted.has('symbol') && meta.symbol === undefined) {
252
+ index.symbol = calls.push(asStringOrBytes32(nft.symbol())) - 1
253
+ }
254
+ if (wanted.has('totalSupply')) {
255
+ index.totalSupply = calls.push(nft.totalSupply()) - 1
256
+ }
257
+ return { address, meta, index }
258
+ })
259
+ const results = calls.length ? await provider.tryAll(calls, overrides) : []
260
+ const read = <T>(i: number | undefined): T | null => (i === undefined ? null : ((results[i] as T | null) ?? null))
261
+
262
+ return plan.map(({ address, meta, index }) => {
263
+ let standard: NftStandard | null | undefined = meta.standard
264
+ if (standard === undefined && index.erc721 !== undefined) {
265
+ const is721 = read<boolean>(index.erc721)
266
+ const is1155 = read<boolean>(index.erc1155)
267
+ standard = is721 ? 'ERC721' : is1155 ? 'ERC1155' : null
268
+ // 两个调用都成功(返回 false)才缓存“不是 NFT”,失败可能只是暂时的
269
+ if (standard !== null || (is721 !== null && is1155 !== null)) {
270
+ setCachedTokenMeta(chainId, address, { standard })
271
+ }
272
+ }
273
+ const name = meta.name ?? read<string>(index.name)
274
+ const symbol = meta.symbol ?? read<string>(index.symbol)
275
+ const totalSupply = read<bigint>(index.totalSupply)
276
+ setCachedTokenMeta(chainId, address, {
277
+ ...(index.name !== undefined && name !== null ? { name } : {}),
278
+ ...(index.symbol !== undefined && symbol !== null ? { symbol } : {}),
279
+ })
280
+
281
+ const values: Record<NftCollectionField, unknown> = {
282
+ standard: standard ?? null,
283
+ name,
284
+ symbol,
285
+ totalSupply: totalSupply === null ? null : totalSupply.toString(),
286
+ }
287
+ const out: Record<string, unknown> = { address }
288
+ let success = true
289
+ for (const field of fields) {
290
+ out[field] = values[field]
291
+ if (values[field] === null) {
292
+ success = false
293
+ }
294
+ }
295
+ out.success = success
296
+ return out as NftCollection<F>
297
+ })
298
+ }