@w3lib/solana-batch-call 0.4.0 → 0.6.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 +45 -0
- package/README.md +343 -3
- package/dist/approvals.d.ts +129 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +221 -0
- package/dist/approvals.js.map +1 -0
- package/dist/batchPlan.d.ts +52 -0
- package/dist/batchPlan.d.ts.map +1 -0
- package/dist/batchPlan.js +85 -0
- package/dist/batchPlan.js.map +1 -0
- package/dist/claim.d.ts +161 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +197 -0
- package/dist/claim.js.map +1 -0
- package/dist/decode.d.ts +22 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +187 -0
- package/dist/decode.js.map +1 -0
- package/dist/describe.d.ts +26 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +58 -0
- package/dist/describe.js.map +1 -0
- package/dist/fees.d.ts +89 -0
- package/dist/fees.d.ts.map +1 -0
- package/dist/fees.js +166 -0
- package/dist/fees.js.map +1 -0
- package/dist/history.d.ts +125 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +231 -0
- package/dist/history.js.map +1 -0
- package/dist/historyHelius.d.ts +52 -0
- package/dist/historyHelius.d.ts.map +1 -0
- package/dist/historyHelius.js +125 -0
- package/dist/historyHelius.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -2
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -1
- package/dist/liquidity/orca.js +14 -3
- package/dist/liquidity/orca.js.map +1 -1
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
- package/dist/liquidity/raydiumClmm.js +12 -5
- package/dist/liquidity/raydiumClmm.js.map +1 -1
- package/dist/lp.d.ts +55 -15
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +98 -46
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +22 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +21 -0
- package/dist/lpMath.js.map +1 -1
- package/dist/names.d.ts +68 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +257 -0
- package/dist/names.js.map +1 -0
- package/dist/pools.d.ts +9 -0
- package/dist/pools.d.ts.map +1 -0
- package/dist/pools.js +45 -0
- package/dist/pools.js.map +1 -0
- package/dist/portfolio.d.ts +64 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +57 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +90 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +287 -0
- package/dist/price.js.map +1 -0
- package/dist/programs.d.ts +19 -0
- package/dist/programs.d.ts.map +1 -0
- package/dist/programs.js +175 -0
- package/dist/programs.js.map +1 -0
- package/dist/reclaim.d.ts +190 -0
- package/dist/reclaim.d.ts.map +1 -0
- package/dist/reclaim.js +399 -0
- package/dist/reclaim.js.map +1 -0
- package/dist/risk.d.ts +84 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +211 -0
- package/dist/risk.js.map +1 -0
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +36 -10
- package/dist/rpc.js.map +1 -1
- package/dist/simulate.d.ts +106 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +418 -0
- package/dist/simulate.js.map +1 -0
- package/dist/subpaths/approvals.d.ts +29 -0
- package/dist/subpaths/approvals.d.ts.map +1 -0
- package/dist/subpaths/approvals.js +30 -0
- package/dist/subpaths/approvals.js.map +1 -0
- package/dist/subpaths/claim.d.ts +31 -0
- package/dist/subpaths/claim.d.ts.map +1 -0
- package/dist/subpaths/claim.js +31 -0
- package/dist/subpaths/claim.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 +21 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +22 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/lp.d.ts +2 -2
- package/dist/subpaths/lp.d.ts.map +1 -1
- package/dist/subpaths/lp.js +2 -2
- package/dist/subpaths/lp.js.map +1 -1
- package/dist/subpaths/names.d.ts +37 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +27 -0
- package/dist/subpaths/names.js.map +1 -0
- package/dist/subpaths/price.d.ts +40 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +43 -0
- package/dist/subpaths/price.js.map +1 -0
- package/dist/subpaths/reclaim.d.ts +34 -0
- package/dist/subpaths/reclaim.d.ts.map +1 -0
- package/dist/subpaths/reclaim.js +34 -0
- package/dist/subpaths/reclaim.js.map +1 -0
- package/dist/subpaths/risk.d.ts +28 -0
- package/dist/subpaths/risk.d.ts.map +1 -0
- package/dist/subpaths/risk.js +30 -0
- package/dist/subpaths/risk.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +22 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +23 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/subpaths/swap.d.ts +22 -0
- package/dist/subpaths/swap.d.ts.map +1 -0
- package/dist/subpaths/swap.js +21 -0
- package/dist/subpaths/swap.js.map +1 -0
- package/dist/subpaths/write.d.ts +1 -1
- package/dist/subpaths/write.d.ts.map +1 -1
- package/dist/subpaths/write.js +1 -1
- package/dist/subpaths/write.js.map +1 -1
- package/dist/swap/amm.d.ts +20 -0
- package/dist/swap/amm.d.ts.map +1 -0
- package/dist/swap/amm.js +77 -0
- package/dist/swap/amm.js.map +1 -0
- package/dist/swap/clmmMath.d.ts +24 -0
- package/dist/swap/clmmMath.d.ts.map +1 -0
- package/dist/swap/clmmMath.js +82 -0
- package/dist/swap/clmmMath.js.map +1 -0
- package/dist/swap/common.d.ts +68 -0
- package/dist/swap/common.d.ts.map +1 -0
- package/dist/swap/common.js +65 -0
- package/dist/swap/common.js.map +1 -0
- package/dist/swap/cpmm.d.ts +28 -0
- package/dist/swap/cpmm.d.ts.map +1 -0
- package/dist/swap/cpmm.js +113 -0
- package/dist/swap/cpmm.js.map +1 -0
- package/dist/swap/index.d.ts +145 -0
- package/dist/swap/index.d.ts.map +1 -0
- package/dist/swap/index.js +349 -0
- package/dist/swap/index.js.map +1 -0
- package/dist/swap/orca.d.ts +50 -0
- package/dist/swap/orca.d.ts.map +1 -0
- package/dist/swap/orca.js +382 -0
- package/dist/swap/orca.js.map +1 -0
- package/dist/swap/raydiumClmm.d.ts +62 -0
- package/dist/swap/raydiumClmm.d.ts.map +1 -0
- package/dist/swap/raydiumClmm.js +447 -0
- package/dist/swap/raydiumClmm.js.map +1 -0
- package/dist/tradeCheck.d.ts +45 -0
- package/dist/tradeCheck.d.ts.map +1 -0
- package/dist/tradeCheck.js +131 -0
- package/dist/tradeCheck.js.map +1 -0
- package/dist/write/instructions.d.ts +9 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +40 -1
- package/dist/write/instructions.js.map +1 -1
- package/dist/write/transaction.d.ts +23 -0
- package/dist/write/transaction.d.ts.map +1 -1
- package/dist/write/transaction.js +50 -0
- package/dist/write/transaction.js.map +1 -1
- package/package.json +66 -14
- package/src/approvals.ts +358 -0
- package/src/batchPlan.ts +137 -0
- package/src/claim.ts +323 -0
- package/src/decode.ts +202 -0
- package/src/describe.ts +77 -0
- package/src/fees.ts +259 -0
- package/src/history.ts +336 -0
- package/src/historyHelius.ts +136 -0
- package/src/layout.ts +2 -2
- package/src/liquidity/orca.ts +15 -5
- package/src/liquidity/raydiumClmm.ts +13 -5
- package/src/lp.ts +148 -59
- package/src/lpMath.ts +38 -0
- package/src/names.ts +329 -0
- package/src/pools.ts +43 -0
- package/src/portfolio.ts +124 -0
- package/src/price.ts +366 -0
- package/src/programs.ts +216 -0
- package/src/reclaim.ts +547 -0
- package/src/risk.ts +243 -0
- package/src/rpc.ts +39 -10
- package/src/simulate.ts +535 -0
- package/src/subpaths/approvals.ts +47 -0
- package/src/subpaths/claim.ts +53 -0
- package/src/subpaths/fees.ts +41 -0
- package/src/subpaths/history.ts +38 -0
- package/src/subpaths/lp.ts +5 -1
- package/src/subpaths/names.ts +45 -0
- package/src/subpaths/price.ts +62 -0
- package/src/subpaths/reclaim.ts +57 -0
- package/src/subpaths/risk.ts +36 -0
- package/src/subpaths/simulate.ts +37 -0
- package/src/subpaths/swap.ts +41 -0
- package/src/subpaths/write.ts +2 -0
- package/src/swap/amm.ts +88 -0
- package/src/swap/clmmMath.ts +96 -0
- package/src/swap/common.ts +113 -0
- package/src/swap/cpmm.ts +132 -0
- package/src/swap/index.ts +467 -0
- package/src/swap/orca.ts +397 -0
- package/src/swap/raydiumClmm.ts +464 -0
- package/src/tradeCheck.ts +166 -0
- package/src/write/instructions.ts +43 -1
- package/src/write/transaction.ts +62 -0
package/src/risk.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代币风险检查(子路径 `@w3lib/solana-batch-call/risk`):mint 的权限(还能不能增发、能不能冻结持有人)和 Token-2022 扩展里
|
|
3
|
+
* 影响持有人的设置(转账手续费、永久授权、转账 hook、不可转让、新账户默认冻结、可暂停、保密转账、mint 可关闭等),一次 getMultipleAccounts 读完。
|
|
4
|
+
* 可选:持币集中度(getTokenLargestAccounts,免费公共节点大多不支持);能不能买、能不能卖用 tradeChecks(在主网模拟“买入 → 卖出”)
|
|
5
|
+
*/
|
|
6
|
+
import { isAddress } from './address.js'
|
|
7
|
+
import type { RequestOptions, SolanaClient } from './client.js'
|
|
8
|
+
import { NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
|
|
9
|
+
import { findExtension, parseMint, parseTransferFeeConfig, type AccountInfo } from './layout.js'
|
|
10
|
+
import { Bytes } from './lp.js'
|
|
11
|
+
import { CLOCK_SYSVAR_ID, parseClock } from './swap/common.js'
|
|
12
|
+
|
|
13
|
+
/** Token-2022 扩展的类型编号(spl-token-2022 ExtensionType) */
|
|
14
|
+
const EXT = {
|
|
15
|
+
transferFeeConfig: 1,
|
|
16
|
+
mintCloseAuthority: 3,
|
|
17
|
+
confidentialTransferMint: 4,
|
|
18
|
+
defaultAccountState: 6,
|
|
19
|
+
nonTransferable: 9,
|
|
20
|
+
interestBearingConfig: 10,
|
|
21
|
+
permanentDelegate: 12,
|
|
22
|
+
transferHook: 14,
|
|
23
|
+
scaledUiAmount: 25,
|
|
24
|
+
pausable: 26,
|
|
25
|
+
permissionedBurn: 28,
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
const ZERO = '11111111111111111111111111111111'
|
|
29
|
+
const key = (b: Bytes, o: number) => {
|
|
30
|
+
const k = b.key(o)
|
|
31
|
+
return k === ZERO ? null : k
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SolanaTokenRisk {
|
|
35
|
+
mint: string
|
|
36
|
+
/** mint 账户存在且是 SPL Token / Token-2022 的 mint */
|
|
37
|
+
exists: boolean
|
|
38
|
+
program: 'spl-token' | 'token-2022' | null
|
|
39
|
+
decimals: number | null
|
|
40
|
+
/** 总量(最小单位) */
|
|
41
|
+
supply: string | null
|
|
42
|
+
/** 增发权限:不为 null 时还能继续增发 */
|
|
43
|
+
mintAuthority: string | null
|
|
44
|
+
/** 冻结权限:不为 null 时可以冻结任何持有人的代币账户 */
|
|
45
|
+
freezeAuthority: string | null
|
|
46
|
+
/** Token-2022 转账手续费(当前 epoch 生效的);authority 不为 null 时费率还能改(新费率两个 epoch 后生效) */
|
|
47
|
+
transferFee: { basisPoints: number; maximumFee: string; authority: string | null; next: { basisPoints: number; maximumFee: string; epoch: string } | null } | null
|
|
48
|
+
/** 永久授权:这个地址可以转走 / 销毁任何持有人的代币 */
|
|
49
|
+
permanentDelegate: string | null
|
|
50
|
+
/** 转账 hook:每次转账都会调用这个程序(可以拒绝转账);authority 不为 null 时程序还能换 */
|
|
51
|
+
transferHook: { program: string | null; authority: string | null } | null
|
|
52
|
+
/** 不可转让(灵魂绑定) */
|
|
53
|
+
nonTransferable: boolean
|
|
54
|
+
/** 新建的代币账户默认是冻结的(要冻结权限解冻后才能用) */
|
|
55
|
+
defaultFrozen: boolean
|
|
56
|
+
/** 可暂停:authority 可以暂停全部转账、增发、销毁;paused 为当前状态 */
|
|
57
|
+
pausable: { paused: boolean; authority: string | null } | null
|
|
58
|
+
/** 保密转账(余额和转账数量加密) */
|
|
59
|
+
confidentialTransfer: { authority: string | null; autoApprove: boolean } | null
|
|
60
|
+
/** mint 关闭权限(总量为 0 时可以关闭 mint) */
|
|
61
|
+
mintCloseAuthority: string | null
|
|
62
|
+
/** 显示数量会随时间 / 系数变化(InterestBearingConfig、ScaledUiAmount),authority 可以改利率 / 系数 */
|
|
63
|
+
uiAmountAuthority: string | null
|
|
64
|
+
/** 只有这个地址能批准销毁(PermissionedBurn) */
|
|
65
|
+
burnAuthority: string | null
|
|
66
|
+
/** 持币集中度(concentration: true 时):最大的 20 个代币账户里前 10 个占总量的比例;节点不支持或总量为 0 时为 null */
|
|
67
|
+
topHolders: { top10Share: number; accounts: Array<{ address: string; amount: string; share: number }> } | null
|
|
68
|
+
/**
|
|
69
|
+
* 汇总的风险标记:not-found、mintable、freezable、transfer-fee(> 0)、high-transfer-fee(> 10%)、fee-changeable、permanent-delegate、
|
|
70
|
+
* transfer-hook、non-transferable、default-frozen、pausable、paused、confidential-transfer、closable、ui-amount-changes、permissioned-burn、
|
|
71
|
+
* concentrated(前 10 个账户 > 50%,含池子金库、交易所地址)
|
|
72
|
+
*/
|
|
73
|
+
flags: string[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TokenRisksOptions extends RequestOptions {
|
|
77
|
+
/** 查持币集中度(getTokenLargestAccounts,每个 mint 一次请求;免费公共节点大多不支持,不支持时 topHolders 为 null),默认 false */
|
|
78
|
+
concentration?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 检查代币的风险(mint 和 Clock 一次 getMultipleAccounts 读完)。原生 SOL(客户端的 nativeMints)按 Wrapped SOL 的 mint 查。
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* const [risk] = await tokenRisks(client, [PYUSD])
|
|
86
|
+
* // { program: 'token-2022', mintAuthority: '22mK…', freezeAuthority: '2apB…', permanentDelegate: '2apB…', flags: ['mintable', 'freezable', 'permanent-delegate', …] }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export async function tokenRisks(client: SolanaClient, mints: readonly string[], options: TokenRisksOptions = {}): Promise<SolanaTokenRisk[]> {
|
|
90
|
+
const keys = mints.map((m) => (client.isNativeMint(m) ? NATIVE_MINT : m))
|
|
91
|
+
for (const m of keys) {
|
|
92
|
+
if (!isAddress(m)) throw new Error(`Invalid mint address: ${m}`)
|
|
93
|
+
}
|
|
94
|
+
const unique = [...new Set(keys)]
|
|
95
|
+
const accounts = await client.accounts([CLOCK_SYSVAR_ID, ...unique], { signal: options.signal })
|
|
96
|
+
const clock = accounts[0]
|
|
97
|
+
const epoch = clock ? parseClock(clock.data).epoch : null
|
|
98
|
+
const byMint = new Map(unique.map((m, i) => [m, accounts[i + 1] ?? null]))
|
|
99
|
+
const holders = options.concentration ? await largestAccounts(client, unique, options.signal) : new Map<string, Array<{ address: string; amount: bigint }> | null>()
|
|
100
|
+
return mints.map((mint, i) => {
|
|
101
|
+
const k = keys[i] as string
|
|
102
|
+
const risk = parseRisk(mint, byMint.get(k) ?? null, epoch)
|
|
103
|
+
const list = holders.get(k)
|
|
104
|
+
if (list && risk.supply && BigInt(risk.supply) > 0n) {
|
|
105
|
+
const supply = BigInt(risk.supply)
|
|
106
|
+
const share = (a: bigint) => Number((a * 1_000_000n) / supply) / 1_000_000
|
|
107
|
+
const sorted = [...list].sort((a, b) => (a.amount < b.amount ? 1 : a.amount > b.amount ? -1 : 0))
|
|
108
|
+
risk.topHolders = {
|
|
109
|
+
top10Share: share(sorted.slice(0, 10).reduce((s, a) => s + a.amount, 0n)),
|
|
110
|
+
accounts: sorted.map((a) => ({ address: a.address, amount: a.amount.toString(), share: share(a.amount) })),
|
|
111
|
+
}
|
|
112
|
+
if (risk.topHolders.top10Share > 0.5) risk.flags.push('concentrated')
|
|
113
|
+
}
|
|
114
|
+
return risk
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function largestAccounts(client: SolanaClient, mints: readonly string[], signal: AbortSignal | undefined) {
|
|
119
|
+
const out = new Map<string, Array<{ address: string; amount: bigint }> | null>()
|
|
120
|
+
await Promise.all(
|
|
121
|
+
mints.map(async (mint) => {
|
|
122
|
+
try {
|
|
123
|
+
const res = await client.request<{ value: Array<{ address: string; amount: string }> }>('getTokenLargestAccounts', [mint, { commitment: client.commitment }], { signal })
|
|
124
|
+
out.set(
|
|
125
|
+
mint,
|
|
126
|
+
res.value.map((a) => ({ address: a.address, amount: BigInt(a.amount) })),
|
|
127
|
+
)
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (signal?.aborted) throw err
|
|
130
|
+
out.set(mint, null) // 节点不支持
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function empty(mint: string): SolanaTokenRisk {
|
|
138
|
+
return {
|
|
139
|
+
mint,
|
|
140
|
+
exists: false,
|
|
141
|
+
program: null,
|
|
142
|
+
decimals: null,
|
|
143
|
+
supply: null,
|
|
144
|
+
mintAuthority: null,
|
|
145
|
+
freezeAuthority: null,
|
|
146
|
+
transferFee: null,
|
|
147
|
+
permanentDelegate: null,
|
|
148
|
+
transferHook: null,
|
|
149
|
+
nonTransferable: false,
|
|
150
|
+
defaultFrozen: false,
|
|
151
|
+
pausable: null,
|
|
152
|
+
confidentialTransfer: null,
|
|
153
|
+
mintCloseAuthority: null,
|
|
154
|
+
uiAmountAuthority: null,
|
|
155
|
+
burnAuthority: null,
|
|
156
|
+
topHolders: null,
|
|
157
|
+
flags: ['not-found'],
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 解析一个 mint 账户的风险项(包内使用,测试也用);epoch 用来选转账手续费的新旧配置(null 时按新配置) */
|
|
162
|
+
export function parseRisk(mint: string, account: AccountInfo | null, epoch: bigint | null): SolanaTokenRisk {
|
|
163
|
+
if (!account || (account.owner !== TOKEN_PROGRAM_ID && account.owner !== TOKEN_2022_PROGRAM_ID)) return empty(mint)
|
|
164
|
+
let info: ReturnType<typeof parseMint>
|
|
165
|
+
try {
|
|
166
|
+
info = parseMint(account.data)
|
|
167
|
+
} catch {
|
|
168
|
+
return empty(mint)
|
|
169
|
+
}
|
|
170
|
+
const risk: SolanaTokenRisk = {
|
|
171
|
+
...empty(mint),
|
|
172
|
+
exists: true,
|
|
173
|
+
program: account.owner === TOKEN_PROGRAM_ID ? 'spl-token' : 'token-2022',
|
|
174
|
+
decimals: info.decimals,
|
|
175
|
+
supply: info.supply.toString(),
|
|
176
|
+
mintAuthority: info.mintAuthority,
|
|
177
|
+
freezeAuthority: info.freezeAuthority,
|
|
178
|
+
flags: [],
|
|
179
|
+
}
|
|
180
|
+
if (account.owner === TOKEN_2022_PROGRAM_ID) {
|
|
181
|
+
const data = account.data
|
|
182
|
+
const ext = (type: number) => findExtension(data, type)
|
|
183
|
+
const fee = parseTransferFeeConfig(data)
|
|
184
|
+
if (fee) {
|
|
185
|
+
const current = epoch === null || epoch >= fee.newer.epoch ? fee.newer : fee.older
|
|
186
|
+
const pending = epoch !== null && epoch < fee.newer.epoch ? fee.newer : null
|
|
187
|
+
risk.transferFee = {
|
|
188
|
+
basisPoints: current.basisPoints,
|
|
189
|
+
maximumFee: current.maximumFee.toString(),
|
|
190
|
+
authority: fee.transferFeeConfigAuthority,
|
|
191
|
+
next: pending ? { basisPoints: pending.basisPoints, maximumFee: pending.maximumFee.toString(), epoch: pending.epoch.toString() } : null,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const close = ext(EXT.mintCloseAuthority)
|
|
195
|
+
if (close && close.length >= 32) risk.mintCloseAuthority = key(new Bytes(close), 0)
|
|
196
|
+
const confidential = ext(EXT.confidentialTransferMint)
|
|
197
|
+
if (confidential && confidential.length >= 33) risk.confidentialTransfer = { authority: key(new Bytes(confidential), 0), autoApprove: confidential[32] === 1 }
|
|
198
|
+
const state = ext(EXT.defaultAccountState)
|
|
199
|
+
// AccountState:0 未初始化、1 正常、2 冻结
|
|
200
|
+
risk.defaultFrozen = !!state && state[0] === 2
|
|
201
|
+
risk.nonTransferable = ext(EXT.nonTransferable) !== null
|
|
202
|
+
const delegate = ext(EXT.permanentDelegate)
|
|
203
|
+
if (delegate && delegate.length >= 32) risk.permanentDelegate = key(new Bytes(delegate), 0)
|
|
204
|
+
const hook = ext(EXT.transferHook)
|
|
205
|
+
if (hook && hook.length >= 64) {
|
|
206
|
+
const b = new Bytes(hook)
|
|
207
|
+
risk.transferHook = { authority: key(b, 0), program: key(b, 32) }
|
|
208
|
+
}
|
|
209
|
+
const pausable = ext(EXT.pausable)
|
|
210
|
+
if (pausable && pausable.length >= 33) risk.pausable = { authority: key(new Bytes(pausable), 0), paused: pausable[32] === 1 }
|
|
211
|
+
for (const type of [EXT.interestBearingConfig, EXT.scaledUiAmount]) {
|
|
212
|
+
const e = ext(type)
|
|
213
|
+
if (e && e.length >= 32) risk.uiAmountAuthority ??= key(new Bytes(e), 0) ?? null
|
|
214
|
+
}
|
|
215
|
+
const burn = ext(EXT.permissionedBurn)
|
|
216
|
+
if (burn && burn.length >= 32) risk.burnAuthority = key(new Bytes(burn), 0)
|
|
217
|
+
}
|
|
218
|
+
risk.flags = flagsOf(risk)
|
|
219
|
+
return risk
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function flagsOf(r: SolanaTokenRisk): string[] {
|
|
223
|
+
const flags: string[] = []
|
|
224
|
+
if (r.mintAuthority) flags.push('mintable')
|
|
225
|
+
if (r.freezeAuthority) flags.push('freezable')
|
|
226
|
+
if (r.transferFee) {
|
|
227
|
+
const bps = Math.max(r.transferFee.basisPoints, r.transferFee.next?.basisPoints ?? 0)
|
|
228
|
+
if (bps > 0) flags.push('transfer-fee')
|
|
229
|
+
if (bps > 1000) flags.push('high-transfer-fee')
|
|
230
|
+
if (r.transferFee.authority) flags.push('fee-changeable')
|
|
231
|
+
}
|
|
232
|
+
if (r.permanentDelegate) flags.push('permanent-delegate')
|
|
233
|
+
if (r.transferHook?.program || r.transferHook?.authority) flags.push('transfer-hook')
|
|
234
|
+
if (r.nonTransferable) flags.push('non-transferable')
|
|
235
|
+
if (r.defaultFrozen) flags.push('default-frozen')
|
|
236
|
+
if (r.pausable) flags.push('pausable')
|
|
237
|
+
if (r.pausable?.paused) flags.push('paused')
|
|
238
|
+
if (r.confidentialTransfer) flags.push('confidential-transfer')
|
|
239
|
+
if (r.mintCloseAuthority) flags.push('closable')
|
|
240
|
+
if (r.uiAmountAuthority) flags.push('ui-amount-changes')
|
|
241
|
+
if (r.burnAuthority) flags.push('permissioned-burn')
|
|
242
|
+
return flags
|
|
243
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -61,7 +61,32 @@ const RATE_LIMITED = /rate.?limit|too many requests/i
|
|
|
61
61
|
const TRANSIENT = /\bbusy\b|overload|temporar(il)?y|unavailable|try again|timed? ?out|timeout/i
|
|
62
62
|
|
|
63
63
|
/** 节点限制单次 getMultipleAccounts 的账户数(如 “Too many accounts requested”):缩小每次的账户数后重试,不是节点故障 */
|
|
64
|
-
const TOO_MANY_ACCOUNTS = /too many accounts/i
|
|
64
|
+
const TOO_MANY_ACCOUNTS = /too many accounts|maximum of \d+ accounts/i
|
|
65
|
+
/**
|
|
66
|
+
* publicnode 对单次超过 10 个账户的 getMultipleAccounts 返回 HTTP 403 +“Request blocked”(2026-10,而且要等 3 秒多才返回):
|
|
67
|
+
* 账户数超过 BLOCKED_LIMIT 时直接按它拆分(逐次减半要多等好几轮),还被拒绝再减半;
|
|
68
|
+
* 账户数已经很少(≤ BLOCKED_FLOOR)还被拒绝时,是节点真的拒绝访问,交给上层换节点
|
|
69
|
+
*/
|
|
70
|
+
const REQUEST_BLOCKED = /request blocked/i
|
|
71
|
+
const BLOCKED_LIMIT = 10
|
|
72
|
+
const BLOCKED_FLOOR = 8
|
|
73
|
+
|
|
74
|
+
/** 从报错里学到的节点限制,按节点地址共享(同一个节点的多个客户端不用各自再试一遍) */
|
|
75
|
+
const learnedLimits = new Map<string, { methods: Map<string, number>; maxAccounts?: number }>()
|
|
76
|
+
|
|
77
|
+
function learnedFor(url: string): { methods: Map<string, number>; maxAccounts?: number } {
|
|
78
|
+
let entry = learnedLimits.get(url)
|
|
79
|
+
if (!entry) {
|
|
80
|
+
entry = { methods: new Map() }
|
|
81
|
+
learnedLimits.set(url, entry)
|
|
82
|
+
}
|
|
83
|
+
return entry
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 测试用:清空学到的节点限制 */
|
|
87
|
+
export function resetLearnedLimits(): void {
|
|
88
|
+
learnedLimits.clear()
|
|
89
|
+
}
|
|
65
90
|
|
|
66
91
|
/** HTTP 层错误(非 2xx、网络错误、超时) */
|
|
67
92
|
export class HttpError extends Error {
|
|
@@ -178,20 +203,19 @@ export class HttpRpc implements RpcTransport {
|
|
|
178
203
|
readonly #retries: number
|
|
179
204
|
readonly #fetch: typeof fetch
|
|
180
205
|
#maxBatchSize: number
|
|
181
|
-
/**
|
|
182
|
-
readonly #
|
|
206
|
+
/** 从报错里学到的限制(按节点地址共享):单个方法在一个批量里的数量(如 publicnode:getMultipleAccounts 最多 1 个)、单次的账户数 */
|
|
207
|
+
readonly #learned: { methods: Map<string, number>; maxAccounts?: number }
|
|
183
208
|
/** options.batch:调用方是否允许批量 */
|
|
184
209
|
readonly #batchOption: boolean
|
|
185
210
|
/** 节点明确表示不支持批量时,在这之前逐条请求(之后重新尝试批量) */
|
|
186
211
|
#batchDisabledUntil = 0
|
|
187
|
-
/** 节点限制单次 getMultipleAccounts 的账户数时学到的上限 */
|
|
188
|
-
#maxAccounts: number | undefined
|
|
189
212
|
#queue: Pending[] = []
|
|
190
213
|
#timer: ReturnType<typeof setTimeout> | null = null
|
|
191
214
|
|
|
192
215
|
constructor(url: string, options: HttpRpcOptions = {}) {
|
|
193
216
|
this.url = url
|
|
194
217
|
this.#safeUrl = safeOrigin(url)
|
|
218
|
+
this.#learned = learnedFor(url)
|
|
195
219
|
this.#timeout = options.timeout ?? 10_000
|
|
196
220
|
this.#headers = { 'content-type': 'application/json', ...options.headers }
|
|
197
221
|
this.#maxBatchSize = Math.max(1, options.maxBatchSize ?? 20)
|
|
@@ -214,7 +238,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
214
238
|
*/
|
|
215
239
|
async #getMultipleAccounts(params: readonly unknown[]): Promise<unknown> {
|
|
216
240
|
const [addresses, ...rest] = params as [unknown[], ...unknown[]]
|
|
217
|
-
const max = this.#maxAccounts
|
|
241
|
+
const max = this.#learned.maxAccounts
|
|
218
242
|
if (max !== undefined && addresses.length > max) {
|
|
219
243
|
const parts: unknown[][] = []
|
|
220
244
|
for (let i = 0; i < addresses.length; i += max) {
|
|
@@ -234,8 +258,13 @@ export class HttpRpc implements RpcTransport {
|
|
|
234
258
|
try {
|
|
235
259
|
return await this.#enqueue('getMultipleAccounts', params)
|
|
236
260
|
} catch (err) {
|
|
237
|
-
|
|
238
|
-
|
|
261
|
+
const tooMany = err instanceof RpcError && TOO_MANY_ACCOUNTS.test(err.message) && addresses.length > 1
|
|
262
|
+
const blocked = err instanceof RpcError && REQUEST_BLOCKED.test(err.message) && addresses.length > BLOCKED_FLOOR
|
|
263
|
+
if (tooMany || blocked) {
|
|
264
|
+
// publicnode(2026-10):“A maximum of 10 accounts is allowed”,报错里直接给出上限
|
|
265
|
+
const stated = Number(/maximum of (\d+) accounts/i.exec(err.message)?.[1])
|
|
266
|
+
const next = stated >= 1 ? stated : blocked && addresses.length > BLOCKED_LIMIT ? BLOCKED_LIMIT : Math.floor(addresses.length / 2)
|
|
267
|
+
this.#learned.maxAccounts = Math.max(1, Math.min(this.#learned.maxAccounts ?? Number.POSITIVE_INFINITY, next))
|
|
239
268
|
return this.#getMultipleAccounts(params)
|
|
240
269
|
}
|
|
241
270
|
throw err
|
|
@@ -276,7 +305,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
276
305
|
let current: Pending[] = []
|
|
277
306
|
let counts = new Map<string, number>()
|
|
278
307
|
for (const item of items) {
|
|
279
|
-
const limit = this.#
|
|
308
|
+
const limit = this.#learned.methods.get(item.method)
|
|
280
309
|
const count = counts.get(item.method) ?? 0
|
|
281
310
|
if (current.length >= this.#maxBatchSize || (limit !== undefined && count >= limit)) {
|
|
282
311
|
chunks.push(current)
|
|
@@ -303,7 +332,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
303
332
|
if (!method || !Number.isFinite(limit) || limit < 1 || items.filter((item) => item.method === method).length <= limit) {
|
|
304
333
|
return false
|
|
305
334
|
}
|
|
306
|
-
this.#
|
|
335
|
+
this.#learned.methods.set(method, limit)
|
|
307
336
|
await Promise.all(this.#split(items).map((chunk) => this.#dispatch(chunk)))
|
|
308
337
|
return true
|
|
309
338
|
}
|