@w3lib/solana-batch-call 0.4.0 → 0.5.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 +31 -0
- package/README.md +231 -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/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/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/lp.d.ts +8 -15
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +7 -31
- package/dist/lp.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/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 +33 -9
- package/dist/rpc.js.map +1 -1
- 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/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/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/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/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 +7 -0
- package/dist/write/instructions.d.ts.map +1 -1
- package/dist/write/instructions.js +24 -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 +46 -14
- package/src/approvals.ts +358 -0
- package/src/fees.ts +259 -0
- package/src/layout.ts +2 -2
- package/src/lp.ts +8 -44
- 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/risk.ts +243 -0
- package/src/rpc.ts +36 -9
- package/src/subpaths/approvals.ts +47 -0
- package/src/subpaths/fees.ts +41 -0
- package/src/subpaths/names.ts +45 -0
- package/src/subpaths/price.ts +62 -0
- package/src/subpaths/risk.ts +36 -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 +26 -1
- package/src/write/transaction.ts +62 -0
package/src/approvals.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 授权管理(子路径 `@w3lib/solana-batch-call/approvals`):Solana 上的“授权”是代币账户的 delegate(Approve 设置,可以转走 delegatedAmount 以内的代币)。
|
|
3
|
+
* 批量查一个地址的代币账户给出的授权、可以关闭回收租金的空账户;批量撤销(Revoke)、关闭(CloseAccount,租金退回)
|
|
4
|
+
*/
|
|
5
|
+
import { base64 } from '@scure/base'
|
|
6
|
+
|
|
7
|
+
import { encodeAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress } from './address.js'
|
|
8
|
+
import type { RequestOptions, SolanaClient } from './client.js'
|
|
9
|
+
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
|
|
10
|
+
import { findExtension, formatUnits, parseMetaplexMetadata, parseMint, type AccountInfo } from './layout.js'
|
|
11
|
+
import { methodUnavailable } from './lp.js'
|
|
12
|
+
import { closeAccount, harvestWithheldTokensToMint, revoke } from './write/instructions.js'
|
|
13
|
+
import type { BatchCallOptions } from './write/batch.js'
|
|
14
|
+
import type { WriteResult } from './write/send.js'
|
|
15
|
+
import type { SignerInput } from './write/signer.js'
|
|
16
|
+
import type { Instruction } from './write/transaction.js'
|
|
17
|
+
|
|
18
|
+
export interface ApprovalsQuery extends RequestOptions {
|
|
19
|
+
/**
|
|
20
|
+
* 要检查的代币 mint:读这些代币的关联代币账户(ATA,SPL Token 和 Token-2022 都看),免费公共节点也能用;非 ATA 的代币账户查不到。
|
|
21
|
+
* 不传时列出地址的全部代币账户(getTokenAccountsByOwner),需要支持它的节点
|
|
22
|
+
*/
|
|
23
|
+
mints?: readonly string[]
|
|
24
|
+
/** 返回全部代币账户(含没有授权、有余额的),默认 false:只返回有授权(delegate / 别人的关闭权限)的账户和余额为 0 的账户 */
|
|
25
|
+
all?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 关不掉的原因:balance 有余额(Wrapped SOL 除外);close-authority 关闭权限是别的地址;confidential-balance Token-2022 机密余额不为 0 */
|
|
29
|
+
export type CloseBlocker = 'balance' | 'close-authority' | 'confidential-balance'
|
|
30
|
+
|
|
31
|
+
/** 一个代币账户的授权状态,字段都是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify */
|
|
32
|
+
export interface TokenAccountApproval {
|
|
33
|
+
/** 代币账户地址 */
|
|
34
|
+
account: string
|
|
35
|
+
mint: string
|
|
36
|
+
owner: string
|
|
37
|
+
tokenProgram: string
|
|
38
|
+
/** 是 owner 的关联代币账户(ATA) */
|
|
39
|
+
ata: boolean
|
|
40
|
+
/** Wrapped SOL 账户:amount 是包装的 SOL,关闭时连同租金一起退回(相当于 unwrap) */
|
|
41
|
+
native: boolean
|
|
42
|
+
symbol: string | null
|
|
43
|
+
/** 精度;mint 读不到时为 null(formatted 也为 null) */
|
|
44
|
+
decimals: number | null
|
|
45
|
+
amount: string
|
|
46
|
+
formatted: string | null
|
|
47
|
+
/** 被授权方(delegate);没有授权时为 null */
|
|
48
|
+
delegate: string | null
|
|
49
|
+
/** 被授权方还能转走的数量(最小单位) */
|
|
50
|
+
delegatedAmount: string
|
|
51
|
+
delegatedFormatted: string | null
|
|
52
|
+
/** 关闭权限:设置了且不是 owner 时,owner 自己关不掉,余额为 0 时它可以关闭账户并拿走租金(只有它自己能撤销) */
|
|
53
|
+
closeAuthority: string | null
|
|
54
|
+
/** mint 的永久授权(Token-2022 PermanentDelegate 扩展):可以转走 / 销毁任何账户里的这种代币,撤销不了 */
|
|
55
|
+
permanentDelegate: string | null
|
|
56
|
+
/** 账户被冻结:不能 Revoke(关闭不受影响) */
|
|
57
|
+
frozen: boolean
|
|
58
|
+
/** 账户的 lamports:关闭时全部退回 destination(Wrapped SOL 含包装的 SOL) */
|
|
59
|
+
lamports: string
|
|
60
|
+
/** Token-2022 账户里暂存的转账手续费(不属于 owner):不为 0 时要先收到 mint 上才能关闭,buildCloses 自动加上这一步 */
|
|
61
|
+
withheldAmount: string
|
|
62
|
+
/** 能撤销授权(有 delegate、没有冻结) */
|
|
63
|
+
revocable: boolean
|
|
64
|
+
/** 能关闭;不能时 closeBlocker 是原因 */
|
|
65
|
+
closable: boolean
|
|
66
|
+
closeBlocker: CloseBlocker | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ACCOUNT_SIZE = 165
|
|
70
|
+
const ACCOUNT_TYPE_ACCOUNT = 2
|
|
71
|
+
const EXT_TRANSFER_FEE_AMOUNT = 2
|
|
72
|
+
const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT = 5
|
|
73
|
+
const EXT_PERMANENT_DELEGATE = 12
|
|
74
|
+
const EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT = 17
|
|
75
|
+
const ZERO_PUBKEY = '11111111111111111111111111111111'
|
|
76
|
+
|
|
77
|
+
/** 代币账户的完整布局(SPL Token / Token-2022 前 165 字节相同)及关闭相关的 Token-2022 扩展 */
|
|
78
|
+
interface ParsedAccount {
|
|
79
|
+
mint: string
|
|
80
|
+
owner: string
|
|
81
|
+
amount: bigint
|
|
82
|
+
delegate: string | null
|
|
83
|
+
frozen: boolean
|
|
84
|
+
native: boolean
|
|
85
|
+
delegatedAmount: bigint
|
|
86
|
+
closeAuthority: string | null
|
|
87
|
+
withheldAmount: bigint
|
|
88
|
+
confidentialBalance: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 解析代币账户;不是已初始化的代币账户时为 null */
|
|
92
|
+
export function parseTokenAccountState(data: Uint8Array): ParsedAccount | null {
|
|
93
|
+
if (data.length !== ACCOUNT_SIZE && !(data.length > ACCOUNT_SIZE && data[ACCOUNT_SIZE] === ACCOUNT_TYPE_ACCOUNT)) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
|
|
97
|
+
const pubkey = (offset: number) => encodeAddress(data.slice(offset, offset + 32))
|
|
98
|
+
// COption:u32 标记(1 有、0 没有)+ 值
|
|
99
|
+
const option = (offset: number) => view.getUint32(offset, true) === 1
|
|
100
|
+
const state = data[108] as number
|
|
101
|
+
if (state === 0) {
|
|
102
|
+
return null // 未初始化
|
|
103
|
+
}
|
|
104
|
+
const withheld = findExtension(data, EXT_TRANSFER_FEE_AMOUNT)
|
|
105
|
+
// 机密转账:待入账(lo / hi)和可用余额的密文全为 0 才能关闭;手续费扩展同理(Token-2022 的 closable 检查)
|
|
106
|
+
const confidential = findExtension(data, EXT_CONFIDENTIAL_TRANSFER_ACCOUNT)
|
|
107
|
+
const confidentialFee = findExtension(data, EXT_CONFIDENTIAL_TRANSFER_FEE_AMOUNT)
|
|
108
|
+
const nonZero = (bytes: Uint8Array | null, from: number, to: number) => !!bytes && bytes.slice(from, to).some((b) => b !== 0)
|
|
109
|
+
return {
|
|
110
|
+
mint: pubkey(0),
|
|
111
|
+
owner: pubkey(32),
|
|
112
|
+
amount: view.getBigUint64(64, true),
|
|
113
|
+
delegate: option(72) ? pubkey(76) : null,
|
|
114
|
+
frozen: state === 2,
|
|
115
|
+
native: option(109),
|
|
116
|
+
delegatedAmount: view.getBigUint64(121, true),
|
|
117
|
+
closeAuthority: option(129) ? pubkey(133) : null,
|
|
118
|
+
withheldAmount: withheld && withheld.length >= 8 ? new DataView(withheld.buffer, withheld.byteOffset).getBigUint64(0, true) : 0n,
|
|
119
|
+
confidentialBalance: nonZero(confidential, 33, 225) || nonZero(confidentialFee, 0, 64),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface RawTokenAccount {
|
|
124
|
+
pubkey: string
|
|
125
|
+
account: { data: [string, string]; owner: string; lamports: number | string }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface Found {
|
|
129
|
+
address: string
|
|
130
|
+
program: string
|
|
131
|
+
lamports: bigint
|
|
132
|
+
parsed: ParsedAccount
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 查一个地址的代币账户给出的授权,和可以关闭回收租金的空账户(结果按账户,同一账户的授权和关闭状态在一起)。
|
|
137
|
+
* 传 mints 时读这些代币的 ATA(一次 getMultipleAccounts,免费公共节点也能用);不传时列出全部代币账户(需要支持 getTokenAccountsByOwner 的节点)
|
|
138
|
+
*
|
|
139
|
+
* ```ts
|
|
140
|
+
* await approvals(client, owner, { mints: [USDC, BONK] })
|
|
141
|
+
* // [{ account: '…', mint: USDC, delegate: 'JUP…', delegatedAmount: '1000000', revocable: true, closable: false, closeBlocker: 'balance', … }]
|
|
142
|
+
* await approvals(new SolanaClient('https://my-rpc…'), owner) // 全部代币账户
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export async function approvals(client: SolanaClient, owner: string, query: ApprovalsQuery = {}): Promise<TokenAccountApproval[]> {
|
|
146
|
+
if (!isAddress(owner)) {
|
|
147
|
+
throw new Error(`Invalid owner address: ${owner}`)
|
|
148
|
+
}
|
|
149
|
+
const { signal } = query
|
|
150
|
+
const found = query.mints ? await ataAccounts(client, owner, query.mints, signal) : await listAccounts(client, owner, signal)
|
|
151
|
+
// 只看 owner 自己的账户(ATA 的 owner 可能被改过:SPL Token 的 ATA 能 SetAuthority 换 owner)
|
|
152
|
+
const mine = found.filter((f) => f.parsed.owner === owner)
|
|
153
|
+
const mints = [...new Set(mine.map((f) => f.parsed.mint))]
|
|
154
|
+
const metaAddresses = mints.flatMap((m) => [m, getMetadataAddress(m)])
|
|
155
|
+
const metaAccounts = metaAddresses.length ? await client.accounts(metaAddresses, { signal }) : []
|
|
156
|
+
const meta = new Map(mints.map((mint, i) => [mint, mintMeta(metaAccounts[2 * i] ?? null, metaAccounts[2 * i + 1] ?? null)]))
|
|
157
|
+
|
|
158
|
+
const list = mine.map((f): TokenAccountApproval => {
|
|
159
|
+
const p = f.parsed
|
|
160
|
+
const m = meta.get(p.mint) ?? { decimals: null, symbol: null, permanentDelegate: null }
|
|
161
|
+
const format = (v: bigint) => (m.decimals === null ? null : formatUnits(v, m.decimals))
|
|
162
|
+
const closeBlocker: CloseBlocker | null =
|
|
163
|
+
p.closeAuthority !== null && p.closeAuthority !== owner ? 'close-authority' : !p.native && p.amount > 0n ? 'balance' : p.confidentialBalance ? 'confidential-balance' : null
|
|
164
|
+
return {
|
|
165
|
+
account: f.address,
|
|
166
|
+
mint: p.mint,
|
|
167
|
+
owner: p.owner,
|
|
168
|
+
tokenProgram: f.program,
|
|
169
|
+
ata: getAssociatedTokenAddress(owner, p.mint, f.program) === f.address,
|
|
170
|
+
native: p.native,
|
|
171
|
+
symbol: m.symbol,
|
|
172
|
+
decimals: m.decimals,
|
|
173
|
+
amount: p.amount.toString(),
|
|
174
|
+
formatted: format(p.amount),
|
|
175
|
+
delegate: p.delegate,
|
|
176
|
+
delegatedAmount: p.delegatedAmount.toString(),
|
|
177
|
+
delegatedFormatted: format(p.delegatedAmount),
|
|
178
|
+
closeAuthority: p.closeAuthority,
|
|
179
|
+
permanentDelegate: m.permanentDelegate,
|
|
180
|
+
frozen: p.frozen,
|
|
181
|
+
lamports: f.lamports.toString(),
|
|
182
|
+
withheldAmount: p.withheldAmount.toString(),
|
|
183
|
+
revocable: p.delegate !== null && !p.frozen,
|
|
184
|
+
closable: closeBlocker === null,
|
|
185
|
+
closeBlocker,
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
return query.all ? list : list.filter((a) => a.delegate !== null || a.closeBlocker === 'close-authority' || BigInt(a.amount) === 0n)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 指定 mints:两个程序的 ATA 都读(mint 所属程序未知时只有一个存在) */
|
|
192
|
+
async function ataAccounts(client: SolanaClient, owner: string, mints: readonly string[], signal: AbortSignal | undefined): Promise<Found[]> {
|
|
193
|
+
const targets = [...new Set(mints.filter((m) => isAddress(m)))].flatMap((mint) =>
|
|
194
|
+
[TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((program) => ({ program, address: getAssociatedTokenAddress(owner, mint, program) })),
|
|
195
|
+
)
|
|
196
|
+
const accounts = targets.length ? await client.accounts(targets.map((t) => t.address), { signal }) : []
|
|
197
|
+
return targets.flatMap((t, i) => {
|
|
198
|
+
const account = accounts[i]
|
|
199
|
+
const parsed = account && account.owner === t.program ? parseTokenAccountState(account.data) : null
|
|
200
|
+
return parsed && account ? [{ address: t.address, program: t.program, lamports: account.lamports, parsed }] : []
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 不传 mints:用 getTokenAccountsByOwner 列出两个程序的全部代币账户(base64,自己解析扩展) */
|
|
205
|
+
async function listAccounts(client: SolanaClient, owner: string, signal: AbortSignal | undefined): Promise<Found[]> {
|
|
206
|
+
const lists = await Promise.all(
|
|
207
|
+
[TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].map((programId) =>
|
|
208
|
+
client.request<{ value: RawTokenAccount[] }>('getTokenAccountsByOwner', [owner, { programId }, { encoding: 'base64', commitment: client.commitment }], { signal }).then(
|
|
209
|
+
(res) => ({ programId, value: res.value ?? [] }),
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
).catch((err: unknown) => {
|
|
213
|
+
if (signal?.aborted || !methodUnavailable(err)) {
|
|
214
|
+
throw err // 取消、限频、超时等原样抛出(保留错误类型)
|
|
215
|
+
}
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Listing the token accounts of ${owner} failed (${err instanceof Error ? err.message : String(err)}). ` +
|
|
218
|
+
'Listing needs getTokenAccountsByOwner (most free public nodes do not support it): pass the mints in `mints`, or use a node that supports it',
|
|
219
|
+
{ cause: err },
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
return lists.flatMap(({ programId, value }) =>
|
|
223
|
+
value.flatMap((entry) => {
|
|
224
|
+
const parsed = entry.account.owner === programId ? parseTokenAccountState(base64.decode(entry.account.data[0])) : null
|
|
225
|
+
return parsed ? [{ address: entry.pubkey, program: programId, lamports: BigInt(entry.account.lamports), parsed }] : []
|
|
226
|
+
}),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** mint 的精度、symbol(Token-2022 TokenMetadata 扩展或 Metaplex 元数据)、永久授权 */
|
|
231
|
+
function mintMeta(mint: AccountInfo | null, metadata: AccountInfo | null): { decimals: number | null; symbol: string | null; permanentDelegate: string | null } {
|
|
232
|
+
if (!mint) {
|
|
233
|
+
return { decimals: null, symbol: null, permanentDelegate: null }
|
|
234
|
+
}
|
|
235
|
+
let decimals: number | null = null
|
|
236
|
+
let symbol: string | null = null
|
|
237
|
+
try {
|
|
238
|
+
const info = parseMint(mint.data)
|
|
239
|
+
decimals = info.decimals
|
|
240
|
+
symbol = info.metadata?.symbol || null
|
|
241
|
+
} catch {
|
|
242
|
+
// 不是 mint
|
|
243
|
+
}
|
|
244
|
+
if (!symbol && metadata) {
|
|
245
|
+
try {
|
|
246
|
+
symbol = parseMetaplexMetadata(metadata.data).symbol || null
|
|
247
|
+
} catch {
|
|
248
|
+
// 元数据格式不对
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const ext = findExtension(mint.data, EXT_PERMANENT_DELEGATE)
|
|
252
|
+
const delegate = ext && ext.length >= 32 ? encodeAddress(ext.slice(0, 32)) : null
|
|
253
|
+
return { decimals, symbol, permanentDelegate: delegate && delegate !== ZERO_PUBKEY ? delegate : null }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// 撤销、关闭
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/** 一个账户要执行的指令(作为 batchCall 的一个调用,原子执行) */
|
|
261
|
+
export interface ApprovalCall {
|
|
262
|
+
/** revoke:撤销 delegate;close:关闭账户(有暂存的转账手续费时先收到 mint 上) */
|
|
263
|
+
kind: 'revoke' | 'close'
|
|
264
|
+
account: string
|
|
265
|
+
/** 要签名的地址(代币账户的 owner) */
|
|
266
|
+
owner: string
|
|
267
|
+
instructions: Instruction[]
|
|
268
|
+
approval: TokenAccountApproval
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 撤销授权的指令:每个有 delegate、没有冻结的账户一条 Revoke;其他的跳过 */
|
|
272
|
+
export function buildRevokes(list: readonly TokenAccountApproval[]): ApprovalCall[] {
|
|
273
|
+
return list
|
|
274
|
+
.filter((a) => a.revocable)
|
|
275
|
+
.map((a) => ({ kind: 'revoke', account: a.account, owner: a.owner, instructions: [revoke(a.account, a.owner, a.tokenProgram)], approval: a }))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface BuildClosesOptions {
|
|
279
|
+
/** 租金(和 Wrapped SOL 包装的 SOL)退给谁,默认账户的 owner */
|
|
280
|
+
destination?: string
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 关闭账户的指令:每个 closable 的账户一条 CloseAccount(关闭同时去掉授权);Token-2022 账户里有暂存的转账手续费时,
|
|
285
|
+
* 先加一条 HarvestWithheldTokensToMint(不需要签名)。关不掉的跳过。
|
|
286
|
+
* 注意 Wrapped SOL 账户有余额时也能关闭(余额作为 SOL 退回 destination):只想回收空账户时先按 amount 过滤
|
|
287
|
+
*/
|
|
288
|
+
export function buildCloses(list: readonly TokenAccountApproval[], options: BuildClosesOptions = {}): ApprovalCall[] {
|
|
289
|
+
if (options.destination !== undefined && !isAddress(options.destination)) {
|
|
290
|
+
throw new Error(`Invalid destination: ${options.destination}`)
|
|
291
|
+
}
|
|
292
|
+
return list
|
|
293
|
+
.filter((a) => a.closable)
|
|
294
|
+
.map((a) => ({
|
|
295
|
+
kind: 'close',
|
|
296
|
+
account: a.account,
|
|
297
|
+
owner: a.owner,
|
|
298
|
+
instructions: [
|
|
299
|
+
...(a.tokenProgram === TOKEN_2022_PROGRAM_ID && BigInt(a.withheldAmount) > 0n ? [harvestWithheldTokensToMint(a.mint, [a.account])] : []),
|
|
300
|
+
closeAccount(a.account, options.destination ?? a.owner, a.owner, a.tokenProgram),
|
|
301
|
+
],
|
|
302
|
+
approval: a,
|
|
303
|
+
}))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface ApprovalResult extends WriteResult {
|
|
307
|
+
kind: ApprovalCall['kind']
|
|
308
|
+
account: string
|
|
309
|
+
approval: TokenAccountApproval
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface CloseAccountsOptions extends BatchCallOptions, BuildClosesOptions {}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 撤销授权:构建(同 buildRevokes)后用 owner 签名发送,钱包和选项同 batchCall(多个账户打包进同一笔交易,feePayer 可以替 owner 付手续费)。
|
|
316
|
+
* 默认一个失败不影响后面的(continueOnError: true);结果与构建出的调用一一对应
|
|
317
|
+
*
|
|
318
|
+
* ```ts
|
|
319
|
+
* const list = await approvals(client, owner, { mints })
|
|
320
|
+
* await revokeApprovals(client, key, list)
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
export function revokeApprovals(client: SolanaClient, from: SignerInput, list: readonly TokenAccountApproval[], options: BatchCallOptions = {}): Promise<ApprovalResult[]> {
|
|
324
|
+
return send(client, from, buildRevokes(list), options)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 关闭代币账户回收租金(同 buildCloses),发送同 revokeApprovals。
|
|
329
|
+
*
|
|
330
|
+
* ```ts
|
|
331
|
+
* const empty = (await approvals(client, owner, { mints })).filter((a) => a.amount === '0')
|
|
332
|
+
* await closeTokenAccounts(client, key, empty) // 租金退回 owner
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
export function closeTokenAccounts(client: SolanaClient, from: SignerInput, list: readonly TokenAccountApproval[], options: CloseAccountsOptions = {}): Promise<ApprovalResult[]> {
|
|
336
|
+
const { destination, ...rest } = options
|
|
337
|
+
return send(client, from, buildCloses(list, { destination }), rest)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function send(client: SolanaClient, from: SignerInput, calls: readonly ApprovalCall[], options: BatchCallOptions): Promise<ApprovalResult[]> {
|
|
341
|
+
if (!calls.length) return []
|
|
342
|
+
// 发送的代码按需加载:只查授权时打包不会带上
|
|
343
|
+
const [{ batchCall }, { resolveSigner }] = await Promise.all([import('./write/batch.js'), import('./write/signer.js')])
|
|
344
|
+
const { address } = await resolveSigner(from)
|
|
345
|
+
const other = calls.find((c) => c.owner !== address)
|
|
346
|
+
if (other) {
|
|
347
|
+
throw new Error(`Token account ${other.account} belongs to ${other.owner}, not the signer ${address}`)
|
|
348
|
+
}
|
|
349
|
+
const results = await batchCall(
|
|
350
|
+
client,
|
|
351
|
+
calls.map((c) => ({ from, instructions: c.instructions })),
|
|
352
|
+
{ continueOnError: true, ...options },
|
|
353
|
+
)
|
|
354
|
+
return results.map((r, i) => {
|
|
355
|
+
const call = calls[i] as ApprovalCall
|
|
356
|
+
return { ...r, kind: call.kind, account: call.account, approval: call.approval }
|
|
357
|
+
})
|
|
358
|
+
}
|
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/layout.ts
CHANGED
|
@@ -134,8 +134,8 @@ export function parseMint(data: Uint8Array): MintInfo {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null */
|
|
138
|
-
function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
|
|
137
|
+
/** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null(包内使用,不从主入口导出) */
|
|
138
|
+
export function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
|
|
139
139
|
if (data.length <= EXTENSIONS_OFFSET) {
|
|
140
140
|
return null
|
|
141
141
|
}
|