@w3lib/solana-batch-call 0.5.0 → 0.7.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 +47 -0
- package/README.md +129 -9
- package/dist/accountReader.d.ts +57 -0
- package/dist/accountReader.d.ts.map +1 -0
- package/dist/accountReader.js +149 -0
- package/dist/accountReader.js.map +1 -0
- package/dist/address.d.ts +9 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +36 -6
- package/dist/address.js.map +1 -1
- package/dist/approvals.d.ts +5 -3
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +25 -36
- package/dist/approvals.js.map +1 -1
- package/dist/balances.d.ts +178 -0
- package/dist/balances.d.ts.map +1 -0
- package/dist/balances.js +524 -0
- package/dist/balances.js.map +1 -0
- package/dist/batchPlan.d.ts +52 -0
- package/dist/batchPlan.d.ts.map +1 -0
- package/dist/batchPlan.js +64 -0
- package/dist/batchPlan.js.map +1 -0
- package/dist/bytes.d.ts +31 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +74 -0
- package/dist/bytes.js.map +1 -0
- package/dist/claim.d.ts +163 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +197 -0
- package/dist/claim.js.map +1 -0
- package/dist/client.d.ts +26 -41
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +41 -986
- package/dist/client.js.map +1 -1
- package/dist/clientConfig.d.ts +11 -0
- package/dist/clientConfig.d.ts.map +1 -0
- package/dist/clientConfig.js +3 -0
- package/dist/clientConfig.js.map +1 -0
- package/dist/decode.d.ts +22 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +160 -0
- package/dist/decode.js.map +1 -0
- package/dist/describe.d.ts +26 -0
- package/dist/describe.d.ts.map +1 -0
- package/dist/describe.js +58 -0
- package/dist/describe.js.map +1 -0
- package/dist/fees.d.ts.map +1 -1
- package/dist/fees.js +2 -12
- package/dist/fees.js.map +1 -1
- package/dist/history.d.ts +125 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +220 -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/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +21 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +50 -69
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/common.js +2 -2
- package/dist/liquidity/common.js.map +1 -1
- package/dist/liquidity/orca.d.ts +6 -0
- package/dist/liquidity/orca.d.ts.map +1 -1
- package/dist/liquidity/orca.js +14 -3
- package/dist/liquidity/orca.js.map +1 -1
- package/dist/liquidity/raydiumClmm.d.ts +6 -0
- package/dist/liquidity/raydiumClmm.d.ts.map +1 -1
- package/dist/liquidity/raydiumClmm.js +12 -5
- package/dist/liquidity/raydiumClmm.js.map +1 -1
- package/dist/lp.d.ts +55 -12
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +158 -70
- 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/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +3 -8
- package/dist/portfolio.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 +192 -0
- package/dist/reclaim.d.ts.map +1 -0
- package/dist/reclaim.js +399 -0
- package/dist/reclaim.js.map +1 -0
- package/dist/risk.js +2 -2
- package/dist/risk.js.map +1 -1
- package/dist/rpc.d.ts +30 -7
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +404 -111
- 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 +395 -0
- package/dist/simulate.js.map +1 -0
- package/dist/source.d.ts +8 -4
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +11 -4
- package/dist/source.js.map +1 -1
- package/dist/subpaths/approvals.d.ts +3 -3
- package/dist/subpaths/approvals.d.ts.map +1 -1
- package/dist/subpaths/approvals.js.map +1 -1
- 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/swap/index.js +3 -3
- package/dist/swap/index.js.map +1 -1
- package/dist/tokenDetails.d.ts +12 -0
- package/dist/tokenDetails.d.ts.map +1 -0
- package/dist/tokenDetails.js +196 -0
- package/dist/tokenDetails.js.map +1 -0
- package/dist/tokenMeta.d.ts +63 -0
- package/dist/tokenMeta.d.ts.map +1 -0
- package/dist/tokenMeta.js +226 -0
- package/dist/tokenMeta.js.map +1 -0
- package/dist/tradeCheck.js +3 -3
- package/dist/tradeCheck.js.map +1 -1
- package/dist/util.d.ts +22 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +107 -0
- package/dist/util.js.map +1 -0
- package/dist/write/chain.d.ts +51 -0
- package/dist/write/chain.d.ts.map +1 -0
- package/dist/write/chain.js +184 -0
- package/dist/write/chain.js.map +1 -0
- package/dist/write/collect.js +2 -2
- package/dist/write/collect.js.map +1 -1
- package/dist/write/errors.d.ts +10 -0
- package/dist/write/errors.d.ts.map +1 -0
- package/dist/write/errors.js +77 -0
- package/dist/write/errors.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/dist/write/pack.d.ts +28 -0
- package/dist/write/pack.d.ts.map +1 -0
- package/dist/write/pack.js +125 -0
- package/dist/write/pack.js.map +1 -0
- package/dist/write/send.d.ts +23 -9
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js +53 -351
- package/dist/write/send.js.map +1 -1
- package/dist/write/tracker.d.ts +24 -0
- package/dist/write/tracker.d.ts.map +1 -0
- package/dist/write/tracker.js +129 -0
- package/dist/write/tracker.js.map +1 -0
- package/package.json +21 -1
- package/src/accountReader.ts +187 -0
- package/src/address.ts +41 -6
- package/src/approvals.ts +36 -44
- package/src/balances.ts +678 -0
- package/src/batchPlan.ts +117 -0
- package/src/bytes.ts +89 -0
- package/src/claim.ts +327 -0
- package/src/client.ts +59 -1239
- package/src/clientConfig.ts +12 -0
- package/src/decode.ts +176 -0
- package/src/describe.ts +77 -0
- package/src/fees.ts +2 -9
- package/src/history.ts +325 -0
- package/src/historyHelius.ts +136 -0
- package/src/index.ts +13 -1
- package/src/layout.ts +63 -82
- package/src/liquidity/common.ts +2 -2
- package/src/liquidity/orca.ts +15 -5
- package/src/liquidity/raydiumClmm.ts +13 -5
- package/src/lp.ts +226 -75
- package/src/lpMath.ts +38 -0
- package/src/portfolio.ts +8 -11
- package/src/programs.ts +216 -0
- package/src/reclaim.ts +550 -0
- package/src/risk.ts +2 -2
- package/src/rpc.ts +476 -118
- package/src/simulate.ts +517 -0
- package/src/source.ts +14 -5
- package/src/subpaths/approvals.ts +3 -2
- package/src/subpaths/claim.ts +54 -0
- package/src/subpaths/history.ts +38 -0
- package/src/subpaths/lp.ts +5 -1
- package/src/subpaths/reclaim.ts +58 -0
- package/src/subpaths/simulate.ts +37 -0
- package/src/swap/index.ts +3 -3
- package/src/tokenDetails.ts +240 -0
- package/src/tokenMeta.ts +270 -0
- package/src/tradeCheck.ts +3 -3
- package/src/util.ts +110 -0
- package/src/write/chain.ts +218 -0
- package/src/write/collect.ts +2 -2
- package/src/write/errors.ts +82 -0
- package/src/write/instructions.ts +17 -0
- package/src/write/pack.ts +142 -0
- package/src/write/send.ts +74 -407
- package/src/write/tracker.ts +152 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Helius 增强交易历史数据源(需要应用自己的 API key):把 `/v0/addresses/{address}/transactions` 的结果整理成和 rpcHistory 一样的 Activity */
|
|
2
|
+
import { NATIVE_MINT, SOL_DECIMALS } from './constants.js'
|
|
3
|
+
import { formatUnits } from './layout.js'
|
|
4
|
+
import type { Activity, ActivityChange, ActivityType, HistorySource } from './history.js'
|
|
5
|
+
|
|
6
|
+
export interface HeliusHistoryOptions {
|
|
7
|
+
apiKey: string
|
|
8
|
+
/** 默认 https://api.helius.xyz */
|
|
9
|
+
baseUrl?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface HeliusItem {
|
|
13
|
+
signature: string
|
|
14
|
+
timestamp?: number
|
|
15
|
+
slot?: number
|
|
16
|
+
fee?: number
|
|
17
|
+
feePayer?: string
|
|
18
|
+
type?: string
|
|
19
|
+
source?: string
|
|
20
|
+
description?: string
|
|
21
|
+
transactionError?: unknown
|
|
22
|
+
nativeTransfers?: Array<{ fromUserAccount: string; toUserAccount: string; amount: number }>
|
|
23
|
+
tokenTransfers?: Array<{ fromUserAccount: string | null; toUserAccount: string | null; tokenAmount: number; mint: string }>
|
|
24
|
+
accountData?: Array<{
|
|
25
|
+
account: string
|
|
26
|
+
nativeBalanceChange?: number
|
|
27
|
+
tokenBalanceChanges?: Array<{ userAccount: string; mint: string; rawTokenAmount: { tokenAmount: string; decimals: number } }>
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
|
|
32
|
+
|
|
33
|
+
/** Helius 的交易类型 → Activity 类型(认不出的按资产变化判断) */
|
|
34
|
+
function typeOf(item: HeliusItem, changes: readonly ActivityChange[]): ActivityType {
|
|
35
|
+
const t = item.type ?? ''
|
|
36
|
+
if (/SWAP/.test(t)) return 'swap'
|
|
37
|
+
if (t === 'TRANSFER') return 'transfer'
|
|
38
|
+
if (/LIQUIDITY|POSITION|POOL/.test(t)) return 'liquidity'
|
|
39
|
+
if (/MINT/.test(t)) return 'mint'
|
|
40
|
+
if (/APPROVE|REVOKE|DELEGATE/.test(t)) return 'approve'
|
|
41
|
+
const out = changes.filter((c) => c.delta.startsWith('-'))
|
|
42
|
+
if (out.length && changes.some((c) => !c.delta.startsWith('-') && out.some((o) => o.asset !== c.asset))) return 'swap'
|
|
43
|
+
return t && t !== 'UNKNOWN' ? 'contract' : 'other'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function activityFromHelius(address: string, item: HeliusItem): Activity {
|
|
47
|
+
const me = item.accountData?.find((a) => a.account === address)
|
|
48
|
+
const isPayer = item.feePayer === address
|
|
49
|
+
const fee = BigInt(item.fee ?? 0)
|
|
50
|
+
let native = BigInt(me?.nativeBalanceChange ?? 0) + (isPayer ? fee : 0n)
|
|
51
|
+
const tokens = new Map<string, { delta: bigint; decimals: number }>()
|
|
52
|
+
for (const entry of item.accountData ?? []) {
|
|
53
|
+
for (const t of entry.tokenBalanceChanges ?? []) {
|
|
54
|
+
if (t.userAccount !== address) continue
|
|
55
|
+
const prev = tokens.get(t.mint)
|
|
56
|
+
tokens.set(t.mint, { delta: (prev?.delta ?? 0n) + BigInt(t.rawTokenAmount.tokenAmount), decimals: t.rawTokenAmount.decimals })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const wsol = tokens.get(NATIVE_MINT)
|
|
60
|
+
if (wsol) {
|
|
61
|
+
native += wsol.delta
|
|
62
|
+
tokens.delete(NATIVE_MINT)
|
|
63
|
+
}
|
|
64
|
+
const failed = item.transactionError !== null && item.transactionError !== undefined
|
|
65
|
+
const changes: ActivityChange[] = []
|
|
66
|
+
if (!failed) {
|
|
67
|
+
if (native !== 0n) changes.push({ asset: 'native', symbol: 'SOL', decimals: SOL_DECIMALS, delta: native.toString(), formatted: formatUnits(native, SOL_DECIMALS) })
|
|
68
|
+
for (const [asset, { delta, decimals }] of tokens) {
|
|
69
|
+
if (delta !== 0n) changes.push({ asset, symbol: short(asset), decimals, delta: delta.toString(), formatted: formatUnits(delta, decimals) })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const type = typeOf(item, changes)
|
|
73
|
+
let counterparty: string | undefined
|
|
74
|
+
for (const t of [...(item.nativeTransfers ?? []), ...(item.tokenTransfers ?? []).map((x) => ({ fromUserAccount: x.fromUserAccount ?? '', toUserAccount: x.toUserAccount ?? '' }))]) {
|
|
75
|
+
if (t.fromUserAccount === address && t.toUserAccount) counterparty = t.toUserAccount
|
|
76
|
+
else if (t.toUserAccount === address && t.fromUserAccount) counterparty = t.fromUserAccount
|
|
77
|
+
if (counterparty) break
|
|
78
|
+
}
|
|
79
|
+
const negative = changes.filter((c) => c.delta.startsWith('-')).length
|
|
80
|
+
const protocol = item.source && !/^(SYSTEM_PROGRAM|UNKNOWN)$/.test(item.source) ? item.source[0] + item.source.slice(1).toLowerCase().replace(/_/g, ' ') : undefined
|
|
81
|
+
return {
|
|
82
|
+
hash: item.signature,
|
|
83
|
+
time: item.timestamp ?? 0,
|
|
84
|
+
block: item.slot,
|
|
85
|
+
status: failed ? 'failed' : 'success',
|
|
86
|
+
type,
|
|
87
|
+
direction: (type === 'transfer' || type === 'other') && changes.length ? (negative === 0 ? 'in' : negative === changes.length ? 'out' : undefined) : undefined,
|
|
88
|
+
counterparty,
|
|
89
|
+
label: item.description || undefined,
|
|
90
|
+
changes,
|
|
91
|
+
...(isPayer ? { fee: fee.toString() } : {}),
|
|
92
|
+
protocol,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Helius 增强交易历史(`GET /v0/addresses/{address}/transactions`,按 before 签名翻页)。apiKey 由应用传入,库里不带。
|
|
98
|
+
* 按交易类型过滤时翻页最多再取 3 页
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* await getHistory(address, { source: [heliusHistory({ apiKey }), rpcHistory()] }) // Helius 失败时用节点
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function heliusHistory(options: HeliusHistoryOptions): HistorySource {
|
|
105
|
+
if (!options.apiKey) throw new Error('heliusHistory needs an apiKey')
|
|
106
|
+
const base = (options.baseUrl ?? 'https://api.helius.xyz').replace(/\/+$/, '')
|
|
107
|
+
return {
|
|
108
|
+
name: 'helius',
|
|
109
|
+
async getHistory(ctx) {
|
|
110
|
+
const items: Activity[] = []
|
|
111
|
+
let cursor = ctx.cursor
|
|
112
|
+
let more = true
|
|
113
|
+
for (let page = 0; page < (ctx.types ? 4 : 1) && items.length < ctx.limit && more; page++) {
|
|
114
|
+
const url = `${base}/v0/addresses/${ctx.address}/transactions?api-key=${encodeURIComponent(options.apiKey)}&limit=${Math.min(100, ctx.limit)}${cursor ? `&before=${encodeURIComponent(cursor)}` : ''}`
|
|
115
|
+
const res = await ctx.fetch(url, { signal: ctx.signal })
|
|
116
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
117
|
+
const raw = (await res.json()) as HeliusItem[]
|
|
118
|
+
if (!Array.isArray(raw)) throw new Error('Unexpected response')
|
|
119
|
+
more = raw.length >= Math.min(100, ctx.limit)
|
|
120
|
+
for (const item of raw) {
|
|
121
|
+
cursor = item.signature
|
|
122
|
+
const t = item.timestamp ?? 0
|
|
123
|
+
if (ctx.before !== undefined && t >= ctx.before) continue
|
|
124
|
+
if (ctx.after !== undefined && t < ctx.after) {
|
|
125
|
+
more = false
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
const activity = activityFromHelius(ctx.address, item)
|
|
129
|
+
if (!ctx.types || ctx.types.includes(activity.type)) items.push(activity)
|
|
130
|
+
if (items.length >= ctx.limit) break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { items, ...(more && cursor ? { nextCursor: cursor } : {}) }
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
type AccountsOptions,
|
|
7
7
|
type BalanceQuery,
|
|
8
8
|
type BalancesOptions,
|
|
9
|
+
type BalancesQueryOptions,
|
|
9
10
|
type ClientConfig,
|
|
10
11
|
type OwnerTokensOptions,
|
|
11
12
|
type PersistTokenMetaOptions,
|
|
@@ -64,7 +65,17 @@ export {
|
|
|
64
65
|
clusterOfGenesis,
|
|
65
66
|
type Cluster,
|
|
66
67
|
} from './constants.js'
|
|
67
|
-
export {
|
|
68
|
+
export {
|
|
69
|
+
InvalidAddressError,
|
|
70
|
+
createProgramAddress,
|
|
71
|
+
decodeAddress,
|
|
72
|
+
encodeAddress,
|
|
73
|
+
findProgramAddress,
|
|
74
|
+
getAssociatedTokenAddress,
|
|
75
|
+
getMetadataAddress,
|
|
76
|
+
isAddress,
|
|
77
|
+
isOnCurve,
|
|
78
|
+
} from './address.js'
|
|
68
79
|
export {
|
|
69
80
|
formatAmount,
|
|
70
81
|
formatUnits,
|
|
@@ -94,6 +105,7 @@ export {
|
|
|
94
105
|
type FallbackOptions,
|
|
95
106
|
type HttpRpcOptions,
|
|
96
107
|
type RequestEvent,
|
|
108
|
+
type RpcRequestOptions,
|
|
97
109
|
type RpcTransport,
|
|
98
110
|
} from './rpc.js'
|
|
99
111
|
export { NetworkCheckedRpc, NetworkMismatchError, type RpcSource, type SourceOptions } from './source.js'
|
package/src/layout.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bytes } from './bytes.js'
|
|
2
2
|
|
|
3
3
|
/** getMultipleAccounts 解析后的账户 */
|
|
4
4
|
export interface AccountInfo {
|
|
@@ -10,80 +10,6 @@ export interface AccountInfo {
|
|
|
10
10
|
executable: boolean
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
class Reader {
|
|
14
|
-
#offset: number
|
|
15
|
-
readonly #view: DataView
|
|
16
|
-
|
|
17
|
-
constructor(
|
|
18
|
-
readonly bytes: Uint8Array,
|
|
19
|
-
offset = 0,
|
|
20
|
-
) {
|
|
21
|
-
this.#offset = offset
|
|
22
|
-
this.#view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
get offset(): number {
|
|
26
|
-
return this.#offset
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
#need(n: number): void {
|
|
30
|
-
if (this.#offset + n > this.bytes.length) {
|
|
31
|
-
throw new RangeError('Account data is shorter than expected')
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
skip(n: number): void {
|
|
36
|
-
this.#need(n)
|
|
37
|
-
this.#offset += n
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
u8(): number {
|
|
41
|
-
this.#need(1)
|
|
42
|
-
return this.bytes[this.#offset++] as number
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
u16(): number {
|
|
46
|
-
this.#need(2)
|
|
47
|
-
const v = this.#view.getUint16(this.#offset, true)
|
|
48
|
-
this.#offset += 2
|
|
49
|
-
return v
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
u32(): number {
|
|
53
|
-
this.#need(4)
|
|
54
|
-
const v = this.#view.getUint32(this.#offset, true)
|
|
55
|
-
this.#offset += 4
|
|
56
|
-
return v
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
u64(): bigint {
|
|
60
|
-
this.#need(8)
|
|
61
|
-
const v = this.#view.getBigUint64(this.#offset, true)
|
|
62
|
-
this.#offset += 8
|
|
63
|
-
return v
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
pubkey(): string {
|
|
67
|
-
this.#need(32)
|
|
68
|
-
const v = encodeAddress(this.bytes.slice(this.#offset, this.#offset + 32))
|
|
69
|
-
this.#offset += 32
|
|
70
|
-
return v
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** borsh string:u32 长度 + UTF-8;去掉 Metaplex 用来补齐长度的 \0 */
|
|
74
|
-
string(): string {
|
|
75
|
-
const len = this.u32()
|
|
76
|
-
this.#need(len)
|
|
77
|
-
const v = new TextDecoder().decode(this.bytes.slice(this.#offset, this.#offset + len))
|
|
78
|
-
this.#offset += len
|
|
79
|
-
return v.replace(/\0+$/, '').trim()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
option<T>(read: () => T): T | null {
|
|
83
|
-
return this.u8() === 1 ? read() : null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
13
|
export interface MintInfo {
|
|
88
14
|
mintAuthority: string | null
|
|
89
15
|
supply: bigint
|
|
@@ -116,7 +42,7 @@ export function parseMint(data: Uint8Array): MintInfo {
|
|
|
116
42
|
if (data.length !== MINT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT)) {
|
|
117
43
|
throw new RangeError('Not a mint account')
|
|
118
44
|
}
|
|
119
|
-
const r = new
|
|
45
|
+
const r = new Bytes(data)
|
|
120
46
|
const hasMintAuthority = r.u32() === 1
|
|
121
47
|
const mintAuthority = r.pubkey()
|
|
122
48
|
const supply = r.u64()
|
|
@@ -140,7 +66,7 @@ export function findExtension(data: Uint8Array, extensionType: number): Uint8Arr
|
|
|
140
66
|
return null
|
|
141
67
|
}
|
|
142
68
|
try {
|
|
143
|
-
const r = new
|
|
69
|
+
const r = new Bytes(data, EXTENSIONS_OFFSET)
|
|
144
70
|
while (r.offset + 4 <= data.length) {
|
|
145
71
|
const type = r.u16()
|
|
146
72
|
const length = r.u16()
|
|
@@ -167,7 +93,7 @@ function parseTokenMetadataExtension(data: Uint8Array): TokenMetadata | null {
|
|
|
167
93
|
return null
|
|
168
94
|
}
|
|
169
95
|
try {
|
|
170
|
-
const m = new
|
|
96
|
+
const m = new Bytes(ext)
|
|
171
97
|
m.pubkey() // updateAuthority
|
|
172
98
|
m.pubkey() // mint
|
|
173
99
|
return { name: m.string(), symbol: m.string(), uri: m.string() }
|
|
@@ -212,7 +138,7 @@ export function parseTransferFeeConfig(data: Uint8Array): TransferFeeConfigInfo
|
|
|
212
138
|
return null
|
|
213
139
|
}
|
|
214
140
|
try {
|
|
215
|
-
const r = new
|
|
141
|
+
const r = new Bytes(ext)
|
|
216
142
|
// OptionalNonZeroPubkey:全 0 表示没有
|
|
217
143
|
const configAuthority = r.pubkey()
|
|
218
144
|
const withdrawAuthority = r.pubkey()
|
|
@@ -238,15 +164,70 @@ export interface TokenAccountInfo {
|
|
|
238
164
|
amount: bigint
|
|
239
165
|
}
|
|
240
166
|
|
|
167
|
+
function isTokenAccountData(data: Uint8Array): boolean {
|
|
168
|
+
return data.length === ACCOUNT_SIZE || (data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_ACCOUNT)
|
|
169
|
+
}
|
|
170
|
+
|
|
241
171
|
/** 解析代币账户(SPL Token / Token-2022 前 72 字节相同) */
|
|
242
172
|
export function parseTokenAccount(data: Uint8Array): TokenAccountInfo {
|
|
243
|
-
if (
|
|
173
|
+
if (!isTokenAccountData(data)) {
|
|
244
174
|
throw new RangeError('Not a token account')
|
|
245
175
|
}
|
|
246
|
-
const r = new
|
|
176
|
+
const r = new Bytes(data)
|
|
247
177
|
return { mint: r.pubkey(), owner: r.pubkey(), amount: r.u64() }
|
|
248
178
|
}
|
|
249
179
|
|
|
180
|
+
/** 代币账户的完整基础布局(前 165 字节,SPL Token / Token-2022 相同) */
|
|
181
|
+
export interface TokenAccountState extends TokenAccountInfo {
|
|
182
|
+
delegate: string | null
|
|
183
|
+
/** 0 未初始化、1 正常、2 冻结 */
|
|
184
|
+
state: number
|
|
185
|
+
frozen: boolean
|
|
186
|
+
/** Wrapped SOL 账户(is_native 有值) */
|
|
187
|
+
native: boolean
|
|
188
|
+
delegatedAmount: bigint
|
|
189
|
+
closeAuthority: string | null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 解析代币账户的完整基础布局;不是代币账户或未初始化时为 null(包内使用:/approvals、/simulate) */
|
|
193
|
+
export function parseTokenAccountState(data: Uint8Array): TokenAccountState | null {
|
|
194
|
+
if (!isTokenAccountData(data)) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
const b = new Bytes(data)
|
|
198
|
+
const state = b.u8(108)
|
|
199
|
+
if (state === 0) {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
// COption:u32 标记(1 有、0 没有)+ 值
|
|
203
|
+
return {
|
|
204
|
+
mint: b.key(0),
|
|
205
|
+
owner: b.key(32),
|
|
206
|
+
amount: b.u64(64),
|
|
207
|
+
delegate: b.u32(72) === 1 ? b.key(76) : null,
|
|
208
|
+
state,
|
|
209
|
+
frozen: state === 2,
|
|
210
|
+
native: b.u32(109) === 1,
|
|
211
|
+
delegatedAmount: b.u64(121),
|
|
212
|
+
closeAuthority: b.u32(129) === 1 ? b.key(133) : null,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 计算预算指令里的计算单元上限(SetComputeUnitLimit)和单价(SetComputeUnitPrice,micro-lamports);没有时 limit 为 null、price 为 0 */
|
|
217
|
+
export function computeBudgetOf(instructions: Iterable<{ programId: string; data: Uint8Array }>): { limit: number | null; price: bigint } {
|
|
218
|
+
let limit: number | null = null
|
|
219
|
+
let price = 0n
|
|
220
|
+
for (const ix of instructions) {
|
|
221
|
+
if (ix.programId !== COMPUTE_BUDGET_PROGRAM) continue
|
|
222
|
+
const b = new Bytes(ix.data)
|
|
223
|
+
if (ix.data[0] === 2 && ix.data.length >= 5) limit = b.u32(1)
|
|
224
|
+
if (ix.data[0] === 3 && ix.data.length >= 9) price = b.u64(1)
|
|
225
|
+
}
|
|
226
|
+
return { limit, price }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111'
|
|
230
|
+
|
|
250
231
|
export interface MetaplexMetadata extends TokenMetadata {
|
|
251
232
|
updateAuthority: string
|
|
252
233
|
mint: string
|
|
@@ -261,7 +242,7 @@ export interface MetaplexMetadata extends TokenMetadata {
|
|
|
261
242
|
|
|
262
243
|
/** 解析 Metaplex Token Metadata 账户;老版本账户缺少后面的字段时,缺的部分为默认值 */
|
|
263
244
|
export function parseMetaplexMetadata(data: Uint8Array): MetaplexMetadata {
|
|
264
|
-
const r = new
|
|
245
|
+
const r = new Bytes(data)
|
|
265
246
|
if (r.u8() !== 4) {
|
|
266
247
|
throw new RangeError('Not a Metaplex metadata account') // Key::MetadataV1 = 4
|
|
267
248
|
}
|
package/src/liquidity/common.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
2
|
|
|
3
|
-
import { encodeAddress, getAssociatedTokenAddress, isAddress } from '../address.js'
|
|
3
|
+
import { encodeAddress, getAssociatedTokenAddress, InvalidAddressError, isAddress } from '../address.js'
|
|
4
4
|
import type { SolanaClient } from '../client.js'
|
|
5
5
|
import { NATIVE_MINT, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
|
|
6
6
|
import { formatUnits, parseMint, parseTokenAccount, parseTransferFeeConfig, parseUnits, type AccountInfo } from '../layout.js'
|
|
@@ -25,7 +25,7 @@ export interface Ctx {
|
|
|
25
25
|
export function createCtx(client: SolanaClient, owner: string, options: SolanaLiquidityOptions): Ctx {
|
|
26
26
|
for (const address of [owner, options.recipient ?? owner]) {
|
|
27
27
|
if (!isAddress(address)) {
|
|
28
|
-
throw new
|
|
28
|
+
throw new InvalidAddressError(`Invalid address: ${address}`, address)
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const slippage = options.slippage ?? 0.005
|
package/src/liquidity/orca.ts
CHANGED
|
@@ -223,6 +223,20 @@ export async function orcaAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams |
|
|
|
223
223
|
})
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭仓位账户和 NFT 的代币账户(nftAccount,默认 owner 的 ATA),租金退给 receiver。
|
|
228
|
+
* NFT 是 Token-2022(带元数据扩展)时连 mint 一起关闭,否则(Metaplex 时代的 SPL Token NFT)mint 留着
|
|
229
|
+
*/
|
|
230
|
+
export function orcaClosePosition(owner: string, receiver: string, positionMint: string, nftProgram: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
|
|
231
|
+
const position = positionAddress(positionMint)
|
|
232
|
+
const token2022 = nftProgram === TOKEN_2022_PROGRAM_ID
|
|
233
|
+
return {
|
|
234
|
+
programId: PROGRAM,
|
|
235
|
+
keys: [r(owner, true), w(receiver), w(position), w(positionMint), w(nftAccount), r(nftProgram)],
|
|
236
|
+
data: anchorIx(token2022 ? 'close_position_with_token_extensions' : 'close_position'),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
226
240
|
/** 移除(decrease + 领取手续费和奖励)/ 只领取(collectOnly) */
|
|
227
241
|
export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
|
|
228
242
|
const mint = refMint(params.position)
|
|
@@ -304,11 +318,7 @@ export async function orcaRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams,
|
|
|
304
318
|
})
|
|
305
319
|
})
|
|
306
320
|
if (burn) {
|
|
307
|
-
main.push(
|
|
308
|
-
nftProgram === TOKEN_2022_PROGRAM_ID
|
|
309
|
-
? { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_2022_PROGRAM_ID)], data: anchorIx('close_position_with_token_extensions') }
|
|
310
|
-
: { programId: PROGRAM, keys: [r(ctx.owner, true), w(ctx.owner), w(positionAccount), w(mint), w(nftAccount), r(TOKEN_PROGRAM_ID)], data: anchorIx('close_position') },
|
|
311
|
-
)
|
|
321
|
+
main.push(orcaClosePosition(ctx.owner, ctx.owner, mint, nftProgram))
|
|
312
322
|
}
|
|
313
323
|
return makePlan(ctx, {
|
|
314
324
|
action: collectOnly ? 'collect' : 'remove',
|
|
@@ -249,6 +249,18 @@ export async function raydiumAdd(ctx: Ctx, params: SolanaIncreaseLiquidityParams
|
|
|
249
249
|
})
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* 关闭已清空的仓位(流动性、手续费、奖励都为 0):销毁仓位 NFT、关闭 NFT 的代币账户和仓位账户,租金退给 owner(程序固定,不能指定别人)。
|
|
254
|
+
* pool 是仓位所在的池子(官方 SDK 也带着它)
|
|
255
|
+
*/
|
|
256
|
+
export function raydiumClosePosition(owner: string, positionMint: string, nftProgram: string, pool: string, nftAccount = getAssociatedTokenAddress(owner, positionMint, nftProgram)): Instruction {
|
|
257
|
+
return {
|
|
258
|
+
programId: PROGRAM,
|
|
259
|
+
keys: [w(owner, true), w(positionMint), w(nftAccount), w(personalPositionAddress(positionMint)), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool)],
|
|
260
|
+
data: anchorIx('close_position'),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
252
264
|
/** 移除(liquidity > 0)/ 只领取手续费和奖励(collectOnly:流动性为 0 的 decrease) */
|
|
253
265
|
export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParams, collectOnly = false): Promise<SolanaLiquidityPlan> {
|
|
254
266
|
const mint = refMint(params.position)
|
|
@@ -308,11 +320,7 @@ export async function raydiumRemove(ctx: Ctx, params: SolanaRemoveLiquidityParam
|
|
|
308
320
|
}
|
|
309
321
|
const main: Instruction[] = [decrease]
|
|
310
322
|
if (burn) {
|
|
311
|
-
main.push(
|
|
312
|
-
programId: PROGRAM,
|
|
313
|
-
keys: [w(ctx.owner, true), w(mint), w(nftAta), w(personal), r(SYSTEM_PROGRAM_ID), r(nftProgram), w(pool.address)],
|
|
314
|
-
data: anchorIx('close_position'),
|
|
315
|
-
})
|
|
323
|
+
main.push(raydiumClosePosition(ctx.owner, mint, nftProgram, pool.address))
|
|
316
324
|
}
|
|
317
325
|
return makePlan(ctx, {
|
|
318
326
|
action: collectOnly ? 'collect' : 'remove',
|