@w3lib/solana-batch-call 0.2.2 → 0.4.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 +26 -0
- package/README.md +205 -2
- package/dist/address.d.ts +2 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +8 -0
- package/dist/address.js.map +1 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +5 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +16 -0
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/amm.d.ts +7 -0
- package/dist/liquidity/amm.d.ts.map +1 -0
- package/dist/liquidity/amm.js +307 -0
- package/dist/liquidity/amm.js.map +1 -0
- package/dist/liquidity/clmm.d.ts +18 -0
- package/dist/liquidity/clmm.d.ts.map +1 -0
- package/dist/liquidity/clmm.js +85 -0
- package/dist/liquidity/clmm.js.map +1 -0
- package/dist/liquidity/common.d.ts +94 -0
- package/dist/liquidity/common.d.ts.map +1 -0
- package/dist/liquidity/common.js +228 -0
- package/dist/liquidity/common.js.map +1 -0
- package/dist/liquidity/index.d.ts +34 -0
- package/dist/liquidity/index.d.ts.map +1 -0
- package/dist/liquidity/index.js +157 -0
- package/dist/liquidity/index.js.map +1 -0
- package/dist/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -0
- package/dist/liquidity/orca.js +303 -0
- package/dist/liquidity/orca.js.map +1 -0
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -0
- package/dist/liquidity/raydiumClmm.js +307 -0
- package/dist/liquidity/raydiumClmm.js.map +1 -0
- package/dist/liquidity/types.d.ts +150 -0
- package/dist/liquidity/types.d.ts.map +1 -0
- package/dist/liquidity/types.js +2 -0
- package/dist/liquidity/types.js.map +1 -0
- package/dist/lp.d.ts +168 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +632 -0
- package/dist/lp.js.map +1 -0
- package/dist/lpMath.d.ts +82 -0
- package/dist/lpMath.d.ts.map +1 -0
- package/dist/lpMath.js +365 -0
- package/dist/lpMath.js.map +1 -0
- package/dist/shortcuts.d.ts +8 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +1 -1
- package/dist/shortcuts.js.map +1 -1
- package/dist/subpaths/liquidity.d.ts +23 -0
- package/dist/subpaths/liquidity.d.ts.map +1 -0
- package/dist/subpaths/liquidity.js +19 -0
- package/dist/subpaths/liquidity.js.map +1 -0
- package/dist/subpaths/lp.d.ts +31 -0
- package/dist/subpaths/lp.d.ts.map +1 -0
- package/dist/subpaths/lp.js +33 -0
- package/dist/subpaths/lp.js.map +1 -0
- package/dist/subpaths/write.d.ts +22 -0
- package/dist/subpaths/write.d.ts.map +1 -0
- package/dist/subpaths/write.js +19 -0
- package/dist/subpaths/write.js.map +1 -0
- package/dist/write/batch.d.ts +41 -0
- package/dist/write/batch.d.ts.map +1 -0
- package/dist/write/batch.js +46 -0
- package/dist/write/batch.js.map +1 -0
- package/dist/write/collect.d.ts +50 -0
- package/dist/write/collect.d.ts.map +1 -0
- package/dist/write/collect.js +220 -0
- package/dist/write/collect.js.map +1 -0
- package/dist/write/instructions.d.ts +16 -0
- package/dist/write/instructions.d.ts.map +1 -0
- package/dist/write/instructions.js +89 -0
- package/dist/write/instructions.js.map +1 -0
- package/dist/write/send.d.ts +115 -0
- package/dist/write/send.d.ts.map +1 -0
- package/dist/write/send.js +720 -0
- package/dist/write/send.js.map +1 -0
- package/dist/write/signer.d.ts +94 -0
- package/dist/write/signer.d.ts.map +1 -0
- package/dist/write/signer.js +197 -0
- package/dist/write/signer.js.map +1 -0
- package/dist/write/transaction.d.ts +75 -0
- package/dist/write/transaction.d.ts.map +1 -0
- package/dist/write/transaction.js +133 -0
- package/dist/write/transaction.js.map +1 -0
- package/dist/write/transfer.d.ts +72 -0
- package/dist/write/transfer.d.ts.map +1 -0
- package/dist/write/transfer.js +127 -0
- package/dist/write/transfer.js.map +1 -0
- package/package.json +23 -5
- package/src/address.ts +9 -0
- package/src/client.ts +10 -0
- package/src/index.ts +2 -1
- package/src/layout.ts +17 -0
- package/src/liquidity/amm.ts +353 -0
- package/src/liquidity/clmm.ts +108 -0
- package/src/liquidity/common.ts +285 -0
- package/src/liquidity/index.ts +177 -0
- package/src/liquidity/orca.ts +344 -0
- package/src/liquidity/raydiumClmm.ts +348 -0
- package/src/liquidity/types.ts +158 -0
- package/src/lp.ts +850 -0
- package/src/lpMath.ts +420 -0
- package/src/shortcuts.ts +1 -1
- package/src/subpaths/liquidity.ts +45 -0
- package/src/subpaths/lp.ts +71 -0
- package/src/subpaths/write.ts +29 -0
- package/src/write/batch.ts +78 -0
- package/src/write/collect.ts +286 -0
- package/src/write/instructions.ts +98 -0
- package/src/write/send.ts +876 -0
- package/src/write/signer.ts +314 -0
- package/src/write/transaction.ts +181 -0
- package/src/write/transfer.ts +189 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { getAssociatedTokenAddress, isAddress } from '../address.js'
|
|
2
|
+
import type { SolanaClient } from '../client.js'
|
|
3
|
+
import { formatUnits } from '../layout.js'
|
|
4
|
+
import type { TokenBalance } from '../types.js'
|
|
5
|
+
import { closeAccount, createAssociatedTokenAccountIdempotent, systemTransfer } from './instructions.js'
|
|
6
|
+
import { emptyOutcome, priorityFeeFor, sendUnits, toWriteResult, transactionFee, type SendOptions, type TxOutcome, type Unit, type WriteResult } from './send.js'
|
|
7
|
+
import { resolveSigner, type ResolvedSigner, type SignerInput } from './signer.js'
|
|
8
|
+
import { transferInstructions } from './transfer.js'
|
|
9
|
+
|
|
10
|
+
/** 扫空 SOL 的交易(一条 System 转账 + 两条计算预算指令)用的计算单元上限 */
|
|
11
|
+
const SWEEP_COMPUTE_UNITS = 1_000
|
|
12
|
+
|
|
13
|
+
export interface BatchCollectOptions extends SendOptions {
|
|
14
|
+
/**
|
|
15
|
+
* 要归集的代币 mint。包含原生 SOL(NATIVE_MINT)时,在代币归集确认后把剩余 SOL 全部转走:
|
|
16
|
+
* 传了 feePayer 时整个余额转走(手续费由 feePayer 付),否则扣掉这笔的手续费后转走,钱包余额归零
|
|
17
|
+
*/
|
|
18
|
+
tokens: readonly string[]
|
|
19
|
+
/**
|
|
20
|
+
* 代币转完后关闭钱包的代币账户(ATA),租金(约 0.00204 SOL / 个)退回钱包本身;同时扫 SOL 时一并转走。默认 false。
|
|
21
|
+
* Token-2022 账户里有未提取的转账手续费等情况关不掉,只影响 close 那一项
|
|
22
|
+
*/
|
|
23
|
+
closeAccounts?: boolean
|
|
24
|
+
/** 各代币的最小归集数量(最小单位),余额低于它的不归集:`{ [USDC]: 1_000_000n }`;对 SOL 同样有效 */
|
|
25
|
+
minAmount?: Readonly<Record<string, bigint | number | string>>
|
|
26
|
+
/** 每笔交易最多几个调用,默认不限(按交易大小) */
|
|
27
|
+
batchSize?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 单个钱包单个代币的归集结果;from 是钱包,to 是归集地址 */
|
|
31
|
+
export interface CollectItemResult extends WriteResult {
|
|
32
|
+
to: string
|
|
33
|
+
token: string
|
|
34
|
+
native: boolean
|
|
35
|
+
/** 归集的数量(最小单位);SOL 扫尾在发送前为 '0' */
|
|
36
|
+
amount: string
|
|
37
|
+
formatted: string | null
|
|
38
|
+
/** closeAccounts 时关闭代币账户的结果;没有关闭(未开启、SOL、没有归集)时为 null */
|
|
39
|
+
close: WriteResult | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CollectResult {
|
|
43
|
+
/** 按 wallets × tokens 顺序(第 i 个钱包第 j 个代币在 i × tokens.length + j) */
|
|
44
|
+
results: CollectItemResult[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Item {
|
|
48
|
+
index: number
|
|
49
|
+
signer: ResolvedSigner
|
|
50
|
+
token: string
|
|
51
|
+
native: boolean
|
|
52
|
+
amount: bigint
|
|
53
|
+
decimals: number | null
|
|
54
|
+
tokenProgram: string | null
|
|
55
|
+
result: CollectItemResult
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 归集:多个钱包的代币 / SOL 转到同一个地址。
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const { results } = await batchCollect(client, [key1, key2, key3], hotWallet, {
|
|
63
|
+
* tokens: [USDC, NATIVE_MINT], // 包含 SOL 表示代币归集完后扫空 SOL
|
|
64
|
+
* feePayer: gasKey, // 手续费都由 gasKey 付:子钱包不需要有 SOL,多个钱包的转账打包进同一笔交易
|
|
65
|
+
* })
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* 流程:一次读出所有钱包的余额 → 归集地址没有该代币的 ATA 时随第一批转账创建 → 打包发送代币转账(可选关闭代币账户)
|
|
69
|
+
* →(包含 SOL 时)等代币归集确认,按最新余额扫空 SOL。
|
|
70
|
+
* 不传 feePayer 时每个钱包自己付手续费,没有 SOL 的钱包对应的项失败(Solana 上不需要先补 gas:传 feePayer 即可)
|
|
71
|
+
*/
|
|
72
|
+
export async function batchCollect(client: SolanaClient, wallets: readonly SignerInput[], to: string, options: BatchCollectOptions): Promise<CollectResult> {
|
|
73
|
+
if (!isAddress(to)) {
|
|
74
|
+
throw new Error(`Invalid collect address: ${to}`)
|
|
75
|
+
}
|
|
76
|
+
const cache = new Map()
|
|
77
|
+
const signers = await Promise.all(wallets.map((w) => resolveSigner(w, cache)))
|
|
78
|
+
const feePayer = options.feePayer === undefined ? null : await resolveSigner(options.feePayer, cache)
|
|
79
|
+
const tokens = options.tokens
|
|
80
|
+
const sweep = tokens.some((t) => client.isNativeMint(t))
|
|
81
|
+
const minAmount = new Map(Object.entries(options.minAmount ?? {}).map(([k, v]) => [k, BigInt(v)]))
|
|
82
|
+
const balances = await client.multiBalances(
|
|
83
|
+
signers.map((s) => ({ owner: s.address, mints: tokens })),
|
|
84
|
+
{ signal: options.signal },
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const items: Item[] = []
|
|
88
|
+
const seen = new Set<string>()
|
|
89
|
+
signers.forEach((signer, w) => {
|
|
90
|
+
const duplicate = seen.has(signer.address)
|
|
91
|
+
seen.add(signer.address)
|
|
92
|
+
tokens.forEach((token, t) => {
|
|
93
|
+
const balance = balances[w]?.[t] as TokenBalance
|
|
94
|
+
const native = client.isNativeMint(token)
|
|
95
|
+
const item: Item = {
|
|
96
|
+
index: w * tokens.length + t,
|
|
97
|
+
signer,
|
|
98
|
+
token,
|
|
99
|
+
native,
|
|
100
|
+
amount: native || !balance.success ? 0n : BigInt(balance.balance),
|
|
101
|
+
decimals: balance.success ? balance.decimals : null,
|
|
102
|
+
tokenProgram: balance.tokenProgram,
|
|
103
|
+
result: null as unknown as CollectItemResult,
|
|
104
|
+
}
|
|
105
|
+
const skip = !balance.success
|
|
106
|
+
? `cannot read balance: ${balance.error ?? 'unknown error'}`
|
|
107
|
+
: signer.address === to
|
|
108
|
+
? 'skipped: wallet is the collect address'
|
|
109
|
+
: duplicate
|
|
110
|
+
? 'skipped: duplicate wallet'
|
|
111
|
+
: native && feePayer?.address === signer.address
|
|
112
|
+
? 'skipped: wallet is the fee payer'
|
|
113
|
+
: !native && item.amount === 0n
|
|
114
|
+
? 'skipped: zero balance'
|
|
115
|
+
: !native && item.amount < (minAmount.get(token) ?? 0n)
|
|
116
|
+
? 'skipped: below minAmount'
|
|
117
|
+
: null
|
|
118
|
+
item.result = toCollectResult(item, to, emptyOutcome(skip ? (balance.success ? 'skipped' : 'failed') : 'skipped', skip), null)
|
|
119
|
+
items.push(item)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
const results = items.map((i) => i.result)
|
|
123
|
+
const report = (item: Item, outcome: TxOutcome, close: WriteResult | null = item.result.close) => {
|
|
124
|
+
const hint =
|
|
125
|
+
!feePayer && outcome.status === 'failed' && /transaction fee|fee payer/.test(outcome.error ?? '') && !outcome.error?.includes('feePayer')
|
|
126
|
+
? ' (pass feePayer to pay fees from another wallet)'
|
|
127
|
+
: ''
|
|
128
|
+
item.result = toCollectResult(item, to, { ...outcome, error: outcome.error && outcome.error + hint }, close)
|
|
129
|
+
results[item.index] = item.result
|
|
130
|
+
options.onUpdate?.(item.result)
|
|
131
|
+
}
|
|
132
|
+
const active = (item: Item) => item.result.error === null
|
|
133
|
+
|
|
134
|
+
// 归集地址的 ATA:没有的话随转账创建(创建指令是幂等的,同一代币的多笔转账都带上,只有第一笔真正创建)
|
|
135
|
+
const tokenItems = items.filter((i) => !i.native && active(i))
|
|
136
|
+
const targetAtas = new Map(tokenItems.map((i) => [i.token, getAssociatedTokenAddress(to, i.token, i.tokenProgram as string)]))
|
|
137
|
+
const missing = new Set<string>()
|
|
138
|
+
if (targetAtas.size) {
|
|
139
|
+
const keys = [...targetAtas.values()]
|
|
140
|
+
const accounts = await client.accounts(keys, { signal: options.signal })
|
|
141
|
+
keys.forEach((k, i) => !accounts[i] && missing.add(k))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 不传 feePayer 时每个钱包自己付费:如果每笔转账都带创建指令,模拟时每个钱包都要付得起租金(实际只创建一次)。
|
|
145
|
+
// 先由第一个有这种代币的钱包单独创建,确认后再归集;创建失败时退回到每笔都带创建指令
|
|
146
|
+
if (!feePayer && missing.size && !options.dryRun) {
|
|
147
|
+
const setup: Unit[] = []
|
|
148
|
+
for (const [mint, ata] of targetAtas) {
|
|
149
|
+
if (!missing.has(ata)) continue
|
|
150
|
+
const first = tokenItems.find((i) => i.token === mint) as Item
|
|
151
|
+
setup.push({
|
|
152
|
+
instructions: [createAssociatedTokenAccountIdempotent(first.signer.address, ata, to, mint, first.tokenProgram as string)],
|
|
153
|
+
payer: first.signer,
|
|
154
|
+
signers: [],
|
|
155
|
+
continueOnError: true,
|
|
156
|
+
onUpdate: (outcome) => outcome.status === 'confirmed' && missing.delete(ata),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
await sendUnits(client, setup, { ...options, wait: true }, { batchSize: 1 })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const units: Unit[] = []
|
|
163
|
+
/** 每个钱包最后一笔上链交易的 slot(扫 SOL 时按不早于它的状态读余额) */
|
|
164
|
+
let minSlot: number | undefined
|
|
165
|
+
const track = (outcome: TxOutcome) => {
|
|
166
|
+
if (outcome.slot !== null) minSlot = Math.max(minSlot ?? 0, outcome.slot)
|
|
167
|
+
}
|
|
168
|
+
for (const item of tokenItems) {
|
|
169
|
+
const payer = feePayer ?? item.signer
|
|
170
|
+
const signerList = payer.address === item.signer.address ? [] : [item.signer]
|
|
171
|
+
const target = targetAtas.get(item.token) as string
|
|
172
|
+
units.push({
|
|
173
|
+
instructions: transferInstructions(item.signer, payer, { ...item, to }, target, missing.has(target)),
|
|
174
|
+
payer,
|
|
175
|
+
signers: signerList,
|
|
176
|
+
continueOnError: true,
|
|
177
|
+
onUpdate: (outcome) => {
|
|
178
|
+
track(outcome)
|
|
179
|
+
report(item, outcome)
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
if (options.closeAccounts) {
|
|
183
|
+
const program = item.tokenProgram as string
|
|
184
|
+
const account = getAssociatedTokenAddress(item.signer.address, item.token, program)
|
|
185
|
+
units.push({
|
|
186
|
+
instructions: [closeAccount(account, item.signer.address, item.signer.address, program)],
|
|
187
|
+
payer,
|
|
188
|
+
signers: signerList,
|
|
189
|
+
continueOnError: true,
|
|
190
|
+
onUpdate: (outcome) => {
|
|
191
|
+
track(outcome)
|
|
192
|
+
report(item, outcomeOf(item.result), toWriteResult(item.index, item.signer.address, outcome))
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 要扫 SOL 时必须等代币归集确认(扫的是最新余额,关闭账户退回的租金也要算进去)
|
|
198
|
+
await sendUnits(client, units, sweep ? { ...options, wait: true } : options, { batchSize: options.batchSize })
|
|
199
|
+
|
|
200
|
+
if (!sweep) {
|
|
201
|
+
return { results }
|
|
202
|
+
}
|
|
203
|
+
// 代币没有全部归集成功的钱包不扫 SOL:留着付之后重新归集的手续费
|
|
204
|
+
items
|
|
205
|
+
.filter((i) => i.native && active(i))
|
|
206
|
+
.forEach((native) => {
|
|
207
|
+
// dryRun 时代币项是 simulated,也算成功
|
|
208
|
+
const done = (status: string) => status === 'confirmed' || status === 'skipped' || (options.dryRun && status === 'simulated')
|
|
209
|
+
const unfinished = items.find((i) => !i.native && i.signer.address === native.signer.address && !done(i.result.status))
|
|
210
|
+
if (unfinished) {
|
|
211
|
+
report(native, emptyOutcome('skipped', `skipped: collecting ${unfinished.token} did not succeed (${unfinished.result.status})`))
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
const sweepItems = items.filter((i) => i.native && active(i))
|
|
215
|
+
if (!sweepItems.length || options.signal?.aborted) {
|
|
216
|
+
return { results }
|
|
217
|
+
}
|
|
218
|
+
if (options.dryRun) {
|
|
219
|
+
// 代币归集没有真的执行,扫 SOL 的金额要等归集之后才知道:只标记 simulated,不做模拟
|
|
220
|
+
sweepItems.forEach((i) => report(i, emptyOutcome('simulated')))
|
|
221
|
+
return { results }
|
|
222
|
+
}
|
|
223
|
+
let lamports: Awaited<ReturnType<SolanaClient['solBalances']>>
|
|
224
|
+
try {
|
|
225
|
+
lamports = await client.solBalances(
|
|
226
|
+
sweepItems.map((i) => i.signer.address),
|
|
227
|
+
{ minContextSlot: minSlot, signal: options.signal },
|
|
228
|
+
)
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// 代币归集已经发出:不能整体抛错(调用方会拿不到这些结果),扫 SOL 的项标记失败
|
|
231
|
+
sweepItems.forEach((i) => report(i, emptyOutcome('failed', `cannot read SOL balance: ${err instanceof Error ? err.message : String(err)}`)))
|
|
232
|
+
return { results }
|
|
233
|
+
}
|
|
234
|
+
const prepared = await Promise.all(
|
|
235
|
+
sweepItems.map(async (item, k): Promise<Unit | null> => {
|
|
236
|
+
const balance = BigInt(lamports[k]?.balance ?? '0')
|
|
237
|
+
let amount = balance
|
|
238
|
+
let fixed: Unit['fixed']
|
|
239
|
+
if (!feePayer) {
|
|
240
|
+
// 自己付手续费:单独一笔、手续费事先算好,转走剩下的全部(余额归零,不受租金下限限制)
|
|
241
|
+
const price = await priorityFeeFor(client, options, [item.signer.address, to])
|
|
242
|
+
fixed = { computeUnits: SWEEP_COMPUTE_UNITS, price }
|
|
243
|
+
amount = balance - transactionFee(1, SWEEP_COMPUTE_UNITS, price)
|
|
244
|
+
}
|
|
245
|
+
const min = minAmount.get(item.token) ?? 0n
|
|
246
|
+
if (amount <= 0n || amount < min) {
|
|
247
|
+
report(item, emptyOutcome('skipped', amount <= 0n ? 'skipped: SOL balance does not cover the fee' : 'skipped: below minAmount'))
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
item.amount = amount
|
|
251
|
+
const payer = feePayer ?? item.signer
|
|
252
|
+
return {
|
|
253
|
+
instructions: [systemTransfer(item.signer.address, to, amount)],
|
|
254
|
+
payer,
|
|
255
|
+
signers: payer.address === item.signer.address ? [] : [item.signer],
|
|
256
|
+
fixed,
|
|
257
|
+
continueOnError: true,
|
|
258
|
+
onUpdate: (outcome) => report(item, outcome),
|
|
259
|
+
}
|
|
260
|
+
}),
|
|
261
|
+
)
|
|
262
|
+
await sendUnits(
|
|
263
|
+
client,
|
|
264
|
+
prepared.filter((u): u is Unit => u !== null),
|
|
265
|
+
options,
|
|
266
|
+
{ batchSize: options.batchSize },
|
|
267
|
+
)
|
|
268
|
+
return { results }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 结果转回交易结果(更新 close 字段时保留转账本身的状态) */
|
|
272
|
+
function outcomeOf(r: WriteResult): TxOutcome {
|
|
273
|
+
return { status: r.status, signature: r.signature, slot: r.slot, fee: r.fee === null ? null : BigInt(r.fee), computeUnits: r.computeUnits, error: r.error }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function toCollectResult(item: Item, to: string, outcome: TxOutcome, close: WriteResult | null): CollectItemResult {
|
|
277
|
+
return {
|
|
278
|
+
...toWriteResult(item.index, item.signer.address, outcome),
|
|
279
|
+
to,
|
|
280
|
+
token: item.token,
|
|
281
|
+
native: item.native,
|
|
282
|
+
amount: item.amount.toString(),
|
|
283
|
+
formatted: item.decimals === null ? null : formatUnits(item.amount, item.decimals),
|
|
284
|
+
close,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID } from '../constants.js'
|
|
2
|
+
import type { Instruction } from './transaction.js'
|
|
3
|
+
|
|
4
|
+
export const COMPUTE_BUDGET_PROGRAM_ID = 'ComputeBudget111111111111111111111111111111'
|
|
5
|
+
|
|
6
|
+
/** 单笔交易的计算单元上限 */
|
|
7
|
+
export const MAX_COMPUTE_UNITS = 1_400_000
|
|
8
|
+
|
|
9
|
+
function bytes(size: number, write: (view: DataView) => void): Uint8Array {
|
|
10
|
+
const out = new Uint8Array(size)
|
|
11
|
+
write(new DataView(out.buffer))
|
|
12
|
+
return out
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** System Program Transfer:转 SOL */
|
|
16
|
+
export function systemTransfer(from: string, to: string, lamports: bigint): Instruction {
|
|
17
|
+
return {
|
|
18
|
+
programId: SYSTEM_PROGRAM_ID,
|
|
19
|
+
keys: [
|
|
20
|
+
{ pubkey: from, isSigner: true, isWritable: true },
|
|
21
|
+
{ pubkey: to, isSigner: false, isWritable: true },
|
|
22
|
+
],
|
|
23
|
+
data: bytes(12, (v) => {
|
|
24
|
+
v.setUint32(0, 2, true)
|
|
25
|
+
v.setBigUint64(4, lamports, true)
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** SPL Token / Token-2022 TransferChecked(校验 mint 和精度) */
|
|
31
|
+
export function transferChecked(source: string, mint: string, destination: string, owner: string, amount: bigint, decimals: number, tokenProgram: string): Instruction {
|
|
32
|
+
return {
|
|
33
|
+
programId: tokenProgram,
|
|
34
|
+
keys: [
|
|
35
|
+
{ pubkey: source, isSigner: false, isWritable: true },
|
|
36
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
37
|
+
{ pubkey: destination, isSigner: false, isWritable: true },
|
|
38
|
+
{ pubkey: owner, isSigner: true, isWritable: false },
|
|
39
|
+
],
|
|
40
|
+
data: bytes(10, (v) => {
|
|
41
|
+
v.setUint8(0, 12)
|
|
42
|
+
v.setBigUint64(1, amount, true)
|
|
43
|
+
v.setUint8(9, decimals)
|
|
44
|
+
}),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 创建关联代币账户(已存在时什么都不做),租金由 payer 付 */
|
|
49
|
+
export function createAssociatedTokenAccountIdempotent(payer: string, ata: string, owner: string, mint: string, tokenProgram: string): Instruction {
|
|
50
|
+
return {
|
|
51
|
+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
52
|
+
keys: [
|
|
53
|
+
{ pubkey: payer, isSigner: true, isWritable: true },
|
|
54
|
+
{ pubkey: ata, isSigner: false, isWritable: true },
|
|
55
|
+
{ pubkey: owner, isSigner: false, isWritable: false },
|
|
56
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
57
|
+
{ pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
58
|
+
{ pubkey: tokenProgram, isSigner: false, isWritable: false },
|
|
59
|
+
],
|
|
60
|
+
data: new Uint8Array([1]),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 关闭代币账户(余额必须为 0),租金退给 destination */
|
|
65
|
+
export function closeAccount(account: string, destination: string, owner: string, tokenProgram: string): Instruction {
|
|
66
|
+
return {
|
|
67
|
+
programId: tokenProgram,
|
|
68
|
+
keys: [
|
|
69
|
+
{ pubkey: account, isSigner: false, isWritable: true },
|
|
70
|
+
{ pubkey: destination, isSigner: false, isWritable: true },
|
|
71
|
+
{ pubkey: owner, isSigner: true, isWritable: false },
|
|
72
|
+
],
|
|
73
|
+
data: new Uint8Array([9]),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function setComputeUnitLimit(units: number): Instruction {
|
|
78
|
+
return {
|
|
79
|
+
programId: COMPUTE_BUDGET_PROGRAM_ID,
|
|
80
|
+
keys: [],
|
|
81
|
+
data: bytes(5, (v) => {
|
|
82
|
+
v.setUint8(0, 2)
|
|
83
|
+
v.setUint32(1, units, true)
|
|
84
|
+
}),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 优先费用:每个计算单元多付多少 micro-lamports */
|
|
89
|
+
export function setComputeUnitPrice(microLamports: bigint): Instruction {
|
|
90
|
+
return {
|
|
91
|
+
programId: COMPUTE_BUDGET_PROGRAM_ID,
|
|
92
|
+
keys: [],
|
|
93
|
+
data: bytes(9, (v) => {
|
|
94
|
+
v.setUint8(0, 3)
|
|
95
|
+
v.setBigUint64(1, microLamports, true)
|
|
96
|
+
}),
|
|
97
|
+
}
|
|
98
|
+
}
|