@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/simulate.ts
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交易模拟(子路径 `@w3lib/solana-batch-call/simulate`):simulateTransaction(不验签名,换最新 blockhash)+ 钱包和交易里可写账户的前后状态,
|
|
3
|
+
* 算出余额变化、授权变化(delegate / SetAuthority / 关闭账户)、指令说明、程序错误、手续费和警告。支持 legacy 和 v0(地址查找表)交易
|
|
4
|
+
*/
|
|
5
|
+
import { base64 } from '@scure/base'
|
|
6
|
+
|
|
7
|
+
import { encodeAddress, isAddress } from './address.js'
|
|
8
|
+
import type { RequestOptions, SolanaClient } from './client.js'
|
|
9
|
+
import { NATIVE_MINT, SOL_DECIMALS, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
|
|
10
|
+
import { decodeInstruction, type DecodedInstruction } from './decode.js'
|
|
11
|
+
import { classify, describe, type AssetDelta, type TxKind } from './describe.js'
|
|
12
|
+
import { formatUnits, parseMint, type AccountInfo } from './layout.js'
|
|
13
|
+
import { BPF_LOADER_UPGRADEABLE, COMPUTE_BUDGET, PROGRAMS, programErrorName } from './programs.js'
|
|
14
|
+
import type { SolanaPriceSource } from './price.js'
|
|
15
|
+
import {
|
|
16
|
+
MAX_ACCOUNT_LOCKS,
|
|
17
|
+
PACKET_DATA_SIZE,
|
|
18
|
+
compileMessage,
|
|
19
|
+
parseMessage,
|
|
20
|
+
serializeTransaction,
|
|
21
|
+
splitTransaction,
|
|
22
|
+
transactionSize,
|
|
23
|
+
normalizeInstruction,
|
|
24
|
+
type InstructionInput,
|
|
25
|
+
} from './write/transaction.js'
|
|
26
|
+
|
|
27
|
+
export interface SimulatedBalanceChange extends AssetDelta {
|
|
28
|
+
/** 谁的余额(钱包地址);asset 原生 SOL 为 'native'(Wrapped SOL 账户的变化也并入这里),其他是 mint */
|
|
29
|
+
address: string
|
|
30
|
+
/** 美元价值(prices 选项开启时;没有价格为 null) */
|
|
31
|
+
usd?: number | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ApprovalChangeKind = 'delegate-set' | 'delegate-changed' | 'delegate-revoked' | 'authority-changed' | 'account-closed'
|
|
35
|
+
|
|
36
|
+
export interface ApprovalChange {
|
|
37
|
+
kind: ApprovalChangeKind
|
|
38
|
+
/** 代币账户(或 mint、钱包账户) */
|
|
39
|
+
account: string
|
|
40
|
+
mint?: string
|
|
41
|
+
/** 代币账户的 owner */
|
|
42
|
+
owner?: string
|
|
43
|
+
/** delegate-*:新 / 旧的被授权方,和授权数量(最小单位;unlimited:接近 u64 上限) */
|
|
44
|
+
delegate?: string | null
|
|
45
|
+
previousDelegate?: string | null
|
|
46
|
+
amount?: string
|
|
47
|
+
unlimited?: boolean
|
|
48
|
+
/** authority-changed:权限类型(AccountOwner、CloseAccount、MintTokens、FreezeAccount)和前后的地址 */
|
|
49
|
+
authorityType?: string
|
|
50
|
+
authority?: string | null
|
|
51
|
+
previousAuthority?: string | null
|
|
52
|
+
/** account-closed:关闭时账户里的数量,和租金退给谁(从指令里读) */
|
|
53
|
+
balance?: string
|
|
54
|
+
destination?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type WarningSeverity = 'info' | 'warn' | 'danger'
|
|
58
|
+
|
|
59
|
+
export interface SimulationWarning {
|
|
60
|
+
code: string
|
|
61
|
+
severity: WarningSeverity
|
|
62
|
+
message: string
|
|
63
|
+
address?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SimulationResult {
|
|
67
|
+
success: boolean
|
|
68
|
+
/** 失败时的原因:程序错误码解码后的名字 + 日志里的说明 */
|
|
69
|
+
error?: string
|
|
70
|
+
/** 消耗的计算单元(十进制字符串) */
|
|
71
|
+
gasUsed: string
|
|
72
|
+
/** 手续费(lamports):每个签名 5000 + 计算单元上限 × 单价;不含新建账户的租金 */
|
|
73
|
+
fee?: string
|
|
74
|
+
balanceChanges: SimulatedBalanceChange[]
|
|
75
|
+
approvalChanges: ApprovalChange[]
|
|
76
|
+
decoded: {
|
|
77
|
+
type: TxKind
|
|
78
|
+
protocol?: string
|
|
79
|
+
method?: string
|
|
80
|
+
summary: string
|
|
81
|
+
/** 没有认得的指令、按余额变化推断出来的类型 */
|
|
82
|
+
inferred?: boolean
|
|
83
|
+
instructions: DecodedInstruction[]
|
|
84
|
+
}
|
|
85
|
+
warnings: SimulationWarning[]
|
|
86
|
+
logs: string[]
|
|
87
|
+
/** 交易需要签名的地址和版本 */
|
|
88
|
+
signers: string[]
|
|
89
|
+
version: 'legacy' | 0
|
|
90
|
+
/** risk: true 时:变化的代币的风险标记(mint → flags) */
|
|
91
|
+
risks?: Record<string, string[]>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SimulateOptions extends RequestOptions {
|
|
95
|
+
/** 站在哪个钱包的角度算余额变化,默认交易的第一个签名者(手续费支付者);构建好的计划 / 指令用它们自己的 owner */
|
|
96
|
+
owner?: string
|
|
97
|
+
/** 预期还会签名的地址(如新建仓位的 NFT mint);交易需要别的签名者时给警告 */
|
|
98
|
+
signers?: readonly string[]
|
|
99
|
+
/** 额外要看余额变化的地址(如收款人) */
|
|
100
|
+
watch?: readonly string[]
|
|
101
|
+
/** 计划里有多步(setup + 主指令)放不进一笔交易时,只模拟这一步(下标) */
|
|
102
|
+
step?: number
|
|
103
|
+
/** 算 usd:true 用默认价格源,或传价格源;默认不查 */
|
|
104
|
+
prices?: boolean | readonly SolanaPriceSource[]
|
|
105
|
+
/** 变化的代币的风险检查(/risk),危险的标记加进 warnings;默认不查 */
|
|
106
|
+
risk?: boolean
|
|
107
|
+
fetch?: typeof fetch
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 本库构建器的输出:兑换 / 流动性计划(steps)、指令列表、一组 ApprovalCall */
|
|
111
|
+
export type SimulateInput =
|
|
112
|
+
| string
|
|
113
|
+
| Uint8Array
|
|
114
|
+
| { owner: string; steps: ReadonlyArray<{ instructions: readonly InstructionInput[] }>; signers?: ReadonlyArray<{ address: string }> }
|
|
115
|
+
| { owner?: string; feePayer?: string; instructions: InstructionInput | readonly InstructionInput[] }
|
|
116
|
+
| ReadonlyArray<{ owner: string; instructions: readonly InstructionInput[] }>
|
|
117
|
+
|
|
118
|
+
const U64_HALF = 1n << 63n
|
|
119
|
+
const HIGH_FEE_LAMPORTS = 10_000_000n
|
|
120
|
+
const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
|
|
121
|
+
|
|
122
|
+
interface Built {
|
|
123
|
+
bytes: Uint8Array
|
|
124
|
+
owner: string | undefined
|
|
125
|
+
expected: string[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function build(input: SimulateInput, options: SimulateOptions): Built {
|
|
129
|
+
if (typeof input === 'string') return { bytes: base64.decode(input), owner: options.owner, expected: [...(options.signers ?? [])] }
|
|
130
|
+
if (input instanceof Uint8Array) return { bytes: input, owner: options.owner, expected: [...(options.signers ?? [])] }
|
|
131
|
+
let payer: string | undefined
|
|
132
|
+
let list: InstructionInput[]
|
|
133
|
+
let expected = [...(options.signers ?? [])]
|
|
134
|
+
if (Array.isArray(input)) {
|
|
135
|
+
const calls = input as ReadonlyArray<{ owner: string; instructions: readonly InstructionInput[] }>
|
|
136
|
+
payer = calls[0]?.owner
|
|
137
|
+
list = calls.flatMap((c) => [...c.instructions])
|
|
138
|
+
expected.push(...calls.map((c) => c.owner))
|
|
139
|
+
} else if ('steps' in input) {
|
|
140
|
+
payer = input.owner
|
|
141
|
+
const steps = options.step === undefined ? input.steps : input.steps.filter((_, i) => i === options.step)
|
|
142
|
+
list = steps.flatMap((s) => [...s.instructions])
|
|
143
|
+
expected.push(...(input.signers ?? []).map((s) => s.address))
|
|
144
|
+
} else {
|
|
145
|
+
payer = (input as { feePayer?: string }).feePayer ?? (input as { owner?: string }).owner
|
|
146
|
+
const ix = (input as { instructions: InstructionInput | readonly InstructionInput[] }).instructions
|
|
147
|
+
list = Array.isArray(ix) ? [...ix] : [ix as InstructionInput]
|
|
148
|
+
}
|
|
149
|
+
payer = options.owner ?? payer
|
|
150
|
+
if (!payer || !isAddress(payer)) throw new Error('Simulation needs a valid owner / feePayer address')
|
|
151
|
+
const instructions = list.map(normalizeInstruction)
|
|
152
|
+
const hasLimit = instructions.some((ix) => ix.programId === COMPUTE_BUDGET && ix.data[0] === 2)
|
|
153
|
+
const withLimit = hasLimit ? instructions : [{ programId: COMPUTE_BUDGET, keys: [], data: new Uint8Array([2, 0xc0, 0x5c, 0x15, 0x00]) }, ...instructions]
|
|
154
|
+
const message = compileMessage(payer, withLimit, SYSTEM_PROGRAM_ID)
|
|
155
|
+
if (transactionSize(message) > PACKET_DATA_SIZE || message.accountKeys.length > MAX_ACCOUNT_LOCKS) {
|
|
156
|
+
throw new Error('The instructions do not fit in one transaction; simulate the steps one by one with the `step` option')
|
|
157
|
+
}
|
|
158
|
+
return { bytes: serializeTransaction(message, message.signers.map(() => null)), owner: payer, expected: [payer, ...expected] }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface Resolved {
|
|
162
|
+
version: 'legacy' | 0
|
|
163
|
+
signers: string[]
|
|
164
|
+
keys: string[]
|
|
165
|
+
writable: Set<string>
|
|
166
|
+
instructions: Array<{ programId: string; accounts: string[]; data: Uint8Array }>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 解析交易:地址查找表里的账户读表补上(静态账户 + 表里可写的 + 表里只读的) */
|
|
170
|
+
async function resolveTransaction(client: SolanaClient, bytes: Uint8Array, signal: AbortSignal | undefined): Promise<Resolved> {
|
|
171
|
+
if (((bytes[0] ?? 0) & 0x80) !== 0) throw new Error('Unsupported transaction format (v1): only legacy and v0 transactions can be simulated')
|
|
172
|
+
const { message } = splitTransaction(bytes)
|
|
173
|
+
const parsed = parseMessage(message)
|
|
174
|
+
const loadedWritable: string[] = []
|
|
175
|
+
const loadedReadonly: string[] = []
|
|
176
|
+
if (parsed.lookups.length) {
|
|
177
|
+
const tables = await client.accounts(parsed.lookups.map((l) => l.table), { signal })
|
|
178
|
+
parsed.lookups.forEach((l, i) => {
|
|
179
|
+
const data = tables[i]?.data
|
|
180
|
+
if (!data) throw new Error(`Address lookup table not found: ${l.table}`)
|
|
181
|
+
const at = (index: number) => {
|
|
182
|
+
const start = 56 + 32 * index
|
|
183
|
+
if (start + 32 > data.length) throw new Error(`Address lookup table ${l.table} has no entry ${index}`)
|
|
184
|
+
return encodeAddress(data.slice(start, start + 32))
|
|
185
|
+
}
|
|
186
|
+
loadedWritable.push(...l.writable.map(at))
|
|
187
|
+
loadedReadonly.push(...l.readonly.map(at))
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
const keys = [...parsed.staticKeys, ...loadedWritable, ...loadedReadonly]
|
|
191
|
+
const key = (i: number) => {
|
|
192
|
+
const k = keys[i]
|
|
193
|
+
if (k === undefined) throw new Error('Invalid transaction: bad account index')
|
|
194
|
+
return k
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
version: parsed.version,
|
|
198
|
+
signers: parsed.staticKeys.slice(0, parsed.signatures),
|
|
199
|
+
keys,
|
|
200
|
+
writable: new Set([...parsed.writable, ...loadedWritable]),
|
|
201
|
+
instructions: parsed.instructions.map((ix) => ({ programId: ix.programId, accounts: ix.accounts.map(key), data: ix.data })),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---- 账户状态 ----
|
|
206
|
+
|
|
207
|
+
interface Snap {
|
|
208
|
+
lamports: bigint
|
|
209
|
+
owner: string
|
|
210
|
+
data: Uint8Array
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface TokenState {
|
|
214
|
+
mint: string
|
|
215
|
+
owner: string
|
|
216
|
+
amount: bigint
|
|
217
|
+
delegate: string | null
|
|
218
|
+
delegatedAmount: bigint
|
|
219
|
+
closeAuthority: string | null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function tokenState(snap: Snap | null): TokenState | null {
|
|
223
|
+
const d = snap?.data
|
|
224
|
+
if (!snap || !d || (snap.owner !== TOKEN_PROGRAM_ID && snap.owner !== TOKEN_2022_PROGRAM_ID)) return null
|
|
225
|
+
if (d.length !== 165 && !(d.length > 165 && d[165] === 2)) return null
|
|
226
|
+
const view = new DataView(d.buffer, d.byteOffset, d.byteLength)
|
|
227
|
+
const key = (o: number) => encodeAddress(d.slice(o, o + 32))
|
|
228
|
+
if (d[108] === 0) return null
|
|
229
|
+
return {
|
|
230
|
+
mint: key(0),
|
|
231
|
+
owner: key(32),
|
|
232
|
+
amount: view.getBigUint64(64, true),
|
|
233
|
+
delegate: view.getUint32(72, true) === 1 ? key(76) : null,
|
|
234
|
+
delegatedAmount: view.getBigUint64(121, true),
|
|
235
|
+
closeAuthority: view.getUint32(129, true) === 1 ? key(133) : null,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function mintAuthorities(snap: Snap | null): { mintAuthority: string | null; freezeAuthority: string | null } | null {
|
|
240
|
+
if (!snap || (snap.owner !== TOKEN_PROGRAM_ID && snap.owner !== TOKEN_2022_PROGRAM_ID)) return null
|
|
241
|
+
try {
|
|
242
|
+
const m = parseMint(snap.data)
|
|
243
|
+
return { mintAuthority: m.mintAuthority, freezeAuthority: m.freezeAuthority }
|
|
244
|
+
} catch {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---- 错误 ----
|
|
250
|
+
|
|
251
|
+
const TX_ERRORS: Record<string, string> = {
|
|
252
|
+
InsufficientFundsForFee: 'insufficient SOL for the transaction fee',
|
|
253
|
+
AccountNotFound: 'fee payer account not found (it has no SOL)',
|
|
254
|
+
BlockhashNotFound: 'blockhash not found',
|
|
255
|
+
AccountInUse: 'account in use',
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 失败原因:Anchor 日志的错误名 > 程序错误码(按失败的程序查表)> 日志里程序打印的说明 > 错误本身 */
|
|
259
|
+
export function failureText(err: unknown, logs: readonly string[], ixs: readonly DecodedInstruction[]): string {
|
|
260
|
+
if (typeof err === 'string') return TX_ERRORS[err] ?? err
|
|
261
|
+
const ie = (err as { InstructionError?: [number, unknown] } | null)?.InstructionError
|
|
262
|
+
if (!ie) {
|
|
263
|
+
if ((err as { InsufficientFundsForRent?: unknown } | null)?.InsufficientFundsForRent) return 'account would be left with less SOL than the rent-exempt minimum'
|
|
264
|
+
return JSON.stringify(err)
|
|
265
|
+
}
|
|
266
|
+
const [index, reason] = ie
|
|
267
|
+
const at = ixs[index]
|
|
268
|
+
const where = at ? `instruction ${index} (${at.program ?? short(at.programId)}${at.method ? ` ${at.method}` : ''})` : `instruction ${index}`
|
|
269
|
+
const reversed = [...logs].reverse()
|
|
270
|
+
const anchor = reversed.find((l) => /AnchorError/.test(l))
|
|
271
|
+
const anchorText = anchor && /Error Code: (\w+)\. Error Number: (\d+)\. Error Message: (.*?)\.?$/.exec(anchor)
|
|
272
|
+
if (anchorText) return `${where} failed: ${anchorText[1]} (${anchorText[2]}): ${anchorText[3]}`
|
|
273
|
+
const custom = (reason as { Custom?: number } | null)?.Custom
|
|
274
|
+
if (custom !== undefined) {
|
|
275
|
+
const failed = reversed.map((l) => /^Program (\w+) failed: custom program error/.exec(l)?.[1]).find(Boolean) ?? at?.programId
|
|
276
|
+
const name = programErrorName(failed, custom)
|
|
277
|
+
const detail = reversed.find((l) => /^Program log: (Error|error)/.test(l))?.replace(/^Program log: /, '')
|
|
278
|
+
return `${where} failed: ${name ?? `custom program error 0x${custom.toString(16)}`}${name ? ` (0x${custom.toString(16)})` : ''}${detail ? ` - ${detail}` : ''}`
|
|
279
|
+
}
|
|
280
|
+
const plain = reversed.find((l) => !l.startsWith('Program ') || /^Program log: /.test(l))?.replace(/^Program log: /, '')
|
|
281
|
+
return `${where} failed: ${typeof reason === 'string' ? reason : JSON.stringify(reason)}${plain ? ` - ${plain}` : ''}`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
interface RawSimulation {
|
|
285
|
+
err: unknown
|
|
286
|
+
logs: string[] | null
|
|
287
|
+
unitsConsumed?: number
|
|
288
|
+
accounts: Array<{ lamports: number | string; owner: string; data: [string, string] } | null> | null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 模拟一笔交易,不需要签名、不会发送。输入:base64 / 字节的交易,或本库构建器的输出
|
|
293
|
+
* (buildSwap / buildAddLiquidity… 的计划、`{ owner, instructions }`、buildRevokes / buildCloses 的结果)
|
|
294
|
+
*/
|
|
295
|
+
export async function simulate(client: SolanaClient, input: SimulateInput, options: SimulateOptions = {}): Promise<SimulationResult> {
|
|
296
|
+
const { signal } = options
|
|
297
|
+
const built = build(input, options)
|
|
298
|
+
const tx = await resolveTransaction(client, built.bytes, signal)
|
|
299
|
+
const wallet = built.owner ?? tx.signers[0]
|
|
300
|
+
if (!wallet) throw new Error('Transaction has no signer')
|
|
301
|
+
const decodedIxs = tx.instructions.map((ix, i) => decodeInstruction(i, ix.programId, ix.accounts, ix.data))
|
|
302
|
+
|
|
303
|
+
// 要看前后状态的账户:钱包、额外要看的、交易里可写的里和钱包有关的(新建的账户、钱包的代币账户、mint);
|
|
304
|
+
// 公共节点的 simulateTransaction 一次最多返回 10 个账户(publicnode),所以先读前状态筛一遍,不够再分批模拟
|
|
305
|
+
const candidates = [...new Set([wallet, ...(options.watch ?? []).filter(isAddress), ...tx.keys.filter((k) => tx.writable.has(k))])]
|
|
306
|
+
const preAll = await client.accounts(candidates, { signal })
|
|
307
|
+
const keep = (address: string, i: number) => {
|
|
308
|
+
const before = preAll[i]
|
|
309
|
+
if (address === wallet || options.watch?.includes(address) || !before) return true
|
|
310
|
+
return tokenState(before)?.owner === wallet || mintAuthorities(before) !== null
|
|
311
|
+
}
|
|
312
|
+
const watch = candidates.filter(keep)
|
|
313
|
+
const pre = watch.map((a) => preAll[candidates.indexOf(a)] ?? null)
|
|
314
|
+
const raw = base64.encode(built.bytes)
|
|
315
|
+
const run = (addresses: string[]) =>
|
|
316
|
+
client.request<{ value: RawSimulation }>(
|
|
317
|
+
'simulateTransaction',
|
|
318
|
+
[raw, { encoding: 'base64', sigVerify: false, replaceRecentBlockhash: true, commitment: 'confirmed', accounts: { encoding: 'base64', addresses } }],
|
|
319
|
+
{ signal },
|
|
320
|
+
)
|
|
321
|
+
let sim: { value: RawSimulation }
|
|
322
|
+
try {
|
|
323
|
+
sim = await run(watch)
|
|
324
|
+
} catch (err) {
|
|
325
|
+
const max = Number(/maximum of (\d+) accounts/i.exec(err instanceof Error ? err.message : '')?.[1])
|
|
326
|
+
if (!(max >= 1)) throw err
|
|
327
|
+
const chunks = Array.from({ length: Math.ceil(watch.length / max) }, (_, i) => watch.slice(i * max, (i + 1) * max))
|
|
328
|
+
const parts = await Promise.all(chunks.map(run))
|
|
329
|
+
sim = { value: { ...(parts[0] as { value: RawSimulation }).value, accounts: parts.every((x) => x.value.accounts) ? parts.flatMap((x) => x.value.accounts ?? []) : null } }
|
|
330
|
+
}
|
|
331
|
+
const value = sim.value
|
|
332
|
+
const logs = value.logs ?? []
|
|
333
|
+
const success = !value.err
|
|
334
|
+
const post: Array<Snap | null> | null = success && value.accounts ? value.accounts.map((a) => (a ? { lamports: BigInt(a.lamports), owner: a.owner, data: base64.decode(a.data[0]) } : null)) : null
|
|
335
|
+
const preAt = (i: number): Snap | null => {
|
|
336
|
+
const a: AccountInfo | null | undefined = pre[i]
|
|
337
|
+
return a ? { lamports: a.lamports, owner: a.owner, data: a.data } : null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 手续费:签名费 + 计算单元上限 × 单价
|
|
341
|
+
let limit: bigint | null = null
|
|
342
|
+
let price = 0n
|
|
343
|
+
for (const ix of tx.instructions) {
|
|
344
|
+
if (ix.programId !== COMPUTE_BUDGET) continue
|
|
345
|
+
const view = new DataView(ix.data.buffer, ix.data.byteOffset, ix.data.byteLength)
|
|
346
|
+
if (ix.data[0] === 2 && ix.data.length >= 5) limit = BigInt(view.getUint32(1, true))
|
|
347
|
+
if (ix.data[0] === 3 && ix.data.length >= 9) price = view.getBigUint64(1, true)
|
|
348
|
+
}
|
|
349
|
+
const nonBudget = tx.instructions.filter((ix) => ix.programId !== COMPUTE_BUDGET).length
|
|
350
|
+
const units = BigInt(Math.min(1_400_000, limit !== null ? Number(limit) : 200_000 * nonBudget))
|
|
351
|
+
const fee = 5000n * BigInt(tx.signers.length) + (price * units + 999_999n) / 1_000_000n
|
|
352
|
+
const feePayer = tx.signers[0]
|
|
353
|
+
|
|
354
|
+
const warnings: SimulationWarning[] = []
|
|
355
|
+
const warn = (code: string, severity: WarningSeverity, message: string, address?: string) => warnings.push({ code, severity, message, address })
|
|
356
|
+
|
|
357
|
+
const approvalChanges: ApprovalChange[] = []
|
|
358
|
+
const tokenDeltas = new Map<string, bigint>()
|
|
359
|
+
let nativeDelta = 0n
|
|
360
|
+
const mints = new Set<string>()
|
|
361
|
+
|
|
362
|
+
if (post) {
|
|
363
|
+
watch.forEach((address, i) => {
|
|
364
|
+
const before = preAt(i)
|
|
365
|
+
const after = post[i] ?? null
|
|
366
|
+
if (address === wallet) {
|
|
367
|
+
nativeDelta = (after?.lamports ?? 0n) - (before?.lamports ?? 0n)
|
|
368
|
+
if (before && after && before.owner === SYSTEM_PROGRAM_ID && after.owner !== SYSTEM_PROGRAM_ID) {
|
|
369
|
+
approvalChanges.push({ kind: 'authority-changed', account: address, authorityType: 'AccountOwner', authority: after.owner, previousAuthority: before.owner })
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const a = tokenState(before)
|
|
373
|
+
const b = tokenState(after)
|
|
374
|
+
const mine = (t: TokenState | null) => t?.owner === wallet
|
|
375
|
+
const mint = (a ?? b)?.mint
|
|
376
|
+
if (mint && (mine(a) || mine(b))) {
|
|
377
|
+
mints.add(mint)
|
|
378
|
+
tokenDeltas.set(mint, (tokenDeltas.get(mint) ?? 0n) + (mine(b) ? (b as TokenState).amount : 0n) - (mine(a) ? (a as TokenState).amount : 0n))
|
|
379
|
+
const common = { account: address, mint, owner: wallet }
|
|
380
|
+
if (a && !b) {
|
|
381
|
+
approvalChanges.push({ ...common, kind: 'account-closed', balance: a.amount.toString() })
|
|
382
|
+
} else if (b) {
|
|
383
|
+
const unlimited = b.delegatedAmount >= U64_HALF
|
|
384
|
+
if (b.delegate && !a?.delegate) approvalChanges.push({ ...common, kind: 'delegate-set', delegate: b.delegate, previousDelegate: null, amount: b.delegatedAmount.toString(), unlimited })
|
|
385
|
+
else if (b.delegate && a?.delegate && (a.delegate !== b.delegate || b.delegatedAmount > a.delegatedAmount)) {
|
|
386
|
+
approvalChanges.push({ ...common, kind: 'delegate-changed', delegate: b.delegate, previousDelegate: a.delegate, amount: b.delegatedAmount.toString(), unlimited })
|
|
387
|
+
} else if (!b.delegate && a?.delegate) approvalChanges.push({ ...common, kind: 'delegate-revoked', delegate: null, previousDelegate: a.delegate, amount: '0' })
|
|
388
|
+
if (a && a.owner !== b.owner) approvalChanges.push({ ...common, kind: 'authority-changed', authorityType: 'AccountOwner', authority: b.owner, previousAuthority: a.owner })
|
|
389
|
+
if (a && a.closeAuthority !== b.closeAuthority) {
|
|
390
|
+
approvalChanges.push({ ...common, kind: 'authority-changed', authorityType: 'CloseAccount', authority: b.closeAuthority, previousAuthority: a.closeAuthority })
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const ma = mintAuthorities(before)
|
|
395
|
+
const mb = mintAuthorities(after)
|
|
396
|
+
if (ma && mb) {
|
|
397
|
+
for (const [type, field] of [['MintTokens', 'mintAuthority'], ['FreezeAccount', 'freezeAuthority']] as const) {
|
|
398
|
+
if (ma[field] !== mb[field]) approvalChanges.push({ kind: 'authority-changed', account: address, mint: address, authorityType: type, authority: mb[field], previousAuthority: ma[field] })
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
} else {
|
|
403
|
+
// 没有模拟后的状态(模拟失败):只能按指令判断授权
|
|
404
|
+
for (const ix of decodedIxs) {
|
|
405
|
+
const args = ix.args as Record<string, string | null> | undefined
|
|
406
|
+
if (ix.method === 'approve' || ix.method === 'approveChecked') {
|
|
407
|
+
approvalChanges.push({ kind: 'delegate-set', account: args?.account as string, mint: args?.mint ?? undefined, owner: args?.owner as string, delegate: args?.delegate, amount: args?.amount as string, unlimited: BigInt(args?.amount ?? 0) >= U64_HALF })
|
|
408
|
+
} else if (ix.method === 'setAuthority') {
|
|
409
|
+
approvalChanges.push({ kind: 'authority-changed', account: args?.account as string, authorityType: args?.authorityType as string, authority: args?.newAuthority })
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const ix of decodedIxs) {
|
|
414
|
+
const args = ix.args as Record<string, string> | undefined
|
|
415
|
+
if (ix.method === 'closeAccount') {
|
|
416
|
+
const closed = approvalChanges.find((c) => c.kind === 'account-closed' && c.account === args?.account)
|
|
417
|
+
if (closed) closed.destination = args?.destination
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 余额变化:钱包的 SOL(不含手续费)和代币,Wrapped SOL 并入 SOL
|
|
422
|
+
const balanceChanges: SimulatedBalanceChange[] = []
|
|
423
|
+
if (post) {
|
|
424
|
+
const wsol = tokenDeltas.get(NATIVE_MINT) ?? 0n
|
|
425
|
+
tokenDeltas.delete(NATIVE_MINT)
|
|
426
|
+
mints.delete(NATIVE_MINT)
|
|
427
|
+
const sol = nativeDelta + (feePayer === wallet ? fee : 0n) + wsol
|
|
428
|
+
if (sol !== 0n) balanceChanges.push({ address: wallet, asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS, delta: sol.toString(), formatted: formatUnits(sol, SOL_DECIMALS) })
|
|
429
|
+
const ids = [...tokenDeltas.entries()].filter(([, v]) => v !== 0n)
|
|
430
|
+
const details = ids.length
|
|
431
|
+
? await client.tokens(ids.map(([m]) => m), { fields: ['symbol', 'decimals'], signal }).catch(() => null)
|
|
432
|
+
: []
|
|
433
|
+
if (!details) warn('metadata-unavailable', 'info', 'Token symbols / decimals could not be read')
|
|
434
|
+
ids.forEach(([mint, delta], i) => {
|
|
435
|
+
const detail = details?.[i]
|
|
436
|
+
const decimals = detail?.decimals ?? 0
|
|
437
|
+
balanceChanges.push({ address: wallet, asset: mint, symbol: detail?.symbol ?? short(mint), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const kind = classify(decodedIxs, balanceChanges)
|
|
442
|
+
const recipient = decodedIxs.find((ix) => ix.type === 'transfer')?.args as Record<string, string> | undefined
|
|
443
|
+
const decoded: SimulationResult['decoded'] = {
|
|
444
|
+
type: kind?.type ?? 'unknown',
|
|
445
|
+
protocol: kind?.protocol,
|
|
446
|
+
method: kind?.method,
|
|
447
|
+
summary: describe(kind, decodedIxs, balanceChanges, recipient?.to ?? recipient?.destination),
|
|
448
|
+
inferred: kind?.inferred,
|
|
449
|
+
instructions: decodedIxs,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ---- 警告 ----
|
|
453
|
+
if (!success) warn('simulation-failed', 'warn', 'The transaction fails in simulation')
|
|
454
|
+
for (const c of approvalChanges) {
|
|
455
|
+
if (c.kind === 'delegate-set' || c.kind === 'delegate-changed') {
|
|
456
|
+
warn(c.unlimited ? 'unlimited-delegate' : 'delegate-approved', c.unlimited ? 'danger' : 'warn', `${short(c.delegate as string)} can spend ${c.unlimited ? 'an unlimited amount of' : c.amount} of the tokens in ${short(c.account)}`, c.delegate as string)
|
|
457
|
+
} else if (c.kind === 'authority-changed') {
|
|
458
|
+
const sev = c.authorityType === 'AccountOwner' || c.authorityType === 'CloseAccount' ? 'danger' : 'warn'
|
|
459
|
+
warn('authority-changed', sev, `${c.authorityType} authority of ${short(c.account)} changes to ${c.authority ? short(c.authority) : 'none'}`, c.authority ?? undefined)
|
|
460
|
+
} else if (c.kind === 'account-closed' && c.destination && c.destination !== wallet) {
|
|
461
|
+
warn('close-to-other', 'warn', `Closing ${short(c.account)}${c.balance && c.balance !== '0' ? ` (balance ${c.balance})` : ''} sends its lamports to ${short(c.destination)}`, c.destination)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const ix of decodedIxs) {
|
|
465
|
+
if (ix.method === 'assign' || ix.method === 'assignWithSeed') warn('assign-owner', 'danger', `Assigns ${short(String(ix.args?.account))} to another program`, String(ix.args?.account))
|
|
466
|
+
if (ix.programId === BPF_LOADER_UPGRADEABLE && (ix.method === 'upgrade' || ix.method === 'setAuthority')) warn('program-upgrade', 'danger', `Program loader instruction: ${ix.method}`)
|
|
467
|
+
}
|
|
468
|
+
const unknown = new Set(decodedIxs.filter((ix) => !PROGRAMS[ix.programId]).map((ix) => ix.programId))
|
|
469
|
+
for (const program of unknown) warn('unknown-program', 'warn', `Calls an unrecognized program ${short(program)}`, program)
|
|
470
|
+
const expected = new Set(built.expected.length ? built.expected : [wallet])
|
|
471
|
+
expected.add(wallet)
|
|
472
|
+
for (const s of tx.signers) {
|
|
473
|
+
if (!expected.has(s)) warn('unexpected-signer', 'warn', `The transaction also needs a signature from ${short(s)}`, s)
|
|
474
|
+
}
|
|
475
|
+
if (!tx.signers.includes(wallet)) warn('wallet-not-signer', 'info', `${short(wallet)} does not sign this transaction`, wallet)
|
|
476
|
+
if (fee > HIGH_FEE_LAMPORTS) warn('high-fee', 'warn', `Network fee is ${formatUnits(fee, SOL_DECIMALS)} SOL`)
|
|
477
|
+
if (post) {
|
|
478
|
+
const walletPre = preAt(watch.indexOf(wallet))?.lamports ?? 0n
|
|
479
|
+
const spent = -(nativeDelta + (feePayer === wallet ? fee : 0n))
|
|
480
|
+
if (walletPre > 0n && spent > 0n && spent * 10n >= walletPre * 9n) warn('drains-balance', 'warn', 'Spends 90% or more of the wallet SOL balance', wallet)
|
|
481
|
+
watch.forEach((address, i) => {
|
|
482
|
+
if (address === wallet) return
|
|
483
|
+
const before = preAt(i)
|
|
484
|
+
const after = post[i] ?? null
|
|
485
|
+
const isDestination = decodedIxs.some((ix) => (ix.method === 'transfer' || ix.method === 'transferWithSeed') && ix.programId === SYSTEM_PROGRAM_ID && ix.args?.to === address)
|
|
486
|
+
if (isDestination && (!before || before.lamports === 0n)) warn('new-recipient', 'warn', `${short(address)} has never held SOL (new account)`, address)
|
|
487
|
+
const t = tokenState(after)
|
|
488
|
+
if (!before && t && t.owner !== wallet && decodedIxs.some((ix) => ix.type === 'transfer' && ix.args?.destination === address)) {
|
|
489
|
+
warn('new-recipient', 'warn', `${short(t.owner)} did not have a token account for this mint yet`, t.owner)
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const result: SimulationResult = {
|
|
495
|
+
success,
|
|
496
|
+
...(success ? {} : { error: failureText(value.err, logs, decodedIxs) }),
|
|
497
|
+
gasUsed: String(value.unitsConsumed ?? 0),
|
|
498
|
+
fee: fee.toString(),
|
|
499
|
+
balanceChanges,
|
|
500
|
+
approvalChanges,
|
|
501
|
+
decoded,
|
|
502
|
+
warnings,
|
|
503
|
+
logs,
|
|
504
|
+
signers: tx.signers,
|
|
505
|
+
version: tx.version,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---- 可选:价格、风险(按需加载) ----
|
|
509
|
+
const assets = balanceChanges.map((c) => (c.asset === 'native' ? NATIVE_MINT : c.asset))
|
|
510
|
+
if (options.prices && balanceChanges.length) {
|
|
511
|
+
const { tokenPrices } = await import('./price.js')
|
|
512
|
+
const sources = Array.isArray(options.prices) ? { sources: options.prices as readonly SolanaPriceSource[] } : {}
|
|
513
|
+
const prices = await tokenPrices(client, assets, { ...sources, signal, fetch: options.fetch }).catch(() => null)
|
|
514
|
+
balanceChanges.forEach((c, i) => {
|
|
515
|
+
const p = prices?.[i]?.price
|
|
516
|
+
c.usd = p == null ? null : Number(c.formatted) * p
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
if (options.risk) {
|
|
520
|
+
const tokens = [...mints].filter((m) => balanceChanges.some((c) => c.asset === m))
|
|
521
|
+
if (tokens.length) {
|
|
522
|
+
const { tokenRisks } = await import('./risk.js')
|
|
523
|
+
const risks = await tokenRisks(client, tokens, { signal }).catch(() => null)
|
|
524
|
+
if (risks) {
|
|
525
|
+
result.risks = Object.fromEntries(risks.map((r) => [r.mint, r.flags]))
|
|
526
|
+
const severe = ['permanent-delegate', 'transfer-hook', 'high-transfer-fee', 'non-transferable', 'default-frozen', 'paused', 'not-found']
|
|
527
|
+
for (const r of risks) {
|
|
528
|
+
const hit = r.flags.filter((f) => severe.includes(f))
|
|
529
|
+
if (hit.length) warn('token-risk', 'warn', `${short(r.mint)}: ${hit.join(', ')}`, r.mint)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return result
|
|
535
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 授权管理(子路径 `@w3lib/solana-batch-call/approvals`):批量查地址的代币账户给出的授权(delegate)、可以关闭回收租金的空账户,
|
|
3
|
+
* 批量撤销 / 关闭。不放在主入口,不用时打包不会带上这部分代码;发送的代码按需加载。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { closeTokenAccounts, getApprovals, revokeApprovals } from '@w3lib/solana-batch-call/approvals'
|
|
7
|
+
*
|
|
8
|
+
* const list = await getApprovals(owner, { mints: [USDC, BONK] })
|
|
9
|
+
* await revokeApprovals(client, key, list)
|
|
10
|
+
* await closeTokenAccounts(client, key, list.filter((a) => a.amount === '0'))
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { approvals, type ApprovalsQuery, type TokenAccountApproval } from '../approvals.js'
|
|
14
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
approvals,
|
|
18
|
+
buildCloses,
|
|
19
|
+
buildRevokes,
|
|
20
|
+
closeTokenAccounts,
|
|
21
|
+
parseTokenAccountState,
|
|
22
|
+
revokeApprovals,
|
|
23
|
+
type ApprovalCall,
|
|
24
|
+
type ApprovalResult,
|
|
25
|
+
type ApprovalsQuery,
|
|
26
|
+
type BuildClosesOptions,
|
|
27
|
+
type CloseAccountsOptions,
|
|
28
|
+
type CloseBlocker,
|
|
29
|
+
type TokenAccountApproval,
|
|
30
|
+
} from '../approvals.js'
|
|
31
|
+
export { closeAccount, harvestWithheldTokensToMint, revoke } from '../write/instructions.js'
|
|
32
|
+
|
|
33
|
+
export interface GetApprovalsOptions extends ApprovalsQuery, ShortcutOptions {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 查地址的代币账户给出的授权和可以关闭的空账户,节点参数与 getBalances 相同。
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await getApprovals(owner, { mints: [USDC, BONK] }) // 指定代币:读 ATA,免费公共节点也能用
|
|
40
|
+
* await getApprovals(owner, { provider: 'https://my-rpc…' }) // 全部代币账户:需要支持 getTokenAccountsByOwner 的节点
|
|
41
|
+
* // [{ account, mint: USDC, symbol: 'USDC', delegate: 'GYFk…', delegatedAmount: '6993125', revocable: true, closable: false, closeBlocker: 'balance', … }]
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function getApprovals(owner: string, options: GetApprovalsOptions = {}): Promise<TokenAccountApproval[]> {
|
|
45
|
+
const { client, own } = resolve(options, ['mints', 'all', 'signal'])
|
|
46
|
+
return approvals(client, owner, own)
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量领取(子路径 `@w3lib/solana-batch-call/claim`):Orca Whirlpool、Raydium CLMM 仓位的未领取手续费和挖矿奖励(农场奖励)。
|
|
3
|
+
* 先只读预览(每个仓位能领多少、美元价值),再把全部(或选中的)仓位的领取指令打包成交易,发送前用 /simulate 模拟。
|
|
4
|
+
* 不放在主入口,不用时打包不会带上这部分代码;指令来自 /liquidity 的 collectFees。
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { claimAll, getClaimable } from '@w3lib/solana-batch-call/claim'
|
|
8
|
+
*
|
|
9
|
+
* const c = await getClaimable(owner, { provider: 'https://my-rpc…' }) // 预览,不发交易
|
|
10
|
+
* c.totalUsd // 全部能领的美元价值
|
|
11
|
+
* await claimAll(client, key, c.positions) // 批量领取
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { claimable, type Claimable, type ClaimQuery } from '../claim.js'
|
|
15
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
buildClaimAll,
|
|
19
|
+
claimAll,
|
|
20
|
+
claimable,
|
|
21
|
+
simulateClaim,
|
|
22
|
+
type BuildClaimOptions,
|
|
23
|
+
type ClaimCall,
|
|
24
|
+
type ClaimCallResult,
|
|
25
|
+
type ClaimOptions,
|
|
26
|
+
type ClaimPlan,
|
|
27
|
+
type ClaimProtocol,
|
|
28
|
+
type ClaimQuery,
|
|
29
|
+
type ClaimResult,
|
|
30
|
+
type ClaimSimulation,
|
|
31
|
+
type ClaimTarget,
|
|
32
|
+
type Claimable,
|
|
33
|
+
type ClaimableAmount,
|
|
34
|
+
type ClaimablePosition,
|
|
35
|
+
} from '../claim.js'
|
|
36
|
+
export type { PlanSimulation } from '../batchPlan.js'
|
|
37
|
+
|
|
38
|
+
export interface GetClaimableOptions extends ClaimQuery, ShortcutOptions {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 预览一个地址能领取的手续费和挖矿奖励(只读取),节点参数与 getBalances 相同。默认列出全部代币账户(需要支持 getTokenAccountsByOwner 的节点),
|
|
42
|
+
* 免费公共节点传 `mints`(仓位 NFT 的 mint)只看指定仓位。
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const c = await getClaimable(owner, { provider: 'https://my-rpc…' })
|
|
46
|
+
* // { totalUsd: 345.6, tokens: [{ symbol: 'SOL', formatted: '1.627', usd: 290 }, { symbol: 'RAY', formatted: '0.0427', usd: 0.08 }, …],
|
|
47
|
+
* // positions: [{ protocol: 'raydium-clmm', positionMint, fees: [{ symbol: 'SOL', … }, { symbol: 'USDC', … }], rewards: [{ index: 0, symbol: 'RAY', … }], totalUsd: 345.6 }] }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function getClaimable(owner: string, options: GetClaimableOptions = {}): Promise<Claimable> {
|
|
51
|
+
const { client, own } = resolve(options, ['mints', 'protocols', 'prices', 'now', 'signal'])
|
|
52
|
+
return claimable(client, owner, { ...own, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
53
|
+
}
|