@w3lib/solana-batch-call 0.5.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 +14 -0
- package/README.md +113 -1
- 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/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/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 +47 -0
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +91 -15
- 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/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/rpc.d.ts.map +1 -1
- package/dist/rpc.js +4 -2
- 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/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/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/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/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/write/instructions.d.ts +2 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +16 -0
- package/dist/write/instructions.js.map +1 -1
- package/package.json +21 -1
- 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/history.ts +336 -0
- package/src/historyHelius.ts +136 -0
- package/src/liquidity/orca.ts +15 -5
- package/src/liquidity/raydiumClmm.ts +13 -5
- package/src/lp.ts +140 -15
- package/src/lpMath.ts +38 -0
- package/src/programs.ts +216 -0
- package/src/reclaim.ts +547 -0
- package/src/rpc.ts +4 -2
- package/src/simulate.ts +535 -0
- package/src/subpaths/claim.ts +53 -0
- package/src/subpaths/history.ts +38 -0
- package/src/subpaths/lp.ts +5 -1
- package/src/subpaths/reclaim.ts +57 -0
- package/src/subpaths/simulate.ts +37 -0
- package/src/write/instructions.ts +17 -0
package/src/claim.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量领取(子路径 `@w3lib/solana-batch-call/claim`):Orca Whirlpool、Raydium CLMM 仓位的未领取手续费和挖矿奖励(Orca 的奖励 slot、Raydium 的农场奖励)。
|
|
3
|
+
* 先只读预览(每个仓位能领多少、美元价值),再把全部(或选中的)仓位的领取指令打包成交易,发送前用 /simulate 模拟。
|
|
4
|
+
* 领取的指令就是 /liquidity 的 collectFees(手续费 + 全部奖励,SOL 自动解包、缺少的代币账户自动创建),这里只做批量和打包
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestOptions, SolanaClient } from './client.js'
|
|
7
|
+
import { isAddress } from './address.js'
|
|
8
|
+
import { buildCollectFees } from './liquidity/index.js'
|
|
9
|
+
import type { SolanaLiquidityPlan } from './liquidity/types.js'
|
|
10
|
+
import { lpPositions, type SolanaLpClmmPosition, type SolanaLpProtocol, type SolanaLpToken } from './lp.js'
|
|
11
|
+
import { defillamaPrices, type SolanaPriceSource } from './price.js'
|
|
12
|
+
import { simulatePlan, sendCalls, type PlanSimulation, type PlanSimulationOptions, type PlannedCall } from './batchPlan.js'
|
|
13
|
+
import { formatUnits } from './layout.js'
|
|
14
|
+
import type { SimulatedBalanceChange } from './simulate.js'
|
|
15
|
+
import type { BatchCallOptions } from './write/batch.js'
|
|
16
|
+
import { mapPool, type WriteResult } from './write/send.js'
|
|
17
|
+
import type { SignerInput } from './write/signer.js'
|
|
18
|
+
|
|
19
|
+
export type ClaimProtocol = Extract<SolanaLpProtocol, 'orca-whirlpool' | 'raydium-clmm'>
|
|
20
|
+
|
|
21
|
+
/** 一项能领取的东西:手续费或挖矿奖励 */
|
|
22
|
+
export interface ClaimableAmount {
|
|
23
|
+
mint: string
|
|
24
|
+
symbol: string | null
|
|
25
|
+
decimals: number | null
|
|
26
|
+
/** 数量(最小单位);读不到两端的 tick 数据时为 null */
|
|
27
|
+
amount: string | null
|
|
28
|
+
formatted: string | null
|
|
29
|
+
/** 美元价值(查到价格时;没有价格为 null;数量为 0 时为 0) */
|
|
30
|
+
usd: number | null
|
|
31
|
+
/** 只在按代币汇总(`Claimable.tokens`)里:true 表示有仓位的数量读不到,`amount` 只是已读到部分的合计(偏低),`usd` 为 null */
|
|
32
|
+
incomplete?: true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ClaimablePosition {
|
|
36
|
+
protocol: ClaimProtocol
|
|
37
|
+
protocolName: string
|
|
38
|
+
/** 仓位 NFT 的 mint 和仓位账户 */
|
|
39
|
+
positionMint: string
|
|
40
|
+
position: string
|
|
41
|
+
pool: string
|
|
42
|
+
token0: SolanaLpToken
|
|
43
|
+
token1: SolanaLpToken
|
|
44
|
+
liquidity: string
|
|
45
|
+
inRange: boolean
|
|
46
|
+
/** token0 / token1 的未领取手续费 */
|
|
47
|
+
fees: [ClaimableAmount, ClaimableAmount]
|
|
48
|
+
/** 池子里已初始化的挖矿奖励(Orca 的奖励 slot / Raydium 的 reward_infos),数量按程序的方式结算到当前时间 */
|
|
49
|
+
rewards: Array<ClaimableAmount & { index: number }>
|
|
50
|
+
feesUsd: number | null
|
|
51
|
+
rewardsUsd: number | null
|
|
52
|
+
/** 手续费 + 奖励的美元价值:有数量的项缺价格或读不到时为 null */
|
|
53
|
+
totalUsd: number | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Claimable {
|
|
57
|
+
owner: string
|
|
58
|
+
/** 有手续费或奖励可领的仓位(没有流动性也没有可领的不在内),按美元价值从大到小 */
|
|
59
|
+
positions: ClaimablePosition[]
|
|
60
|
+
/** 按代币汇总:手续费 + 奖励,所有仓位合计 */
|
|
61
|
+
tokens: ClaimableAmount[]
|
|
62
|
+
/** true:有仓位的数量读不到(tick 数据读不到),`tokens` 的数量和各项合计都可能偏低 */
|
|
63
|
+
incomplete: boolean
|
|
64
|
+
feesUsd: number | null
|
|
65
|
+
rewardsUsd: number | null
|
|
66
|
+
totalUsd: number | null
|
|
67
|
+
warnings: string[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ClaimQuery extends RequestOptions {
|
|
71
|
+
/** 只看这些仓位 NFT 的 mint(读 ATA,免费公共节点也能用);不传时列出全部代币账户(getTokenAccountsByOwner,需要支持它的节点) */
|
|
72
|
+
mints?: readonly string[]
|
|
73
|
+
/** 协议,默认两个都查 */
|
|
74
|
+
protocols?: readonly ClaimProtocol[]
|
|
75
|
+
/** 价格:true(默认)用 DefiLlama;false 不查;也可以传自己的价格源(如 combinePrices([defillamaPrices(), onchainPrices()])) */
|
|
76
|
+
prices?: boolean | SolanaPriceSource
|
|
77
|
+
/** 奖励结算到哪个时刻(unix 秒),默认当前时间 */
|
|
78
|
+
now?: number
|
|
79
|
+
fetch?: typeof fetch
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const MAX_ERROR = 200
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 预览能领取的手续费和挖矿奖励(只读取,不构建交易):每个仓位的两种手续费、每个奖励的数量和美元价值,以及按代币汇总。
|
|
86
|
+
* 奖励照搬程序的结算:池子的奖励增长按时间摊给当前流动性,再按仓位区间内的增长计算
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* const c = await claimable(client, owner)
|
|
90
|
+
* c.totalUsd // 全部能领的美元价值
|
|
91
|
+
* c.positions[0].rewards // [{ index: 0, symbol: 'RAY', formatted: '0.042698', usd: 0.083 }]
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export async function claimable(client: SolanaClient, owner: string, query: ClaimQuery = {}): Promise<Claimable> {
|
|
95
|
+
if (!isAddress(owner)) {
|
|
96
|
+
throw new Error(`Invalid owner address: ${owner}`)
|
|
97
|
+
}
|
|
98
|
+
const { signal } = query
|
|
99
|
+
const protocols: ClaimProtocol[] = query.protocols ? [...query.protocols] : ['orca-whirlpool', 'raydium-clmm']
|
|
100
|
+
const list = (
|
|
101
|
+
await lpPositions(client, owner, { protocols, mints: query.mints, rewards: true, now: query.now, signal })
|
|
102
|
+
).filter((p): p is SolanaLpClmmPosition => p.type === 'clmm')
|
|
103
|
+
|
|
104
|
+
const warnings: string[] = []
|
|
105
|
+
const mints = [...new Set(list.flatMap((p) => [p.token0.mint, p.token1.mint, ...(p.rewards ?? []).map((r) => r.mint)]))]
|
|
106
|
+
const source = query.prices === false ? null : query.prices && query.prices !== true ? query.prices : defillamaPrices()
|
|
107
|
+
let priceMap = new Map<string, number>()
|
|
108
|
+
if (source && mints.length) {
|
|
109
|
+
try {
|
|
110
|
+
priceMap = await source.prices({ mints, fetch: query.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args)), signal, client })
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (signal?.aborted) throw err
|
|
113
|
+
warnings.push(`Price lookup failed (${(err instanceof Error ? err.message : String(err)).slice(0, MAX_ERROR)}): usd values are null`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const amountOf = (token: { mint: string; symbol: string | null; decimals: number | null }, amount: bigint | null): ClaimableAmount => {
|
|
117
|
+
const formatted = amount === null || token.decimals === null ? null : formatUnits(amount, token.decimals)
|
|
118
|
+
const price = priceMap.get(token.mint)
|
|
119
|
+
// 数量为 0 的不需要价格
|
|
120
|
+
const usd = amount === 0n ? 0 : formatted === null || price === undefined || !source ? null : Number(formatted) * price
|
|
121
|
+
return { mint: token.mint, symbol: token.symbol, decimals: token.decimals, amount: amount === null ? null : amount.toString(), formatted, usd }
|
|
122
|
+
}
|
|
123
|
+
const sum = (items: readonly ClaimableAmount[]) => (items.some((i) => i.usd === null) ? null : items.reduce((acc, i) => acc + (i.usd as number), 0))
|
|
124
|
+
|
|
125
|
+
const positions: ClaimablePosition[] = []
|
|
126
|
+
for (const p of list) {
|
|
127
|
+
const fees: [ClaimableAmount, ClaimableAmount] = [amountOf(p.token0, p.fees0 === null ? null : BigInt(p.fees0)), amountOf(p.token1, p.fees1 === null ? null : BigInt(p.fees1))]
|
|
128
|
+
const rewards = (p.rewards ?? []).map((r) => ({ ...amountOf(r, r.amount === null ? null : BigInt(r.amount)), index: r.index }))
|
|
129
|
+
const unknown = [...fees, ...rewards].some((x) => x.amount === null)
|
|
130
|
+
if (!unknown && [...fees, ...rewards].every((x) => x.amount === '0')) continue
|
|
131
|
+
positions.push({
|
|
132
|
+
protocol: p.protocol as ClaimProtocol,
|
|
133
|
+
protocolName: p.protocolName,
|
|
134
|
+
positionMint: p.positionMint,
|
|
135
|
+
position: p.position,
|
|
136
|
+
pool: p.pool,
|
|
137
|
+
token0: p.token0,
|
|
138
|
+
token1: p.token1,
|
|
139
|
+
liquidity: p.liquidity,
|
|
140
|
+
inRange: p.inRange,
|
|
141
|
+
fees,
|
|
142
|
+
rewards,
|
|
143
|
+
feesUsd: sum(fees),
|
|
144
|
+
rewardsUsd: sum(rewards),
|
|
145
|
+
totalUsd: sum([...fees, ...rewards]),
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
positions.sort((a, b) => (b.totalUsd ?? -1) - (a.totalUsd ?? -1))
|
|
149
|
+
if (positions.some((p) => [...p.fees, ...p.rewards].some((x) => x.amount === null))) {
|
|
150
|
+
warnings.push("Some positions' tick arrays could not be read: their amounts are null (they are still included)")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 按代币汇总
|
|
154
|
+
const byMint = new Map<string, { token: { mint: string; symbol: string | null; decimals: number | null }; amount: bigint; unknown: boolean }>()
|
|
155
|
+
for (const item of positions.flatMap((p) => [...p.fees, ...p.rewards])) {
|
|
156
|
+
const entry = byMint.get(item.mint) ?? { token: item, amount: 0n, unknown: false }
|
|
157
|
+
entry.amount += BigInt(item.amount ?? 0)
|
|
158
|
+
entry.unknown ||= item.amount === null
|
|
159
|
+
byMint.set(item.mint, entry)
|
|
160
|
+
}
|
|
161
|
+
// 有读不到的数量时保留该代币(即使已读到的部分为 0),标记 incomplete,美元价值为 null
|
|
162
|
+
const tokens = [...byMint.values()]
|
|
163
|
+
.filter((e) => e.unknown || e.amount !== 0n)
|
|
164
|
+
.map((e): ClaimableAmount => (e.unknown ? { ...amountOf(e.token, e.amount), usd: null, incomplete: true } : amountOf(e.token, e.amount)))
|
|
165
|
+
const sumKnown = (items: ReadonlyArray<number | null>) => (items.some((x) => x === null) ? null : items.reduce<number>((acc, x) => acc + (x as number), 0))
|
|
166
|
+
return {
|
|
167
|
+
owner,
|
|
168
|
+
positions,
|
|
169
|
+
tokens,
|
|
170
|
+
incomplete: tokens.some((t) => t.incomplete),
|
|
171
|
+
feesUsd: sumKnown(positions.map((p) => p.feesUsd)),
|
|
172
|
+
rewardsUsd: sumKnown(positions.map((p) => p.rewardsUsd)),
|
|
173
|
+
totalUsd: sumKnown(positions.map((p) => p.totalUsd)),
|
|
174
|
+
warnings,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// 构建、模拟、发送
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/** 要领取的仓位:claimable 的结果,或只给协议 + 仓位 NFT 的 mint(getLpPositions 的结果也可以) */
|
|
183
|
+
export type ClaimTarget = Pick<ClaimablePosition, 'protocol' | 'positionMint'> & { pool?: string }
|
|
184
|
+
|
|
185
|
+
export interface BuildClaimOptions extends RequestOptions {
|
|
186
|
+
/** wSOL(池子的一边或奖励)原来没有账户时用完关闭、换回 SOL,默认 true(同 /liquidity) */
|
|
187
|
+
native?: boolean
|
|
188
|
+
/** 同时构建几个仓位(每个要读几次账户),默认 4 */
|
|
189
|
+
concurrency?: number
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface ClaimCall extends PlannedCall {
|
|
193
|
+
protocol: ClaimProtocol
|
|
194
|
+
positionMint: string
|
|
195
|
+
/** /liquidity 的领取计划(预计的手续费、奖励的代币、步骤) */
|
|
196
|
+
plan: SolanaLiquidityPlan
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface ClaimPlan {
|
|
200
|
+
owner: string
|
|
201
|
+
/** 每个仓位一个调用(创建缺少的代币账户 + 领取手续费 + 领取每个奖励,原子执行) */
|
|
202
|
+
calls: ClaimCall[]
|
|
203
|
+
/** 构建失败的仓位:仓位不在 owner 名下、不支持的代币(带转账手续费的 Token-2022 池子)等 */
|
|
204
|
+
failed: Array<{ position: ClaimTarget; error: string }>
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 构建批量领取(只读取、不签名):每个仓位一个调用,指令来自 /liquidity 的 buildCollectFees,构建时重新读取仓位。
|
|
209
|
+
* 打包成交易由 simulateClaim / claimAll(batchCall)完成
|
|
210
|
+
*
|
|
211
|
+
* ```ts
|
|
212
|
+
* const c = await claimable(client, owner)
|
|
213
|
+
* const plan = await buildClaimAll(client, owner, c.positions)
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export async function buildClaimAll(client: SolanaClient, owner: string, positions: readonly ClaimTarget[], options: BuildClaimOptions = {}): Promise<ClaimPlan> {
|
|
217
|
+
if (!isAddress(owner)) {
|
|
218
|
+
throw new Error(`Invalid owner address: ${owner}`)
|
|
219
|
+
}
|
|
220
|
+
const unique = [...new Map(positions.map((p) => [p.positionMint, p])).values()]
|
|
221
|
+
const calls: ClaimCall[] = []
|
|
222
|
+
const failed: ClaimPlan['failed'] = []
|
|
223
|
+
const built = await mapPool(unique, options.concurrency ?? 4, async (position) => {
|
|
224
|
+
try {
|
|
225
|
+
const plan = await buildCollectFees(client, owner, { position: { protocol: position.protocol, positionMint: position.positionMint, pool: position.pool } }, { native: options.native, signal: options.signal })
|
|
226
|
+
return { position, plan, error: null }
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (options.signal?.aborted) throw err
|
|
229
|
+
return { position, plan: null, error: (err instanceof Error ? err.message : String(err)).slice(0, MAX_ERROR) }
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
for (const item of built) {
|
|
233
|
+
if (item.plan === null) {
|
|
234
|
+
failed.push({ position: item.position, error: item.error })
|
|
235
|
+
} else {
|
|
236
|
+
calls.push({ owner, protocol: item.position.protocol as ClaimProtocol, positionMint: item.position.positionMint, plan: item.plan, instructions: item.plan.steps.flatMap((s) => s.instructions) })
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { owner, calls, failed }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface ClaimSimulation extends PlanSimulation {
|
|
243
|
+
/** 模拟里收到的代币合计(手续费支付者 / owner 的余额变化,按资产汇总;SOL 为 'native') */
|
|
244
|
+
received: Array<{ asset: string; symbol: string; decimals: number; delta: string; formatted: string; usd?: number | null }>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 用 /simulate 模拟构建好的批量领取(不签名、不发送):逐笔交易的结果、哪些仓位会失败、收到的代币合计 */
|
|
248
|
+
export async function simulateClaim(client: SolanaClient, plan: ClaimPlan, options: Omit<PlanSimulationOptions, 'watch'> = {}): Promise<ClaimSimulation> {
|
|
249
|
+
const sim = await simulatePlan(client, plan.calls, options)
|
|
250
|
+
const totals = new Map<string, { change: SimulatedBalanceChange; delta: bigint; usd: number | null | undefined }>()
|
|
251
|
+
const payer = options.feePayer ?? plan.owner
|
|
252
|
+
for (const { simulation } of sim.transactions) {
|
|
253
|
+
for (const change of simulation.balanceChanges) {
|
|
254
|
+
if (change.address !== payer && change.address !== plan.owner) continue
|
|
255
|
+
const entry = totals.get(change.asset) ?? { change, delta: 0n, usd: undefined }
|
|
256
|
+
entry.delta += BigInt(change.delta)
|
|
257
|
+
// 美元价值是所有变化的合计(任何一笔没有价格就是 null);都没有 usd 字段时不给
|
|
258
|
+
if (change.usd !== undefined) entry.usd = entry.usd === null || change.usd === null ? null : (entry.usd ?? 0) + change.usd
|
|
259
|
+
totals.set(change.asset, entry)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const received = [...totals.values()]
|
|
263
|
+
.filter((e) => e.delta !== 0n)
|
|
264
|
+
.map((e) => ({
|
|
265
|
+
asset: e.change.asset,
|
|
266
|
+
symbol: e.change.symbol,
|
|
267
|
+
decimals: e.change.decimals,
|
|
268
|
+
delta: e.delta.toString(),
|
|
269
|
+
formatted: `${e.delta < 0n ? '-' : ''}${formatUnits(e.delta < 0n ? -e.delta : e.delta, e.change.decimals)}`,
|
|
270
|
+
...(e.usd !== undefined ? { usd: e.usd } : {}),
|
|
271
|
+
}))
|
|
272
|
+
return { ...sim, received }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface ClaimOptions extends BatchCallOptions, BuildClaimOptions {
|
|
276
|
+
/** 发送前先用 /simulate 模拟,模拟失败的仓位不发送,默认 true */
|
|
277
|
+
simulate?: boolean
|
|
278
|
+
/** 只领取预计价值不低于它的仓位(美元);没有价格的仓位照常领取。默认 0 */
|
|
279
|
+
minUsd?: number
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface ClaimCallResult extends WriteResult {
|
|
283
|
+
protocol: ClaimProtocol
|
|
284
|
+
positionMint: string
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface ClaimResult {
|
|
288
|
+
plan: ClaimPlan
|
|
289
|
+
/** 发送前的模拟(simulate: false 时为 null) */
|
|
290
|
+
simulation: ClaimSimulation | null
|
|
291
|
+
/** 与 plan.calls 一一对应;dryRun 时 status 为 'simulated' / 'failed',没有签名 */
|
|
292
|
+
results: ClaimCallResult[]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 批量领取:构建 → 模拟 → 用 from 签名发送(钱包同 /write 的 batchCall,多个仓位打包进同一笔交易,feePayer 可以替 owner 付手续费)。
|
|
297
|
+
* positions 不传时先预览、领取全部有东西可领的仓位;一个仓位失败不影响其他的。dryRun: true 只构建和模拟(不签名,from 只用来取地址)
|
|
298
|
+
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* await claimAll(client, key) // 全部
|
|
301
|
+
* await claimAll(client, key, c.positions.slice(0, 5), { dryRun: true })
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
export async function claimAll(client: SolanaClient, from: SignerInput, positions?: readonly (ClaimTarget & Partial<Pick<ClaimablePosition, 'totalUsd'>>)[], options: ClaimOptions = {}): Promise<ClaimResult> {
|
|
305
|
+
const { resolveSigner } = await import('./write/signer.js')
|
|
306
|
+
const owner = (await resolveSigner(from)).address
|
|
307
|
+
const minUsd = options.minUsd ?? 0
|
|
308
|
+
const targets = (positions ?? (await claimable(client, owner, { signal: options.signal })).positions).filter((p) => p.totalUsd === undefined || p.totalUsd === null || p.totalUsd >= minUsd)
|
|
309
|
+
const plan = await buildClaimAll(client, owner, targets, options)
|
|
310
|
+
const feePayer = options.feePayer === undefined ? undefined : (await resolveSigner(options.feePayer)).address
|
|
311
|
+
const simulation = options.simulate === false && !options.dryRun ? null : await simulateClaim(client, plan, { feePayer, signal: options.signal, batchSize: (options.mode ?? 'pack') === 'tx' ? 1 : options.batchSize })
|
|
312
|
+
const failed = new Map(simulation?.failed.map((f) => [f.index, f.error]))
|
|
313
|
+
const sent = options.dryRun ? [] : await sendCalls(client, from, plan.calls.filter((_, i) => !failed.has(i)), options)
|
|
314
|
+
let next = 0
|
|
315
|
+
const results = plan.calls.map((call, index): ClaimCallResult => {
|
|
316
|
+
const base = { protocol: call.protocol, positionMint: call.positionMint }
|
|
317
|
+
const error = failed.get(index)
|
|
318
|
+
if (error !== undefined) return { index, from: owner, status: 'failed', signature: null, slot: null, fee: null, computeUnits: null, error, ...base }
|
|
319
|
+
if (options.dryRun) return { index, from: owner, status: 'simulated', signature: null, slot: null, fee: null, computeUnits: null, error: null, ...base }
|
|
320
|
+
return { ...(sent[next++] as WriteResult), index, ...base }
|
|
321
|
+
})
|
|
322
|
+
return { plan, simulation, results }
|
|
323
|
+
}
|
package/src/decode.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 指令解码(/simulate 和 /history 共用):System、SPL Token / Token-2022、关联代币账户、计算预算、Memo、升级程序加载器,
|
|
3
|
+
* 以及 programs.ts 里认得的 DEX / 聚合器(协议 + 指令名 + 大类)。其他程序只给程序名(认得时)和大类 contract / unknown
|
|
4
|
+
*/
|
|
5
|
+
import { encodeAddress } from './address.js'
|
|
6
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
|
|
7
|
+
import { BPF_LOADER_UPGRADEABLE, COMPUTE_BUDGET, MEMO_V1, MEMO_V2, PROGRAMS, knownMethod, type IxType } from './programs.js'
|
|
8
|
+
|
|
9
|
+
export interface DecodedInstruction {
|
|
10
|
+
/** 在交易里的下标(顶层指令) */
|
|
11
|
+
index: number
|
|
12
|
+
programId: string
|
|
13
|
+
/** 程序名;不认识时为 null */
|
|
14
|
+
program: string | null
|
|
15
|
+
type: IxType
|
|
16
|
+
protocol?: string
|
|
17
|
+
method?: string
|
|
18
|
+
summary: string
|
|
19
|
+
/** 金额都是最小单位的十进制字符串 */
|
|
20
|
+
args?: Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Token 的 SetAuthority 的权限类型 */
|
|
24
|
+
export const AUTHORITY_TYPES = [
|
|
25
|
+
'MintTokens', 'FreezeAccount', 'AccountOwner', 'CloseAccount', 'TransferFeeConfig', 'WithheldWithdraw', 'CloseMint', 'InterestRate',
|
|
26
|
+
'PermanentDelegate', 'ConfidentialTransferMint', 'TransferHookProgramId', 'ConfidentialTransferFeeConfig', 'MetadataPointer', 'GroupPointer', 'GroupMemberPointer',
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
const short = (a: string | undefined) => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '?')
|
|
30
|
+
|
|
31
|
+
class Data {
|
|
32
|
+
readonly view: DataView
|
|
33
|
+
offset = 0
|
|
34
|
+
constructor(readonly bytes: Uint8Array) {
|
|
35
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
36
|
+
}
|
|
37
|
+
u8() {
|
|
38
|
+
return this.view.getUint8(this.offset++)
|
|
39
|
+
}
|
|
40
|
+
u32() {
|
|
41
|
+
const v = this.view.getUint32(this.offset, true)
|
|
42
|
+
this.offset += 4
|
|
43
|
+
return v
|
|
44
|
+
}
|
|
45
|
+
u64() {
|
|
46
|
+
const v = this.view.getBigUint64(this.offset, true)
|
|
47
|
+
this.offset += 8
|
|
48
|
+
return v
|
|
49
|
+
}
|
|
50
|
+
key() {
|
|
51
|
+
const v = encodeAddress(this.bytes.slice(this.offset, this.offset + 32))
|
|
52
|
+
this.offset += 32
|
|
53
|
+
return v
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 解码一条指令。accounts 是指令用到的账户地址(已按指令里的下标解析,包含地址查找表里的),data 是指令数据。
|
|
59
|
+
* 数据格式不对时不抛错,按“不认识的指令”返回
|
|
60
|
+
*/
|
|
61
|
+
export function decodeInstruction(index: number, programId: string, accounts: readonly string[], data: Uint8Array): DecodedInstruction {
|
|
62
|
+
const info = PROGRAMS[programId]
|
|
63
|
+
const base = { index, programId, program: info?.name ?? null }
|
|
64
|
+
try {
|
|
65
|
+
const decoded = decodeKnown(programId, accounts, data)
|
|
66
|
+
if (decoded) return { ...base, ...decoded }
|
|
67
|
+
const known = knownMethod(programId, data)
|
|
68
|
+
if (known) {
|
|
69
|
+
return { ...base, type: known[1], protocol: info?.protocol, method: known[0], summary: `${info?.protocol ?? info?.name} ${known[0]}` }
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// 数据不符合格式:当作不认识
|
|
73
|
+
}
|
|
74
|
+
if (info) return { ...base, type: 'contract', protocol: info.protocol, summary: `Call ${info.name}` }
|
|
75
|
+
return { ...base, type: 'unknown', summary: `Call unknown program ${short(programId)}` }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type Partial_ = Pick<DecodedInstruction, 'type' | 'summary'> & Partial<Pick<DecodedInstruction, 'method' | 'args' | 'protocol'>>
|
|
79
|
+
|
|
80
|
+
function decodeKnown(programId: string, a: readonly string[], data: Uint8Array): Partial_ | null {
|
|
81
|
+
const d = new Data(data)
|
|
82
|
+
switch (programId) {
|
|
83
|
+
case SYSTEM_PROGRAM_ID:
|
|
84
|
+
return decodeSystem(d, a)
|
|
85
|
+
case TOKEN_PROGRAM_ID:
|
|
86
|
+
case TOKEN_2022_PROGRAM_ID:
|
|
87
|
+
return decodeToken(d, a)
|
|
88
|
+
case ASSOCIATED_TOKEN_PROGRAM_ID: {
|
|
89
|
+
const method = data.length === 0 || data[0] === 0 ? 'create' : data[0] === 1 ? 'createIdempotent' : 'recoverNested'
|
|
90
|
+
return { type: 'setup', method, summary: `Create token account for ${short(a[2])}`, args: { payer: a[0], account: a[1], owner: a[2], mint: a[3] } }
|
|
91
|
+
}
|
|
92
|
+
case COMPUTE_BUDGET: {
|
|
93
|
+
const tag = d.u8()
|
|
94
|
+
if (tag === 2) return { type: 'budget', method: 'setComputeUnitLimit', summary: 'Set compute unit limit', args: { units: d.u32() } }
|
|
95
|
+
if (tag === 3) return { type: 'budget', method: 'setComputeUnitPrice', summary: 'Set priority fee', args: { microLamports: d.u64().toString() } }
|
|
96
|
+
return { type: 'budget', summary: 'Compute budget' }
|
|
97
|
+
}
|
|
98
|
+
case MEMO_V1:
|
|
99
|
+
case MEMO_V2:
|
|
100
|
+
return { type: 'memo', method: 'memo', summary: 'Memo', args: { text: new TextDecoder().decode(data) } }
|
|
101
|
+
case BPF_LOADER_UPGRADEABLE: {
|
|
102
|
+
const tag = d.u32()
|
|
103
|
+
const method = ['initializeBuffer', 'write', 'deploy', 'upgrade', 'setAuthority', 'close', 'extendProgram', 'setAuthorityChecked'][tag]
|
|
104
|
+
return { type: 'contract', method, summary: `Program loader: ${method ?? `instruction ${tag}`}`, args: { program: a[tag === 3 ? 1 : 0] } }
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function decodeSystem(d: Data, a: readonly string[]): Partial_ {
|
|
112
|
+
const tag = d.u32()
|
|
113
|
+
switch (tag) {
|
|
114
|
+
case 0: {
|
|
115
|
+
const lamports = d.u64()
|
|
116
|
+
const space = d.u64()
|
|
117
|
+
return { type: 'setup', method: 'createAccount', summary: `Create account ${short(a[1])}`, args: { from: a[0], account: a[1], lamports: lamports.toString(), space: space.toString(), owner: d.key() } }
|
|
118
|
+
}
|
|
119
|
+
case 1: {
|
|
120
|
+
const owner = d.key()
|
|
121
|
+
return { type: 'approve', method: 'assign', summary: `Assign ${short(a[0])} to program ${short(owner)}`, args: { account: a[0], owner } }
|
|
122
|
+
}
|
|
123
|
+
case 2: {
|
|
124
|
+
const lamports = d.u64()
|
|
125
|
+
return { type: 'transfer', method: 'transfer', summary: `Transfer SOL to ${short(a[1])}`, args: { from: a[0], to: a[1], lamports: lamports.toString() } }
|
|
126
|
+
}
|
|
127
|
+
case 11: {
|
|
128
|
+
const lamports = d.u64()
|
|
129
|
+
return { type: 'transfer', method: 'transferWithSeed', summary: `Transfer SOL to ${short(a[2])}`, args: { from: a[0], to: a[2], lamports: lamports.toString() } }
|
|
130
|
+
}
|
|
131
|
+
case 5: {
|
|
132
|
+
const lamports = d.u64()
|
|
133
|
+
return { type: 'transfer', method: 'withdrawNonce', summary: `Withdraw nonce account to ${short(a[1])}`, args: { from: a[0], to: a[1], lamports: lamports.toString() } }
|
|
134
|
+
}
|
|
135
|
+
case 10:
|
|
136
|
+
return { type: 'approve', method: 'assignWithSeed', summary: `Assign ${short(a[0])} to another program`, args: { account: a[0] } }
|
|
137
|
+
default:
|
|
138
|
+
return { type: 'setup', summary: 'System instruction', method: ['createAccount', 'assign', 'transfer', 'createAccountWithSeed', 'advanceNonce', 'withdrawNonce', 'initializeNonce', 'authorizeNonce', 'allocate', 'allocateWithSeed'][tag] }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function decodeToken(d: Data, a: readonly string[]): Partial_ {
|
|
143
|
+
const tag = d.u8()
|
|
144
|
+
const amountArgs = (extra: Record<string, unknown>, amount: bigint) => ({ ...extra, amount: amount.toString() })
|
|
145
|
+
switch (tag) {
|
|
146
|
+
case 3: {
|
|
147
|
+
const amount = d.u64()
|
|
148
|
+
return { type: 'transfer', method: 'transfer', summary: `Transfer tokens to ${short(a[1])}`, args: amountArgs({ source: a[0], destination: a[1], authority: a[2] }, amount) }
|
|
149
|
+
}
|
|
150
|
+
case 12: {
|
|
151
|
+
const amount = d.u64()
|
|
152
|
+
return { type: 'transfer', method: 'transferChecked', summary: `Transfer tokens to ${short(a[2])}`, args: amountArgs({ source: a[0], mint: a[1], destination: a[2], authority: a[3] }, amount) }
|
|
153
|
+
}
|
|
154
|
+
case 26: {
|
|
155
|
+
const sub = d.u8()
|
|
156
|
+
if (sub === 1) {
|
|
157
|
+
const amount = d.u64()
|
|
158
|
+
return { type: 'transfer', method: 'transferCheckedWithFee', summary: `Transfer tokens to ${short(a[2])}`, args: amountArgs({ source: a[0], mint: a[1], destination: a[2], authority: a[3] }, amount) }
|
|
159
|
+
}
|
|
160
|
+
return { type: 'setup', method: 'transferFeeExtension', summary: 'Token-2022 transfer fee instruction' }
|
|
161
|
+
}
|
|
162
|
+
case 4: {
|
|
163
|
+
const amount = d.u64()
|
|
164
|
+
return { type: 'approve', method: 'approve', summary: `Approve ${short(a[1])} to spend tokens`, args: amountArgs({ account: a[0], delegate: a[1], owner: a[2] }, amount) }
|
|
165
|
+
}
|
|
166
|
+
case 13: {
|
|
167
|
+
const amount = d.u64()
|
|
168
|
+
return { type: 'approve', method: 'approveChecked', summary: `Approve ${short(a[2])} to spend tokens`, args: amountArgs({ account: a[0], mint: a[1], delegate: a[2], owner: a[3] }, amount) }
|
|
169
|
+
}
|
|
170
|
+
case 5:
|
|
171
|
+
return { type: 'approve', method: 'revoke', summary: 'Revoke token delegate', args: { account: a[0], owner: a[1] } }
|
|
172
|
+
case 6: {
|
|
173
|
+
const authorityType = AUTHORITY_TYPES[d.u8()] ?? 'Unknown'
|
|
174
|
+
const newAuthority = d.u8() === 1 ? d.key() : null
|
|
175
|
+
return {
|
|
176
|
+
type: 'approve',
|
|
177
|
+
method: 'setAuthority',
|
|
178
|
+
summary: newAuthority ? `Set ${authorityType} authority to ${short(newAuthority)}` : `Remove ${authorityType} authority`,
|
|
179
|
+
args: { account: a[0], authorityType, newAuthority },
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
case 7:
|
|
183
|
+
case 14: {
|
|
184
|
+
const amount = d.u64()
|
|
185
|
+
return { type: 'mint', method: tag === 7 ? 'mintTo' : 'mintToChecked', summary: `Mint tokens to ${short(a[1])}`, args: amountArgs({ mint: a[0], destination: a[1] }, amount) }
|
|
186
|
+
}
|
|
187
|
+
case 8:
|
|
188
|
+
case 15: {
|
|
189
|
+
const amount = d.u64()
|
|
190
|
+
return { type: 'burn', method: tag === 8 ? 'burn' : 'burnChecked', summary: 'Burn tokens', args: amountArgs({ account: a[0], mint: a[1] }, amount) }
|
|
191
|
+
}
|
|
192
|
+
case 9:
|
|
193
|
+
return { type: 'close', method: 'closeAccount', summary: `Close token account ${short(a[0])}`, args: { account: a[0], destination: a[1], owner: a[2] } }
|
|
194
|
+
case 17:
|
|
195
|
+
return { type: 'setup', method: 'syncNative', summary: 'Sync wrapped SOL', args: { account: a[0] } }
|
|
196
|
+
case 10:
|
|
197
|
+
case 11:
|
|
198
|
+
return { type: 'contract', method: tag === 10 ? 'freezeAccount' : 'thawAccount', summary: tag === 10 ? 'Freeze token account' : 'Thaw token account', args: { account: a[0] } }
|
|
199
|
+
default:
|
|
200
|
+
return { type: 'setup', summary: tag <= 25 ? 'Initialize token account / mint' : 'Token-2022 extension instruction' }
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/describe.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** 交易的类型和一句话说明(/simulate 和 /history 共用):指令的大类 + 钱包的余额变化 */
|
|
2
|
+
import type { DecodedInstruction } from './decode.js'
|
|
3
|
+
import { PROGRAMS } from './programs.js'
|
|
4
|
+
|
|
5
|
+
export interface AssetDelta {
|
|
6
|
+
asset: string
|
|
7
|
+
symbol: string
|
|
8
|
+
decimals: number
|
|
9
|
+
/** 带符号的最小单位十进制字符串 */
|
|
10
|
+
delta: string
|
|
11
|
+
formatted: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TxKind = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'contract' | 'unknown'
|
|
15
|
+
|
|
16
|
+
export interface Classified {
|
|
17
|
+
type: TxKind
|
|
18
|
+
protocol?: string
|
|
19
|
+
method?: string
|
|
20
|
+
/** 没有认得的指令、按余额变化(一个资产减少、另一个增加)推断为 swap */
|
|
21
|
+
inferred?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 判断类型:先看认得的指令(流动性 > 兑换 > 授权 > 铸造),再看是不是只有转账;有不认得的程序时,
|
|
28
|
+
* 钱包一个资产减少、另一个增加就推断为兑换,否则是合约调用(程序认得)或未知。没有任何有意义的指令(只有建账户、关闭等)时为 null
|
|
29
|
+
*/
|
|
30
|
+
export function classify(ixs: readonly DecodedInstruction[], changes: readonly Pick<AssetDelta, 'asset' | 'delta'>[]): Classified | null {
|
|
31
|
+
const pick = (type: DecodedInstruction['type']) => ixs.find((ix) => ix.type === type)
|
|
32
|
+
const result = (ix: DecodedInstruction, type: TxKind): Classified => ({ type, protocol: ix.protocol, method: ix.method })
|
|
33
|
+
for (const type of ['liquidity', 'swap', 'approve', 'mint'] as const) {
|
|
34
|
+
const ix = pick(type)
|
|
35
|
+
if (ix) return result(ix, type)
|
|
36
|
+
}
|
|
37
|
+
const others = ixs.filter((ix) => ix.type === 'contract' || ix.type === 'unknown')
|
|
38
|
+
const out = changes.filter((c) => c.delta.startsWith('-'))
|
|
39
|
+
const received = changes.filter((c) => !c.delta.startsWith('-') && c.delta !== '0')
|
|
40
|
+
if (others.length) {
|
|
41
|
+
const first = others.find((ix) => ix.protocol) ?? others[0]!
|
|
42
|
+
if (out.length && received.some((r) => out.some((o) => o.asset !== r.asset))) return { type: 'swap', protocol: first.protocol, inferred: true }
|
|
43
|
+
return { type: others.some((ix) => PROGRAMS[ix.programId]) ? 'contract' : 'unknown', protocol: first.protocol, method: first.method }
|
|
44
|
+
}
|
|
45
|
+
const transfer = pick('transfer')
|
|
46
|
+
if (transfer) return result(transfer, 'transfer')
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const amount = (c: AssetDelta) => `${c.formatted.replace(/^-/, '')} ${c.symbol}`
|
|
51
|
+
const join = (list: readonly AssetDelta[]) => list.map(amount).join(' + ')
|
|
52
|
+
|
|
53
|
+
/** 一句话说明,如 “Swap 1 SOL for 150.2 USDC on Jupiter” */
|
|
54
|
+
export function describe(kind: Classified | null, ixs: readonly DecodedInstruction[], changes: readonly AssetDelta[], counterparty?: string): string {
|
|
55
|
+
const out = changes.filter((c) => c.delta.startsWith('-'))
|
|
56
|
+
const received = changes.filter((c) => !c.delta.startsWith('-') && c.delta !== '0')
|
|
57
|
+
const via = kind?.protocol ? ` on ${kind.protocol}` : ''
|
|
58
|
+
switch (kind?.type) {
|
|
59
|
+
case 'swap':
|
|
60
|
+
return out.length && received.length ? `Swap ${join(out)} for ${join(received)}${via}` : `Swap${via}`
|
|
61
|
+
case 'transfer':
|
|
62
|
+
if (out.length && !received.length) return `Send ${join(out)}${counterparty ? ` to ${short(counterparty)}` : ''}`
|
|
63
|
+
if (received.length && !out.length) return `Receive ${join(received)}${counterparty ? ` from ${short(counterparty)}` : ''}`
|
|
64
|
+
return ixs.find((ix) => ix.type === 'transfer')?.summary ?? 'Transfer'
|
|
65
|
+
case 'liquidity':
|
|
66
|
+
return `${kind.method ? kind.method.replace(/_/g, ' ') : 'Liquidity'}${via}${out.length ? `: pay ${join(out)}` : ''}${received.length ? `${out.length ? ',' : ':'} receive ${join(received)}` : ''}`
|
|
67
|
+
case 'approve':
|
|
68
|
+
return ixs.find((ix) => ix.type === 'approve')?.summary ?? 'Approve'
|
|
69
|
+
case 'mint':
|
|
70
|
+
return `Mint${received.length ? `: receive ${join(received)}` : ''}${via}`
|
|
71
|
+
case 'contract':
|
|
72
|
+
case 'unknown':
|
|
73
|
+
return `${ixs.find((ix) => ix.type === kind.type)?.summary ?? 'Call program'}${out.length ? `, pay ${join(out)}` : ''}${received.length ? `, receive ${join(received)}` : ''}`
|
|
74
|
+
default:
|
|
75
|
+
return ixs.find((ix) => ix.type === 'close')?.summary ?? (changes.length ? `Balance change: ${join(changes)}` : 'No balance change')
|
|
76
|
+
}
|
|
77
|
+
}
|