@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/fees.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 手续费估算(子路径 `@w3lib/solana-batch-call/fees`):三档优先费(getRecentPrioritizationFees 的百分位,可以只看交易会写的账户),
|
|
3
|
+
* 一笔交易的花费:基础费(每个签名 5000 lamports)+ 优先费(模拟得到的计算单元上限 × 单价)+ 新建账户的免租押金
|
|
4
|
+
*/
|
|
5
|
+
import { base64 } from '@scure/base'
|
|
6
|
+
|
|
7
|
+
import { encodeAddress } from './address.js'
|
|
8
|
+
import type { RequestOptions, SolanaClient } from './client.js'
|
|
9
|
+
import { SOL_DECIMALS, SYSTEM_PROGRAM_ID } from './constants.js'
|
|
10
|
+
import { formatUnits } from './layout.js'
|
|
11
|
+
import type { InstructionInput } from './write/transaction.js'
|
|
12
|
+
|
|
13
|
+
export type FeeLevel = 'slow' | 'normal' | 'fast'
|
|
14
|
+
|
|
15
|
+
/** 三档优先费单价(每个计算单元多少 micro-lamports,十进制字符串) */
|
|
16
|
+
export interface PriorityFeeEstimate {
|
|
17
|
+
slow: string
|
|
18
|
+
normal: string
|
|
19
|
+
fast: string
|
|
20
|
+
/** 统计了多少个 slot(节点返回最近 150 个) */
|
|
21
|
+
slots: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EstimateFeesOptions extends RequestOptions {
|
|
25
|
+
/** 只看会写这些账户的交易(如交易的可写账户,最多 128 个);不传时看全网 */
|
|
26
|
+
accounts?: readonly string[]
|
|
27
|
+
/** 三档取各 slot 最低落块费用的百分位,默认 [50, 75, 90](normal 与 /write 的 priorityFee: 'auto' 相同) */
|
|
28
|
+
percentiles?: readonly [number, number, number]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PrioritizationFee {
|
|
32
|
+
slot: number
|
|
33
|
+
prioritizationFee: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 当前的优先费建议:getRecentPrioritizationFees 返回最近 150 个 slot 里(会写这些账户的)落块交易的最低单价,三档取它们的百分位。
|
|
38
|
+
* 不传 accounts 时大多是 0(每个区块总有不加优先费的交易),要估某笔交易时传它的可写账户(estimateCost 自动这样做)
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* await estimateFees(client, { accounts: [poolAddress] }) // { slow: '0', normal: '12000', fast: '150000', slots: 150 }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export async function estimateFees(client: SolanaClient, options: EstimateFeesOptions = {}): Promise<PriorityFeeEstimate> {
|
|
45
|
+
const [p1, p2, p3] = options.percentiles ?? [50, 75, 90]
|
|
46
|
+
const accounts = [...new Set(options.accounts ?? [])].slice(0, 128)
|
|
47
|
+
const list = await client.request<PrioritizationFee[]>('getRecentPrioritizationFees', accounts.length ? [accounts] : [], { signal: options.signal })
|
|
48
|
+
const fees = list.map((f) => BigInt(f.prioritizationFee)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
49
|
+
// 与 /write 的 'auto' 相同的取法:排序后第 ⌊n × p⌋ 个
|
|
50
|
+
const pick = (p: number) => (fees.length ? (fees[Math.min(fees.length - 1, Math.max(0, Math.floor((fees.length * p) / 100)))] as bigint) : 0n)
|
|
51
|
+
const slow = pick(p1)
|
|
52
|
+
const normal = pick(p2) > slow ? pick(p2) : slow
|
|
53
|
+
const fast = pick(p3) > normal ? pick(p3) : normal
|
|
54
|
+
return { slow: slow.toString(), normal: normal.toString(), fast: fast.toString(), slots: list.length }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 要估算的交易:
|
|
59
|
+
* - 指令 + 手续费支付者:按 /write 的方式发送时的花费(计算单元上限 = 模拟值 × 1.1 + 1000,单价按这一档)。指令里自带的计算预算指令会被去掉
|
|
60
|
+
* - 已经构建好的交易(序列化的字节或 base64,legacy / v0,签名可以是空的):按交易自带的计算预算指令算(没有 SetComputeUnitPrice 时优先费为 0)
|
|
61
|
+
*/
|
|
62
|
+
export type CostInput = { feePayer: string; instructions: InstructionInput | readonly InstructionInput[] } | { transaction: Uint8Array | string }
|
|
63
|
+
|
|
64
|
+
export interface EstimateCostOptions extends Omit<EstimateFeesOptions, 'accounts'> {
|
|
65
|
+
/** 用哪一档优先费,默认 'normal'(只对指令有效) */
|
|
66
|
+
level?: FeeLevel
|
|
67
|
+
/** 直接指定优先费单价(micro-lamports / 计算单元),不查建议(只对指令有效) */
|
|
68
|
+
priorityFee?: number | bigint
|
|
69
|
+
/** 已经查好的费用建议(多笔交易共用);不传时按交易的可写账户查 */
|
|
70
|
+
fees?: PriorityFeeEstimate
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CreatedAccount {
|
|
74
|
+
address: string
|
|
75
|
+
/** 所属程序 */
|
|
76
|
+
owner: string
|
|
77
|
+
/** 数据大小(字节) */
|
|
78
|
+
space: number
|
|
79
|
+
/** 免租押金(lamports) */
|
|
80
|
+
rent: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CostEstimate {
|
|
84
|
+
/** 模拟消耗的计算单元 */
|
|
85
|
+
computeUnits: number
|
|
86
|
+
/** 计算单元上限(优先费按它收) */
|
|
87
|
+
computeUnitLimit: number
|
|
88
|
+
/** 优先费单价(micro-lamports / 计算单元) */
|
|
89
|
+
priorityFee: string
|
|
90
|
+
signatures: number
|
|
91
|
+
/** 基础费:每个签名 5000 lamports */
|
|
92
|
+
baseFee: string
|
|
93
|
+
/** 优先费:单价 × 上限(lamports,向上取整) */
|
|
94
|
+
priorityFeeLamports: string
|
|
95
|
+
/** 手续费:baseFee + priorityFeeLamports(lamports) */
|
|
96
|
+
fee: string
|
|
97
|
+
/** 新建账户的免租押金合计(lamports,关闭账户时退回) */
|
|
98
|
+
rent: string
|
|
99
|
+
createdAccounts: CreatedAccount[]
|
|
100
|
+
/** fee + rent(lamports)和按 SOL 换算的值 */
|
|
101
|
+
total: string
|
|
102
|
+
totalFormatted: string
|
|
103
|
+
symbol: 'SOL'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface SimulatedAccount {
|
|
107
|
+
lamports: number
|
|
108
|
+
owner: string
|
|
109
|
+
data: [string, string]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface Simulation {
|
|
113
|
+
err: unknown
|
|
114
|
+
logs?: string[] | null
|
|
115
|
+
unitsConsumed?: number
|
|
116
|
+
accounts?: Array<SimulatedAccount | null> | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 没有 SetComputeUnitLimit 时每条(非计算预算)指令的默认上限 */
|
|
120
|
+
const DEFAULT_UNITS_PER_INSTRUCTION = 200_000
|
|
121
|
+
/** 地址查找表账户的头部大小,之后每 32 字节一个地址 */
|
|
122
|
+
const LOOKUP_TABLE_META_SIZE = 56
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 估算一笔交易的花费(模拟一次,sigVerify: false,不需要签名、不会发送)。模拟失败时抛出(带程序日志里的原因)。
|
|
126
|
+
* 新建账户:交易前不存在(lamports 为 0)、模拟后存在、有数据或不归 System Program 的可写账户,押金按 getMinimumBalanceForRentExemption 算;
|
|
127
|
+
* 普通转账给新地址(System 账户、没有数据)不算押金
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* await estimateCost(client, { feePayer: owner, instructions: [ix1, ix2] })
|
|
131
|
+
* // { computeUnits: 41230, computeUnitLimit: 46353, priorityFee: '12000', fee: '5557', rent: '1488440', totalFormatted: '0.001493997', … }
|
|
132
|
+
* await estimateCost(client, { transaction: base64FromJupiter })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export async function estimateCost(client: SolanaClient, input: CostInput, options: EstimateCostOptions = {}): Promise<CostEstimate> {
|
|
136
|
+
const { signal } = options
|
|
137
|
+
// 构建交易的代码按需加载:只查费用建议时打包不会带上
|
|
138
|
+
const [{ MAX_COMPUTE_UNITS, COMPUTE_BUDGET_PROGRAM_ID, setComputeUnitLimit, setComputeUnitPrice }, tx, { transactionErrorText, transactionFee }] = await Promise.all([
|
|
139
|
+
import('./write/instructions.js'),
|
|
140
|
+
import('./write/transaction.js'),
|
|
141
|
+
import('./write/send.js'),
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
let raw: string
|
|
145
|
+
let signatures: number
|
|
146
|
+
let writable: string[]
|
|
147
|
+
/** 交易自带的计算预算(只对构建好的交易) */
|
|
148
|
+
let budget: { limit: number; price: bigint } | null = null
|
|
149
|
+
if ('transaction' in input) {
|
|
150
|
+
const bytes = typeof input.transaction === 'string' ? base64.decode(input.transaction) : input.transaction
|
|
151
|
+
// legacy / v0 的交易以签名数(compact-u16,不会超过 127)开头;v1 等新格式以版本字节(0x80 | 版本)开头,布局不同
|
|
152
|
+
if (((bytes[0] ?? 0) & 0x80) !== 0) {
|
|
153
|
+
throw new Error(`Unsupported transaction format (version ${(bytes[0] as number) & 0x7f}); only legacy and v0 transactions can be estimated, pass the instructions instead`)
|
|
154
|
+
}
|
|
155
|
+
const message = tx.parseMessage(tx.splitTransaction(bytes).message)
|
|
156
|
+
raw = base64.encode(bytes)
|
|
157
|
+
signatures = message.signatures
|
|
158
|
+
writable = [...message.writable, ...(await lookupWritable(client, message.lookups, signal))]
|
|
159
|
+
let limit: number | null = null
|
|
160
|
+
let price = 0n
|
|
161
|
+
for (const ix of message.instructions) {
|
|
162
|
+
if (ix.programId !== COMPUTE_BUDGET_PROGRAM_ID) continue
|
|
163
|
+
const view = new DataView(ix.data.buffer, ix.data.byteOffset, ix.data.byteLength)
|
|
164
|
+
if (ix.data[0] === 2 && ix.data.length >= 5) limit = view.getUint32(1, true)
|
|
165
|
+
if (ix.data[0] === 3 && ix.data.length >= 9) price = view.getBigUint64(1, true)
|
|
166
|
+
}
|
|
167
|
+
const others = message.instructions.filter((ix) => ix.programId !== COMPUTE_BUDGET_PROGRAM_ID).length
|
|
168
|
+
budget = { limit: Math.min(MAX_COMPUTE_UNITS, limit ?? DEFAULT_UNITS_PER_INSTRUCTION * others), price }
|
|
169
|
+
} else {
|
|
170
|
+
const list = Array.isArray(input.instructions) ? (input.instructions as readonly InstructionInput[]) : [input.instructions as InstructionInput]
|
|
171
|
+
// 和 batchCall 一样:去掉自带的计算单元上限 / 单价指令,模拟时上限取最大、单价为 0
|
|
172
|
+
const instructions = list.map(tx.normalizeInstruction).filter((ix) => !(ix.programId === COMPUTE_BUDGET_PROGRAM_ID && (ix.data[0] === 2 || ix.data[0] === 3)))
|
|
173
|
+
const message = tx.compileMessage(input.feePayer, [setComputeUnitLimit(MAX_COMPUTE_UNITS), setComputeUnitPrice(0n), ...instructions], SYSTEM_PROGRAM_ID)
|
|
174
|
+
raw = base64.encode(tx.serializeTransaction(message, message.signers.map(() => null)))
|
|
175
|
+
signatures = message.signers.length
|
|
176
|
+
writable = message.writable
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 交易前不存在的可写账户:模拟后读回它们的状态,判断是否新建
|
|
180
|
+
const before = writable.length ? await client.solBalances(writable, { signal }) : []
|
|
181
|
+
const fresh = writable.filter((_, i) => before[i]?.balance === '0')
|
|
182
|
+
const priceFor = async () => {
|
|
183
|
+
if (budget) return budget.price
|
|
184
|
+
if (options.priorityFee !== undefined) return BigInt(options.priorityFee)
|
|
185
|
+
const fees = options.fees ?? (await estimateFees(client, { accounts: writable, percentiles: options.percentiles, signal }))
|
|
186
|
+
return BigInt(fees[options.level ?? 'normal'])
|
|
187
|
+
}
|
|
188
|
+
const [simulation, price] = await Promise.all([
|
|
189
|
+
client.request<{ value: Simulation }>(
|
|
190
|
+
'simulateTransaction',
|
|
191
|
+
[
|
|
192
|
+
raw,
|
|
193
|
+
{
|
|
194
|
+
encoding: 'base64',
|
|
195
|
+
sigVerify: false,
|
|
196
|
+
replaceRecentBlockhash: true,
|
|
197
|
+
commitment: 'confirmed',
|
|
198
|
+
...(fresh.length ? { accounts: { encoding: 'base64', addresses: fresh } } : {}),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
{ signal },
|
|
202
|
+
),
|
|
203
|
+
priceFor(),
|
|
204
|
+
])
|
|
205
|
+
const sim = simulation.value
|
|
206
|
+
if (sim.err) {
|
|
207
|
+
throw new Error(`Simulation failed: ${transactionErrorText(sim.err, sim.logs)}`)
|
|
208
|
+
}
|
|
209
|
+
const consumed = sim.unitsConsumed ?? 0
|
|
210
|
+
const limit = budget?.limit ?? Math.min(MAX_COMPUTE_UNITS, Math.ceil(consumed * 1.1) + 1000)
|
|
211
|
+
|
|
212
|
+
const created = fresh.flatMap((address, i) => {
|
|
213
|
+
const a = sim.accounts?.[i]
|
|
214
|
+
if (!a || !(a.lamports > 0)) return []
|
|
215
|
+
const space = base64.decode(a.data[0]).length
|
|
216
|
+
return space > 0 || a.owner !== SYSTEM_PROGRAM_ID ? [{ address, owner: a.owner, space }] : []
|
|
217
|
+
})
|
|
218
|
+
const sizes = [...new Set(created.map((c) => c.space))]
|
|
219
|
+
const rents = await Promise.all(sizes.map((size) => client.request<number>('getMinimumBalanceForRentExemption', [size], { signal })))
|
|
220
|
+
const rentOf = new Map(sizes.map((size, i) => [size, BigInt(rents[i] as number)]))
|
|
221
|
+
const createdAccounts = created.map((c) => ({ ...c, rent: (rentOf.get(c.space) as bigint).toString() }))
|
|
222
|
+
const rent = createdAccounts.reduce((sum, c) => sum + BigInt(c.rent), 0n)
|
|
223
|
+
|
|
224
|
+
const fee = transactionFee(signatures, limit, price)
|
|
225
|
+
const baseFee = 5000n * BigInt(signatures)
|
|
226
|
+
const total = fee + rent
|
|
227
|
+
return {
|
|
228
|
+
computeUnits: consumed,
|
|
229
|
+
computeUnitLimit: limit,
|
|
230
|
+
priorityFee: price.toString(),
|
|
231
|
+
signatures,
|
|
232
|
+
baseFee: baseFee.toString(),
|
|
233
|
+
priorityFeeLamports: (fee - baseFee).toString(),
|
|
234
|
+
fee: fee.toString(),
|
|
235
|
+
rent: rent.toString(),
|
|
236
|
+
createdAccounts,
|
|
237
|
+
total: total.toString(),
|
|
238
|
+
totalFormatted: formatUnits(total, SOL_DECIMALS),
|
|
239
|
+
symbol: 'SOL',
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** v0 交易的地址查找表里要写的账户(读查找表账户:56 字节头部之后每 32 字节一个地址);表读不到时跳过 */
|
|
244
|
+
async function lookupWritable(client: SolanaClient, lookups: ReadonlyArray<{ table: string; writable: number[] }>, signal: AbortSignal | undefined): Promise<string[]> {
|
|
245
|
+
const used = lookups.filter((l) => l.writable.length)
|
|
246
|
+
if (!used.length) return []
|
|
247
|
+
const tables = await client.accounts(
|
|
248
|
+
used.map((l) => l.table),
|
|
249
|
+
{ signal },
|
|
250
|
+
)
|
|
251
|
+
return used.flatMap((l, i) => {
|
|
252
|
+
const data = tables[i]?.data
|
|
253
|
+
if (!data) return []
|
|
254
|
+
return l.writable.flatMap((index) => {
|
|
255
|
+
const start = LOOKUP_TABLE_META_SIZE + 32 * index
|
|
256
|
+
return start + 32 <= data.length ? [encodeAddress(data.slice(start, start + 32))] : []
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
}
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交易历史(子路径 `@w3lib/solana-batch-call/history`):把地址的交易整理成统一的 Activity(类型、方向、对手方、资产变化、手续费)。
|
|
3
|
+
* 数据源可以换:默认 rpcHistory(getSignaturesForAddress 翻页 + getTransaction,免费节点就能用,节点失败自动切换),
|
|
4
|
+
* 也可以传 heliusHistory 等;传多个时按顺序尝试。游标就是最后一条签名,换数据源也能接着翻
|
|
5
|
+
*/
|
|
6
|
+
import { base58 } from '@scure/base'
|
|
7
|
+
|
|
8
|
+
import { isAddress } from './address.js'
|
|
9
|
+
import type { RequestOptions, SolanaClient } from './client.js'
|
|
10
|
+
import { NATIVE_MINT, SOL_DECIMALS, SYSTEM_PROGRAM_ID } from './constants.js'
|
|
11
|
+
import { decodeInstruction, type DecodedInstruction } from './decode.js'
|
|
12
|
+
import { classify, describe, type AssetDelta, type Classified } from './describe.js'
|
|
13
|
+
import { formatUnits } from './layout.js'
|
|
14
|
+
|
|
15
|
+
export type ActivityType = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'contract' | 'other'
|
|
16
|
+
|
|
17
|
+
export interface ActivityChange extends AssetDelta {}
|
|
18
|
+
|
|
19
|
+
export interface Activity {
|
|
20
|
+
/** 交易签名 */
|
|
21
|
+
hash: string
|
|
22
|
+
/** 区块时间(unix 秒);节点没有给时为 0 */
|
|
23
|
+
time: number
|
|
24
|
+
/** slot */
|
|
25
|
+
block?: number
|
|
26
|
+
status: 'success' | 'failed'
|
|
27
|
+
type: ActivityType
|
|
28
|
+
/** 对这个地址来说:资产只增加 in、只减少 out、转给自己 self;兑换等有增有减时不填 */
|
|
29
|
+
direction?: 'in' | 'out' | 'self'
|
|
30
|
+
/** 转账的对手方 */
|
|
31
|
+
counterparty?: string
|
|
32
|
+
/** 一句话说明,如 “Swap 1 SOL for 150 USDC on Jupiter” */
|
|
33
|
+
label?: string
|
|
34
|
+
/** 这个地址的资产变化(SOL 不含手续费,Wrapped SOL 并入 SOL;asset 原生为 'native',其他是 mint) */
|
|
35
|
+
changes: ActivityChange[]
|
|
36
|
+
/** 这个地址付的手续费(lamports,十进制字符串);不是手续费支付者时不填 */
|
|
37
|
+
fee?: string
|
|
38
|
+
protocol?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HistoryPage {
|
|
42
|
+
items: Activity[]
|
|
43
|
+
/** 还有更多时:下一页的游标(最后一条处理过的签名);没有更多时不返回 */
|
|
44
|
+
nextCursor?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HistoryContext {
|
|
48
|
+
client: SolanaClient
|
|
49
|
+
address: string
|
|
50
|
+
limit: number
|
|
51
|
+
cursor?: string
|
|
52
|
+
/** 只要这个时间(unix 秒)之前 / 之后的 */
|
|
53
|
+
before?: number
|
|
54
|
+
after?: number
|
|
55
|
+
types?: readonly ActivityType[]
|
|
56
|
+
fetch: typeof fetch
|
|
57
|
+
signal?: AbortSignal
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 历史数据源:返回统一格式的 Activity(代币的 symbol 先用缩写的 mint,getHistory 统一补全);游标是签名 */
|
|
61
|
+
export interface HistorySource {
|
|
62
|
+
readonly name: string
|
|
63
|
+
getHistory(ctx: HistoryContext): Promise<HistoryPage>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface HistoryOptions extends RequestOptions {
|
|
67
|
+
/** 每页条数,默认 20,最多 100 */
|
|
68
|
+
limit?: number
|
|
69
|
+
/** 上一页的 nextCursor */
|
|
70
|
+
cursor?: string
|
|
71
|
+
/** 区块时间上限(不含)/ 下限(含),unix 秒 */
|
|
72
|
+
before?: number
|
|
73
|
+
after?: number
|
|
74
|
+
/** 只要这些类型(不够 limit 时会往后多找,最多看 limit × 5 笔) */
|
|
75
|
+
types?: readonly ActivityType[]
|
|
76
|
+
/** 数据源,默认 rpcHistory();传数组时按顺序尝试,前一个失败换下一个 */
|
|
77
|
+
source?: HistorySource | readonly HistorySource[]
|
|
78
|
+
/** 补全代币符号(读 mint 的元数据),默认 true */
|
|
79
|
+
symbols?: boolean
|
|
80
|
+
fetch?: typeof fetch
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const MAX_HISTORY_LIMIT = 100
|
|
84
|
+
/** getTransaction 支持的交易版本:主网已经有 v1 格式的交易(2026-10),只给 0 时遇到它们整个请求报错(-32015);json 编码下 v1 的账户和指令字段与 v0 相同 */
|
|
85
|
+
const MAX_TX_VERSION = 1
|
|
86
|
+
|
|
87
|
+
const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 查地址的交易历史。
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* const page = await history(client, address, { limit: 20 })
|
|
94
|
+
* const next = await history(client, address, { cursor: page.nextCursor })
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export async function history(client: SolanaClient, address: string, options: HistoryOptions = {}): Promise<HistoryPage> {
|
|
98
|
+
if (!isAddress(address)) throw new Error(`Invalid address: ${address}`)
|
|
99
|
+
const limit = options.limit ?? 20
|
|
100
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_HISTORY_LIMIT) throw new RangeError(`limit must be an integer between 1 and ${MAX_HISTORY_LIMIT}`)
|
|
101
|
+
const sources = options.source === undefined ? [rpcHistory()] : Array.isArray(options.source) ? options.source : [options.source as HistorySource]
|
|
102
|
+
if (!sources.length) throw new Error('No history source')
|
|
103
|
+
const ctx: HistoryContext = {
|
|
104
|
+
client,
|
|
105
|
+
address,
|
|
106
|
+
limit,
|
|
107
|
+
cursor: options.cursor,
|
|
108
|
+
before: options.before,
|
|
109
|
+
after: options.after,
|
|
110
|
+
types: options.types?.length ? options.types : undefined,
|
|
111
|
+
fetch: options.fetch ?? ((...args) => fetch(...args)),
|
|
112
|
+
signal: options.signal,
|
|
113
|
+
}
|
|
114
|
+
const errors: string[] = []
|
|
115
|
+
for (const source of sources) {
|
|
116
|
+
try {
|
|
117
|
+
const page = await (source as HistorySource).getHistory(ctx)
|
|
118
|
+
if (options.symbols !== false) await fillSymbols(client, page.items, options.signal)
|
|
119
|
+
return page
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (options.signal?.aborted) throw err
|
|
122
|
+
errors.push(`${(source as HistorySource).name}: ${err instanceof Error ? err.message : String(err)}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`All history sources failed (${errors.join('; ')})`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 补全还是缩写 mint 的 symbol(一次 tokens 查询,label 里的同步替换);读不到的用缩写的 mint */
|
|
129
|
+
async function fillSymbols(client: SolanaClient, items: Activity[], signal: AbortSignal | undefined): Promise<void> {
|
|
130
|
+
const pending = (c: ActivityChange) => c.symbol === short(c.asset)
|
|
131
|
+
const missing = [...new Set(items.flatMap((a) => a.changes.filter(pending).map((c) => c.asset)))]
|
|
132
|
+
if (!missing.length) return
|
|
133
|
+
const details = await client.tokens(missing, { fields: ['symbol'], signal }).catch(() => null)
|
|
134
|
+
const symbols = new Map(missing.map((m, i) => [m, details?.[i]?.symbol ?? short(m)]))
|
|
135
|
+
for (const a of items) {
|
|
136
|
+
for (const c of a.changes) {
|
|
137
|
+
if (!pending(c)) continue
|
|
138
|
+
const symbol = symbols.get(c.asset) as string
|
|
139
|
+
if (a.label) a.label = a.label.split(c.symbol).join(symbol)
|
|
140
|
+
c.symbol = symbol
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// RPC 数据源
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export interface RpcHistoryOptions {
|
|
150
|
+
/** 同时取多少笔交易的详情,默认 6(公共节点限频很严,节点自己会对 429 退避重试) */
|
|
151
|
+
concurrency?: number
|
|
152
|
+
/** 一次翻页最多检查多少笔签名(含被 types / 时间过滤掉的),默认 limit × 5,最多 500 */
|
|
153
|
+
maxScan?: number
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface RawTransaction {
|
|
157
|
+
slot: number
|
|
158
|
+
blockTime: number | null
|
|
159
|
+
meta: {
|
|
160
|
+
err: unknown
|
|
161
|
+
fee: number
|
|
162
|
+
preBalances: number[]
|
|
163
|
+
postBalances: number[]
|
|
164
|
+
preTokenBalances?: RawTokenBalance[]
|
|
165
|
+
postTokenBalances?: RawTokenBalance[]
|
|
166
|
+
loadedAddresses?: { writable: string[]; readonly: string[] }
|
|
167
|
+
} | null
|
|
168
|
+
transaction: {
|
|
169
|
+
signatures: string[]
|
|
170
|
+
message: { accountKeys: string[]; instructions: Array<{ programIdIndex: number; accounts: number[]; data: string }> }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface RawTokenBalance {
|
|
175
|
+
accountIndex: number
|
|
176
|
+
mint: string
|
|
177
|
+
owner?: string
|
|
178
|
+
uiTokenAmount: { amount: string; decimals: number }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface SignatureInfo {
|
|
182
|
+
signature: string
|
|
183
|
+
slot: number
|
|
184
|
+
err: unknown
|
|
185
|
+
blockTime: number | null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function pool<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
|
|
189
|
+
const results = new Array<R>(items.length)
|
|
190
|
+
let next = 0
|
|
191
|
+
const worker = async () => {
|
|
192
|
+
while (next < items.length) {
|
|
193
|
+
const i = next++
|
|
194
|
+
results[i] = await fn(items[i] as T)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker))
|
|
198
|
+
return results
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 默认数据源:getSignaturesForAddress(用 before 翻页)+ getTransaction(逐笔,json 编码、支持 v0)。
|
|
203
|
+
* 先按区块时间过滤签名再取详情,只取需要的笔数;失败的交易也列出(status 为 failed)。每页的请求数 ≈ limit + 1
|
|
204
|
+
*/
|
|
205
|
+
export function rpcHistory(options: RpcHistoryOptions = {}): HistorySource {
|
|
206
|
+
const concurrency = options.concurrency ?? 6
|
|
207
|
+
return {
|
|
208
|
+
name: 'rpc',
|
|
209
|
+
async getHistory(ctx) {
|
|
210
|
+
const { client, address, limit, signal } = ctx
|
|
211
|
+
const maxScan = Math.min(options.maxScan ?? limit * 5, 500)
|
|
212
|
+
const items: Activity[] = []
|
|
213
|
+
let cursor = ctx.cursor
|
|
214
|
+
let scanned = 0
|
|
215
|
+
let more = true
|
|
216
|
+
while (items.length < limit && scanned < maxScan && more) {
|
|
217
|
+
const pageSize = Math.min(1000, Math.max(limit, 20) * (ctx.types ? 3 : 1))
|
|
218
|
+
const commitment = client.commitment === 'processed' ? 'confirmed' : client.commitment
|
|
219
|
+
const sigs = await client.request<SignatureInfo[]>('getSignaturesForAddress', [address, { limit: pageSize, ...(cursor ? { before: cursor } : {}), commitment }], { signal })
|
|
220
|
+
if (!sigs.length) {
|
|
221
|
+
more = false
|
|
222
|
+
break
|
|
223
|
+
}
|
|
224
|
+
// 按时间过滤:比 before 新的跳过;比 after 旧的之后都不用看了(签名按从新到旧排列)
|
|
225
|
+
let rows = sigs.filter((s) => ctx.before === undefined || s.blockTime === null || s.blockTime < ctx.before)
|
|
226
|
+
const old = ctx.after === undefined ? -1 : rows.findIndex((s) => s.blockTime !== null && s.blockTime < (ctx.after as number))
|
|
227
|
+
if (old >= 0) rows = rows.slice(0, old)
|
|
228
|
+
let i = 0
|
|
229
|
+
while (i < rows.length && items.length < limit && scanned < maxScan) {
|
|
230
|
+
const chunk = rows.slice(i, i + Math.max(1, Math.min(limit - items.length, maxScan - scanned)))
|
|
231
|
+
const txs = await pool(chunk, concurrency, (row) => client.request<RawTransaction | null>('getTransaction', [row.signature, { encoding: 'json', maxSupportedTransactionVersion: MAX_TX_VERSION, commitment }], { signal }))
|
|
232
|
+
for (const [j, tx] of txs.entries()) {
|
|
233
|
+
i++
|
|
234
|
+
scanned++
|
|
235
|
+
cursor = (chunk[j] as SignatureInfo).signature
|
|
236
|
+
const activity = tx ? activityFromTransaction(address, tx) : null
|
|
237
|
+
if (activity && (!ctx.types || ctx.types.includes(activity.type))) items.push(activity)
|
|
238
|
+
if (items.length >= limit) break
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (i >= rows.length) {
|
|
242
|
+
// 这一页看完了:接着从这一页最后一条往后翻;没有更多(不足一页、或已经到 after)时结束
|
|
243
|
+
cursor = (sigs[sigs.length - 1] as SignatureInfo).signature
|
|
244
|
+
more = sigs.length >= pageSize && old < 0
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { items, ...(more && cursor ? { nextCursor: cursor } : {}) }
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const SOL: Pick<AssetDelta, 'asset' | 'symbol' | 'decimals'> = { asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS }
|
|
253
|
+
|
|
254
|
+
/** 合并资产变化:同一资产相加,去掉 0 */
|
|
255
|
+
function toChanges(native: bigint, tokens: Map<string, { delta: bigint; decimals: number }>): ActivityChange[] {
|
|
256
|
+
const out: ActivityChange[] = []
|
|
257
|
+
if (native !== 0n) out.push({ ...SOL, delta: native.toString(), formatted: formatUnits(native, SOL_DECIMALS) })
|
|
258
|
+
for (const [asset, { delta, decimals }] of tokens) {
|
|
259
|
+
if (delta !== 0n) out.push({ asset, symbol: short(asset), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
|
|
260
|
+
}
|
|
261
|
+
return out
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function directionOf(changes: readonly ActivityChange[], counterparty: string | undefined, address: string): Activity['direction'] {
|
|
265
|
+
if (!changes.length) return counterparty === address ? 'self' : undefined
|
|
266
|
+
const negative = changes.filter((c) => c.delta.startsWith('-')).length
|
|
267
|
+
if (negative === 0) return 'in'
|
|
268
|
+
return negative === changes.length ? 'out' : undefined
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const toActivityType = (kind: Classified | null): ActivityType => (kind ? (kind.type === 'unknown' ? 'contract' : kind.type) : 'other')
|
|
272
|
+
|
|
273
|
+
/** 一笔 getTransaction(json 编码)整理成 Activity,address 的角度;导出供测试和自己取到交易的场景使用 */
|
|
274
|
+
export function activityFromTransaction(address: string, tx: RawTransaction): Activity | null {
|
|
275
|
+
const meta = tx.meta
|
|
276
|
+
const hash = tx.transaction.signatures[0]
|
|
277
|
+
if (!meta || !hash) return null
|
|
278
|
+
const message = tx.transaction.message
|
|
279
|
+
const keys = [...message.accountKeys, ...(meta.loadedAddresses?.writable ?? []), ...(meta.loadedAddresses?.readonly ?? [])]
|
|
280
|
+
const failed = meta.err !== null && meta.err !== undefined
|
|
281
|
+
const ixs: DecodedInstruction[] = message.instructions.map((ix, i) => {
|
|
282
|
+
const programId = keys[ix.programIdIndex] as string
|
|
283
|
+
return decodeInstruction(i, programId, ix.accounts.map((a) => keys[a] as string), base58.decode(ix.data))
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const index = keys.indexOf(address)
|
|
287
|
+
const isPayer = index === 0
|
|
288
|
+
const fee = BigInt(meta.fee)
|
|
289
|
+
let native = index >= 0 ? BigInt(meta.postBalances[index] ?? 0) - BigInt(meta.preBalances[index] ?? 0) + (isPayer ? fee : 0n) : 0n
|
|
290
|
+
const tokens = new Map<string, { delta: bigint; decimals: number }>()
|
|
291
|
+
const ownerOf = new Map<number, string>()
|
|
292
|
+
for (const [list, sign] of [[meta.preTokenBalances ?? [], -1n], [meta.postTokenBalances ?? [], 1n]] as const) {
|
|
293
|
+
for (const b of list) {
|
|
294
|
+
if (b.owner) ownerOf.set(b.accountIndex, b.owner)
|
|
295
|
+
if (b.owner !== address) continue
|
|
296
|
+
const prev = tokens.get(b.mint)
|
|
297
|
+
tokens.set(b.mint, { delta: (prev?.delta ?? 0n) + sign * BigInt(b.uiTokenAmount.amount), decimals: b.uiTokenAmount.decimals })
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Wrapped SOL 并入 SOL
|
|
301
|
+
const wsol = tokens.get(NATIVE_MINT)
|
|
302
|
+
if (wsol) {
|
|
303
|
+
native += wsol.delta
|
|
304
|
+
tokens.delete(NATIVE_MINT)
|
|
305
|
+
}
|
|
306
|
+
const changes = failed ? [] : toChanges(native, tokens)
|
|
307
|
+
|
|
308
|
+
// 对手方:第一笔转账的另一头(代币转账取代币账户的 owner)
|
|
309
|
+
let counterparty: string | undefined
|
|
310
|
+
for (const ix of ixs) {
|
|
311
|
+
if (ix.type !== 'transfer') continue
|
|
312
|
+
const args = ix.args as Record<string, string> | undefined
|
|
313
|
+
const owner = (account: string | undefined) => (account ? (ownerOf.get(keys.indexOf(account)) ?? (ix.programId === SYSTEM_PROGRAM_ID ? account : undefined)) : undefined)
|
|
314
|
+
const from = owner(args?.from ?? args?.source)
|
|
315
|
+
const to = owner(args?.to ?? args?.destination)
|
|
316
|
+
counterparty = from === address ? to : from
|
|
317
|
+
if (counterparty) break
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const kind = classify(ixs, changes)
|
|
321
|
+
const type = toActivityType(kind)
|
|
322
|
+
const label = describe(kind, ixs, changes, counterparty)
|
|
323
|
+
return {
|
|
324
|
+
hash,
|
|
325
|
+
time: tx.blockTime ?? 0,
|
|
326
|
+
block: tx.slot,
|
|
327
|
+
status: failed ? 'failed' : 'success',
|
|
328
|
+
type,
|
|
329
|
+
direction: type === 'transfer' || type === 'other' ? directionOf(changes, counterparty, address) : undefined,
|
|
330
|
+
counterparty,
|
|
331
|
+
label: failed ? `Failed: ${label}` : label,
|
|
332
|
+
changes,
|
|
333
|
+
...(isPayer ? { fee: fee.toString() } : {}),
|
|
334
|
+
protocol: kind?.protocol,
|
|
335
|
+
}
|
|
336
|
+
}
|