@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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交易历史(子路径 `@w3lib/solana-batch-call/history`):地址的交易整理成统一的 Activity(兑换、转账、授权、流动性、铸造、失败……),
|
|
3
|
+
* 数据源可换(默认节点:getSignaturesForAddress + getTransaction;可选 Helius 增强历史)。不放在主入口,不用时打包不会带上这部分代码。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getHistory, heliusHistory } from '@w3lib/solana-batch-call/history'
|
|
7
|
+
*
|
|
8
|
+
* const page = await getHistory(address, { limit: 20 })
|
|
9
|
+
* const next = await getHistory(address, { cursor: page.nextCursor, types: ['swap'] })
|
|
10
|
+
* await getHistory(address, { source: [heliusHistory({ apiKey }), rpcHistory()] }) // 依次尝试
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { history, type HistoryOptions, type HistoryPage } from '../history.js'
|
|
14
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
MAX_HISTORY_LIMIT,
|
|
18
|
+
activityFromTransaction,
|
|
19
|
+
history,
|
|
20
|
+
rpcHistory,
|
|
21
|
+
type Activity,
|
|
22
|
+
type ActivityChange,
|
|
23
|
+
type ActivityType,
|
|
24
|
+
type HistoryContext,
|
|
25
|
+
type HistoryOptions,
|
|
26
|
+
type HistoryPage,
|
|
27
|
+
type HistorySource,
|
|
28
|
+
type RpcHistoryOptions,
|
|
29
|
+
} from '../history.js'
|
|
30
|
+
export { activityFromHelius, heliusHistory, type HeliusHistoryOptions } from '../historyHelius.js'
|
|
31
|
+
|
|
32
|
+
export interface GetHistoryOptions extends HistoryOptions, ShortcutOptions {}
|
|
33
|
+
|
|
34
|
+
/** 查地址的交易历史,节点参数与 getBalances 相同 */
|
|
35
|
+
export function getHistory(address: string, options: GetHistoryOptions = {}): Promise<HistoryPage> {
|
|
36
|
+
const { client, own } = resolve(options, ['limit', 'cursor', 'before', 'after', 'types', 'source', 'symbols', 'fetch', 'signal'])
|
|
37
|
+
return history(client, address, own)
|
|
38
|
+
}
|
package/src/subpaths/lp.ts
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
type SolanaLpClmmPosition,
|
|
26
26
|
type SolanaLpPosition,
|
|
27
27
|
type SolanaLpProtocol,
|
|
28
|
+
type SolanaLpReward,
|
|
28
29
|
type SolanaLpToken,
|
|
29
30
|
type SolanaPriceSource,
|
|
30
31
|
} from '../lp.js'
|
|
@@ -38,15 +39,18 @@ export {
|
|
|
38
39
|
getMaxAmounts,
|
|
39
40
|
getMinAmounts,
|
|
40
41
|
getMintAmounts,
|
|
42
|
+
getRewardGrowthInside,
|
|
41
43
|
nearestUsableTick,
|
|
42
44
|
orcaSqrtPriceAtTick,
|
|
43
45
|
priceToTick,
|
|
44
46
|
raydiumSqrtPriceAtTick,
|
|
47
|
+
rewardGrowthGlobal,
|
|
45
48
|
sqrtPriceAtTick,
|
|
46
49
|
sqrtPriceToPrice,
|
|
47
50
|
tickAtSqrtPrice,
|
|
48
51
|
tickToPrice,
|
|
49
52
|
type ClmmProgram,
|
|
53
|
+
type RewardEmission,
|
|
50
54
|
type TickFeeData,
|
|
51
55
|
} from '../lpMath.js'
|
|
52
56
|
|
|
@@ -66,6 +70,6 @@ export interface GetLpPositionsOptions extends LpPositionsOptions, ShortcutOptio
|
|
|
66
70
|
*/
|
|
67
71
|
export function getLpPositions(owner: string, options: GetLpPositionsOptions = {}): Promise<SolanaLpPosition[]> {
|
|
68
72
|
// fetch 同时给节点请求(客户端配置)和查价用,与其他快捷函数一致
|
|
69
|
-
const { client, own } = resolve(options, ['protocols', 'mints', 'pools', 'includeClosed', 'prices', 'onPriceError', 'signal'])
|
|
73
|
+
const { client, own } = resolve(options, ['protocols', 'mints', 'pools', 'includeClosed', 'rewards', 'now', 'prices', 'onPriceError', 'signal'])
|
|
70
74
|
return lpPositions(client, owner, { ...own, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
71
75
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账户回收(子路径 `@w3lib/solana-batch-call/reclaim`):扫描钱包里关闭后能退回租金(SOL)的账户并批量关闭:空的代币账户、垃圾币(销毁后关闭)、
|
|
3
|
+
* Wrapped SOL(解包)、已清空的 Orca Whirlpool / Raydium CLMM 仓位。给出每项和合计能拿回多少 lamports,发送前用 /simulate 模拟。
|
|
4
|
+
* 不放在主入口,不用时打包不会带上这部分代码;发送的代码按需加载。
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { getReclaimable, reclaim } from '@w3lib/solana-batch-call/reclaim'
|
|
8
|
+
*
|
|
9
|
+
* const r = await getReclaimable(owner, { provider: 'https://my-rpc…' })
|
|
10
|
+
* r.totalLamports // 全部能拿回的 lamports
|
|
11
|
+
* const { simulation, results } = await reclaim(client, key, r.items.filter((i) => i.selected))
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { reclaimable, type Reclaimable, type ReclaimableOptions } from '../reclaim.js'
|
|
15
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
buildReclaim,
|
|
19
|
+
reclaim,
|
|
20
|
+
reclaimable,
|
|
21
|
+
simulateReclaim,
|
|
22
|
+
type BuildReclaimOptions,
|
|
23
|
+
type JunkContext,
|
|
24
|
+
type ReclaimBlocker,
|
|
25
|
+
type ReclaimCall,
|
|
26
|
+
type ReclaimCallResult,
|
|
27
|
+
type ReclaimItem,
|
|
28
|
+
type ReclaimKind,
|
|
29
|
+
type ReclaimOptions,
|
|
30
|
+
type ReclaimPlan,
|
|
31
|
+
type ReclaimableOptions,
|
|
32
|
+
type ReclaimQuery,
|
|
33
|
+
type ReclaimResult,
|
|
34
|
+
type ReclaimSimulation,
|
|
35
|
+
type ReclaimSimulationOptions,
|
|
36
|
+
type Reclaimable,
|
|
37
|
+
} from '../reclaim.js'
|
|
38
|
+
export type { PlanSimulation } from '../batchPlan.js'
|
|
39
|
+
export { burnChecked, closeAccount, harvestWithheldTokensToMint } from '../write/instructions.js'
|
|
40
|
+
|
|
41
|
+
export interface GetReclaimableOptions extends ReclaimableOptions, ShortcutOptions {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 扫描钱包里能回收租金的账户,节点参数与 getBalances 相同。默认列出全部代币账户(需要支持 getTokenAccountsByOwner 的节点),
|
|
45
|
+
* 免费公共节点传 `mints` 只看指定代币的 ATA。
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const r = await getReclaimable(owner, { provider: 'https://my-rpc…' })
|
|
49
|
+
* // { totalLamports: '45862080', selectedLamports: '41302080',
|
|
50
|
+
* // items: [{ kind: 'empty-token', account, mint, lamports: '2039280', selected: true, reasons: ['empty'] },
|
|
51
|
+
* // { kind: 'junk-token', symbol: 'claim-rewards.xyz', lamports: '2039280', selected: true, reasons: ['no-price', 'spam-name'] }, …],
|
|
52
|
+
* // blocked: [{ kind: 'empty-token', blocker: 'frozen', … }] }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function getReclaimable(owner: string, options: GetReclaimableOptions = {}): Promise<Reclaimable> {
|
|
56
|
+
const { client, own } = resolve(options, ['mints', 'junk', 'positions', 'prices', 'maxJunkUsd', 'risk', 'isJunk', 'signal'])
|
|
57
|
+
return reclaimable(client, owner, { ...own, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
58
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交易模拟(子路径 `@w3lib/solana-batch-call/simulate`):不签名、不发送,在主网模拟一笔交易,给出余额变化、授权变化(delegate / SetAuthority / 关闭账户)、
|
|
3
|
+
* 指令说明、程序错误、手续费和警告。输入可以是 base64 的交易,也可以直接是本库构建器的输出。不放在主入口,不用时打包不会带上这部分代码;价格、风险检查按需加载。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { buildSwap } from '@w3lib/solana-batch-call/swap'
|
|
7
|
+
* import { getSimulation } from '@w3lib/solana-batch-call/simulate'
|
|
8
|
+
*
|
|
9
|
+
* const plan = await getSwapQuote(owner, { tokenIn: NATIVE_MINT, tokenOut: USDC, amountInFormatted: '0.5' })
|
|
10
|
+
* const sim = await getSimulation(plan) // 或 getSimulation(base64Tx, { owner })
|
|
11
|
+
* if (!sim.success || sim.warnings.some((w) => w.severity === 'danger')) throw new Error(sim.error ?? sim.warnings[0].message)
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
15
|
+
import { simulate, type SimulateInput, type SimulateOptions, type SimulationResult } from '../simulate.js'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
failureText,
|
|
19
|
+
simulate,
|
|
20
|
+
type ApprovalChange,
|
|
21
|
+
type ApprovalChangeKind,
|
|
22
|
+
type SimulateInput,
|
|
23
|
+
type SimulateOptions,
|
|
24
|
+
type SimulatedBalanceChange,
|
|
25
|
+
type SimulationResult,
|
|
26
|
+
type SimulationWarning,
|
|
27
|
+
type WarningSeverity,
|
|
28
|
+
} from '../simulate.js'
|
|
29
|
+
export { decodeInstruction, type DecodedInstruction } from '../decode.js'
|
|
30
|
+
|
|
31
|
+
export interface GetSimulationOptions extends SimulateOptions, ShortcutOptions {}
|
|
32
|
+
|
|
33
|
+
/** 模拟一笔交易,节点参数与 getBalances 相同 */
|
|
34
|
+
export function getSimulation(input: SimulateInput, options: GetSimulationOptions = {}): Promise<SimulationResult> {
|
|
35
|
+
const { client, own } = resolve(options, ['owner', 'signers', 'watch', 'step', 'prices', 'risk', 'fetch', 'signal'])
|
|
36
|
+
return simulate(client, input, own)
|
|
37
|
+
}
|
package/src/swap/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (直接兑换,或经过 SOL / USDC / USDT 的两跳),按链上状态在本地报价(照搬各程序的计算),选输出最多的;
|
|
4
4
|
* 按滑点算最少收到的数量,构建指令(SOL 自动包装 / 解包、缺少的代币账户自动创建),发送同 /write
|
|
5
5
|
*/
|
|
6
|
-
import { getAssociatedTokenAddress, isAddress } from '../address.js'
|
|
6
|
+
import { getAssociatedTokenAddress, InvalidAddressError, isAddress } from '../address.js'
|
|
7
7
|
import type { SolanaClient } from '../client.js'
|
|
8
8
|
import { NATIVE_MINT, TOKEN_PROGRAM_ID } from '../constants.js'
|
|
9
9
|
import { formatUnits, parseTokenAccount, type AccountInfo } from '../layout.js'
|
|
@@ -180,7 +180,7 @@ export async function buildSwap(client: SolanaClient, owner: string, params: Sol
|
|
|
180
180
|
const tokenIn = nativeIn ? NATIVE_MINT : params.tokenIn
|
|
181
181
|
const tokenOut = nativeOut ? NATIVE_MINT : params.tokenOut
|
|
182
182
|
for (const mint of [tokenIn, tokenOut]) {
|
|
183
|
-
if (!isAddress(mint)) throw new
|
|
183
|
+
if (!isAddress(mint)) throw new InvalidAddressError(`Invalid mint address: ${mint}`, mint)
|
|
184
184
|
}
|
|
185
185
|
if (tokenIn === tokenOut) throw new Error(nativeIn || nativeOut ? 'Wrapping / unwrapping SOL is not a swap' : 'tokenIn and tokenOut are the same')
|
|
186
186
|
const protocols = new Set(options.protocols ?? SWAP_PROTOCOLS)
|
|
@@ -192,7 +192,7 @@ export async function buildSwap(client: SolanaClient, owner: string, params: Sol
|
|
|
192
192
|
if (!isAddress(m)) throw new Error(`Invalid intermediate mint: ${m}`)
|
|
193
193
|
}
|
|
194
194
|
for (const p of options.pools ?? []) {
|
|
195
|
-
if (!isAddress(p)) throw new
|
|
195
|
+
if (!isAddress(p)) throw new InvalidAddressError(`Invalid pool address: ${p}`, p)
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// 1. 一次读:候选池子(PDA 推导 + 传入的)、Clock、各代币的 mint
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// 代币详情(tokens)、NFT 元数据(nfts)、NFT 持有人(nftOwners)
|
|
2
|
+
|
|
3
|
+
import type { AccountInfo, TransferFeeInfo } from './layout.js'
|
|
4
|
+
import { getMetadataAddress, isAddress } from './address.js'
|
|
5
|
+
import type { RequestOptions, TokensOptions } from './client.js'
|
|
6
|
+
import { SOL_DECIMALS, TOKEN_2022_PROGRAM_ID } from './constants.js'
|
|
7
|
+
import { formatUnits, parseTokenAccount, parseTransferFeeConfig } from './layout.js'
|
|
8
|
+
import { RpcError } from './rpc.js'
|
|
9
|
+
import type { ReadContext } from './balances.js'
|
|
10
|
+
import type { QueryContext } from './accountReader.js'
|
|
11
|
+
import { getMeta, learnMeta, safeParseMetaplex, safeParseMint } from './tokenMeta.js'
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_TOKEN_FIELDS,
|
|
14
|
+
type FailureReason,
|
|
15
|
+
type NftDetails,
|
|
16
|
+
type NftOwner,
|
|
17
|
+
type TokenDetails,
|
|
18
|
+
type TokenField,
|
|
19
|
+
type TokenStandard,
|
|
20
|
+
type TransferFee,
|
|
21
|
+
type TransferFeeConfig,
|
|
22
|
+
} from './types.js'
|
|
23
|
+
|
|
24
|
+
const TOKEN_STANDARDS: readonly TokenStandard[] = [
|
|
25
|
+
'NonFungible',
|
|
26
|
+
'FungibleAsset',
|
|
27
|
+
'Fungible',
|
|
28
|
+
'NonFungibleEdition',
|
|
29
|
+
'ProgrammableNonFungible',
|
|
30
|
+
'ProgrammableNonFungibleEdition',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export async function tokensOf<F extends TokenField>(rc: ReadContext, mints: readonly string[], options: TokensOptions<F>): Promise<TokenDetails<F>[]> {
|
|
34
|
+
const fields = (options.fields ?? DEFAULT_TOKEN_FIELDS) as readonly F[]
|
|
35
|
+
const wanted = new Set<TokenField>(fields)
|
|
36
|
+
const scope = rc.scope
|
|
37
|
+
const ctx: QueryContext = { signal: options.signal }
|
|
38
|
+
const needsNames = wanted.has('name') || wanted.has('symbol') || wanted.has('uri')
|
|
39
|
+
const addresses: string[] = []
|
|
40
|
+
const plan = mints.map((mint) => {
|
|
41
|
+
if (rc.nativeMints.has(mint) || !isAddress(mint)) {
|
|
42
|
+
return { mint, mintIndex: -1, metadataIndex: -1 }
|
|
43
|
+
}
|
|
44
|
+
const meta = getMeta(scope, mint)
|
|
45
|
+
// uri / supply / 权限 / 手续费不缓存,需要时总要读 mint;name / symbol 命中缓存时可以省掉元数据账户
|
|
46
|
+
const cachedOnly =
|
|
47
|
+
!wanted.has('uri') &&
|
|
48
|
+
!wanted.has('supply') &&
|
|
49
|
+
!wanted.has('mintAuthority') &&
|
|
50
|
+
!wanted.has('freezeAuthority') &&
|
|
51
|
+
!wanted.has('transferFee') &&
|
|
52
|
+
(!wanted.has('decimals') || meta.decimals !== undefined) &&
|
|
53
|
+
(!wanted.has('tokenProgram') || meta.tokenProgram !== undefined) &&
|
|
54
|
+
(!wanted.has('name') || meta.name !== undefined) &&
|
|
55
|
+
(!wanted.has('symbol') || meta.symbol !== undefined)
|
|
56
|
+
if (cachedOnly) {
|
|
57
|
+
return { mint, mintIndex: -1, metadataIndex: -1 }
|
|
58
|
+
}
|
|
59
|
+
const namesCached = (!wanted.has('name') || meta.name !== undefined) && (!wanted.has('symbol') || meta.symbol !== undefined) && !wanted.has('uri')
|
|
60
|
+
return {
|
|
61
|
+
mint,
|
|
62
|
+
mintIndex: addresses.push(mint) - 1,
|
|
63
|
+
metadataIndex: needsNames && !namesCached ? addresses.push(getMetadataAddress(mint)) - 1 : -1,
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
const [accounts, epoch] = await Promise.all([
|
|
67
|
+
addresses.length ? rc.reader.accounts(addresses, ctx) : Promise.resolve([] as (AccountInfo | null)[]),
|
|
68
|
+
// 手续费按当前 epoch 选新 / 旧配置;和账户读取在同一个批量请求里
|
|
69
|
+
wanted.has('transferFee') && addresses.length
|
|
70
|
+
? rc.reader.call<{ epoch: number | string }>(ctx, 'getEpochInfo', [{ commitment: rc.commitment }]).then((res) => BigInt(res.epoch))
|
|
71
|
+
: Promise.resolve(null),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
return plan.map(({ mint, mintIndex, metadataIndex }) => {
|
|
75
|
+
const native = rc.nativeMints.has(mint)
|
|
76
|
+
const values: Record<TokenField, unknown> & { exists: boolean } = native
|
|
77
|
+
? { exists: true, name: 'Solana', symbol: 'SOL', uri: null, decimals: SOL_DECIMALS, supply: null, tokenProgram: null, mintAuthority: null, freezeAuthority: null, transferFee: null }
|
|
78
|
+
: tokenValues(
|
|
79
|
+
rc,
|
|
80
|
+
mint,
|
|
81
|
+
mintIndex === -1 ? null : (accounts[mintIndex] ?? null),
|
|
82
|
+
metadataIndex === -1 ? null : (accounts[metadataIndex] ?? null),
|
|
83
|
+
mintIndex !== -1,
|
|
84
|
+
mintIndex !== -1 && metadataIndex !== -1,
|
|
85
|
+
epoch,
|
|
86
|
+
)
|
|
87
|
+
const out: Record<string, unknown> = { address: mint, native }
|
|
88
|
+
let missing: F | undefined
|
|
89
|
+
for (const field of fields) {
|
|
90
|
+
out[field] = values[field] ?? null
|
|
91
|
+
// 权限为 null 表示没有权限、手续费为 null 表示没有手续费,都是有效值;主币没有 uri / supply / tokenProgram
|
|
92
|
+
const nullable =
|
|
93
|
+
field === 'mintAuthority' || field === 'freezeAuthority' || field === 'transferFee' || (native && (field === 'uri' || field === 'supply' || field === 'tokenProgram'))
|
|
94
|
+
if (out[field] === null && !nullable) {
|
|
95
|
+
missing ??= field
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (wanted.has('supply')) {
|
|
99
|
+
const supply = values.supply as string | null
|
|
100
|
+
const decimals = values.decimals as number | null
|
|
101
|
+
out.supplyFormatted = supply === null || decimals === null ? null : formatUnits(BigInt(supply), decimals)
|
|
102
|
+
}
|
|
103
|
+
const error: FailureReason | undefined = native
|
|
104
|
+
? undefined
|
|
105
|
+
: !isAddress(mint)
|
|
106
|
+
? 'invalid-address'
|
|
107
|
+
: !values.exists
|
|
108
|
+
? accounts[mintIndex]
|
|
109
|
+
? 'not-token'
|
|
110
|
+
: 'not-found'
|
|
111
|
+
: missing
|
|
112
|
+
? 'missing-field'
|
|
113
|
+
: undefined
|
|
114
|
+
out.success = error === undefined
|
|
115
|
+
if (error) {
|
|
116
|
+
out.error = error
|
|
117
|
+
if (error === 'missing-field') {
|
|
118
|
+
out.errorField = missing
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out as TokenDetails<F>
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tokenValues(
|
|
126
|
+
rc: ReadContext,
|
|
127
|
+
mint: string,
|
|
128
|
+
mintAccount: AccountInfo | null,
|
|
129
|
+
metadataAccount: AccountInfo | null,
|
|
130
|
+
fetchedMint: boolean,
|
|
131
|
+
lookedForNames: boolean,
|
|
132
|
+
epoch: bigint | null,
|
|
133
|
+
): Record<TokenField, unknown> & { exists: boolean } {
|
|
134
|
+
const scope = rc.scope
|
|
135
|
+
const meta = learnMeta(scope, mint, mintAccount, metadataAccount, lookedForNames)
|
|
136
|
+
const info = mintAccount ? safeParseMint(mintAccount) : null
|
|
137
|
+
const uri = info?.metadata?.uri ?? (metadataAccount ? (safeParseMetaplex(metadataAccount)?.uri ?? null) : null)
|
|
138
|
+
const feeConfig = info && mintAccount?.owner === TOKEN_2022_PROGRAM_ID ? parseTransferFeeConfig(mintAccount.data) : null
|
|
139
|
+
return {
|
|
140
|
+
// 读了 mint 账户就以它为准(不存在则 false);没读说明请求的字段全部命中缓存
|
|
141
|
+
exists: fetchedMint ? info !== null : true,
|
|
142
|
+
name: meta.name ?? null,
|
|
143
|
+
symbol: meta.symbol ?? null,
|
|
144
|
+
uri,
|
|
145
|
+
decimals: meta.decimals ?? null,
|
|
146
|
+
supply: info ? info.supply.toString() : null,
|
|
147
|
+
tokenProgram: meta.tokenProgram ?? null,
|
|
148
|
+
mintAuthority: info?.mintAuthority ?? null,
|
|
149
|
+
freezeAuthority: info?.freezeAuthority ?? null,
|
|
150
|
+
transferFee: feeConfig ? toTransferFeeConfig(feeConfig.older, feeConfig.newer, epoch) : null,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function toTransferFee(fee: TransferFeeInfo): TransferFee {
|
|
155
|
+
return { basisPoints: fee.basisPoints, maximumFee: fee.maximumFee.toString(), epoch: fee.epoch.toString() }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 当前生效的配置:epoch >= newer.epoch 用 newer,否则 older;不知道当前 epoch 时按 newer */
|
|
159
|
+
function toTransferFeeConfig(older: TransferFeeInfo, newer: TransferFeeInfo, epoch: bigint | null): TransferFeeConfig {
|
|
160
|
+
const current = epoch === null || epoch >= newer.epoch ? newer : older
|
|
161
|
+
return { ...toTransferFee(current), older: toTransferFee(older), newer: toTransferFee(newer) }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 批量查 NFT 元数据(Metaplex;Token-2022 NFT 取 TokenMetadata 扩展),一次 getMultipleAccounts */
|
|
165
|
+
export async function nftsOf(rc: ReadContext, mints: readonly string[], options: RequestOptions): Promise<NftDetails[]> {
|
|
166
|
+
const scope = rc.scope
|
|
167
|
+
const addresses: string[] = []
|
|
168
|
+
const plan = mints.map((mint) =>
|
|
169
|
+
isAddress(mint) ? { mint, metadataIndex: addresses.push(getMetadataAddress(mint)) - 1, mintIndex: addresses.push(mint) - 1 } : { mint, metadataIndex: -1, mintIndex: -1 },
|
|
170
|
+
)
|
|
171
|
+
const accounts = addresses.length ? await rc.reader.accounts(addresses, { signal: options.signal }) : []
|
|
172
|
+
return plan.map(({ mint, metadataIndex, mintIndex }): NftDetails => {
|
|
173
|
+
const metadataAccount = metadataIndex === -1 ? null : (accounts[metadataIndex] ?? null)
|
|
174
|
+
const mintAccount = mintIndex === -1 ? null : (accounts[mintIndex] ?? null)
|
|
175
|
+
learnMeta(scope, mint, mintAccount, metadataAccount, mintIndex !== -1)
|
|
176
|
+
const metaplex = metadataAccount ? safeParseMetaplex(metadataAccount) : null
|
|
177
|
+
const ext = mintAccount ? (safeParseMint(mintAccount)?.metadata ?? null) : null
|
|
178
|
+
if (!metaplex && !ext) {
|
|
179
|
+
const error: FailureReason = mintIndex === -1 ? 'invalid-address' : !mintAccount ? 'not-found' : !safeParseMint(mintAccount) ? 'not-token' : 'no-metadata'
|
|
180
|
+
return { mint, name: null, symbol: null, uri: null, collection: null, creators: [], sellerFeeBasisPoints: null, tokenStandard: null, isMutable: null, updateAuthority: null, success: false, error }
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
mint,
|
|
184
|
+
name: ext?.name ?? metaplex?.name ?? null,
|
|
185
|
+
symbol: ext?.symbol ?? metaplex?.symbol ?? null,
|
|
186
|
+
uri: ext?.uri ?? metaplex?.uri ?? null,
|
|
187
|
+
collection: metaplex?.collection ?? null,
|
|
188
|
+
creators: metaplex?.creators ?? [],
|
|
189
|
+
sellerFeeBasisPoints: metaplex?.sellerFeeBasisPoints ?? null,
|
|
190
|
+
tokenStandard: metaplex?.tokenStandard == null ? null : (TOKEN_STANDARDS[metaplex.tokenStandard] ?? null),
|
|
191
|
+
isMutable: metaplex?.isMutable ?? null,
|
|
192
|
+
updateAuthority: metaplex?.updateAuthority ?? null,
|
|
193
|
+
success: true,
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 批量查 NFT 持有人:getTokenLargestAccounts(合并成一个批量请求)+ 一次 getMultipleAccounts。
|
|
200
|
+
* 节点拒绝的 mint(不存在 / 不是代币)在同一次 getMultipleAccounts 里读 mint 账户区分原因
|
|
201
|
+
*/
|
|
202
|
+
export async function nftOwnersOf(rc: ReadContext, mints: readonly string[], options: RequestOptions): Promise<NftOwner[]> {
|
|
203
|
+
const ctx: QueryContext = { signal: options.signal }
|
|
204
|
+
const largest = await Promise.all(
|
|
205
|
+
mints.map((mint): Promise<{ tokenAccount: string | null; rejected: boolean }> =>
|
|
206
|
+
isAddress(mint)
|
|
207
|
+
? rc.reader.call<{ value: Array<{ address: string; amount: string }> }>(ctx, 'getTokenLargestAccounts', [mint, { commitment: rc.commitment }])
|
|
208
|
+
.then((res) => ({ tokenAccount: res.value.find((a) => a.amount !== '0')?.address ?? null, rejected: false }))
|
|
209
|
+
.catch((err: unknown) => {
|
|
210
|
+
// mint 不存在 / 不是代币:该项失败;节点问题照常抛出
|
|
211
|
+
if (err instanceof RpcError && !err.nodeFault) return { tokenAccount: null, rejected: true }
|
|
212
|
+
throw err
|
|
213
|
+
})
|
|
214
|
+
: Promise.resolve({ tokenAccount: null, rejected: false }),
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
const toRead = largest.flatMap(({ tokenAccount, rejected }, i) => (tokenAccount ? [tokenAccount] : rejected ? [mints[i] as string] : []))
|
|
218
|
+
const accounts = toRead.length ? await rc.reader.accounts(toRead, ctx) : []
|
|
219
|
+
const byAddress = new Map(toRead.map((a, i) => [a, accounts[i] ?? null]))
|
|
220
|
+
return mints.map((mint, i): NftOwner => {
|
|
221
|
+
const { tokenAccount, rejected } = largest[i] ?? { tokenAccount: null, rejected: false }
|
|
222
|
+
if (!isAddress(mint)) {
|
|
223
|
+
return { mint, owner: null, tokenAccount: null, success: false, error: 'invalid-address' }
|
|
224
|
+
}
|
|
225
|
+
if (rejected) {
|
|
226
|
+
return { mint, owner: null, tokenAccount: null, success: false, error: byAddress.get(mint) ? 'not-token' : 'not-found' }
|
|
227
|
+
}
|
|
228
|
+
const account = tokenAccount ? byAddress.get(tokenAccount) : null
|
|
229
|
+
let owner: string | null = null
|
|
230
|
+
if (account) {
|
|
231
|
+
try {
|
|
232
|
+
owner = parseTokenAccount(account.data).owner
|
|
233
|
+
} catch {
|
|
234
|
+
owner = null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 没有余额大于 0 的代币账户(供应量为 0 / 已销毁),或代币账户刚被关闭
|
|
238
|
+
return owner ? { mint, owner, tokenAccount, success: true } : { mint, owner: null, tokenAccount: null, success: false, error: 'no-holder' }
|
|
239
|
+
})
|
|
240
|
+
}
|