@w3lib/solana-batch-call 0.3.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 +43 -0
- package/README.md +304 -3
- 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/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/index.d.ts +1 -1
- 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 +2 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -2
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/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 +53 -15
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +15 -38
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +45 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +189 -0
- package/dist/lpMath.js.map +1 -1
- package/dist/names.d.ts +68 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +257 -0
- package/dist/names.js.map +1 -0
- package/dist/pools.d.ts +9 -0
- package/dist/pools.d.ts.map +1 -0
- package/dist/pools.js +45 -0
- package/dist/pools.js.map +1 -0
- package/dist/portfolio.d.ts +64 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +57 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +90 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +287 -0
- package/dist/price.js.map +1 -0
- package/dist/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/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 +1 -1
- package/dist/subpaths/lp.d.ts.map +1 -1
- package/dist/subpaths/lp.js +1 -1
- package/dist/subpaths/lp.js.map +1 -1
- package/dist/subpaths/names.d.ts +37 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +27 -0
- package/dist/subpaths/names.js.map +1 -0
- package/dist/subpaths/price.d.ts +40 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +43 -0
- package/dist/subpaths/price.js.map +1 -0
- package/dist/subpaths/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 +41 -2
- package/src/address.ts +9 -0
- package/src/approvals.ts +358 -0
- package/src/fees.ts +259 -0
- package/src/index.ts +1 -1
- package/src/layout.ts +2 -2
- 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 +17 -52
- package/src/lpMath.ts +215 -0
- package/src/names.ts +329 -0
- package/src/pools.ts +43 -0
- package/src/portfolio.ts +124 -0
- package/src/price.ts +366 -0
- package/src/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/liquidity.ts +45 -0
- package/src/subpaths/lp.ts +9 -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/rpc.ts
CHANGED
|
@@ -62,6 +62,31 @@ const TRANSIENT = /\bbusy\b|overload|temporar(il)?y|unavailable|try again|timed?
|
|
|
62
62
|
|
|
63
63
|
/** 节点限制单次 getMultipleAccounts 的账户数(如 “Too many accounts requested”):缩小每次的账户数后重试,不是节点故障 */
|
|
64
64
|
const TOO_MANY_ACCOUNTS = /too many accounts/i
|
|
65
|
+
/**
|
|
66
|
+
* publicnode 对单次超过 10 个账户的 getMultipleAccounts 返回 HTTP 403 +“Request blocked”(2026-10,而且要等 3 秒多才返回):
|
|
67
|
+
* 账户数超过 BLOCKED_LIMIT 时直接按它拆分(逐次减半要多等好几轮),还被拒绝再减半;
|
|
68
|
+
* 账户数已经很少(≤ BLOCKED_FLOOR)还被拒绝时,是节点真的拒绝访问,交给上层换节点
|
|
69
|
+
*/
|
|
70
|
+
const REQUEST_BLOCKED = /request blocked/i
|
|
71
|
+
const BLOCKED_LIMIT = 10
|
|
72
|
+
const BLOCKED_FLOOR = 8
|
|
73
|
+
|
|
74
|
+
/** 从报错里学到的节点限制,按节点地址共享(同一个节点的多个客户端不用各自再试一遍) */
|
|
75
|
+
const learnedLimits = new Map<string, { methods: Map<string, number>; maxAccounts?: number }>()
|
|
76
|
+
|
|
77
|
+
function learnedFor(url: string): { methods: Map<string, number>; maxAccounts?: number } {
|
|
78
|
+
let entry = learnedLimits.get(url)
|
|
79
|
+
if (!entry) {
|
|
80
|
+
entry = { methods: new Map() }
|
|
81
|
+
learnedLimits.set(url, entry)
|
|
82
|
+
}
|
|
83
|
+
return entry
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 测试用:清空学到的节点限制 */
|
|
87
|
+
export function resetLearnedLimits(): void {
|
|
88
|
+
learnedLimits.clear()
|
|
89
|
+
}
|
|
65
90
|
|
|
66
91
|
/** HTTP 层错误(非 2xx、网络错误、超时) */
|
|
67
92
|
export class HttpError extends Error {
|
|
@@ -178,20 +203,19 @@ export class HttpRpc implements RpcTransport {
|
|
|
178
203
|
readonly #retries: number
|
|
179
204
|
readonly #fetch: typeof fetch
|
|
180
205
|
#maxBatchSize: number
|
|
181
|
-
/**
|
|
182
|
-
readonly #
|
|
206
|
+
/** 从报错里学到的限制(按节点地址共享):单个方法在一个批量里的数量(如 publicnode:getMultipleAccounts 最多 1 个)、单次的账户数 */
|
|
207
|
+
readonly #learned: { methods: Map<string, number>; maxAccounts?: number }
|
|
183
208
|
/** options.batch:调用方是否允许批量 */
|
|
184
209
|
readonly #batchOption: boolean
|
|
185
210
|
/** 节点明确表示不支持批量时,在这之前逐条请求(之后重新尝试批量) */
|
|
186
211
|
#batchDisabledUntil = 0
|
|
187
|
-
/** 节点限制单次 getMultipleAccounts 的账户数时学到的上限 */
|
|
188
|
-
#maxAccounts: number | undefined
|
|
189
212
|
#queue: Pending[] = []
|
|
190
213
|
#timer: ReturnType<typeof setTimeout> | null = null
|
|
191
214
|
|
|
192
215
|
constructor(url: string, options: HttpRpcOptions = {}) {
|
|
193
216
|
this.url = url
|
|
194
217
|
this.#safeUrl = safeOrigin(url)
|
|
218
|
+
this.#learned = learnedFor(url)
|
|
195
219
|
this.#timeout = options.timeout ?? 10_000
|
|
196
220
|
this.#headers = { 'content-type': 'application/json', ...options.headers }
|
|
197
221
|
this.#maxBatchSize = Math.max(1, options.maxBatchSize ?? 20)
|
|
@@ -214,7 +238,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
214
238
|
*/
|
|
215
239
|
async #getMultipleAccounts(params: readonly unknown[]): Promise<unknown> {
|
|
216
240
|
const [addresses, ...rest] = params as [unknown[], ...unknown[]]
|
|
217
|
-
const max = this.#maxAccounts
|
|
241
|
+
const max = this.#learned.maxAccounts
|
|
218
242
|
if (max !== undefined && addresses.length > max) {
|
|
219
243
|
const parts: unknown[][] = []
|
|
220
244
|
for (let i = 0; i < addresses.length; i += max) {
|
|
@@ -234,8 +258,11 @@ export class HttpRpc implements RpcTransport {
|
|
|
234
258
|
try {
|
|
235
259
|
return await this.#enqueue('getMultipleAccounts', params)
|
|
236
260
|
} catch (err) {
|
|
237
|
-
|
|
238
|
-
|
|
261
|
+
const tooMany = err instanceof RpcError && TOO_MANY_ACCOUNTS.test(err.message) && addresses.length > 1
|
|
262
|
+
const blocked = err instanceof RpcError && REQUEST_BLOCKED.test(err.message) && addresses.length > BLOCKED_FLOOR
|
|
263
|
+
if (tooMany || blocked) {
|
|
264
|
+
const next = blocked && addresses.length > BLOCKED_LIMIT ? BLOCKED_LIMIT : Math.floor(addresses.length / 2)
|
|
265
|
+
this.#learned.maxAccounts = Math.max(1, Math.min(this.#learned.maxAccounts ?? Number.POSITIVE_INFINITY, next))
|
|
239
266
|
return this.#getMultipleAccounts(params)
|
|
240
267
|
}
|
|
241
268
|
throw err
|
|
@@ -276,7 +303,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
276
303
|
let current: Pending[] = []
|
|
277
304
|
let counts = new Map<string, number>()
|
|
278
305
|
for (const item of items) {
|
|
279
|
-
const limit = this.#
|
|
306
|
+
const limit = this.#learned.methods.get(item.method)
|
|
280
307
|
const count = counts.get(item.method) ?? 0
|
|
281
308
|
if (current.length >= this.#maxBatchSize || (limit !== undefined && count >= limit)) {
|
|
282
309
|
chunks.push(current)
|
|
@@ -303,7 +330,7 @@ export class HttpRpc implements RpcTransport {
|
|
|
303
330
|
if (!method || !Number.isFinite(limit) || limit < 1 || items.filter((item) => item.method === method).length <= limit) {
|
|
304
331
|
return false
|
|
305
332
|
}
|
|
306
|
-
this.#
|
|
333
|
+
this.#learned.methods.set(method, limit)
|
|
307
334
|
await Promise.all(this.#split(items).map((chunk) => this.#dispatch(chunk)))
|
|
308
335
|
return true
|
|
309
336
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 授权管理(子路径 `@w3lib/solana-batch-call/approvals`):批量查地址的代币账户给出的授权(delegate)、可以关闭回收租金的空账户,
|
|
3
|
+
* 批量撤销 / 关闭。不放在主入口,不用时打包不会带上这部分代码;发送的代码按需加载。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { closeTokenAccounts, getApprovals, revokeApprovals } from '@w3lib/solana-batch-call/approvals'
|
|
7
|
+
*
|
|
8
|
+
* const list = await getApprovals(owner, { mints: [USDC, BONK] })
|
|
9
|
+
* await revokeApprovals(client, key, list)
|
|
10
|
+
* await closeTokenAccounts(client, key, list.filter((a) => a.amount === '0'))
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { approvals, type ApprovalsQuery, type TokenAccountApproval } from '../approvals.js'
|
|
14
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
approvals,
|
|
18
|
+
buildCloses,
|
|
19
|
+
buildRevokes,
|
|
20
|
+
closeTokenAccounts,
|
|
21
|
+
parseTokenAccountState,
|
|
22
|
+
revokeApprovals,
|
|
23
|
+
type ApprovalCall,
|
|
24
|
+
type ApprovalResult,
|
|
25
|
+
type ApprovalsQuery,
|
|
26
|
+
type BuildClosesOptions,
|
|
27
|
+
type CloseAccountsOptions,
|
|
28
|
+
type CloseBlocker,
|
|
29
|
+
type TokenAccountApproval,
|
|
30
|
+
} from '../approvals.js'
|
|
31
|
+
export { closeAccount, harvestWithheldTokensToMint, revoke } from '../write/instructions.js'
|
|
32
|
+
|
|
33
|
+
export interface GetApprovalsOptions extends ApprovalsQuery, ShortcutOptions {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 查地址的代币账户给出的授权和可以关闭的空账户,节点参数与 getBalances 相同。
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await getApprovals(owner, { mints: [USDC, BONK] }) // 指定代币:读 ATA,免费公共节点也能用
|
|
40
|
+
* await getApprovals(owner, { provider: 'https://my-rpc…' }) // 全部代币账户:需要支持 getTokenAccountsByOwner 的节点
|
|
41
|
+
* // [{ account, mint: USDC, symbol: 'USDC', delegate: 'GYFk…', delegatedAmount: '6993125', revocable: true, closable: false, closeBlocker: 'balance', … }]
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function getApprovals(owner: string, options: GetApprovalsOptions = {}): Promise<TokenAccountApproval[]> {
|
|
45
|
+
const { client, own } = resolve(options, ['mints', 'all', 'signal'])
|
|
46
|
+
return approvals(client, owner, own)
|
|
47
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 手续费估算(子路径 `@w3lib/solana-batch-call/fees`):三档优先费建议,一笔交易的花费(基础费 + 优先费 + 新建账户的押金)。
|
|
3
|
+
* 不放在主入口,不用时打包不会带上这部分代码;模拟交易的代码按需加载。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getCostEstimate, getFeeEstimate } from '@w3lib/solana-batch-call/fees'
|
|
7
|
+
*
|
|
8
|
+
* await getFeeEstimate({ accounts: [pool] })
|
|
9
|
+
* await getCostEstimate({ feePayer: owner, instructions })
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { estimateCost, estimateFees, type CostEstimate, type CostInput, type EstimateCostOptions, type EstimateFeesOptions, type PriorityFeeEstimate } from '../fees.js'
|
|
13
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
estimateCost,
|
|
17
|
+
estimateFees,
|
|
18
|
+
type CostEstimate,
|
|
19
|
+
type CostInput,
|
|
20
|
+
type CreatedAccount,
|
|
21
|
+
type EstimateCostOptions,
|
|
22
|
+
type EstimateFeesOptions,
|
|
23
|
+
type FeeLevel,
|
|
24
|
+
type PriorityFeeEstimate,
|
|
25
|
+
} from '../fees.js'
|
|
26
|
+
|
|
27
|
+
export interface GetFeeEstimateOptions extends EstimateFeesOptions, ShortcutOptions {}
|
|
28
|
+
|
|
29
|
+
/** 当前的三档优先费建议(micro-lamports / 计算单元),节点参数与 getBalances 相同 */
|
|
30
|
+
export function getFeeEstimate(options: GetFeeEstimateOptions = {}): Promise<PriorityFeeEstimate> {
|
|
31
|
+
const { client, own } = resolve(options, ['accounts', 'percentiles', 'signal'])
|
|
32
|
+
return estimateFees(client, own)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface GetCostEstimateOptions extends EstimateCostOptions, ShortcutOptions {}
|
|
36
|
+
|
|
37
|
+
/** 一笔交易的花费(基础费 + 优先费 + 新建账户的押金),模拟一次、不发送 */
|
|
38
|
+
export function getCostEstimate(input: CostInput, options: GetCostEstimateOptions = {}): Promise<CostEstimate> {
|
|
39
|
+
const { client, own } = resolve(options, ['percentiles', 'level', 'priorityFee', 'fees', 'signal'])
|
|
40
|
+
return estimateCost(client, input, own)
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 添加 / 移除流动性(子路径 `@w3lib/solana-batch-call/liquidity`):Raydium CLMM、Orca Whirlpool(仓位 NFT),
|
|
3
|
+
* Raydium AMM v4、CPMM(LP 代币)。不放在主入口,不用时打包不会带上这部分代码。不依赖各 DEX 的 SDK。
|
|
4
|
+
*
|
|
5
|
+
* - build*:只读取、不签名,返回预计的数量和按顺序执行的指令,可以当作报价,也可以自己组交易发送
|
|
6
|
+
* - addLiquidity / removeLiquidity / collectFees:构建后签名发送(同 /write 的 batchCall),SOL 自动包装 / 解包
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { getLpPositions } from '@w3lib/solana-batch-call/lp'
|
|
10
|
+
* import { addLiquidity, removeLiquidity } from '@w3lib/solana-batch-call/liquidity'
|
|
11
|
+
*
|
|
12
|
+
* await addLiquidity(client, secretKey, { protocol: 'raydium-clmm', pool, priceLower: 120, priceUpper: 180, amount0Formatted: '1' })
|
|
13
|
+
* const [position] = await getLpPositions(owner, { provider: rpc })
|
|
14
|
+
* await removeLiquidity(client, secretKey, { position, percent: 50 })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { addLiquidity, buildAddLiquidity, buildCollectFees, buildRemoveLiquidity, collectFees, removeLiquidity } from '../liquidity/index.js'
|
|
18
|
+
export type {
|
|
19
|
+
SolanaAddLiquidityParams,
|
|
20
|
+
SolanaCollectFeesParams,
|
|
21
|
+
SolanaIncreaseLiquidityParams,
|
|
22
|
+
SolanaLiquidityOptions,
|
|
23
|
+
SolanaLiquidityPlan,
|
|
24
|
+
SolanaLiquidityResult,
|
|
25
|
+
SolanaLiquiditySendOptions,
|
|
26
|
+
SolanaLiquidityStep,
|
|
27
|
+
SolanaLiquidityStepResult,
|
|
28
|
+
SolanaNewPositionParams,
|
|
29
|
+
SolanaPositionRef,
|
|
30
|
+
SolanaRemoveLiquidityParams,
|
|
31
|
+
} from '../liquidity/types.js'
|
|
32
|
+
export {
|
|
33
|
+
getLiquidityForAmounts,
|
|
34
|
+
getMaxAmounts,
|
|
35
|
+
getMinAmounts,
|
|
36
|
+
getMintAmounts,
|
|
37
|
+
nearestUsableTick,
|
|
38
|
+
priceToTick,
|
|
39
|
+
sqrtPriceAtTick,
|
|
40
|
+
tickAtSqrtPrice,
|
|
41
|
+
type ClmmProgram,
|
|
42
|
+
} from '../lpMath.js'
|
|
43
|
+
export type { SolanaLpPosition, SolanaLpProtocol, SolanaLpToken } from '../lp.js'
|
|
44
|
+
export type { Instruction } from '../write/transaction.js'
|
|
45
|
+
export type { SignerInput, WriteResult, WriteStatus } from '../subpaths/write.js'
|
package/src/subpaths/lp.ts
CHANGED
|
@@ -34,10 +34,19 @@ export {
|
|
|
34
34
|
getAmountsForLiquidity,
|
|
35
35
|
getFeeGrowthInside,
|
|
36
36
|
getFeesOwed,
|
|
37
|
+
getLiquidityForAmounts,
|
|
38
|
+
getMaxAmounts,
|
|
39
|
+
getMinAmounts,
|
|
40
|
+
getMintAmounts,
|
|
41
|
+
nearestUsableTick,
|
|
37
42
|
orcaSqrtPriceAtTick,
|
|
43
|
+
priceToTick,
|
|
38
44
|
raydiumSqrtPriceAtTick,
|
|
45
|
+
sqrtPriceAtTick,
|
|
39
46
|
sqrtPriceToPrice,
|
|
47
|
+
tickAtSqrtPrice,
|
|
40
48
|
tickToPrice,
|
|
49
|
+
type ClmmProgram,
|
|
41
50
|
type TickFeeData,
|
|
42
51
|
} from '../lpMath.js'
|
|
43
52
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 域名解析(子路径 `@w3lib/solana-batch-call/names`):SNS(Bonfida,`.sns`),名字 → 地址、地址 → 主域名。
|
|
3
|
+
* 不放在主入口,不用时打包不会带上这部分代码。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getAddressNames, getNameAddresses } from '@w3lib/solana-batch-call/names'
|
|
7
|
+
*
|
|
8
|
+
* await getNameAddresses(['bonfida.sns', 'toly.sns'])
|
|
9
|
+
* await getAddressNames([wallet])
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import type { RequestOptions } from '../client.js'
|
|
13
|
+
import { lookupAddresses, resolveNames, type AddressName, type NamesOptions, type ResolvedName } from '../names.js'
|
|
14
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
15
|
+
|
|
16
|
+
export { NAME_PROGRAM_ID, SNS_ROOT_DOMAIN, lookupAddresses, resolveNames, snsDomainKey, type AddressName, type NameSource, type NamesOptions, type ResolvedName } from '../names.js'
|
|
17
|
+
|
|
18
|
+
export interface GetNamesOptions extends NamesOptions, ShortcutOptions {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 名字 → 地址,节点参数与 getBalances 相同。
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* await getNameAddresses(['bonfida.sns', 'toly.sns'])
|
|
25
|
+
* // [{ name: 'bonfida.sns', normalized: 'bonfida.sns', address: 'Fw1E…Lb1v', owner: 'Fw1E…Lb1v', source: 'owner', service: 'sns' }, …]
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function getNameAddresses(names: readonly string[], options: GetNamesOptions = {}): Promise<ResolvedName[]> {
|
|
29
|
+
const { client, own } = resolve(options, ['legacySol', 'allowPda', 'signal'])
|
|
30
|
+
return resolveNames(client, names, own)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GetAddressNamesOptions extends RequestOptions, ShortcutOptions {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 地址 → 主域名(核对过属于这个地址)。
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await getAddressNames([wallet]) // [{ address: wallet, name: 'bonfida.sns', service: 'sns' }]
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function getAddressNames(addresses: readonly string[], options: GetAddressNamesOptions = {}): Promise<AddressName[]> {
|
|
43
|
+
const { client, own } = resolve(options, ['signal'])
|
|
44
|
+
return lookupAddresses(client, addresses, own)
|
|
45
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 价格(子路径 `@w3lib/solana-batch-call/price`):代币的美元价格(DefiLlama 当前 / 历史价格,没有收录的从链上 Raydium / Orca 池子算)、
|
|
3
|
+
* 组合价格源、地址的资产总值。不放在主入口,不用时打包不会带上这部分代码。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { NATIVE_MINT } from '@w3lib/solana-batch-call'
|
|
7
|
+
* import { getPortfolio, getTokenPrices } from '@w3lib/solana-batch-call/price'
|
|
8
|
+
*
|
|
9
|
+
* await getTokenPrices([NATIVE_MINT, USDC, newToken])
|
|
10
|
+
* await getPortfolio(owner, { tokens: [NATIVE_MINT, USDC, JUP] })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { portfolio, type Portfolio, type PortfolioOptions } from '../portfolio.js'
|
|
14
|
+
import { tokenPrices, type TokenPrice, type TokenPricesOptions } from '../price.js'
|
|
15
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
16
|
+
|
|
17
|
+
export { portfolio, type Portfolio, type PortfolioOptions, type PortfolioToken } from '../portfolio.js'
|
|
18
|
+
export {
|
|
19
|
+
STABLECOINS,
|
|
20
|
+
combinePrices,
|
|
21
|
+
defillamaPrices,
|
|
22
|
+
onchainPrices,
|
|
23
|
+
tokenPrices,
|
|
24
|
+
type DefillamaPriceOptions,
|
|
25
|
+
type OnchainPriceOptions,
|
|
26
|
+
type OnchainPriceProtocol,
|
|
27
|
+
type SolanaPriceSource,
|
|
28
|
+
type SolanaPriceSourceContext,
|
|
29
|
+
type TokenPrice,
|
|
30
|
+
type TokenPricesOptions,
|
|
31
|
+
} from '../price.js'
|
|
32
|
+
|
|
33
|
+
export interface GetTokenPricesOptions extends TokenPricesOptions, ShortcutOptions {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 查代币的美元价格(SOL 用 NATIVE_MINT),节点参数与 getBalances 相同。
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await getTokenPrices([NATIVE_MINT, USDC])
|
|
40
|
+
* // [{ token: 'So111…112', native: true, price: 187.4, source: 'defillama' }, { token: USDC, native: false, price: 1, source: 'defillama' }]
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function getTokenPrices(mints: readonly string[], options: GetTokenPricesOptions = {}): Promise<TokenPrice[]> {
|
|
44
|
+
// fetch 同时给节点请求(客户端配置)和查价用,与 getLpPositions 一致
|
|
45
|
+
const { client, own } = resolve(options, ['sources', 'onError', 'signal'])
|
|
46
|
+
return tokenPrices(client, mints, { ...own, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GetPortfolioOptions extends PortfolioOptions, ShortcutOptions {}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 地址的资产总值:代币余额 × 价格,可选加上 LP 仓位(lp: true)。
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* await getPortfolio(owner, { tokens: [NATIVE_MINT, USDC, JUP] }) // 指定代币:免费公共节点也能用
|
|
56
|
+
* await getPortfolio(owner, { provider: 'https://my-rpc…', lp: true }) // 全部持仓 + LP:需要支持 getTokenAccountsByOwner 的节点
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function getPortfolio(owner: string, options: GetPortfolioOptions = {}): Promise<Portfolio> {
|
|
60
|
+
const { client, own } = resolve(options, ['tokens', 'sources', 'lp', 'minUsd', 'onPriceError', 'signal'])
|
|
61
|
+
return portfolio(client, owner, { ...own, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代币风险检查(子路径 `@w3lib/solana-batch-call/risk`):mint 的增发 / 冻结权限,Token-2022 扩展里影响持有人的设置
|
|
3
|
+
* (转账手续费、永久授权、转账 hook、不可转让、默认冻结、可暂停、保密转账、mint 可关闭等),可选持币集中度;
|
|
4
|
+
* tradeChecks 在主网模拟“买入 → 卖出”判断能不能卖(用 /swap,不用时打包不会带上)。不放在主入口,不用时打包不会带上这部分代码。
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { getTokenRisks, getTradeChecks } from '@w3lib/solana-batch-call/risk'
|
|
8
|
+
*
|
|
9
|
+
* const [risk] = await getTokenRisks([mint])
|
|
10
|
+
* if (risk.flags.includes('permanent-delegate')) alert('发行方可以转走你的代币')
|
|
11
|
+
* const [trade] = await getTradeChecks([mint], { owner: myAddress })
|
|
12
|
+
* if (trade.honeypot) alert('卖不出')
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { tokenRisks, type SolanaTokenRisk, type TokenRisksOptions } from '../risk.js'
|
|
16
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
17
|
+
import { tradeChecks, type SolanaTradeCheck, type TradeCheckOptions } from '../tradeCheck.js'
|
|
18
|
+
|
|
19
|
+
export { parseRisk, tokenRisks, type SolanaTokenRisk, type TokenRisksOptions } from '../risk.js'
|
|
20
|
+
export { tradeChecks, type SolanaTradeCheck, type TradeCheckOptions, type TradeStage } from '../tradeCheck.js'
|
|
21
|
+
|
|
22
|
+
export interface GetTokenRisksOptions extends TokenRisksOptions, ShortcutOptions {}
|
|
23
|
+
|
|
24
|
+
/** 检查代币的风险(一次 getMultipleAccounts),节点参数与 getBalances 相同 */
|
|
25
|
+
export function getTokenRisks(mints: readonly string[], options: GetTokenRisksOptions = {}): Promise<SolanaTokenRisk[]> {
|
|
26
|
+
const { client, own } = resolve(options, ['concentration', 'signal'])
|
|
27
|
+
return tokenRisks(client, mints, own)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GetTradeChecksOptions extends TradeCheckOptions, ShortcutOptions {}
|
|
31
|
+
|
|
32
|
+
/** 模拟买入再卖出,判断能不能交易,节点参数与 getBalances 相同 */
|
|
33
|
+
export function getTradeChecks(mints: readonly string[], options: GetTradeChecksOptions): Promise<SolanaTradeCheck[]> {
|
|
34
|
+
const { client, own } = resolve(options, ['owner', 'amount', 'amountFormatted', 'protocols', 'intermediates', 'pools', 'discover', 'signal'])
|
|
35
|
+
return tradeChecks(client, mints, own as TradeCheckOptions)
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 兑换(子路径 `@w3lib/solana-batch-call/swap`):在 Raydium CLMM、Orca Whirlpool、Raydium CPMM、Raydium AMM v4 的池子里找最好的路线
|
|
3
|
+
* (直接或经过 SOL / USDC / USDT 的两跳),本地报价并构建 / 发送兑换交易。不放在主入口,不用时打包不会带上这部分代码。不依赖各 DEX 的 SDK。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { NATIVE_MINT } from '@w3lib/solana-batch-call'
|
|
7
|
+
* import { getSwapQuote, swap } from '@w3lib/solana-batch-call/swap'
|
|
8
|
+
*
|
|
9
|
+
* const plan = await getSwapQuote(owner, { tokenIn: NATIVE_MINT, tokenOut: USDC, amountInFormatted: '0.5' })
|
|
10
|
+
* await swap(client, secretKey, { tokenIn: NATIVE_MINT, tokenOut: USDC, amountInFormatted: '0.5' }, { slippage: 0.01 })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
14
|
+
import { buildSwap, type SolanaSwapOptions, type SolanaSwapParams, type SolanaSwapPlan } from '../swap/index.js'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
SWAP_INTERMEDIATES,
|
|
18
|
+
SWAP_PROTOCOLS,
|
|
19
|
+
buildSwap,
|
|
20
|
+
swap,
|
|
21
|
+
type SolanaSwapHop,
|
|
22
|
+
type SolanaSwapOptions,
|
|
23
|
+
type SolanaSwapParams,
|
|
24
|
+
type SolanaSwapPlan,
|
|
25
|
+
type SolanaSwapProtocol,
|
|
26
|
+
type SolanaSwapResult,
|
|
27
|
+
type SolanaSwapRoute,
|
|
28
|
+
type SolanaSwapSendOptions,
|
|
29
|
+
type SolanaSwapStep,
|
|
30
|
+
type SolanaSwapStepResult,
|
|
31
|
+
} from '../swap/index.js'
|
|
32
|
+
export type { Instruction } from '../write/transaction.js'
|
|
33
|
+
export type { SignerInput, WriteResult, WriteStatus } from '../subpaths/write.js'
|
|
34
|
+
|
|
35
|
+
export interface GetSwapQuoteOptions extends SolanaSwapOptions, ShortcutOptions {}
|
|
36
|
+
|
|
37
|
+
/** 报价并构建兑换的指令(同 buildSwap),节点参数与 getBalances 相同 */
|
|
38
|
+
export function getSwapQuote(owner: string, params: SolanaSwapParams, options: GetSwapQuoteOptions = {}): Promise<SolanaSwapPlan> {
|
|
39
|
+
const { client, own } = resolve(options, ['slippage', 'native', 'protocols', 'intermediates', 'pools', 'discover', 'signal'])
|
|
40
|
+
return buildSwap(client, owner, params, own)
|
|
41
|
+
}
|
package/src/subpaths/write.ts
CHANGED
package/src/swap/amm.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raydium AMM v4 的兑换:swap_base_in_v2(指令 16,不需要 OpenBook 账户)。报价照搬程序(raydium-amm processor.rs):
|
|
3
|
+
* 手续费 = ceil(输入 × swap_fee_numerator / swap_fee_denominator),储备 = 金库余额 − need_take_pnl,恒定乘积向下取整。只支持 SPL Token
|
|
4
|
+
*/
|
|
5
|
+
import { TOKEN_PROGRAM_ID } from '../constants.js'
|
|
6
|
+
import { parseTokenAccount, type AccountInfo } from '../layout.js'
|
|
7
|
+
import { RAYDIUM_AMM_AUTHORITY, RAYDIUM_AMM_PROGRAM_ID } from '../lp.js'
|
|
8
|
+
import { Writer, r, w } from '../liquidity/common.js'
|
|
9
|
+
import type { Instruction } from '../write/transaction.js'
|
|
10
|
+
import { Bytes, UnsupportedPool, mulDivCeil, mulDivFloor, need, swapMint, type AccountGetter, type HopQuote, type SwapMint, type SwapPool } from './common.js'
|
|
11
|
+
|
|
12
|
+
const POOL_SIZE = 752
|
|
13
|
+
/**
|
|
14
|
+
* 只支持 SwapOnly(6,不再用 OpenBook 挂单,现在的池子基本都是):swap_base_in_v2 不带 OpenBook 账户,
|
|
15
|
+
* 还连着 OpenBook 的池子(Initialized 1、WaitingTrade 7)没有对照程序源码验证过能不能用,不报价,免得排第一后在链上失败
|
|
16
|
+
*/
|
|
17
|
+
const SWAP_STATUSES = new Set([6n])
|
|
18
|
+
|
|
19
|
+
interface AmmState {
|
|
20
|
+
address: string
|
|
21
|
+
status: bigint
|
|
22
|
+
swapFeeNumerator: bigint
|
|
23
|
+
swapFeeDenominator: bigint
|
|
24
|
+
needTakePnl0: bigint
|
|
25
|
+
needTakePnl1: bigint
|
|
26
|
+
openTime: bigint
|
|
27
|
+
vault0: string
|
|
28
|
+
vault1: string
|
|
29
|
+
mint0: string
|
|
30
|
+
mint1: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** AmmInfo(无判别符):status 0、fees.swap_fee 176 / 184、need_take_pnl_coin 192、need_take_pnl_pc 200、pool_open_time 224、vault 336 / 368、mint 400 / 432 */
|
|
34
|
+
export function parseAmmV4Pool(account: AccountInfo): AmmState | null {
|
|
35
|
+
if (account.owner !== RAYDIUM_AMM_PROGRAM_ID || account.data.length !== POOL_SIZE) return null
|
|
36
|
+
const b = new Bytes(account.data)
|
|
37
|
+
return {
|
|
38
|
+
address: account.address,
|
|
39
|
+
status: b.u64(0),
|
|
40
|
+
swapFeeNumerator: b.u64(176),
|
|
41
|
+
swapFeeDenominator: b.u64(184),
|
|
42
|
+
needTakePnl0: b.u64(192),
|
|
43
|
+
needTakePnl1: b.u64(200),
|
|
44
|
+
openTime: b.u64(224),
|
|
45
|
+
vault0: b.key(336),
|
|
46
|
+
vault1: b.key(368),
|
|
47
|
+
mint0: b.key(400),
|
|
48
|
+
mint1: b.key(432),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ammV4Pool(state: AmmState): SwapPool {
|
|
53
|
+
const mints = (get: AccountGetter): [SwapMint, SwapMint] => [
|
|
54
|
+
swapMint(state.mint0, need(get, state.mint0, 'Token mint')),
|
|
55
|
+
swapMint(state.mint1, need(get, state.mint1, 'Token mint')),
|
|
56
|
+
]
|
|
57
|
+
return {
|
|
58
|
+
protocol: 'raydium-amm',
|
|
59
|
+
address: state.address,
|
|
60
|
+
mint0: state.mint0,
|
|
61
|
+
mint1: state.mint1,
|
|
62
|
+
mints,
|
|
63
|
+
accounts: () => [state.vault0, state.vault1, state.mint0, state.mint1],
|
|
64
|
+
quote(get, amountIn, zeroForOne, env): HopQuote {
|
|
65
|
+
if (!SWAP_STATUSES.has(state.status)) throw new UnsupportedPool(`Swap is not allowed in pool status ${state.status}`)
|
|
66
|
+
if (env.timestamp < state.openTime) throw new UnsupportedPool('Pool is not open yet')
|
|
67
|
+
if (state.swapFeeDenominator === 0n) throw new UnsupportedPool('Invalid pool fees')
|
|
68
|
+
const [m0, m1] = mints(get)
|
|
69
|
+
if (m0.program !== TOKEN_PROGRAM_ID || m1.program !== TOKEN_PROGRAM_ID) throw new UnsupportedPool('AMM v4 supports only SPL Token mints')
|
|
70
|
+
const v0 = parseTokenAccount(need(get, state.vault0, 'Pool vault').data).amount
|
|
71
|
+
const v1 = parseTokenAccount(need(get, state.vault1, 'Pool vault').data).amount
|
|
72
|
+
if (v0 < state.needTakePnl0 || v1 < state.needTakePnl1) throw new UnsupportedPool('Pool vault is below the pending PnL')
|
|
73
|
+
const [reserveIn, reserveOut] = zeroForOne ? [v0 - state.needTakePnl0, v1 - state.needTakePnl1] : [v1 - state.needTakePnl1, v0 - state.needTakePnl0]
|
|
74
|
+
const fee = mulDivCeil(amountIn, state.swapFeeNumerator, state.swapFeeDenominator)
|
|
75
|
+
const afterFee = amountIn - fee
|
|
76
|
+
const out = afterFee > 0n ? mulDivFloor(reserveOut, afterFee, reserveIn + afterFee) : 0n
|
|
77
|
+
// 程序要求输出小于储备
|
|
78
|
+
return { amountIn, amountOut: out < reserveOut ? out : 0n, accounts: [] }
|
|
79
|
+
},
|
|
80
|
+
instruction(_get, owner, input, output, _zeroForOne, quote, minOut): Instruction {
|
|
81
|
+
return {
|
|
82
|
+
programId: RAYDIUM_AMM_PROGRAM_ID,
|
|
83
|
+
keys: [r(TOKEN_PROGRAM_ID), w(state.address), r(RAYDIUM_AMM_AUTHORITY), w(state.vault0), w(state.vault1), w(input), w(output), r(owner, true)],
|
|
84
|
+
data: new Writer().u8(16).u64(quote.amountIn).u64(minOut).build(),
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 集中流动性兑换一步的计算(只做按输入数量兑换),照搬 Raydium CLMM(libraries/swap_math.rs、sqrt_price_math.rs、liquidity_math.rs)
|
|
3
|
+
* 和 Orca Whirlpool(math/swap_math.rs、token_math.rs)。两个程序“手续费从输入扣”时的公式和取整完全相同;Raydium 另有“手续费从输出扣”的池子
|
|
4
|
+
*/
|
|
5
|
+
import { U64_MAX, mulDivCeil } from './common.js'
|
|
6
|
+
|
|
7
|
+
const Q64 = 1n << 64n
|
|
8
|
+
export const FEE_RATE_DENOMINATOR = 1_000_000n
|
|
9
|
+
|
|
10
|
+
export class SwapMathError extends Error {}
|
|
11
|
+
|
|
12
|
+
/** 价格从 a 到 b(任意顺序)对应的 token0 数量:L · 2^64 · (hi − lo) / (lo · hi) */
|
|
13
|
+
export function amount0Delta(a: bigint, b: bigint, liquidity: bigint, roundUp: boolean): bigint {
|
|
14
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
15
|
+
if (lo === 0n) throw new SwapMathError('zero sqrt price')
|
|
16
|
+
const num = (liquidity * (hi - lo)) << 64n
|
|
17
|
+
const den = lo * hi
|
|
18
|
+
return roundUp ? (num + den - 1n) / den : num / den
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 价格从 a 到 b 对应的 token1 数量:L · (hi − lo) / 2^64 */
|
|
22
|
+
export function amount1Delta(a: bigint, b: bigint, liquidity: bigint, roundUp: boolean): bigint {
|
|
23
|
+
const [lo, hi] = a < b ? [a, b] : [b, a]
|
|
24
|
+
const p = liquidity * (hi - lo)
|
|
25
|
+
return roundUp ? (p + Q64 - 1n) >> 64n : p >> 64n
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 卖出 amount 后的 sqrt 价格(向不越过目标价格的方向取整):卖 token0 时价格下降,卖 token1 时上升 */
|
|
29
|
+
export function nextSqrtPriceFromInput(sqrtPrice: bigint, liquidity: bigint, amount: bigint, zeroForOne: boolean): bigint {
|
|
30
|
+
if (liquidity === 0n) throw new SwapMathError('zero liquidity')
|
|
31
|
+
if (amount === 0n) return sqrtPrice
|
|
32
|
+
if (zeroForOne) {
|
|
33
|
+
const num = liquidity << 64n
|
|
34
|
+
const den = num + amount * sqrtPrice
|
|
35
|
+
return mulDivCeil(num, sqrtPrice, den)
|
|
36
|
+
}
|
|
37
|
+
return sqrtPrice + (amount << 64n) / liquidity
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SwapStep {
|
|
41
|
+
/** 走到的 sqrt 价格 */
|
|
42
|
+
sqrtPrice: bigint
|
|
43
|
+
/** 计入价格变动的输入(不含手续费) */
|
|
44
|
+
amountIn: bigint
|
|
45
|
+
/** 给用户的输出(手续费从输出扣时已扣除) */
|
|
46
|
+
amountOut: bigint
|
|
47
|
+
feeAmount: bigint
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 一步兑换:从当前价格向目标价格,最多用 remaining 个输入。
|
|
52
|
+
* feeOnInput(Orca 和 Raydium 的默认):输入先按费率扣手续费;否则(Raydium fee_on):输出按费率扣
|
|
53
|
+
*/
|
|
54
|
+
export function computeSwapStep(
|
|
55
|
+
sqrtCurrent: bigint,
|
|
56
|
+
sqrtTarget: bigint,
|
|
57
|
+
liquidity: bigint,
|
|
58
|
+
remaining: bigint,
|
|
59
|
+
feeRate: bigint,
|
|
60
|
+
zeroForOne: boolean,
|
|
61
|
+
feeOnInput: boolean,
|
|
62
|
+
program: 'raydium' | 'orca',
|
|
63
|
+
): SwapStep {
|
|
64
|
+
const amountForPrice = feeOnInput ? (remaining * (FEE_RATE_DENOMINATOR - feeRate)) / FEE_RATE_DENOMINATOR : remaining
|
|
65
|
+
// 走到目标价格需要的输入(向上取整)和得到的输出(向下取整);超过 u64 视为走不到
|
|
66
|
+
const inAtTarget = zeroForOne ? amount0Delta(sqrtTarget, sqrtCurrent, liquidity, true) : amount1Delta(sqrtCurrent, sqrtTarget, liquidity, true)
|
|
67
|
+
let sqrtNext: bigint
|
|
68
|
+
let amountIn: bigint
|
|
69
|
+
let amountOut: bigint
|
|
70
|
+
const outAtTarget = () => (zeroForOne ? amount1Delta(sqrtTarget, sqrtCurrent, liquidity, false) : amount0Delta(sqrtCurrent, sqrtTarget, liquidity, false))
|
|
71
|
+
let reached = inAtTarget <= U64_MAX && amountForPrice >= inAtTarget
|
|
72
|
+
// Raydium:目标价格处的输出超过 u64 也算走不到;Orca 照常走到目标(之后报错)
|
|
73
|
+
const out = reached ? outAtTarget() : 0n
|
|
74
|
+
if (reached && program === 'raydium' && out > U64_MAX) reached = false
|
|
75
|
+
if (reached) {
|
|
76
|
+
sqrtNext = sqrtTarget
|
|
77
|
+
amountIn = inAtTarget
|
|
78
|
+
amountOut = out
|
|
79
|
+
} else {
|
|
80
|
+
sqrtNext = nextSqrtPriceFromInput(sqrtCurrent, liquidity, amountForPrice, zeroForOne)
|
|
81
|
+
amountIn = zeroForOne ? amount0Delta(sqrtNext, sqrtCurrent, liquidity, true) : amount1Delta(sqrtCurrent, sqrtNext, liquidity, true)
|
|
82
|
+
amountOut = zeroForOne ? amount1Delta(sqrtNext, sqrtCurrent, liquidity, false) : amount0Delta(sqrtCurrent, sqrtNext, liquidity, false)
|
|
83
|
+
}
|
|
84
|
+
if (amountIn > U64_MAX || amountOut > U64_MAX) throw new SwapMathError('token amount exceeds u64')
|
|
85
|
+
if (zeroForOne ? sqrtNext < sqrtTarget : sqrtNext > sqrtTarget) throw new SwapMathError('price moved past the target')
|
|
86
|
+
let feeAmount: bigint
|
|
87
|
+
if (feeOnInput) {
|
|
88
|
+
feeAmount = sqrtNext !== sqrtTarget ? remaining - amountIn : mulDivCeil(amountIn, feeRate, FEE_RATE_DENOMINATOR - feeRate)
|
|
89
|
+
} else {
|
|
90
|
+
feeAmount = mulDivCeil(amountOut, feeRate, FEE_RATE_DENOMINATOR)
|
|
91
|
+
amountOut -= feeAmount
|
|
92
|
+
// 没走到目标价格:输入全部用掉(不足一个单位的零头归池子)
|
|
93
|
+
if (sqrtNext !== sqrtTarget) amountIn = remaining
|
|
94
|
+
}
|
|
95
|
+
return { sqrtPrice: sqrtNext, amountIn, amountOut, feeAmount }
|
|
96
|
+
}
|