@w3lib/evm-batch-call 0.6.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.
- package/CHANGELOG.md +54 -0
- package/README.md +367 -3
- package/contracts/TokenProbe.sol +103 -0
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +114 -3
- package/dist/aggregate.js.map +1 -1
- package/dist/approvals.d.ts +169 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +310 -0
- package/dist/approvals.js.map +1 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +4 -0
- package/dist/chains.js.map +1 -1
- package/dist/dividends.d.ts +135 -0
- package/dist/dividends.d.ts.map +1 -0
- package/dist/dividends.js +253 -0
- package/dist/dividends.js.map +1 -0
- package/dist/fees.d.ts +97 -0
- package/dist/fees.d.ts.map +1 -0
- package/dist/fees.js +115 -0
- package/dist/fees.js.map +1 -0
- package/dist/history.d.ts +154 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +247 -0
- package/dist/history.js.map +1 -0
- package/dist/historySources.d.ts +32 -0
- package/dist/historySources.d.ts.map +1 -0
- package/dist/historySources.js +319 -0
- package/dist/historySources.js.map +1 -0
- package/dist/http.d.ts +10 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +30 -0
- package/dist/http.js.map +1 -0
- package/dist/liquidity/v3.d.ts +2 -0
- package/dist/liquidity/v3.d.ts.map +1 -1
- package/dist/liquidity/v3.js +1 -1
- package/dist/liquidity/v3.js.map +1 -1
- package/dist/liquidity/v4.d.ts +21 -0
- package/dist/liquidity/v4.d.ts.map +1 -1
- package/dist/liquidity/v4.js +4 -3
- package/dist/liquidity/v4.js.map +1 -1
- package/dist/lp.d.ts +10 -1
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +19 -2
- package/dist/lp.js.map +1 -1
- package/dist/lpV4.d.ts +4 -0
- package/dist/lpV4.d.ts.map +1 -1
- package/dist/lpV4.js +27 -3
- package/dist/lpV4.js.map +1 -1
- package/dist/multicall.d.ts.map +1 -1
- package/dist/multicall.js +3 -0
- package/dist/multicall.js.map +1 -1
- package/dist/names.d.ts +55 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +93 -0
- package/dist/names.js.map +1 -0
- package/dist/owner.d.ts +4 -26
- package/dist/owner.d.ts.map +1 -1
- package/dist/owner.js +7 -112
- package/dist/owner.js.map +1 -1
- package/dist/permit.d.ts +137 -0
- package/dist/permit.d.ts.map +1 -0
- package/dist/permit.js +355 -0
- package/dist/permit.js.map +1 -0
- package/dist/portfolio.d.ts +56 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +54 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +95 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +329 -0
- package/dist/price.js.map +1 -0
- package/dist/rewards/farms.d.ts +26 -0
- package/dist/rewards/farms.d.ts.map +1 -0
- package/dist/rewards/farms.js +210 -0
- package/dist/rewards/farms.js.map +1 -0
- package/dist/rewards/index.d.ts +42 -0
- package/dist/rewards/index.d.ts.map +1 -0
- package/dist/rewards/index.js +382 -0
- package/dist/rewards/index.js.map +1 -0
- package/dist/rewards/types.d.ts +170 -0
- package/dist/rewards/types.d.ts.map +1 -0
- package/dist/rewards/types.js +2 -0
- package/dist/rewards/types.js.map +1 -0
- package/dist/risk.d.ts +42 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +134 -0
- package/dist/risk.js.map +1 -0
- package/dist/rpcNodes.d.ts.map +1 -1
- package/dist/rpcNodes.js +18 -0
- package/dist/rpcNodes.js.map +1 -1
- package/dist/simulate.d.ts +186 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +559 -0
- package/dist/simulate.js.map +1 -0
- package/dist/simulateDecode.d.ts +33 -0
- package/dist/simulateDecode.d.ts.map +1 -0
- package/dist/simulateDecode.js +321 -0
- package/dist/simulateDecode.js.map +1 -0
- package/dist/smartAccount/account.d.ts +31 -0
- package/dist/smartAccount/account.d.ts.map +1 -0
- package/dist/smartAccount/account.js +33 -0
- package/dist/smartAccount/account.js.map +1 -0
- package/dist/smartAccount/clients.d.ts +77 -0
- package/dist/smartAccount/clients.d.ts.map +1 -0
- package/dist/smartAccount/clients.js +122 -0
- package/dist/smartAccount/clients.js.map +1 -0
- package/dist/smartAccount/eip5792.d.ts +77 -0
- package/dist/smartAccount/eip5792.d.ts.map +1 -0
- package/dist/smartAccount/eip5792.js +152 -0
- package/dist/smartAccount/eip5792.js.map +1 -0
- package/dist/smartAccount/send.d.ts +40 -0
- package/dist/smartAccount/send.d.ts.map +1 -0
- package/dist/smartAccount/send.js +271 -0
- package/dist/smartAccount/send.js.map +1 -0
- package/dist/smartAccount/userOp.d.ts +91 -0
- package/dist/smartAccount/userOp.d.ts.map +1 -0
- package/dist/smartAccount/userOp.js +124 -0
- package/dist/smartAccount/userOp.js.map +1 -0
- package/dist/subpaths/approvals.d.ts +26 -0
- package/dist/subpaths/approvals.d.ts.map +1 -0
- package/dist/subpaths/approvals.js +27 -0
- package/dist/subpaths/approvals.js.map +1 -0
- package/dist/subpaths/dividends.d.ts +28 -0
- package/dist/subpaths/dividends.d.ts.map +1 -0
- package/dist/subpaths/dividends.js +29 -0
- package/dist/subpaths/dividends.js.map +1 -0
- package/dist/subpaths/fees.d.ts +23 -0
- package/dist/subpaths/fees.d.ts.map +1 -0
- package/dist/subpaths/fees.js +25 -0
- package/dist/subpaths/fees.js.map +1 -0
- package/dist/subpaths/history.d.ts +22 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +23 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/names.d.ts +35 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +39 -0
- package/dist/subpaths/names.js.map +1 -0
- package/dist/subpaths/permit.d.ts +14 -0
- package/dist/subpaths/permit.d.ts.map +1 -0
- package/dist/subpaths/permit.js +14 -0
- package/dist/subpaths/permit.js.map +1 -0
- package/dist/subpaths/price.d.ts +38 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +40 -0
- package/dist/subpaths/price.js.map +1 -0
- package/dist/subpaths/rewards.d.ts +27 -0
- package/dist/subpaths/rewards.d.ts.map +1 -0
- package/dist/subpaths/rewards.js +25 -0
- package/dist/subpaths/rewards.js.map +1 -0
- package/dist/subpaths/risk.d.ts +19 -0
- package/dist/subpaths/risk.d.ts.map +1 -0
- package/dist/subpaths/risk.js +20 -0
- package/dist/subpaths/risk.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +25 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +28 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/subpaths/smart-account.d.ts +24 -0
- package/dist/subpaths/smart-account.d.ts.map +1 -0
- package/dist/subpaths/smart-account.js +24 -0
- package/dist/subpaths/smart-account.js.map +1 -0
- package/dist/subpaths/swap.d.ts +19 -0
- package/dist/subpaths/swap.d.ts.map +1 -0
- package/dist/subpaths/swap.js +20 -0
- package/dist/subpaths/swap.js.map +1 -0
- package/dist/swap.d.ts +168 -0
- package/dist/swap.d.ts.map +1 -0
- package/dist/swap.js +387 -0
- package/dist/swap.js.map +1 -0
- package/dist/tokenProbe.d.ts +3 -0
- package/dist/tokenProbe.d.ts.map +1 -0
- package/dist/tokenProbe.js +4 -0
- package/dist/tokenProbe.js.map +1 -0
- package/dist/write/batch.d.ts +34 -2
- package/dist/write/batch.d.ts.map +1 -1
- package/dist/write/batch.js +22 -3
- package/dist/write/batch.js.map +1 -1
- package/dist/write/collect.d.ts +9 -0
- package/dist/write/collect.d.ts.map +1 -1
- package/dist/write/collect.js +41 -1
- package/dist/write/collect.js.map +1 -1
- package/dist/write/send.d.ts +4 -0
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js.map +1 -1
- package/dist/write/signer.d.ts +5 -1
- package/dist/write/signer.d.ts.map +1 -1
- package/dist/write/signer.js.map +1 -1
- package/dist/write/transfer.d.ts +5 -3
- package/dist/write/transfer.d.ts.map +1 -1
- package/dist/write/transfer.js +12 -6
- package/dist/write/transfer.js.map +1 -1
- package/dist/write/tron.d.ts +25 -0
- package/dist/write/tron.d.ts.map +1 -1
- package/dist/write/tron.js +26 -0
- package/dist/write/tron.js.map +1 -1
- package/package.json +73 -16
- package/src/aggregate.ts +110 -3
- package/src/approvals.ts +462 -0
- package/src/chains.ts +4 -0
- package/src/dividends.ts +360 -0
- package/src/fees.ts +201 -0
- package/src/history.ts +367 -0
- package/src/historySources.ts +360 -0
- package/src/http.ts +34 -0
- package/src/liquidity/v3.ts +1 -1
- package/src/liquidity/v4.ts +4 -3
- package/src/lp.ts +21 -2
- package/src/lpV4.ts +32 -2
- package/src/multicall.ts +3 -0
- package/src/names.ts +141 -0
- package/src/owner.ts +7 -138
- package/src/permit.ts +457 -0
- package/src/portfolio.ts +98 -0
- package/src/price.ts +415 -0
- package/src/rewards/farms.ts +229 -0
- package/src/rewards/index.ts +431 -0
- package/src/rewards/types.ts +179 -0
- package/src/risk.ts +166 -0
- package/src/rpcNodes.ts +18 -0
- package/src/simulate.ts +727 -0
- package/src/simulateDecode.ts +352 -0
- package/src/smartAccount/account.ts +63 -0
- package/src/smartAccount/clients.ts +180 -0
- package/src/smartAccount/eip5792.ts +187 -0
- package/src/smartAccount/send.ts +341 -0
- package/src/smartAccount/userOp.ts +180 -0
- package/src/subpaths/approvals.ts +48 -0
- package/src/subpaths/dividends.ts +47 -0
- package/src/subpaths/fees.ts +43 -0
- package/src/subpaths/history.ts +41 -0
- package/src/subpaths/names.ts +43 -0
- package/src/subpaths/permit.ts +39 -0
- package/src/subpaths/price.ts +60 -0
- package/src/subpaths/rewards.ts +46 -0
- package/src/subpaths/risk.ts +23 -0
- package/src/subpaths/simulate.ts +51 -0
- package/src/subpaths/smart-account.ts +40 -0
- package/src/subpaths/swap.ts +36 -0
- package/src/swap.ts +534 -0
- package/src/tokenProbe.ts +5 -0
- package/src/write/batch.ts +44 -5
- package/src/write/collect.ts +60 -1
- package/src/write/send.ts +4 -0
- package/src/write/signer.ts +5 -1
- package/src/write/transfer.ts +17 -10
- package/src/write/tron.ts +43 -0
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
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 资产总值(子路径 `@w3lib/evm-batch-call/price`):已知代币的余额 × 价格,可选加上 LP 仓位的价值
|
|
3
|
+
*/
|
|
4
|
+
import { lpPositions, type LpPosition, type LpPositionsOptions } from './lp.js'
|
|
5
|
+
import { combinePrices, defillamaPrices, onchainPrices, tokenPrices, type PriceSource } from './price.js'
|
|
6
|
+
import type { Provider } from './provider.js'
|
|
7
|
+
import { withSignal } from './util.js'
|
|
8
|
+
|
|
9
|
+
export interface PortfolioOptions {
|
|
10
|
+
/** 要计入的代币(含主币:NATIVE_TOKEN),余额用 multicall 一次读出 */
|
|
11
|
+
tokens: readonly string[]
|
|
12
|
+
/** 价格源,依次使用,默认 [defillamaPrices(), onchainPrices()] */
|
|
13
|
+
sources?: readonly PriceSource[]
|
|
14
|
+
/** 同时计入 LP 仓位(V2 LP 代币、V3 仓位 NFT;参数同 lpPositions,价格源用上面的 sources),默认不计入 */
|
|
15
|
+
lp?: boolean | Omit<LpPositionsOptions, 'prices'>
|
|
16
|
+
/** 价值低于它的代币不列出(不影响合计);大于 0 时没有价格的代币也不列出(计入 unpriced),默认 0 */
|
|
17
|
+
minUsd?: number
|
|
18
|
+
/** 某个价格源出错时调用 */
|
|
19
|
+
onPriceError?: (err: unknown) => void
|
|
20
|
+
fetch?: typeof fetch
|
|
21
|
+
/** 取消:余额、LP 的节点请求随之中止,查价格的步骤立即以 signal.reason 失败 */
|
|
22
|
+
signal?: AbortSignal
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PortfolioToken {
|
|
26
|
+
token: string
|
|
27
|
+
native: boolean
|
|
28
|
+
symbol: string | null
|
|
29
|
+
balance: string
|
|
30
|
+
decimals: number
|
|
31
|
+
formatted: string
|
|
32
|
+
price: number | null
|
|
33
|
+
/** 美元价值;没有价格时为 null */
|
|
34
|
+
value: number | null
|
|
35
|
+
/** 给出价格的价格源 */
|
|
36
|
+
priceSource: string | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Portfolio {
|
|
40
|
+
/** 合计(代币 + LP;没有价格的不计入) */
|
|
41
|
+
totalUsd: number
|
|
42
|
+
tokensUsd: number
|
|
43
|
+
lpUsd: number
|
|
44
|
+
/** 有余额的代币,按价值从高到低 */
|
|
45
|
+
tokens: PortfolioToken[]
|
|
46
|
+
/** LP 仓位(开启 lp 时) */
|
|
47
|
+
lp: LpPosition[]
|
|
48
|
+
/** 有余额但没有价格的代币数(LP 仓位里缺价格的也算) */
|
|
49
|
+
unpriced: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 一个地址的资产总值:代币余额(只看传入的代币,EVM 不能只靠节点列出持仓)× 价格,可选加上 LP 仓位。
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* const p = await portfolio(provider, user, { tokens: [NATIVE_TOKEN, USDT, USDC], lp: true })
|
|
57
|
+
* p.totalUsd // 1234.5
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export async function portfolio(provider: Provider, owner: string, options: PortfolioOptions): Promise<Portfolio> {
|
|
61
|
+
const { tokens, sources = [defillamaPrices(), onchainPrices()], lp, minUsd = 0, onPriceError, fetch: fetchFn, signal } = options
|
|
62
|
+
signal?.throwIfAborted()
|
|
63
|
+
const onError = onPriceError ? (_: PriceSource, err: unknown) => onPriceError(err) : undefined
|
|
64
|
+
const sig = signal ? { signal } : {}
|
|
65
|
+
const [balances, positions] = await Promise.all([
|
|
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([]),
|
|
68
|
+
])
|
|
69
|
+
const held = balances.filter((b) => b.success && BigInt(b.balance) > 0n)
|
|
70
|
+
const prices = held.length
|
|
71
|
+
? await withSignal(signal, () => tokenPrices(provider, held.map((b) => b.token), { sources, ...(onError ? { onError } : {}), ...(fetchFn ? { fetch: fetchFn } : {}) }))
|
|
72
|
+
: []
|
|
73
|
+
const list: PortfolioToken[] = held.map((b, i) => {
|
|
74
|
+
const p = prices[i]
|
|
75
|
+
const price = p?.price ?? null
|
|
76
|
+
return {
|
|
77
|
+
token: b.token,
|
|
78
|
+
native: b.native,
|
|
79
|
+
symbol: b.symbol ?? null,
|
|
80
|
+
balance: b.balance,
|
|
81
|
+
decimals: b.decimals,
|
|
82
|
+
formatted: b.formatted,
|
|
83
|
+
price,
|
|
84
|
+
value: price === null ? null : Number(b.formatted) * price,
|
|
85
|
+
priceSource: p?.source ?? null,
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
const tokensUsd = list.reduce((sum, t) => sum + (t.value ?? 0), 0)
|
|
89
|
+
const lpUsd = positions.reduce((sum, p) => sum + (p.valueUsd ?? 0), 0)
|
|
90
|
+
return {
|
|
91
|
+
totalUsd: tokensUsd + lpUsd,
|
|
92
|
+
tokensUsd,
|
|
93
|
+
lpUsd,
|
|
94
|
+
tokens: list.filter((t) => (t.value ?? 0) >= minUsd || (minUsd === 0 && t.value === null)).sort((a, b) => (b.value ?? -1) - (a.value ?? -1)),
|
|
95
|
+
lp: positions,
|
|
96
|
+
unpriced: list.filter((t) => t.price === null).length + positions.filter((p) => p.valueUsd === null).length,
|
|
97
|
+
}
|
|
98
|
+
}
|