@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.
- package/CHANGELOG.md +43 -0
- package/README.md +194 -12
- package/dist/aggregate.d.ts +5 -2
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js.map +1 -1
- package/dist/call.d.ts +8 -2
- package/dist/call.d.ts.map +1 -1
- package/dist/call.js +25 -1
- package/dist/call.js.map +1 -1
- package/dist/chains.d.ts +13 -0
- package/dist/chains.d.ts.map +1 -0
- package/dist/chains.js +35 -0
- package/dist/chains.js.map +1 -0
- package/dist/detect.d.ts +23 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +178 -0
- package/dist/detect.js.map +1 -0
- package/dist/erc20.d.ts +83 -7
- package/dist/erc20.d.ts.map +1 -1
- package/dist/erc20.js +36 -5
- package/dist/erc20.js.map +1 -1
- package/dist/fallback.d.ts +25 -0
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +123 -21
- package/dist/fallback.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/nft.d.ts +71 -0
- package/dist/nft.d.ts.map +1 -0
- package/dist/nft.js +194 -0
- package/dist/nft.js.map +1 -0
- package/dist/owner.d.ts +136 -0
- package/dist/owner.d.ts.map +1 -0
- package/dist/owner.js +501 -0
- package/dist/owner.js.map +1 -0
- package/dist/provider.d.ts +112 -12
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +343 -48
- package/dist/provider.js.map +1 -1
- package/dist/scan.d.ts +59 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +245 -0
- package/dist/scan.js.map +1 -0
- package/dist/shortcuts.d.ts +113 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/shortcuts.js +193 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/source.d.ts +23 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +110 -3
- package/dist/source.js.map +1 -1
- package/dist/tron.d.ts +6 -0
- package/dist/tron.d.ts.map +1 -1
- package/dist/tron.js +16 -1
- package/dist/tron.js.map +1 -1
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +20 -0
- package/dist/util.js.map +1 -0
- package/package.json +3 -2
- package/src/aggregate.ts +5 -2
- package/src/call.ts +32 -3
- package/src/chains.ts +46 -0
- package/src/detect.ts +193 -0
- package/src/erc20.ts +111 -9
- package/src/fallback.ts +127 -23
- package/src/index.ts +90 -4
- package/src/nft.ts +298 -0
- package/src/owner.ts +651 -0
- package/src/provider.ts +458 -54
- package/src/scan.ts +293 -0
- package/src/shortcuts.ts +278 -0
- package/src/source.ts +118 -3
- package/src/tron.ts +21 -2
- 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
|
+
}
|