@w3lib/evm-batch-call 0.7.0 → 0.8.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 (159) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +206 -1
  3. package/dist/dividends.d.ts +135 -0
  4. package/dist/dividends.d.ts.map +1 -0
  5. package/dist/dividends.js +253 -0
  6. package/dist/dividends.js.map +1 -0
  7. package/dist/history.d.ts +154 -0
  8. package/dist/history.d.ts.map +1 -0
  9. package/dist/history.js +247 -0
  10. package/dist/history.js.map +1 -0
  11. package/dist/historySources.d.ts +32 -0
  12. package/dist/historySources.d.ts.map +1 -0
  13. package/dist/historySources.js +319 -0
  14. package/dist/historySources.js.map +1 -0
  15. package/dist/liquidity/v3.d.ts +2 -0
  16. package/dist/liquidity/v3.d.ts.map +1 -1
  17. package/dist/liquidity/v3.js +1 -1
  18. package/dist/liquidity/v3.js.map +1 -1
  19. package/dist/liquidity/v4.d.ts +21 -0
  20. package/dist/liquidity/v4.d.ts.map +1 -1
  21. package/dist/liquidity/v4.js +4 -3
  22. package/dist/liquidity/v4.js.map +1 -1
  23. package/dist/lp.d.ts +8 -0
  24. package/dist/lp.d.ts.map +1 -1
  25. package/dist/lp.js +17 -1
  26. package/dist/lp.js.map +1 -1
  27. package/dist/lpV4.d.ts +4 -0
  28. package/dist/lpV4.d.ts.map +1 -1
  29. package/dist/lpV4.js +27 -3
  30. package/dist/lpV4.js.map +1 -1
  31. package/dist/permit.d.ts +137 -0
  32. package/dist/permit.d.ts.map +1 -0
  33. package/dist/permit.js +355 -0
  34. package/dist/permit.js.map +1 -0
  35. package/dist/portfolio.d.ts +2 -0
  36. package/dist/portfolio.d.ts.map +1 -1
  37. package/dist/portfolio.js +9 -4
  38. package/dist/portfolio.js.map +1 -1
  39. package/dist/rewards/farms.d.ts +26 -0
  40. package/dist/rewards/farms.d.ts.map +1 -0
  41. package/dist/rewards/farms.js +210 -0
  42. package/dist/rewards/farms.js.map +1 -0
  43. package/dist/rewards/index.d.ts +42 -0
  44. package/dist/rewards/index.d.ts.map +1 -0
  45. package/dist/rewards/index.js +382 -0
  46. package/dist/rewards/index.js.map +1 -0
  47. package/dist/rewards/types.d.ts +170 -0
  48. package/dist/rewards/types.d.ts.map +1 -0
  49. package/dist/rewards/types.js +2 -0
  50. package/dist/rewards/types.js.map +1 -0
  51. package/dist/simulate.d.ts +186 -0
  52. package/dist/simulate.d.ts.map +1 -0
  53. package/dist/simulate.js +559 -0
  54. package/dist/simulate.js.map +1 -0
  55. package/dist/simulateDecode.d.ts +33 -0
  56. package/dist/simulateDecode.d.ts.map +1 -0
  57. package/dist/simulateDecode.js +321 -0
  58. package/dist/simulateDecode.js.map +1 -0
  59. package/dist/smartAccount/account.d.ts +31 -0
  60. package/dist/smartAccount/account.d.ts.map +1 -0
  61. package/dist/smartAccount/account.js +33 -0
  62. package/dist/smartAccount/account.js.map +1 -0
  63. package/dist/smartAccount/clients.d.ts +77 -0
  64. package/dist/smartAccount/clients.d.ts.map +1 -0
  65. package/dist/smartAccount/clients.js +122 -0
  66. package/dist/smartAccount/clients.js.map +1 -0
  67. package/dist/smartAccount/eip5792.d.ts +77 -0
  68. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  69. package/dist/smartAccount/eip5792.js +152 -0
  70. package/dist/smartAccount/eip5792.js.map +1 -0
  71. package/dist/smartAccount/send.d.ts +40 -0
  72. package/dist/smartAccount/send.d.ts.map +1 -0
  73. package/dist/smartAccount/send.js +271 -0
  74. package/dist/smartAccount/send.js.map +1 -0
  75. package/dist/smartAccount/userOp.d.ts +91 -0
  76. package/dist/smartAccount/userOp.d.ts.map +1 -0
  77. package/dist/smartAccount/userOp.js +124 -0
  78. package/dist/smartAccount/userOp.js.map +1 -0
  79. package/dist/subpaths/dividends.d.ts +28 -0
  80. package/dist/subpaths/dividends.d.ts.map +1 -0
  81. package/dist/subpaths/dividends.js +29 -0
  82. package/dist/subpaths/dividends.js.map +1 -0
  83. package/dist/subpaths/history.d.ts +22 -0
  84. package/dist/subpaths/history.d.ts.map +1 -0
  85. package/dist/subpaths/history.js +23 -0
  86. package/dist/subpaths/history.js.map +1 -0
  87. package/dist/subpaths/permit.d.ts +14 -0
  88. package/dist/subpaths/permit.d.ts.map +1 -0
  89. package/dist/subpaths/permit.js +14 -0
  90. package/dist/subpaths/permit.js.map +1 -0
  91. package/dist/subpaths/price.js +2 -2
  92. package/dist/subpaths/price.js.map +1 -1
  93. package/dist/subpaths/rewards.d.ts +27 -0
  94. package/dist/subpaths/rewards.d.ts.map +1 -0
  95. package/dist/subpaths/rewards.js +25 -0
  96. package/dist/subpaths/rewards.js.map +1 -0
  97. package/dist/subpaths/simulate.d.ts +25 -0
  98. package/dist/subpaths/simulate.d.ts.map +1 -0
  99. package/dist/subpaths/simulate.js +28 -0
  100. package/dist/subpaths/simulate.js.map +1 -0
  101. package/dist/subpaths/smart-account.d.ts +24 -0
  102. package/dist/subpaths/smart-account.d.ts.map +1 -0
  103. package/dist/subpaths/smart-account.js +24 -0
  104. package/dist/subpaths/smart-account.js.map +1 -0
  105. package/dist/swap.d.ts +45 -5
  106. package/dist/swap.d.ts.map +1 -1
  107. package/dist/swap.js +108 -12
  108. package/dist/swap.js.map +1 -1
  109. package/dist/write/batch.d.ts +34 -2
  110. package/dist/write/batch.d.ts.map +1 -1
  111. package/dist/write/batch.js +22 -3
  112. package/dist/write/batch.js.map +1 -1
  113. package/dist/write/collect.d.ts +9 -0
  114. package/dist/write/collect.d.ts.map +1 -1
  115. package/dist/write/collect.js +41 -1
  116. package/dist/write/collect.js.map +1 -1
  117. package/dist/write/send.d.ts +4 -0
  118. package/dist/write/send.d.ts.map +1 -1
  119. package/dist/write/send.js.map +1 -1
  120. package/dist/write/signer.d.ts +5 -1
  121. package/dist/write/signer.d.ts.map +1 -1
  122. package/dist/write/signer.js.map +1 -1
  123. package/dist/write/transfer.d.ts +5 -3
  124. package/dist/write/transfer.d.ts.map +1 -1
  125. package/dist/write/transfer.js +12 -6
  126. package/dist/write/transfer.js.map +1 -1
  127. package/package.json +35 -1
  128. package/src/dividends.ts +360 -0
  129. package/src/history.ts +367 -0
  130. package/src/historySources.ts +360 -0
  131. package/src/liquidity/v3.ts +1 -1
  132. package/src/liquidity/v4.ts +4 -3
  133. package/src/lp.ts +18 -1
  134. package/src/lpV4.ts +32 -2
  135. package/src/permit.ts +457 -0
  136. package/src/portfolio.ts +11 -4
  137. package/src/rewards/farms.ts +229 -0
  138. package/src/rewards/index.ts +431 -0
  139. package/src/rewards/types.ts +179 -0
  140. package/src/simulate.ts +727 -0
  141. package/src/simulateDecode.ts +352 -0
  142. package/src/smartAccount/account.ts +63 -0
  143. package/src/smartAccount/clients.ts +180 -0
  144. package/src/smartAccount/eip5792.ts +187 -0
  145. package/src/smartAccount/send.ts +341 -0
  146. package/src/smartAccount/userOp.ts +180 -0
  147. package/src/subpaths/dividends.ts +47 -0
  148. package/src/subpaths/history.ts +41 -0
  149. package/src/subpaths/permit.ts +39 -0
  150. package/src/subpaths/price.ts +2 -2
  151. package/src/subpaths/rewards.ts +46 -0
  152. package/src/subpaths/simulate.ts +51 -0
  153. package/src/subpaths/smart-account.ts +40 -0
  154. package/src/swap.ts +139 -18
  155. package/src/write/batch.ts +44 -5
  156. package/src/write/collect.ts +60 -1
  157. package/src/write/send.ts +4 -0
  158. package/src/write/signer.ts +5 -1
  159. package/src/write/transfer.ts +17 -10
package/src/permit.ts ADDED
@@ -0,0 +1,457 @@
1
+ /**
2
+ * 免 gas 授权(子路径 `@w3lib/evm-batch-call/permit`):EIP-2612 permit(含 DAI 风格)、Permit2(AllowanceTransfer 的 PermitSingle / PermitBatch、
3
+ * SignatureTransfer 的 permitTransferFrom)。构建 EIP-712 数据、用 ethers Signer 签名(私钥 / 浏览器钱包都行)、本地验证签名、生成上链的 calldata。
4
+ *
5
+ * 安全默认:金额精确(无限额度要显式 allowUnlimited)、有效期默认 30 分钟(超过 24 小时要显式 allowLongDeadline)、
6
+ * 签名前核对 owner = 签名者、chainId、verifyingContract(代币本身 / Permit2)、spender 不是零地址
7
+ */
8
+ import { Interface, MaxUint256, TypedDataEncoder, getAddress, hexlify, keccak256, randomBytes, toUtf8Bytes, verifyTypedData, Signature, ZeroAddress, type TypedDataDomain, type TypedDataField } from 'ethers'
9
+
10
+ import type { Provider } from './provider.js'
11
+ import type { WriteSigner } from './write/signer.js'
12
+
13
+ /** Uniswap 的 Permit2(各 EVM 链上地址相同) */
14
+ export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
15
+ export const MAX_UINT160 = (1n << 160n) - 1n
16
+ /** 默认的签名有效期(秒) */
17
+ export const DEFAULT_PERMIT_DEADLINE = 1800
18
+ /** 有效期超过这个值(秒)要显式 allowLongDeadline */
19
+ export const MAX_PERMIT_DEADLINE = 86_400
20
+
21
+ export type PermitKind = 'eip2612' | 'dai' | 'permit2-single' | 'permit2-batch' | 'permit2-transfer'
22
+
23
+ export interface TypedData {
24
+ domain: TypedDataDomain
25
+ types: Record<string, TypedDataField[]>
26
+ primaryType: string
27
+ message: Record<string, any>
28
+ }
29
+
30
+ /** 一个待签名的授权:typedData 是要签的内容,其余是它的摘要(签名前核对用) */
31
+ export interface Permit {
32
+ kind: PermitKind
33
+ chainId: number
34
+ owner: string
35
+ spender: string
36
+ /** 代币地址(permit2-batch 为每个代币一项) */
37
+ tokens: string[]
38
+ /** 授权 / 转账的数量(最小单位),与 tokens 一一对应(DAI 风格为 '0' / 无限) */
39
+ amounts: string[]
40
+ /** 签名过期时间(unix 秒) */
41
+ deadline: number
42
+ /** permit2-single / batch:额度本身的过期时间(unix 秒) */
43
+ expiration?: number
44
+ /** 签名要用的 nonce(permit2-batch 为每个代币一项) */
45
+ nonces: string[]
46
+ typedData: TypedData
47
+ }
48
+
49
+ export interface SignedPermit {
50
+ permit: Permit
51
+ signature: string
52
+ /** 紧凑的 v / r / s,给 permit() 的参数用 */
53
+ v: number
54
+ r: string
55
+ s: string
56
+ }
57
+
58
+ export interface PermitOptions {
59
+ /** 签名有效期(秒),默认 1800;也可传 deadlineAt(unix 秒) */
60
+ deadline?: number
61
+ deadlineAt?: number
62
+ /** 允许无限额度(MaxUint256 / MaxUint160,DAI 风格的 permit 本身只能授权无限)。默认拒绝 */
63
+ allowUnlimited?: boolean
64
+ /** 允许超过 24 小时的有效期。默认拒绝 */
65
+ allowLongDeadline?: boolean
66
+ }
67
+
68
+ const now = () => Math.floor(Date.now() / 1000)
69
+
70
+ function resolveDeadline(options: PermitOptions, label = 'deadline'): number {
71
+ const at = options.deadlineAt ?? now() + Math.floor(options.deadline ?? DEFAULT_PERMIT_DEADLINE)
72
+ if (!Number.isSafeInteger(at) || at <= now() + 10) {
73
+ throw new RangeError(`${label} must be at least 10 seconds in the future`)
74
+ }
75
+ if (at > now() + MAX_PERMIT_DEADLINE + 60 && !options.allowLongDeadline) {
76
+ throw new RangeError(`${label} is more than 24 hours away; pass allowLongDeadline: true if that is intended`)
77
+ }
78
+ return at
79
+ }
80
+
81
+ function checkParty(owner: string, spender: string): [string, string] {
82
+ const s = getAddress(spender)
83
+ if (s === ZeroAddress) throw new Error('spender must not be the zero address')
84
+ return [getAddress(owner), s]
85
+ }
86
+
87
+ function checkAmount(amount: bigint, max: bigint, options: PermitOptions, label = 'amount'): void {
88
+ if (amount <= 0n) throw new RangeError(`${label} must be greater than 0 (a blank permit is never signed)`)
89
+ if (amount > max) throw new RangeError(`${label} exceeds the maximum`)
90
+ if (amount === max && !options.allowUnlimited) {
91
+ throw new RangeError(`${label} is unlimited; pass allowUnlimited: true to sign an unlimited permit`)
92
+ }
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------------------------
96
+ // EIP-2612 / DAI
97
+ // ---------------------------------------------------------------------------------------------
98
+
99
+ const DAI_TYPEHASH = keccak256(toUtf8Bytes('Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)'))
100
+ const PERMIT_TYPES: Record<string, TypedDataField[]> = {
101
+ Permit: [
102
+ { name: 'owner', type: 'address' },
103
+ { name: 'spender', type: 'address' },
104
+ { name: 'value', type: 'uint256' },
105
+ { name: 'nonce', type: 'uint256' },
106
+ { name: 'deadline', type: 'uint256' },
107
+ ],
108
+ }
109
+ const DAI_TYPES: Record<string, TypedDataField[]> = {
110
+ Permit: [
111
+ { name: 'holder', type: 'address' },
112
+ { name: 'spender', type: 'address' },
113
+ { name: 'nonce', type: 'uint256' },
114
+ { name: 'expiry', type: 'uint256' },
115
+ { name: 'allowed', type: 'bool' },
116
+ ],
117
+ }
118
+
119
+ const tokenInterface = new Interface([
120
+ 'function name() view returns (string)',
121
+ 'function version() view returns (string)',
122
+ 'function DOMAIN_SEPARATOR() view returns (bytes32)',
123
+ 'function nonces(address owner) view returns (uint256)',
124
+ 'function PERMIT_TYPEHASH() view returns (bytes32)',
125
+ 'function eip712Domain() view returns (bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions)',
126
+ 'function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)',
127
+ 'function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s)',
128
+ ])
129
+
130
+ export interface PermitSupport {
131
+ /** null:不支持(reason 说明原因) */
132
+ kind: 'eip2612' | 'dai' | null
133
+ /** 支持时:EIP-712 domain(已和代币的 DOMAIN_SEPARATOR 核对一致) */
134
+ domain?: TypedDataDomain
135
+ reason?: string
136
+ }
137
+
138
+ /**
139
+ * 检测代币是否支持 permit:读 nonces / DOMAIN_SEPARATOR / name / version / eip712Domain(EIP-5267),
140
+ * 按链 ID 和代币地址重新算 domain,**和代币的 DOMAIN_SEPARATOR 逐位一致**才算支持(不一致说明 domain 参数猜错或代币的实现特殊,签了也会被拒)。
141
+ * 通过 PERMIT_TYPEHASH 区分 DAI 风格(Permit(holder, spender, nonce, expiry, allowed))
142
+ */
143
+ export async function detectPermit(provider: Provider, token: string): Promise<PermitSupport> {
144
+ const address = getAddress(token)
145
+ const chainId = await provider.getChainId()
146
+ const c = provider.contract(address, tokenInterface)
147
+ const [name, version, separator, nonce, domain5267, typehash] = await provider.tryAll([
148
+ c.name(),
149
+ c.version(),
150
+ c.DOMAIN_SEPARATOR(),
151
+ c.nonces(ZeroAddress),
152
+ c.eip712Domain(),
153
+ c.PERMIT_TYPEHASH(),
154
+ ])
155
+ if (separator === null || nonce === null) {
156
+ return { kind: null, reason: 'the token has no nonces() / DOMAIN_SEPARATOR(), so it does not support permit' }
157
+ }
158
+ const candidates: TypedDataDomain[] = []
159
+ if (domain5267) {
160
+ const d = domain5267 as unknown as { name: string; version: string; chainId: bigint; verifyingContract: string }
161
+ candidates.push({ name: d.name, version: d.version, chainId: d.chainId, verifyingContract: d.verifyingContract })
162
+ }
163
+ if (name !== null) {
164
+ for (const v of [version as string | null, '1', '2']) {
165
+ if (v !== null) candidates.push({ name: name as string, version: v, chainId, verifyingContract: address })
166
+ }
167
+ }
168
+ const match = candidates.find((d) => safeDomainHash(d) === (separator as string).toLowerCase() && Number(d.chainId) === chainId && getAddress(d.verifyingContract as string) === address)
169
+ if (!match) {
170
+ return { kind: null, reason: 'the EIP-712 domain computed from name / version / chainId / address does not match the token DOMAIN_SEPARATOR (non-standard permit)' }
171
+ }
172
+ return { kind: typehash !== null && (typehash as string).toLowerCase() === DAI_TYPEHASH ? 'dai' : 'eip2612', domain: match }
173
+ }
174
+
175
+ function safeDomainHash(d: TypedDataDomain): string | null {
176
+ try {
177
+ return TypedDataEncoder.hashDomain(d).toLowerCase()
178
+ } catch {
179
+ return null
180
+ }
181
+ }
182
+
183
+ export interface BuildPermitParams {
184
+ token: string
185
+ owner: string
186
+ spender: string
187
+ /** 授权的数量(最小单位,精确)。DAI 风格的 permit 只能授权无限额度,要 allowUnlimited: true 才会构建 */
188
+ value: bigint | string | number
189
+ }
190
+
191
+ /**
192
+ * 构建 EIP-2612 permit(代币不支持、domain 对不上时抛错):读 owner 当前的 nonce,默认有效期 30 分钟、精确额度。
193
+ * 返回要签的数据(`signPermit` 签名)
194
+ */
195
+ export async function buildPermit(provider: Provider, params: BuildPermitParams, options: PermitOptions = {}): Promise<Permit> {
196
+ const token = getAddress(params.token)
197
+ const [owner, spender] = checkParty(params.owner, params.spender)
198
+ const value = BigInt(params.value)
199
+ const support = await detectPermit(provider, token)
200
+ if (!support.kind || !support.domain) {
201
+ throw new Error(`${token} does not support permit: ${support.reason}`)
202
+ }
203
+ const chainId = await provider.getChainId()
204
+ const deadline = resolveDeadline(options)
205
+ const [nonce] = await provider.all<bigint>([provider.contract(token, tokenInterface).nonces(owner)])
206
+ if (support.kind === 'dai') {
207
+ if (!options.allowUnlimited) {
208
+ throw new RangeError('DAI-style permit can only approve an unlimited allowance; pass allowUnlimited: true or use a normal approve')
209
+ }
210
+ checkAmount(value, MaxUint256, { allowUnlimited: true })
211
+ const message = { holder: owner, spender, nonce: nonce as bigint, expiry: BigInt(deadline), allowed: true }
212
+ return { kind: 'dai', chainId, owner, spender, tokens: [token], amounts: [MaxUint256.toString()], deadline, nonces: [String(nonce)], typedData: { domain: support.domain, types: DAI_TYPES, primaryType: 'Permit', message } }
213
+ }
214
+ checkAmount(value, MaxUint256, options, 'value')
215
+ const message = { owner, spender, value, nonce: nonce as bigint, deadline: BigInt(deadline) }
216
+ return { kind: 'eip2612', chainId, owner, spender, tokens: [token], amounts: [value.toString()], deadline, nonces: [String(nonce)], typedData: { domain: support.domain, types: PERMIT_TYPES, primaryType: 'Permit', message } }
217
+ }
218
+
219
+ /** 代币上的 permit 调用 calldata(任何人都可以提交,不需要是 owner) */
220
+ export function permitCalldata(signed: SignedPermit): string {
221
+ const { permit, v, r, s } = signed
222
+ const m = permit.typedData.message
223
+ if (permit.kind === 'eip2612') {
224
+ return tokenInterface.encodeFunctionData('permit(address,address,uint256,uint256,uint8,bytes32,bytes32)', [m.owner, m.spender, m.value, m.deadline, v, r, s])
225
+ }
226
+ if (permit.kind === 'dai') {
227
+ return tokenInterface.encodeFunctionData('permit(address,address,uint256,uint256,bool,uint8,bytes32,bytes32)', [m.holder, m.spender, m.nonce, m.expiry, m.allowed, v, r, s])
228
+ }
229
+ throw new Error(`${permit.kind} is not a token permit; use permit2Calldata / permitTransferFromCalldata`)
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------------------------
233
+ // Permit2
234
+ // ---------------------------------------------------------------------------------------------
235
+
236
+ const permit2Interface = new Interface([
237
+ 'function allowance(address owner, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)',
238
+ 'function permit(address owner, ((address token, uint160 amount, uint48 expiration, uint48 nonce) details, address spender, uint256 sigDeadline) permitSingle, bytes signature)',
239
+ 'function permit(address owner, ((address token, uint160 amount, uint48 expiration, uint48 nonce)[] details, address spender, uint256 sigDeadline) permitBatch, bytes signature)',
240
+ 'function permitTransferFrom(((address token, uint256 amount) permitted, uint256 nonce, uint256 deadline) permit, (address to, uint256 requestedAmount) transferDetails, address owner, bytes signature)',
241
+ ])
242
+
243
+ const PERMIT_DETAILS = [
244
+ { name: 'token', type: 'address' },
245
+ { name: 'amount', type: 'uint160' },
246
+ { name: 'expiration', type: 'uint48' },
247
+ { name: 'nonce', type: 'uint48' },
248
+ ]
249
+
250
+ function permit2Domain(chainId: number): TypedDataDomain {
251
+ return { name: 'Permit2', chainId, verifyingContract: PERMIT2_ADDRESS }
252
+ }
253
+
254
+ export interface Permit2AllowanceParams {
255
+ token: string
256
+ owner: string
257
+ /** 被授权方(路由合约等),在 Permit2 里能 transferFrom 的地址 */
258
+ spender: string
259
+ /** 额度(最小单位,精确,≤ 2^160 − 1) */
260
+ amount: bigint | string | number
261
+ /** 额度的过期时间(unix 秒),默认和签名一样 30 分钟(Permit2 的额度过期后自动失效) */
262
+ expirationAt?: number
263
+ }
264
+
265
+ /** 读 Permit2 里 owner 对 spender 的当前 nonce */
266
+ async function allowanceNonce(provider: Provider, owner: string, token: string, spender: string): Promise<bigint> {
267
+ const [a] = await provider.all([provider.contract(PERMIT2_ADDRESS, permit2Interface).allowance(owner, token, spender)])
268
+ return BigInt((a as { nonce: bigint }).nonce)
269
+ }
270
+
271
+ /** 构建 Permit2 AllowanceTransfer 的 PermitSingle(owner 签名后任何人可调 permit2.permit,之后 spender 可通过 Permit2 转走额度内的代币) */
272
+ export async function buildPermit2Single(provider: Provider, params: Permit2AllowanceParams, options: PermitOptions = {}): Promise<Permit> {
273
+ return buildPermit2Batch(provider, { owner: params.owner, spender: params.spender, items: [{ token: params.token, amount: params.amount }], expirationAt: params.expirationAt }, options, true)
274
+ }
275
+
276
+ export interface Permit2BatchParams {
277
+ owner: string
278
+ spender: string
279
+ items: ReadonlyArray<{ token: string; amount: bigint | string | number }>
280
+ expirationAt?: number
281
+ }
282
+
283
+ /** 构建 Permit2 的 PermitBatch(多个代币一个签名);items 只有一个且 single 时为 PermitSingle */
284
+ export async function buildPermit2Batch(provider: Provider, params: Permit2BatchParams, options: PermitOptions = {}, single = false): Promise<Permit> {
285
+ const [owner, spender] = checkParty(params.owner, params.spender)
286
+ if (!params.items.length) throw new Error('items must not be empty')
287
+ if (new Set(params.items.map((i) => getAddress(i.token))).size !== params.items.length) throw new Error('duplicate tokens in items')
288
+ const chainId = await provider.getChainId()
289
+ const deadline = resolveDeadline(options, 'sigDeadline')
290
+ const expiration = params.expirationAt ?? deadline
291
+ if (expiration <= now() || expiration > now() + 2 ** 32) throw new RangeError('expirationAt is out of range')
292
+ if (expiration > now() + MAX_PERMIT_DEADLINE + 60 && !options.allowLongDeadline) {
293
+ throw new RangeError('expirationAt is more than 24 hours away; pass allowLongDeadline: true if that is intended')
294
+ }
295
+ const tokens = params.items.map((i) => getAddress(i.token))
296
+ const amounts = params.items.map((i, k) => {
297
+ const amount = BigInt(i.amount)
298
+ checkAmount(amount, MAX_UINT160, options, `items[${k}].amount`)
299
+ return amount
300
+ })
301
+ const nonces = await Promise.all(tokens.map((t) => allowanceNonce(provider, owner, t, spender)))
302
+ const details = tokens.map((token, k) => ({ token, amount: amounts[k] as bigint, expiration, nonce: nonces[k] as bigint }))
303
+ const base = { chainId, owner, spender, tokens, amounts: amounts.map(String), deadline, expiration, nonces: nonces.map(String) }
304
+ if (single) {
305
+ return {
306
+ ...base,
307
+ kind: 'permit2-single',
308
+ typedData: {
309
+ domain: permit2Domain(chainId),
310
+ types: { PermitSingle: [{ name: 'details', type: 'PermitDetails' }, { name: 'spender', type: 'address' }, { name: 'sigDeadline', type: 'uint256' }], PermitDetails: PERMIT_DETAILS },
311
+ primaryType: 'PermitSingle',
312
+ message: { details: details[0], spender, sigDeadline: BigInt(deadline) },
313
+ },
314
+ }
315
+ }
316
+ return {
317
+ ...base,
318
+ kind: 'permit2-batch',
319
+ typedData: {
320
+ domain: permit2Domain(chainId),
321
+ types: { PermitBatch: [{ name: 'details', type: 'PermitDetails[]' }, { name: 'spender', type: 'address' }, { name: 'sigDeadline', type: 'uint256' }], PermitDetails: PERMIT_DETAILS },
322
+ primaryType: 'PermitBatch',
323
+ message: { details, spender, sigDeadline: BigInt(deadline) },
324
+ },
325
+ }
326
+ }
327
+
328
+ export interface Permit2TransferParams {
329
+ token: string
330
+ owner: string
331
+ /** 能调 permitTransferFrom 的合约(只有它能用这个签名) */
332
+ spender: string
333
+ /** 最多转走的数量(最小单位,精确) */
334
+ amount: bigint | string | number
335
+ /** 不传时随机生成(SignatureTransfer 的 nonce 是无序位图,随机数不会冲突) */
336
+ nonce?: bigint
337
+ }
338
+
339
+ /** 构建 Permit2 SignatureTransfer 的 PermitTransferFrom:一次性签名,不留下链上额度(spender 合约在同一笔交易里 permitTransferFrom) */
340
+ export async function buildPermit2Transfer(provider: Provider, params: Permit2TransferParams, options: PermitOptions = {}): Promise<Permit> {
341
+ const [owner, spender] = checkParty(params.owner, params.spender)
342
+ const token = getAddress(params.token)
343
+ const amount = BigInt(params.amount)
344
+ checkAmount(amount, MaxUint256, options)
345
+ const chainId = await provider.getChainId()
346
+ const deadline = resolveDeadline(options)
347
+ const nonce = params.nonce ?? BigInt(hexlify(randomBytes(7)))
348
+ return {
349
+ kind: 'permit2-transfer',
350
+ chainId,
351
+ owner,
352
+ spender,
353
+ tokens: [token],
354
+ amounts: [amount.toString()],
355
+ deadline,
356
+ nonces: [nonce.toString()],
357
+ typedData: {
358
+ domain: permit2Domain(chainId),
359
+ types: {
360
+ PermitTransferFrom: [{ name: 'permitted', type: 'TokenPermissions' }, { name: 'spender', type: 'address' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }],
361
+ TokenPermissions: [{ name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }],
362
+ },
363
+ primaryType: 'PermitTransferFrom',
364
+ message: { permitted: { token, amount }, spender, nonce, deadline: BigInt(deadline) },
365
+ },
366
+ }
367
+ }
368
+
369
+ /** Permit2.permit(owner, permitSingle | permitBatch, signature) 的 calldata */
370
+ export function permit2Calldata(signed: SignedPermit): string {
371
+ const { permit, signature } = signed
372
+ if (permit.kind !== 'permit2-single' && permit.kind !== 'permit2-batch') throw new Error(`${permit.kind} is not an AllowanceTransfer permit`)
373
+ const m = permit.typedData.message
374
+ const fn = permit.kind === 'permit2-single' ? 'permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)' : 'permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)'
375
+ return permit2Interface.encodeFunctionData(fn, [permit.owner, [permit.kind === 'permit2-single' ? tupleOf(m.details) : m.details.map(tupleOf), m.spender, m.sigDeadline], signature])
376
+ }
377
+
378
+ const tupleOf = (d: { token: string; amount: bigint; expiration: bigint | number; nonce: bigint | number }) => [d.token, d.amount, d.expiration, d.nonce]
379
+
380
+ /** Permit2.permitTransferFrom 的 calldata:把 owner 的代币转给 to(spender 合约调用) */
381
+ export function permitTransferFromCalldata(signed: SignedPermit, to: string, requestedAmount?: bigint): string {
382
+ const { permit, signature } = signed
383
+ if (permit.kind !== 'permit2-transfer') throw new Error(`${permit.kind} is not a SignatureTransfer permit`)
384
+ const m = permit.typedData.message
385
+ const amount = requestedAmount ?? m.permitted.amount
386
+ if (amount > m.permitted.amount) throw new RangeError('requestedAmount exceeds the permitted amount')
387
+ return permit2Interface.encodeFunctionData('permitTransferFrom', [[[m.permitted.token, m.permitted.amount], m.nonce, m.deadline], [getAddress(to), amount], permit.owner, signature])
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------------------------
391
+ // 签名 / 验证
392
+ // ---------------------------------------------------------------------------------------------
393
+
394
+ /** 签名前的核对:typedData 里的内容要和 permit 摘要一致(防止被替换)、链 / 合约地址正确、签名者就是 owner */
395
+ export function assertPermitSafe(permit: Permit, signerAddress?: string, chainId?: number): void {
396
+ const { typedData: t } = permit
397
+ const expectedContract = permit.kind === 'eip2612' || permit.kind === 'dai' ? permit.tokens[0] : PERMIT2_ADDRESS
398
+ if (Number(t.domain.chainId) !== permit.chainId || (chainId !== undefined && chainId !== permit.chainId)) {
399
+ throw new Error(`The permit is for chain ${String(t.domain.chainId)}, expected ${chainId ?? permit.chainId}`)
400
+ }
401
+ if (getAddress(String(t.domain.verifyingContract)) !== getAddress(expectedContract as string)) {
402
+ throw new Error('The permit verifyingContract does not match the expected contract; refusing to sign')
403
+ }
404
+ if (signerAddress && getAddress(signerAddress) !== getAddress(permit.owner)) {
405
+ throw new Error('The signer is not the owner of the permit')
406
+ }
407
+ const m = t.message
408
+ const same = (a: unknown, b: unknown) => String(a).toLowerCase() === String(b).toLowerCase()
409
+ const spender = m.spender
410
+ if (!same(spender, permit.spender) || getAddress(permit.spender) === ZeroAddress) throw new Error('The permit spender is inconsistent or the zero address; refusing to sign')
411
+ const ownerField = m.owner ?? m.holder
412
+ if (permit.kind !== 'permit2-single' && permit.kind !== 'permit2-batch' && permit.kind !== 'permit2-transfer' && !same(ownerField, permit.owner)) {
413
+ throw new Error('The permit owner is inconsistent; refusing to sign')
414
+ }
415
+ const amounts: unknown[] = permit.kind === 'permit2-single' ? [m.details.amount] : permit.kind === 'permit2-batch' ? m.details.map((d: { amount: bigint }) => d.amount) : permit.kind === 'permit2-transfer' ? [m.permitted.amount] : permit.kind === 'dai' ? [MaxUint256] : [m.value]
416
+ if (amounts.length !== permit.amounts.length || amounts.some((a, i) => !same(a, permit.amounts[i])) || amounts.some((a) => BigInt(a as bigint) === 0n)) {
417
+ throw new Error('The permit amounts are inconsistent or blank; refusing to sign')
418
+ }
419
+ // 代币、nonce、额度过期时间、DAI 的 allowed 也要和摘要一致:否则界面上显示的和钱包里实际签的不是同一个授权
420
+ const details: Array<{ token?: unknown; nonce?: unknown; expiration?: unknown }> =
421
+ permit.kind === 'permit2-single' ? [m.details] : permit.kind === 'permit2-batch' ? m.details : permit.kind === 'permit2-transfer' ? [{ token: m.permitted.token }] : []
422
+ if (details.length) {
423
+ if (details.length !== permit.tokens.length || details.some((d, i) => !same(d.token, permit.tokens[i]))) throw new Error('The permit tokens are inconsistent; refusing to sign')
424
+ if (permit.kind !== 'permit2-transfer' && details.some((d, i) => d.nonce !== undefined && permit.nonces[i] !== undefined && !same(d.nonce, permit.nonces[i]))) throw new Error('The permit nonce is inconsistent; refusing to sign')
425
+ if (permit.expiration !== undefined && details.some((d) => d.expiration !== undefined && !same(d.expiration, permit.expiration))) throw new Error('The permit expiration is inconsistent; refusing to sign')
426
+ } else if (permit.kind === 'dai' ? m.allowed !== true : permit.nonces[0] !== undefined && !same(m.nonce, permit.nonces[0])) {
427
+ throw new Error('The permit nonce or allowed flag is inconsistent; refusing to sign')
428
+ }
429
+ if (permit.kind === 'permit2-transfer' && permit.nonces[0] !== undefined && !same(m.nonce, permit.nonces[0])) throw new Error('The permit nonce is inconsistent; refusing to sign')
430
+ const deadline = m.deadline ?? m.expiry ?? m.sigDeadline
431
+ if (!same(deadline, permit.deadline) || permit.deadline <= now()) throw new Error('The permit deadline is inconsistent or already passed; refusing to sign')
432
+ }
433
+
434
+ /**
435
+ * 签名:signer 是 ethers Signer(Wallet、浏览器钱包的 JsonRpcSigner 等,用 signTypedData)或私钥以外的任何有 signTypedData 的对象。
436
+ * 签名前核对(assertPermitSafe),签完本地验证签名确实来自 owner
437
+ */
438
+ export async function signPermit(signer: Pick<WriteSigner, 'getAddress' | 'signTypedData'>, permit: Permit, options: { chainId?: number } = {}): Promise<SignedPermit> {
439
+ if (typeof signer.signTypedData !== 'function') throw new Error('The signer does not support signTypedData')
440
+ assertPermitSafe(permit, await signer.getAddress(), options.chainId)
441
+ const { domain, types, message } = permit.typedData
442
+ const signature = await signer.signTypedData(domain, types, message)
443
+ if (!verifyPermitSignature(permit, signature)) {
444
+ throw new Error('The wallet returned a signature that does not recover to the permit owner')
445
+ }
446
+ const sig = Signature.from(signature)
447
+ return { permit, signature, v: sig.v, r: sig.r, s: sig.s }
448
+ }
449
+
450
+ /** 本地验证签名:恢复出的地址是不是 owner(EOA 签名;合约账户的 ERC-1271 签名无法本地验证,返回 false) */
451
+ export function verifyPermitSignature(permit: Permit, signature: string): boolean {
452
+ try {
453
+ return verifyTypedData(permit.typedData.domain, permit.typedData.types, permit.typedData.message, signature).toLowerCase() === permit.owner.toLowerCase()
454
+ } catch {
455
+ return false
456
+ }
457
+ }
package/src/portfolio.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import { lpPositions, type LpPosition, type LpPositionsOptions } from './lp.js'
5
5
  import { combinePrices, defillamaPrices, onchainPrices, tokenPrices, type PriceSource } from './price.js'
6
6
  import type { Provider } from './provider.js'
7
+ import { withSignal } from './util.js'
7
8
 
8
9
  export interface PortfolioOptions {
9
10
  /** 要计入的代币(含主币:NATIVE_TOKEN),余额用 multicall 一次读出 */
@@ -17,6 +18,8 @@ export interface PortfolioOptions {
17
18
  /** 某个价格源出错时调用 */
18
19
  onPriceError?: (err: unknown) => void
19
20
  fetch?: typeof fetch
21
+ /** 取消:余额、LP 的节点请求随之中止,查价格的步骤立即以 signal.reason 失败 */
22
+ signal?: AbortSignal
20
23
  }
21
24
 
22
25
  export interface PortfolioToken {
@@ -55,14 +58,18 @@ export interface Portfolio {
55
58
  * ```
56
59
  */
57
60
  export async function portfolio(provider: Provider, owner: string, options: PortfolioOptions): Promise<Portfolio> {
58
- const { tokens, sources = [defillamaPrices(), onchainPrices()], lp, minUsd = 0, onPriceError, fetch: fetchFn } = options
61
+ const { tokens, sources = [defillamaPrices(), onchainPrices()], lp, minUsd = 0, onPriceError, fetch: fetchFn, signal } = options
62
+ signal?.throwIfAborted()
59
63
  const onError = onPriceError ? (_: PriceSource, err: unknown) => onPriceError(err) : undefined
64
+ const sig = signal ? { signal } : {}
60
65
  const [balances, positions] = await Promise.all([
61
- tokens.length ? provider.balances(owner, tokens, { symbol: true }) : Promise.resolve([]),
62
- lp ? lpPositions(provider, owner, { ...(lp === true ? {} : lp), prices: combinePrices(sources, { onError }), ...(fetchFn ? { fetch: fetchFn } : {}), ...(onPriceError ? { onPriceError } : {}) }) : Promise.resolve([]),
66
+ tokens.length ? provider.balances(owner, tokens, { symbol: true, ...sig }) : Promise.resolve([]),
67
+ lp ? lpPositions(provider, owner, { ...(lp === true ? {} : lp), prices: combinePrices(sources, { onError }), ...(fetchFn ? { fetch: fetchFn } : {}), ...(onPriceError ? { onPriceError } : {}), ...sig }) : Promise.resolve([]),
63
68
  ])
64
69
  const held = balances.filter((b) => b.success && BigInt(b.balance) > 0n)
65
- const prices = held.length ? await tokenPrices(provider, held.map((b) => b.token), { sources, ...(onError ? { onError } : {}), ...(fetchFn ? { fetch: fetchFn } : {}) }) : []
70
+ const prices = held.length
71
+ ? await withSignal(signal, () => tokenPrices(provider, held.map((b) => b.token), { sources, ...(onError ? { onError } : {}), ...(fetchFn ? { fetch: fetchFn } : {}) }))
72
+ : []
66
73
  const list: PortfolioToken[] = held.map((b, i) => {
67
74
  const p = prices[i]
68
75
  const price = p?.price ?? null