@w3lib/evm-batch-call 0.2.0 → 0.4.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 (108) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +184 -30
  3. package/contracts/DeploylessMulticall3.sol +5 -0
  4. package/dist/aggregate.d.ts +25 -3
  5. package/dist/aggregate.d.ts.map +1 -1
  6. package/dist/aggregate.js +143 -5
  7. package/dist/aggregate.js.map +1 -1
  8. package/dist/batcher.d.ts +6 -1
  9. package/dist/batcher.d.ts.map +1 -1
  10. package/dist/batcher.js +59 -9
  11. package/dist/batcher.js.map +1 -1
  12. package/dist/call.d.ts +27 -2
  13. package/dist/call.d.ts.map +1 -1
  14. package/dist/call.js +22 -0
  15. package/dist/call.js.map +1 -1
  16. package/dist/chains.d.ts +7 -0
  17. package/dist/chains.d.ts.map +1 -1
  18. package/dist/chains.js +11 -0
  19. package/dist/chains.js.map +1 -1
  20. package/dist/deployless.d.ts +1 -1
  21. package/dist/deployless.d.ts.map +1 -1
  22. package/dist/deployless.js +1 -1
  23. package/dist/deployless.js.map +1 -1
  24. package/dist/erc20.d.ts +70 -4
  25. package/dist/erc20.d.ts.map +1 -1
  26. package/dist/erc20.js +121 -5
  27. package/dist/erc20.js.map +1 -1
  28. package/dist/events.d.ts +25 -0
  29. package/dist/events.d.ts.map +1 -0
  30. package/dist/events.js +25 -0
  31. package/dist/events.js.map +1 -0
  32. package/dist/fallback.d.ts +54 -3
  33. package/dist/fallback.d.ts.map +1 -1
  34. package/dist/fallback.js +273 -21
  35. package/dist/fallback.js.map +1 -1
  36. package/dist/index.d.ts +10 -8
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/multicall.d.ts.map +1 -1
  41. package/dist/multicall.js +33 -2
  42. package/dist/multicall.js.map +1 -1
  43. package/dist/nft.d.ts +5 -0
  44. package/dist/nft.d.ts.map +1 -1
  45. package/dist/nft.js +5 -0
  46. package/dist/nft.js.map +1 -1
  47. package/dist/owner.d.ts +145 -0
  48. package/dist/owner.d.ts.map +1 -0
  49. package/dist/owner.js +510 -0
  50. package/dist/owner.js.map +1 -0
  51. package/dist/provider.d.ts +63 -18
  52. package/dist/provider.d.ts.map +1 -1
  53. package/dist/provider.js +166 -100
  54. package/dist/provider.js.map +1 -1
  55. package/dist/scan.d.ts +59 -0
  56. package/dist/scan.d.ts.map +1 -0
  57. package/dist/scan.js +245 -0
  58. package/dist/scan.js.map +1 -0
  59. package/dist/shortcuts.d.ts +34 -35
  60. package/dist/shortcuts.d.ts.map +1 -1
  61. package/dist/shortcuts.js +41 -73
  62. package/dist/shortcuts.js.map +1 -1
  63. package/dist/source.d.ts +6 -2
  64. package/dist/source.d.ts.map +1 -1
  65. package/dist/source.js +49 -6
  66. package/dist/source.js.map +1 -1
  67. package/dist/subpaths/nft.d.ts +46 -0
  68. package/dist/subpaths/nft.d.ts.map +1 -0
  69. package/dist/subpaths/nft.js +55 -0
  70. package/dist/subpaths/nft.js.map +1 -0
  71. package/dist/subpaths/owner.d.ts +33 -0
  72. package/dist/subpaths/owner.d.ts.map +1 -0
  73. package/dist/subpaths/owner.js +34 -0
  74. package/dist/subpaths/owner.js.map +1 -0
  75. package/dist/tron.d.ts +2 -0
  76. package/dist/tron.d.ts.map +1 -1
  77. package/dist/tron.js +5 -0
  78. package/dist/tron.js.map +1 -1
  79. package/dist/util.d.ts +5 -0
  80. package/dist/util.d.ts.map +1 -1
  81. package/dist/util.js +23 -0
  82. package/dist/util.js.map +1 -1
  83. package/dist/watch.d.ts +25 -0
  84. package/dist/watch.d.ts.map +1 -0
  85. package/dist/watch.js +105 -0
  86. package/dist/watch.js.map +1 -0
  87. package/package.json +19 -10
  88. package/src/aggregate.ts +177 -9
  89. package/src/batcher.ts +69 -9
  90. package/src/call.ts +42 -2
  91. package/src/chains.ts +15 -0
  92. package/src/deployless.ts +1 -1
  93. package/src/erc20.ts +171 -8
  94. package/src/events.ts +42 -0
  95. package/src/fallback.ts +306 -22
  96. package/src/index.ts +20 -27
  97. package/src/multicall.ts +34 -2
  98. package/src/nft.ts +5 -0
  99. package/src/owner.ts +660 -0
  100. package/src/provider.ts +255 -112
  101. package/src/scan.ts +293 -0
  102. package/src/shortcuts.ts +76 -88
  103. package/src/source.ts +51 -8
  104. package/src/subpaths/nft.ts +107 -0
  105. package/src/subpaths/owner.ts +69 -0
  106. package/src/tron.ts +7 -0
  107. package/src/util.ts +27 -0
  108. package/src/watch.ts +149 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * NFT 查询(子路径 `@w3lib/evm-batch-call/nft`):ERC721 / ERC1155 的集合信息、持有数量、持有人、元数据地址。
3
+ * 不放在主入口,不用 NFT 功能时打包不会带上这部分代码。
4
+ *
5
+ * ```ts
6
+ * import { Provider } from '@w3lib/evm-batch-call'
7
+ * import { getNftOwners, nftOwners } from '@w3lib/evm-batch-call/nft'
8
+ *
9
+ * await getNftOwners([{ contract: BAYC, tokenId: 1 }], { chainId: 1 }) // 节点参数与 getBalances 相同
10
+ * await nftOwners(multi, [{ contract: BAYC, tokenId: 1 }]) // 或传已创建的 Provider
11
+ * ```
12
+ */
13
+ import {
14
+ erc1155Balances,
15
+ nftBalances,
16
+ nftCollections,
17
+ nftOwners,
18
+ nftTokenUris,
19
+ type DefaultNftCollectionField,
20
+ type Erc1155Balance,
21
+ type NftBalance,
22
+ type NftCollection,
23
+ type NftCollectionField,
24
+ type NftItem,
25
+ type NftOwner,
26
+ type NftTokenUri,
27
+ type NftCollectionsOptions,
28
+ } from '../nft.js'
29
+ import { resolve, type ShortcutOptions, type ShortcutProviderOptions } from '../shortcuts.js'
30
+
31
+ export {
32
+ DEFAULT_NFT_COLLECTION_FIELDS,
33
+ ERC1155_ABI,
34
+ ERC721_ABI,
35
+ erc1155Balances,
36
+ nftBalances,
37
+ nftCollections,
38
+ nftOwners,
39
+ nftTokenUris,
40
+ type DefaultNftCollectionField,
41
+ type Erc1155Balance,
42
+ type NftBalance,
43
+ type NftCollection,
44
+ type NftCollectionField,
45
+ type NftCollectionsOptions,
46
+ type NftItem,
47
+ type NftOwner,
48
+ type NftStandard,
49
+ type NftTokenUri,
50
+ type NftTokenUriOptions,
51
+ } from '../nft.js'
52
+
53
+ export interface GetNftCollectionsOptions<F extends NftCollectionField = DefaultNftCollectionField>
54
+ extends NftCollectionsOptions<F>,
55
+ ShortcutProviderOptions {}
56
+
57
+ /**
58
+ * 批量查 NFT 集合信息,字段可选(默认 standard / name / symbol)。
59
+ *
60
+ * ```ts
61
+ * await getNftCollections([BAYC, MAYC], { chainId: 1, fields: ['standard', 'name', 'totalSupply'] })
62
+ * // [{ address: BAYC, standard: 'ERC721', name: 'BoredApeYachtClub', totalSupply: '10000', success: true }, ...]
63
+ * ```
64
+ */
65
+ export function getNftCollections<const F extends NftCollectionField = DefaultNftCollectionField>(
66
+ collections: readonly string[],
67
+ options: GetNftCollectionsOptions<F> = {},
68
+ ): Promise<NftCollection<F>[]> {
69
+ const { provider, overrides, own } = resolve(options, ['fields'])
70
+ return nftCollections<F>(provider, collections, { ...overrides, fields: own.fields })
71
+ }
72
+
73
+ /** 批量查 ERC721 持有数量:`await getNftBalances(user, [BAYC, MAYC], { chainId: 1 })` */
74
+ export function getNftBalances(owner: string, collections: readonly string[], options: ShortcutOptions = {}): Promise<NftBalance[]> {
75
+ const { provider, overrides } = resolve(options)
76
+ return nftBalances(provider, owner, collections, overrides)
77
+ }
78
+
79
+ /** 批量查 ERC721 持有人:`await getNftOwners([{ contract: BAYC, tokenId: 1 }], { chainId: 1 })` */
80
+ export function getNftOwners(items: readonly NftItem[], options: ShortcutOptions = {}): Promise<NftOwner[]> {
81
+ const { provider, overrides } = resolve(options)
82
+ return nftOwners(provider, items, overrides)
83
+ }
84
+
85
+ export interface GetNftTokenUrisOptions extends ShortcutOptions {
86
+ /** 把 ipfs://xxx 转成 `${ipfsGateway}xxx`,如 'https://ipfs.io/ipfs/' */
87
+ ipfsGateway?: string
88
+ }
89
+
90
+ /**
91
+ * 批量查 NFT 元数据地址(ERC721 tokenURI / ERC1155 uri 自动兼容)。
92
+ *
93
+ * ```ts
94
+ * await getNftTokenUris([{ contract: BAYC, tokenId: 1 }], { chainId: 1, ipfsGateway: 'https://ipfs.io/ipfs/' })
95
+ * // [{ contract: BAYC, tokenId: '1', uri: 'https://ipfs.io/ipfs/Qm…/1', success: true }]
96
+ * ```
97
+ */
98
+ export function getNftTokenUris(items: readonly NftItem[], options: GetNftTokenUrisOptions = {}): Promise<NftTokenUri[]> {
99
+ const { provider, overrides, own } = resolve(options, ['ipfsGateway'])
100
+ return nftTokenUris(provider, items, { ...overrides, ipfsGateway: own.ipfsGateway })
101
+ }
102
+
103
+ /** 批量查 ERC1155 余额:`await getErc1155Balances(user, [{ contract, tokenId: 1 }], { chainId: 137 })` */
104
+ export function getErc1155Balances(owner: string, items: readonly NftItem[], options: ShortcutOptions = {}): Promise<Erc1155Balance[]> {
105
+ const { provider, overrides } = resolve(options)
106
+ return erc1155Balances(provider, owner, items, overrides)
107
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 资产列表(子路径 `@w3lib/evm-batch-call/owner`):从代币来源发现候选代币,multicall 核对余额;含代币来源、价格源、
3
+ * Transfer 事件增量扫描。不放在主入口,不用时打包不会带上这部分代码。
4
+ *
5
+ * ```ts
6
+ * import { getOwnerTokens, ownerTokens, alchemy } from '@w3lib/evm-batch-call/owner'
7
+ *
8
+ * await getOwnerTokens(user, { chainId: 56 })
9
+ * await ownerTokens(multi, user, { source: alchemy({ apiKey }) }) // 或传已创建的 Provider
10
+ * ```
11
+ */
12
+ import { ownerTokens, type OwnedToken, type OwnerTokensOptions } from '../owner.js'
13
+ import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
14
+
15
+ export {
16
+ ALCHEMY_NETWORKS,
17
+ COINGECKO_PLATFORMS,
18
+ DEFILLAMA_CHAINS,
19
+ alchemy,
20
+ clearTokenListCache,
21
+ coingeckoTokenList,
22
+ combine,
23
+ defaultTokenSource,
24
+ defillamaPrices,
25
+ firstAvailable,
26
+ metamaskTokenList,
27
+ nodereal,
28
+ ownerTokens,
29
+ staticTokens,
30
+ tokenList,
31
+ type DiscoveredToken,
32
+ type IndexerSourceOptions,
33
+ type OwnedToken,
34
+ type OwnerTokensOptions,
35
+ type PriceSource,
36
+ type PriceSourceContext,
37
+ type TokenSource,
38
+ type TokenSourceContext,
39
+ } from '../owner.js'
40
+ export {
41
+ getTransferScanState,
42
+ keyValueScanStorage,
43
+ memoryScanStorage,
44
+ transferScan,
45
+ type TransferScanOptions,
46
+ type TransferScanState,
47
+ type TransferScanStorage,
48
+ } from '../scan.js'
49
+
50
+ export interface GetOwnerTokensOptions extends OwnerTokensOptions, ShortcutProviderOptions {}
51
+
52
+ /**
53
+ * 列出持有人拥有的代币(资产列表)。默认用免费公开代币列表发现代币,余额用 multicall 在链上核对。
54
+ *
55
+ * ```ts
56
+ * await getOwnerTokens(user, { chainId: 56 }) // 免费,只能发现公开列表里的代币
57
+ * await getOwnerTokens(user, { chainId: 1, prices: true, minUsd: 1 }) // 带美元价值,过滤零头和垃圾币
58
+ * await getOwnerTokens(user, { chainId: 56, source: alchemy({ apiKey }) }) // 用 Alchemy 查全部历史持仓
59
+ * ```
60
+ *
61
+ * @deprecated 不推荐使用:EVM 链上无法只靠节点可靠地列出地址持有的全部代币,这个方法只是尽力而为——
62
+ * 默认来源只能发现公开列表里的代币,依赖第三方免费服务(可能限流、改格式或停止服务),要查几千个代币的余额、耗时数秒,
63
+ * 价格也不一定可信;传 `alchemy` / `nodereal` 来源能查全,但同样受第三方服务的额度和可用性约束。
64
+ * 已知要查哪些代币时请用 `getBalances`;需要可靠的完整持仓请直接接入索引服务的接口或自己的索引。局限性详见 README「资产列表」。
65
+ */
66
+ export function getOwnerTokens(owner: string, options: GetOwnerTokensOptions = {}): Promise<OwnedToken[]> {
67
+ const { provider, overrides, own } = resolve(options, ['source', 'prices', 'minUsd', 'includeNative', 'fetch', 'scanTransfers'])
68
+ return ownerTokens(provider, owner, { ...overrides, ...own })
69
+ }
package/src/tron.ts CHANGED
@@ -19,6 +19,13 @@ export const TRON_CHAIN_ID = {
19
19
  nile: 3448148188,
20
20
  } as const
21
21
 
22
+ const TRON_CHAIN_IDS = new Set<number>(Object.values(TRON_CHAIN_ID))
23
+
24
+ /** 是否是 Tron 链(主网 / 测试网):只能查最新状态,地址区分大小写 */
25
+ export function isTronChain(chainId: number): boolean {
26
+ return TRON_CHAIN_IDS.has(chainId)
27
+ }
28
+
22
29
  const TRON_ADDRESS_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/
23
30
 
24
31
  export function isTronAddress(value: unknown): boolean {
package/src/util.ts CHANGED
@@ -18,3 +18,30 @@ export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
18
18
  }),
19
19
  ]).finally(() => clearTimeout(timer))
20
20
  }
21
+
22
+ /**
23
+ * 支持 AbortSignal:已取消时直接 reject(不执行 work,不发请求);执行中取消时立即 reject(reason 为 signal.reason),
24
+ * 底层请求不中断(可能与其他调用合并在同一个请求里),结果丢弃
25
+ */
26
+ export function withSignal<T>(signal: AbortSignal | undefined, work: () => Promise<T>): Promise<T> {
27
+ if (!signal) {
28
+ return work()
29
+ }
30
+ if (signal.aborted) {
31
+ return Promise.reject(signal.reason)
32
+ }
33
+ return new Promise<T>((resolve, reject) => {
34
+ const onAbort = () => reject(signal.reason)
35
+ signal.addEventListener('abort', onAbort, { once: true })
36
+ work().then(
37
+ (value) => {
38
+ signal.removeEventListener('abort', onAbort)
39
+ resolve(value)
40
+ },
41
+ (err: unknown) => {
42
+ signal.removeEventListener('abort', onAbort)
43
+ reject(err)
44
+ },
45
+ )
46
+ })
47
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,149 @@
1
+ import type { BalanceToken, RawTokenBalance, TokenBalance } from './erc20.js'
2
+ import type { Provider } from './provider.js'
3
+ import { resolve, type GetBalancesOptions } from './shortcuts.js'
4
+
5
+ type AnyBalance = TokenBalance | RawTokenBalance
6
+
7
+ export interface WatchBalancesOptions<B extends AnyBalance = TokenBalance> extends Omit<GetBalancesOptions, 'signal' | 'minBlock' | 'blockTag'> {
8
+ /** 轮询间隔(毫秒),默认 15000;多处订阅同一份数据时取最小的间隔 */
9
+ interval?: number
10
+ /** 第一次拿到结果、以及之后余额变化时调用;previous 是上一次通知的结果(第一次为 null) */
11
+ onChange: (balances: B[], previous: B[] | null) => void
12
+ /** 某次查询失败时调用(轮询继续) */
13
+ onError?: (err: unknown) => void
14
+ }
15
+
16
+ interface Subscriber {
17
+ tokens: readonly BalanceToken[]
18
+ interval: number
19
+ onChange: (balances: AnyBalance[], previous: AnyBalance[] | null) => void
20
+ onError?: (err: unknown) => void
21
+ last: AnyBalance[] | null
22
+ signature: string | null
23
+ }
24
+
25
+ interface Watch {
26
+ subscribers: Set<Subscriber>
27
+ timer: ReturnType<typeof setTimeout> | null
28
+ running: boolean
29
+ /** 最近一次的结果(按规范化的代币 key),新订阅者可以立即拿到 */
30
+ latest: Map<string, AnyBalance> | null
31
+ }
32
+
33
+ // 按 Provider 实例分组(快捷函数对相同节点参数复用同一个实例),组内按 “钱包 + 代币集合 + 选项” 共用一份轮询
34
+ const watches = new WeakMap<Provider, Map<string, Watch>>()
35
+
36
+ /** 调用订阅者的回调:出错不影响其他订阅者和轮询 */
37
+ function safely(fn: () => void): void {
38
+ try {
39
+ fn()
40
+ } catch {
41
+ // 订阅者自己的错误(如渲染出错)不归轮询处理
42
+ }
43
+ }
44
+
45
+ /** 0x 地址不区分大小写,统一小写;Tron 的 T 开头地址区分大小写,原样保留 */
46
+ const normalize = (address: string) => (/^0x/i.test(address) ? address.toLowerCase() : address)
47
+
48
+ const tokenKey = (token: BalanceToken) => (typeof token === 'string' ? normalize(token) : `${normalize(token.address)}:${token.decimals ?? ''}`)
49
+
50
+ /**
51
+ * 轮询余额,多处订阅同一个钱包的同一组代币时合并成一份轮询(代币顺序无关),只在余额变化时通知。返回取消订阅的函数;
52
+ * 最后一个订阅者取消后停止轮询。节点参数与 getBalances 相同(相同参数复用同一个 Provider,才能共用轮询)。
53
+ *
54
+ * ```ts
55
+ * const stop = watchBalances(user, [NATIVE_TOKEN, USDT], { chainId: 56, interval: 10_000, onChange: (list) => render(list) })
56
+ * ```
57
+ */
58
+ export function watchBalances(owner: string, tokens: readonly BalanceToken[], options: WatchBalancesOptions<RawTokenBalance> & { decimals: false }): () => void
59
+ export function watchBalances(owner: string, tokens: readonly BalanceToken[], options: WatchBalancesOptions): () => void
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 实现签名需兼容两种 onChange 参数类型
61
+ export function watchBalances(owner: string, tokens: readonly BalanceToken[], options: WatchBalancesOptions<any>): () => void {
62
+ const { interval = 15_000, onChange, onError, ...rest } = options
63
+ const { provider, overrides, own } = resolve(rest, ['symbol', 'decimals', 'withBlock'])
64
+ const balancesOptions = { ...overrides, ...own }
65
+ const unique = [...new Map(tokens.map((token) => [tokenKey(token), token])).entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
66
+ const key = JSON.stringify([normalize(owner), unique.map(([k]) => k), balancesOptions])
67
+
68
+ let group = watches.get(provider)
69
+ if (!group) {
70
+ group = new Map()
71
+ watches.set(provider, group)
72
+ }
73
+ let watch = group.get(key)
74
+ const isNew = !watch
75
+ if (!watch) {
76
+ watch = { subscribers: new Set(), timer: null, running: false, latest: null }
77
+ group.set(key, watch)
78
+ }
79
+ const subscriber: Subscriber = { tokens, interval, onChange: onChange as Subscriber['onChange'], onError, last: null, signature: null }
80
+ watch.subscribers.add(subscriber)
81
+
82
+ const current = watch
83
+ const groupRef = group
84
+ const notify = (sub: Subscriber) => {
85
+ if (!current.latest) {
86
+ return
87
+ }
88
+ const list = sub.tokens.map((token) => {
89
+ const item = current.latest?.get(tokenKey(token)) as AnyBalance
90
+ return { ...item, token: typeof token === 'string' ? token : token.address }
91
+ })
92
+ const signature = JSON.stringify(list.map((b) => [b.balance, b.success]))
93
+ if (signature === sub.signature) {
94
+ return
95
+ }
96
+ const previous = sub.last
97
+ sub.last = list
98
+ sub.signature = signature
99
+ safely(() => sub.onChange(list, previous))
100
+ }
101
+ const schedule = () => {
102
+ if (!current.subscribers.size) {
103
+ return
104
+ }
105
+ const wait = Math.min(...[...current.subscribers].map((sub) => sub.interval))
106
+ current.timer = setTimeout(poll, wait)
107
+ }
108
+ const poll = async () => {
109
+ current.timer = null
110
+ if (current.running || !current.subscribers.size) {
111
+ return
112
+ }
113
+ current.running = true
114
+ try {
115
+ const list = (await provider.balances(owner, unique.map(([, token]) => token), balancesOptions)) as AnyBalance[]
116
+ current.latest = new Map(unique.map(([k], i) => [k, list[i] as AnyBalance]))
117
+ current.subscribers.forEach(notify)
118
+ } catch (err) {
119
+ current.subscribers.forEach((sub) => safely(() => sub.onError?.(err)))
120
+ } finally {
121
+ current.running = false
122
+ schedule()
123
+ }
124
+ }
125
+
126
+ if (isNew) {
127
+ void poll()
128
+ } else {
129
+ // 已有轮询:有结果就立即给新订阅者;新订阅者的间隔更短时重新计时
130
+ queueMicrotask(() => current.subscribers.has(subscriber) && notify(subscriber))
131
+ if (current.timer && interval < Math.min(...[...current.subscribers].filter((s) => s !== subscriber).map((s) => s.interval))) {
132
+ clearTimeout(current.timer)
133
+ schedule()
134
+ }
135
+ }
136
+
137
+ return () => {
138
+ current.subscribers.delete(subscriber)
139
+ if (!current.subscribers.size) {
140
+ if (current.timer) {
141
+ clearTimeout(current.timer)
142
+ current.timer = null
143
+ }
144
+ if (groupRef.get(key) === current) {
145
+ groupRef.delete(key)
146
+ }
147
+ }
148
+ }
149
+ }