@zoidz123/raydium-cli 0.1.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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/commands/clmm/index.js +2118 -0
- package/dist/commands/config/index.js +113 -0
- package/dist/commands/cpmm/index.js +480 -0
- package/dist/commands/launchpad/index.js +1677 -0
- package/dist/commands/pools/index.js +81 -0
- package/dist/commands/swap/index.js +490 -0
- package/dist/commands/tokens/index.js +93 -0
- package/dist/commands/wallet/index.js +267 -0
- package/dist/index.js +43 -0
- package/dist/lib/clmm-utils.js +296 -0
- package/dist/lib/codex-sdk.js +17 -0
- package/dist/lib/config-manager.js +67 -0
- package/dist/lib/connection.js +9 -0
- package/dist/lib/ipfs.js +117 -0
- package/dist/lib/output.js +59 -0
- package/dist/lib/paths.js +11 -0
- package/dist/lib/prompt.js +58 -0
- package/dist/lib/raydium-client.js +23 -0
- package/dist/lib/token-price.js +45 -0
- package/dist/lib/wallet-manager.js +173 -0
- package/dist/types/config.js +11 -0
- package/package.json +56 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerWalletCommands = registerWalletCommands;
|
|
7
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const bs58_1 = __importDefault(require("bs58"));
|
|
9
|
+
const bn_js_1 = __importDefault(require("bn.js"));
|
|
10
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
11
|
+
const spl_token_1 = require("@solana/spl-token");
|
|
12
|
+
const config_manager_1 = require("../../lib/config-manager");
|
|
13
|
+
const wallet_manager_1 = require("../../lib/wallet-manager");
|
|
14
|
+
const prompt_1 = require("../../lib/prompt");
|
|
15
|
+
const output_1 = require("../../lib/output");
|
|
16
|
+
const connection_1 = require("../../lib/connection");
|
|
17
|
+
function formatAmount(raw, decimals) {
|
|
18
|
+
if (decimals <= 0)
|
|
19
|
+
return raw.toString();
|
|
20
|
+
const rawStr = raw.toString().padStart(decimals + 1, "0");
|
|
21
|
+
const whole = rawStr.slice(0, -decimals);
|
|
22
|
+
const frac = rawStr.slice(-decimals).replace(/0+$/, "");
|
|
23
|
+
return frac ? `${whole}.${frac}` : whole;
|
|
24
|
+
}
|
|
25
|
+
async function fetchRpcBalances(owner) {
|
|
26
|
+
const connection = await (0, connection_1.getConnection)();
|
|
27
|
+
const [solBalance, splAccounts, token2022Accounts] = await Promise.all([
|
|
28
|
+
connection.getBalance(owner),
|
|
29
|
+
connection.getTokenAccountsByOwner(owner, { programId: spl_token_1.TOKEN_PROGRAM_ID }),
|
|
30
|
+
connection.getTokenAccountsByOwner(owner, { programId: spl_token_1.TOKEN_2022_PROGRAM_ID })
|
|
31
|
+
]);
|
|
32
|
+
const tokenAccounts = [...splAccounts.value, ...token2022Accounts.value].map(({ account }) => spl_token_1.AccountLayout.decode(account.data));
|
|
33
|
+
const mintSet = new Set();
|
|
34
|
+
tokenAccounts.forEach((account) => mintSet.add(account.mint.toBase58()));
|
|
35
|
+
const mintList = Array.from(mintSet);
|
|
36
|
+
const mintInfos = new Map();
|
|
37
|
+
const batchSize = 100;
|
|
38
|
+
for (let i = 0; i < mintList.length; i += batchSize) {
|
|
39
|
+
const batch = mintList.slice(i, i + batchSize);
|
|
40
|
+
const accounts = await connection.getMultipleAccountsInfo(batch.map((mint) => new web3_js_1.PublicKey(mint)));
|
|
41
|
+
accounts.forEach((info, idx) => {
|
|
42
|
+
if (!info)
|
|
43
|
+
return;
|
|
44
|
+
const decoded = spl_token_1.MintLayout.decode(info.data);
|
|
45
|
+
mintInfos.set(batch[idx], decoded.decimals);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const balances = [];
|
|
49
|
+
balances.push({
|
|
50
|
+
mint: "SOL",
|
|
51
|
+
symbol: "SOL",
|
|
52
|
+
name: "solana",
|
|
53
|
+
amount: formatAmount(new bn_js_1.default(solBalance.toString()), 9),
|
|
54
|
+
raw: solBalance.toString(),
|
|
55
|
+
decimals: 9
|
|
56
|
+
});
|
|
57
|
+
tokenAccounts.forEach((account) => {
|
|
58
|
+
const mint = account.mint.toBase58();
|
|
59
|
+
const decimals = mintInfos.get(mint) ?? 0;
|
|
60
|
+
const raw = new bn_js_1.default(account.amount.toString());
|
|
61
|
+
if (raw.isZero())
|
|
62
|
+
return;
|
|
63
|
+
const symbol = mint.slice(0, 6);
|
|
64
|
+
balances.push({
|
|
65
|
+
mint,
|
|
66
|
+
symbol,
|
|
67
|
+
name: symbol,
|
|
68
|
+
amount: formatAmount(raw, decimals),
|
|
69
|
+
raw: raw.toString(),
|
|
70
|
+
decimals
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return balances;
|
|
74
|
+
}
|
|
75
|
+
function registerWalletCommands(program) {
|
|
76
|
+
const wallet = program.command("wallet").description("Manage wallets");
|
|
77
|
+
wallet
|
|
78
|
+
.command("create")
|
|
79
|
+
.description("Create a new wallet")
|
|
80
|
+
.argument("[name]")
|
|
81
|
+
.action(async (name) => {
|
|
82
|
+
const walletName = name ?? (await (0, prompt_1.promptInput)("Wallet name"));
|
|
83
|
+
const password = await (0, prompt_1.promptPassword)("Set wallet password", true);
|
|
84
|
+
const { mnemonic, wallet: walletFile } = await (0, wallet_manager_1.createWallet)(walletName, password);
|
|
85
|
+
const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
|
|
86
|
+
if (!config.activeWallet) {
|
|
87
|
+
await (0, config_manager_1.saveConfig)({ ...config, activeWallet: walletName });
|
|
88
|
+
}
|
|
89
|
+
if ((0, output_1.isJsonOutput)()) {
|
|
90
|
+
(0, output_1.logJson)({ name: walletFile.name, publicKey: walletFile.publicKey, mnemonic });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
(0, output_1.logSuccess)(`Wallet created: ${walletFile.name}`);
|
|
94
|
+
(0, output_1.logInfo)(`Public key: ${walletFile.publicKey}`);
|
|
95
|
+
(0, output_1.logInfo)(`Seed phrase (write down, shown once): ${mnemonic}`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
wallet
|
|
99
|
+
.command("import")
|
|
100
|
+
.description("Import an existing wallet")
|
|
101
|
+
.argument("<name>")
|
|
102
|
+
.option("--private-key <base58>", "Base58-encoded private key")
|
|
103
|
+
.option("--seed-phrase <phrase>", "Seed phrase")
|
|
104
|
+
.action(async (name, options) => {
|
|
105
|
+
let { privateKey, seedPhrase } = options;
|
|
106
|
+
if (!privateKey && !seedPhrase) {
|
|
107
|
+
const answer = await inquirer_1.default.prompt([
|
|
108
|
+
{
|
|
109
|
+
type: "list",
|
|
110
|
+
name: "method",
|
|
111
|
+
message: "Import method",
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: "Private key (base58)", value: "private-key" },
|
|
114
|
+
{ name: "Seed phrase", value: "seed-phrase" }
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]);
|
|
118
|
+
if (answer.method === "private-key") {
|
|
119
|
+
privateKey = await (0, prompt_1.promptInput)("Private key (base58)");
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
seedPhrase = await (0, prompt_1.promptInput)("Seed phrase");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const password = await (0, prompt_1.promptPassword)("Set wallet password", true);
|
|
126
|
+
if (privateKey) {
|
|
127
|
+
const walletFile = await (0, wallet_manager_1.importWalletFromPrivateKey)(name, privateKey, password);
|
|
128
|
+
const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
|
|
129
|
+
if (!config.activeWallet) {
|
|
130
|
+
await (0, config_manager_1.saveConfig)({ ...config, activeWallet: walletFile.name });
|
|
131
|
+
}
|
|
132
|
+
if ((0, output_1.isJsonOutput)()) {
|
|
133
|
+
(0, output_1.logJson)({ name: walletFile.name, publicKey: walletFile.publicKey });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
(0, output_1.logSuccess)(`Wallet imported: ${walletFile.name}`);
|
|
137
|
+
(0, output_1.logInfo)(`Public key: ${walletFile.publicKey}`);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!seedPhrase) {
|
|
142
|
+
(0, output_1.logError)("Missing seed phrase");
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const normalizedSeed = seedPhrase.trim().split(/\s+/).join(" ");
|
|
147
|
+
const walletFile = await (0, wallet_manager_1.importWalletFromMnemonic)(name, normalizedSeed, password);
|
|
148
|
+
const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
|
|
149
|
+
if (!config.activeWallet) {
|
|
150
|
+
await (0, config_manager_1.saveConfig)({ ...config, activeWallet: walletFile.name });
|
|
151
|
+
}
|
|
152
|
+
if ((0, output_1.isJsonOutput)()) {
|
|
153
|
+
(0, output_1.logJson)({ name: walletFile.name, publicKey: walletFile.publicKey });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
(0, output_1.logSuccess)(`Wallet imported: ${walletFile.name}`);
|
|
157
|
+
(0, output_1.logInfo)(`Public key: ${walletFile.publicKey}`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
wallet
|
|
161
|
+
.command("list")
|
|
162
|
+
.description("List wallets")
|
|
163
|
+
.action(async () => {
|
|
164
|
+
const wallets = await (0, wallet_manager_1.listWallets)();
|
|
165
|
+
const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
|
|
166
|
+
if ((0, output_1.isJsonOutput)()) {
|
|
167
|
+
(0, output_1.logJson)({ wallets, activeWallet: config.activeWallet });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (wallets.length === 0) {
|
|
171
|
+
(0, output_1.logInfo)("No wallets found");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
wallets.forEach((item) => {
|
|
175
|
+
const activeMark = item.name === config.activeWallet ? " (active)" : "";
|
|
176
|
+
(0, output_1.logInfo)(`${item.name}${activeMark} - ${item.publicKey}`);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
wallet
|
|
180
|
+
.command("use")
|
|
181
|
+
.description("Set active wallet")
|
|
182
|
+
.argument("<name>")
|
|
183
|
+
.action(async (name) => {
|
|
184
|
+
(0, wallet_manager_1.assertValidWalletName)(name);
|
|
185
|
+
if (!(await (0, wallet_manager_1.walletExists)(name))) {
|
|
186
|
+
(0, output_1.logError)(`Wallet not found: ${name}`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
|
|
191
|
+
await (0, config_manager_1.saveConfig)({ ...config, activeWallet: name });
|
|
192
|
+
if ((0, output_1.isJsonOutput)()) {
|
|
193
|
+
(0, output_1.logJson)({ activeWallet: name });
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
(0, output_1.logSuccess)(`Active wallet set to ${name}`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
wallet
|
|
200
|
+
.command("balance")
|
|
201
|
+
.description("Show wallet balances")
|
|
202
|
+
.argument("[name]")
|
|
203
|
+
.action(async (name) => {
|
|
204
|
+
const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
|
|
205
|
+
const walletName = name ?? config.activeWallet;
|
|
206
|
+
if (!walletName) {
|
|
207
|
+
(0, output_1.logError)("No active wallet set");
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
(0, wallet_manager_1.assertValidWalletName)(walletName);
|
|
212
|
+
const owner = await (0, wallet_manager_1.getWalletPublicKey)(walletName);
|
|
213
|
+
const walletAddress = owner.toBase58();
|
|
214
|
+
const balances = await (0, output_1.withSpinner)("Fetching balances", () => fetchRpcBalances(owner));
|
|
215
|
+
if ((0, output_1.isJsonOutput)()) {
|
|
216
|
+
(0, output_1.logJson)({ wallet: walletName, publicKey: walletAddress, tokens: balances });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
(0, output_1.logInfo)(`Wallet: ${walletName}`);
|
|
220
|
+
(0, output_1.logInfo)(`Public key: ${walletAddress}`);
|
|
221
|
+
if (balances.length === 0) {
|
|
222
|
+
(0, output_1.logInfo)("No balances found");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
balances.forEach((item) => {
|
|
226
|
+
// For SOL, just show symbol. For tokens, show full mint address
|
|
227
|
+
if (item.mint === "SOL") {
|
|
228
|
+
(0, output_1.logInfo)(`SOL: ${item.amount}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const label = item.name && item.name !== item.symbol
|
|
232
|
+
? `${item.symbol} (${item.name})`
|
|
233
|
+
: item.symbol;
|
|
234
|
+
(0, output_1.logInfo)(`${label}: ${item.amount}`);
|
|
235
|
+
(0, output_1.logInfo)(` Mint: ${item.mint}`);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
wallet
|
|
240
|
+
.command("export")
|
|
241
|
+
.description("Export a private key")
|
|
242
|
+
.argument("<name>")
|
|
243
|
+
.action(async (name) => {
|
|
244
|
+
(0, wallet_manager_1.assertValidWalletName)(name);
|
|
245
|
+
if (!(await (0, wallet_manager_1.walletExists)(name))) {
|
|
246
|
+
(0, output_1.logError)(`Wallet not found: ${name}`);
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const ok = await (0, prompt_1.promptConfirm)("This will reveal your private key. Continue?", false);
|
|
251
|
+
if (!ok) {
|
|
252
|
+
(0, output_1.logInfo)("Cancelled");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const password = await (0, prompt_1.promptPassword)("Enter wallet password");
|
|
256
|
+
const keypair = await (0, wallet_manager_1.decryptWallet)(name, password);
|
|
257
|
+
const privateKey = bs58_1.default.encode(keypair.secretKey);
|
|
258
|
+
if ((0, output_1.isJsonOutput)()) {
|
|
259
|
+
(0, output_1.logJson)({ name, publicKey: keypair.publicKey.toBase58(), privateKey });
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
(0, output_1.logSuccess)("Private key exported");
|
|
263
|
+
(0, output_1.logInfo)(`Public key: ${keypair.publicKey.toBase58()}`);
|
|
264
|
+
(0, output_1.logInfo)(`Private key (base58): ${privateKey}`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
9
|
+
const config_1 = require("./commands/config");
|
|
10
|
+
const wallet_1 = require("./commands/wallet");
|
|
11
|
+
const pools_1 = require("./commands/pools");
|
|
12
|
+
const swap_1 = require("./commands/swap");
|
|
13
|
+
const launchpad_1 = require("./commands/launchpad");
|
|
14
|
+
const clmm_1 = require("./commands/clmm");
|
|
15
|
+
const cpmm_1 = require("./commands/cpmm");
|
|
16
|
+
const output_1 = require("./lib/output");
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
18
|
+
const { version } = require("../package.json");
|
|
19
|
+
dotenv_1.default.config();
|
|
20
|
+
const program = new commander_1.Command();
|
|
21
|
+
program
|
|
22
|
+
.name("raydium")
|
|
23
|
+
.description("Raydium CLI")
|
|
24
|
+
.version(version)
|
|
25
|
+
.option("--json", "output json");
|
|
26
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
27
|
+
const opts = typeof actionCommand.optsWithGlobals === "function"
|
|
28
|
+
? actionCommand.optsWithGlobals()
|
|
29
|
+
: actionCommand.opts();
|
|
30
|
+
(0, output_1.setJsonOutput)(Boolean(opts.json));
|
|
31
|
+
});
|
|
32
|
+
(0, config_1.registerConfigCommands)(program);
|
|
33
|
+
(0, wallet_1.registerWalletCommands)(program);
|
|
34
|
+
(0, pools_1.registerPoolCommands)(program);
|
|
35
|
+
(0, swap_1.registerSwapCommands)(program);
|
|
36
|
+
(0, launchpad_1.registerLaunchpadCommands)(program);
|
|
37
|
+
(0, clmm_1.registerClmmCommands)(program);
|
|
38
|
+
(0, cpmm_1.registerCpmmCommands)(program);
|
|
39
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
41
|
+
(0, output_1.logError)(message);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sqrtPriceX64ToPrice = sqrtPriceX64ToPrice;
|
|
7
|
+
exports.tickToPrice = tickToPrice;
|
|
8
|
+
exports.tickToSqrtPriceX64 = tickToSqrtPriceX64;
|
|
9
|
+
exports.getAmountsFromLiquidity = getAmountsFromLiquidity;
|
|
10
|
+
exports.getAmountsForTickRange = getAmountsForTickRange;
|
|
11
|
+
exports.formatTokenAmount = formatTokenAmount;
|
|
12
|
+
exports.formatUsd = formatUsd;
|
|
13
|
+
exports.calculateUsdValue = calculateUsdValue;
|
|
14
|
+
exports.formatPrice = formatPrice;
|
|
15
|
+
exports.formatFeeRate = formatFeeRate;
|
|
16
|
+
exports.isPositionInRange = isPositionInRange;
|
|
17
|
+
exports.priceToTick = priceToTick;
|
|
18
|
+
exports.priceToAlignedTick = priceToAlignedTick;
|
|
19
|
+
exports.isTickAligned = isTickAligned;
|
|
20
|
+
exports.getTickSpacingFromFeeTier = getTickSpacingFromFeeTier;
|
|
21
|
+
exports.applySlippage = applySlippage;
|
|
22
|
+
exports.findPositionByNftMint = findPositionByNftMint;
|
|
23
|
+
exports.hasUnclaimedFees = hasUnclaimedFees;
|
|
24
|
+
exports.calculateWithdrawAmounts = calculateWithdrawAmounts;
|
|
25
|
+
const decimal_js_1 = __importDefault(require("decimal.js"));
|
|
26
|
+
const bn_js_1 = __importDefault(require("bn.js"));
|
|
27
|
+
// Q64.64 fixed point constant
|
|
28
|
+
const Q64 = new decimal_js_1.default(2).pow(64);
|
|
29
|
+
/**
|
|
30
|
+
* Convert sqrtPriceX64 (Q64.64 fixed point) to decimal price
|
|
31
|
+
* sqrtPriceX64 = sqrt(price) * 2^64
|
|
32
|
+
* price = (sqrtPriceX64 / 2^64)^2 = sqrtPriceX64^2 / 2^128
|
|
33
|
+
*/
|
|
34
|
+
function sqrtPriceX64ToPrice(sqrtPriceX64, decimalsA, decimalsB) {
|
|
35
|
+
const sqrtPrice = new decimal_js_1.default(sqrtPriceX64.toString()).div(Q64);
|
|
36
|
+
const price = sqrtPrice.pow(2);
|
|
37
|
+
// Adjust for decimal differences: price is token1/token0, so we need to adjust
|
|
38
|
+
const decimalAdjustment = new decimal_js_1.default(10).pow(decimalsA - decimalsB);
|
|
39
|
+
return price.mul(decimalAdjustment);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convert a tick index to price
|
|
43
|
+
* price = 1.0001^tick
|
|
44
|
+
*/
|
|
45
|
+
function tickToPrice(tick, decimalsA, decimalsB) {
|
|
46
|
+
const price = new decimal_js_1.default(1.0001).pow(tick);
|
|
47
|
+
const decimalAdjustment = new decimal_js_1.default(10).pow(decimalsA - decimalsB);
|
|
48
|
+
return price.mul(decimalAdjustment);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Convert tick to sqrtPriceX64
|
|
52
|
+
* sqrtPriceX64 = 1.0001^(tick/2) * 2^64
|
|
53
|
+
*/
|
|
54
|
+
function tickToSqrtPriceX64(tick) {
|
|
55
|
+
return new decimal_js_1.default(1.0001).pow(tick / 2).mul(Q64);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Calculate token amounts from liquidity for a position
|
|
59
|
+
* Based on Uniswap V3 math:
|
|
60
|
+
* - amount0 = L * (1/sqrt(P_lower) - 1/sqrt(P_upper)) when price <= lower
|
|
61
|
+
* - amount1 = L * (sqrt(P_upper) - sqrt(P_lower)) when price >= upper
|
|
62
|
+
* - Both when price is in range
|
|
63
|
+
*/
|
|
64
|
+
function getAmountsFromLiquidity(liquidity, currentSqrtPriceX64, tickLower, tickUpper, decimalsA, decimalsB) {
|
|
65
|
+
const L = new decimal_js_1.default(liquidity.toString());
|
|
66
|
+
const sqrtPriceCurrent = new decimal_js_1.default(currentSqrtPriceX64.toString());
|
|
67
|
+
const sqrtPriceLower = tickToSqrtPriceX64(tickLower);
|
|
68
|
+
const sqrtPriceUpper = tickToSqrtPriceX64(tickUpper);
|
|
69
|
+
let amount0 = new decimal_js_1.default(0);
|
|
70
|
+
let amount1 = new decimal_js_1.default(0);
|
|
71
|
+
if (sqrtPriceCurrent.lte(sqrtPriceLower)) {
|
|
72
|
+
// Current price is below the range - all liquidity is in token0
|
|
73
|
+
// amount0 = L * (sqrt(P_upper) - sqrt(P_lower)) / (sqrt(P_lower) * sqrt(P_upper))
|
|
74
|
+
amount0 = L.mul(sqrtPriceUpper.sub(sqrtPriceLower))
|
|
75
|
+
.div(sqrtPriceLower.mul(sqrtPriceUpper))
|
|
76
|
+
.mul(Q64); // Multiply by Q64 because sqrtPrices are scaled
|
|
77
|
+
}
|
|
78
|
+
else if (sqrtPriceCurrent.gte(sqrtPriceUpper)) {
|
|
79
|
+
// Current price is above the range - all liquidity is in token1
|
|
80
|
+
// amount1 = L * (sqrt(P_upper) - sqrt(P_lower)) / Q64
|
|
81
|
+
amount1 = L.mul(sqrtPriceUpper.sub(sqrtPriceLower)).div(Q64);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Price is in range - split between both tokens
|
|
85
|
+
// amount0 = L * (sqrt(P_upper) - sqrt(P_current)) / (sqrt(P_current) * sqrt(P_upper))
|
|
86
|
+
amount0 = L.mul(sqrtPriceUpper.sub(sqrtPriceCurrent))
|
|
87
|
+
.div(sqrtPriceCurrent.mul(sqrtPriceUpper))
|
|
88
|
+
.mul(Q64);
|
|
89
|
+
// amount1 = L * (sqrt(P_current) - sqrt(P_lower)) / Q64
|
|
90
|
+
amount1 = L.mul(sqrtPriceCurrent.sub(sqrtPriceLower)).div(Q64);
|
|
91
|
+
}
|
|
92
|
+
// Convert to human-readable amounts by dividing by decimals
|
|
93
|
+
const amount0Human = amount0.div(new decimal_js_1.default(10).pow(decimalsA));
|
|
94
|
+
const amount1Human = amount1.div(new decimal_js_1.default(10).pow(decimalsB));
|
|
95
|
+
return { amount0: amount0Human, amount1: amount1Human };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Calculate token amounts from liquidity for a tick range (for tick listing)
|
|
99
|
+
* This shows how much liquidity is available in a given tick range
|
|
100
|
+
*/
|
|
101
|
+
function getAmountsForTickRange(liquidityNet, tickIndex, tickSpacing, currentTick, currentSqrtPriceX64, decimalsA, decimalsB) {
|
|
102
|
+
// For a tick, liquidityNet represents the change in liquidity when crossing
|
|
103
|
+
// We calculate the amounts assuming the liquidity covers the next tick spacing
|
|
104
|
+
const tickLower = tickIndex;
|
|
105
|
+
const tickUpper = tickIndex + tickSpacing;
|
|
106
|
+
const L = new decimal_js_1.default(liquidityNet.toString()).abs();
|
|
107
|
+
if (L.isZero()) {
|
|
108
|
+
return { amount0: new decimal_js_1.default(0), amount1: new decimal_js_1.default(0) };
|
|
109
|
+
}
|
|
110
|
+
const sqrtPriceCurrent = new decimal_js_1.default(currentSqrtPriceX64.toString());
|
|
111
|
+
const sqrtPriceLower = tickToSqrtPriceX64(tickLower);
|
|
112
|
+
const sqrtPriceUpper = tickToSqrtPriceX64(tickUpper);
|
|
113
|
+
let amount0 = new decimal_js_1.default(0);
|
|
114
|
+
let amount1 = new decimal_js_1.default(0);
|
|
115
|
+
if (currentTick < tickLower) {
|
|
116
|
+
// Range is above current price - all in token0
|
|
117
|
+
amount0 = L.mul(sqrtPriceUpper.sub(sqrtPriceLower))
|
|
118
|
+
.div(sqrtPriceLower.mul(sqrtPriceUpper))
|
|
119
|
+
.mul(Q64);
|
|
120
|
+
}
|
|
121
|
+
else if (currentTick >= tickUpper) {
|
|
122
|
+
// Range is below current price - all in token1
|
|
123
|
+
amount1 = L.mul(sqrtPriceUpper.sub(sqrtPriceLower)).div(Q64);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Current price is in this range
|
|
127
|
+
amount0 = L.mul(sqrtPriceUpper.sub(sqrtPriceCurrent))
|
|
128
|
+
.div(sqrtPriceCurrent.mul(sqrtPriceUpper))
|
|
129
|
+
.mul(Q64);
|
|
130
|
+
amount1 = L.mul(sqrtPriceCurrent.sub(sqrtPriceLower)).div(Q64);
|
|
131
|
+
}
|
|
132
|
+
const amount0Human = amount0.div(new decimal_js_1.default(10).pow(decimalsA));
|
|
133
|
+
const amount1Human = amount1.div(new decimal_js_1.default(10).pow(decimalsB));
|
|
134
|
+
return { amount0: amount0Human, amount1: amount1Human };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Format a token amount for display
|
|
138
|
+
*/
|
|
139
|
+
function formatTokenAmount(amount, decimals = 6) {
|
|
140
|
+
if (amount.isZero())
|
|
141
|
+
return "0";
|
|
142
|
+
const fixed = amount.toFixed(decimals);
|
|
143
|
+
// Remove trailing zeros after decimal
|
|
144
|
+
return fixed.replace(/\.?0+$/, "");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Format a USD value for display
|
|
148
|
+
*/
|
|
149
|
+
function formatUsd(value) {
|
|
150
|
+
if (value === null)
|
|
151
|
+
return "";
|
|
152
|
+
const num = typeof value === "number" ? value : value.toNumber();
|
|
153
|
+
if (!Number.isFinite(num))
|
|
154
|
+
return "";
|
|
155
|
+
if (num < 0.01)
|
|
156
|
+
return "<$0.01";
|
|
157
|
+
if (num < 1)
|
|
158
|
+
return `$${num.toFixed(4)}`;
|
|
159
|
+
if (num < 1000)
|
|
160
|
+
return `$${num.toFixed(2)}`;
|
|
161
|
+
return `$${num.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Calculate total USD value from two token amounts and their prices
|
|
165
|
+
* Returns null if neither price is available
|
|
166
|
+
*/
|
|
167
|
+
function calculateUsdValue(amount0, amount1, price0, price1) {
|
|
168
|
+
if (price0 === null && price1 === null)
|
|
169
|
+
return null;
|
|
170
|
+
let total = new decimal_js_1.default(0);
|
|
171
|
+
if (price0 !== null)
|
|
172
|
+
total = total.add(amount0.mul(price0));
|
|
173
|
+
if (price1 !== null)
|
|
174
|
+
total = total.add(amount1.mul(price1));
|
|
175
|
+
return total;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Format a price with appropriate precision
|
|
179
|
+
*/
|
|
180
|
+
function formatPrice(price) {
|
|
181
|
+
const num = price.toNumber();
|
|
182
|
+
if (num === 0)
|
|
183
|
+
return "0";
|
|
184
|
+
if (num < 0.000001)
|
|
185
|
+
return price.toExponential(4);
|
|
186
|
+
if (num < 0.0001)
|
|
187
|
+
return price.toFixed(8);
|
|
188
|
+
if (num < 0.01)
|
|
189
|
+
return price.toFixed(6);
|
|
190
|
+
if (num < 1)
|
|
191
|
+
return price.toFixed(4);
|
|
192
|
+
if (num < 100)
|
|
193
|
+
return price.toFixed(4);
|
|
194
|
+
if (num < 10000)
|
|
195
|
+
return price.toFixed(2);
|
|
196
|
+
return price.toFixed(0);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Format fee rate (e.g., 0.25% = 2500 basis points in 1e6)
|
|
200
|
+
*/
|
|
201
|
+
function formatFeeRate(feeRate) {
|
|
202
|
+
const percent = (feeRate / 1000000) * 100;
|
|
203
|
+
return `${percent}%`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if a position is in range
|
|
207
|
+
*/
|
|
208
|
+
function isPositionInRange(tickLower, tickUpper, currentTick) {
|
|
209
|
+
return currentTick >= tickLower && currentTick < tickUpper;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Convert price to tick, aligned to the given tick spacing
|
|
213
|
+
* price = 1.0001^tick, so tick = log(price) / log(1.0001)
|
|
214
|
+
*/
|
|
215
|
+
function priceToTick(price, decimalsA, decimalsB) {
|
|
216
|
+
// Adjust price for decimals (reverse of tickToPrice)
|
|
217
|
+
const decimalAdjustment = new decimal_js_1.default(10).pow(decimalsA - decimalsB);
|
|
218
|
+
const adjustedPrice = price.div(decimalAdjustment);
|
|
219
|
+
// tick = log(adjustedPrice) / log(1.0001)
|
|
220
|
+
return Math.floor(adjustedPrice.ln().div(new decimal_js_1.default(1.0001).ln()).toNumber());
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Convert price to tick aligned to the given tick spacing
|
|
224
|
+
* Rounds down to nearest valid tick
|
|
225
|
+
*/
|
|
226
|
+
function priceToAlignedTick(price, tickSpacing, decimalsA, decimalsB) {
|
|
227
|
+
const tick = priceToTick(price, decimalsA, decimalsB);
|
|
228
|
+
return Math.floor(tick / tickSpacing) * tickSpacing;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if a tick is aligned to the given tick spacing
|
|
232
|
+
*/
|
|
233
|
+
function isTickAligned(tick, tickSpacing) {
|
|
234
|
+
return tick % tickSpacing === 0;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Fee tier (in basis points) to tick spacing mapping
|
|
238
|
+
* Common Raydium CLMM fee tiers:
|
|
239
|
+
* - 100 bps (1%) -> tick spacing 100
|
|
240
|
+
* - 500 bps (0.5%) -> tick spacing 10
|
|
241
|
+
* - 2500 bps (0.25%) -> tick spacing 60
|
|
242
|
+
* - 10000 bps (1%) -> tick spacing 200
|
|
243
|
+
*
|
|
244
|
+
* Note: The actual mapping depends on the AMM config.
|
|
245
|
+
* This provides common defaults.
|
|
246
|
+
*/
|
|
247
|
+
function getTickSpacingFromFeeTier(feeTierBps) {
|
|
248
|
+
switch (feeTierBps) {
|
|
249
|
+
case 100:
|
|
250
|
+
return 1;
|
|
251
|
+
case 500:
|
|
252
|
+
return 10;
|
|
253
|
+
case 2500:
|
|
254
|
+
return 60;
|
|
255
|
+
case 3000:
|
|
256
|
+
return 60;
|
|
257
|
+
case 10000:
|
|
258
|
+
return 200;
|
|
259
|
+
default:
|
|
260
|
+
throw new Error(`Unknown fee tier: ${feeTierBps} bps`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Apply slippage to an amount
|
|
265
|
+
* @param amount The amount to apply slippage to
|
|
266
|
+
* @param slippagePercent Slippage as a percentage (e.g., 1 for 1%)
|
|
267
|
+
* @param isMin If true, calculates minimum (amount - slippage), else maximum (amount + slippage)
|
|
268
|
+
*/
|
|
269
|
+
function applySlippage(amount, slippagePercent, isMin) {
|
|
270
|
+
const slippageBps = Math.floor(slippagePercent * 100); // Convert percent to basis points
|
|
271
|
+
const factor = isMin ? 10000 - slippageBps : 10000 + slippageBps;
|
|
272
|
+
return amount.mul(new bn_js_1.default(factor)).div(new bn_js_1.default(10000));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Find a position by its NFT mint address from an array of positions
|
|
276
|
+
*/
|
|
277
|
+
function findPositionByNftMint(positions, nftMint) {
|
|
278
|
+
const nftMintStr = nftMint.toBase58();
|
|
279
|
+
return positions.find((p) => p.nftMint?.toBase58() === nftMintStr);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Check if a position has unclaimed fees
|
|
283
|
+
*/
|
|
284
|
+
function hasUnclaimedFees(position) {
|
|
285
|
+
const feesA = position.tokenFeesOwedA;
|
|
286
|
+
const feesB = position.tokenFeesOwedB;
|
|
287
|
+
const hasFeesA = feesA && !feesA.isZero?.();
|
|
288
|
+
const hasFeesB = feesB && !feesB.isZero?.();
|
|
289
|
+
return hasFeesA || hasFeesB || false;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Calculate amounts to receive when removing liquidity
|
|
293
|
+
*/
|
|
294
|
+
function calculateWithdrawAmounts(liquidity, totalLiquidity, currentSqrtPriceX64, tickLower, tickUpper, decimalsA, decimalsB) {
|
|
295
|
+
return getAmountsFromLiquidity(liquidity.toString(), currentSqrtPriceX64.toString(), tickLower, tickUpper, decimalsA, decimalsB);
|
|
296
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SOLANA_CODEX_NETWORK_ID = void 0;
|
|
4
|
+
exports.getCodexClient = getCodexClient;
|
|
5
|
+
const sdk_1 = require("@codex-data/sdk");
|
|
6
|
+
let cachedClient = null;
|
|
7
|
+
function getCodexClient() {
|
|
8
|
+
if (cachedClient)
|
|
9
|
+
return cachedClient;
|
|
10
|
+
const apiKey = process.env.CODEX_API_KEY;
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
throw new Error("Missing CODEX_API_KEY in environment");
|
|
13
|
+
}
|
|
14
|
+
cachedClient = new sdk_1.Codex(apiKey);
|
|
15
|
+
return cachedClient;
|
|
16
|
+
}
|
|
17
|
+
exports.SOLANA_CODEX_NETWORK_ID = 1399811149;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureConfigDir = ensureConfigDir;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
8
|
+
exports.saveConfig = saveConfig;
|
|
9
|
+
exports.isValidConfigKey = isValidConfigKey;
|
|
10
|
+
exports.parseConfigValue = parseConfigValue;
|
|
11
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
12
|
+
const paths_1 = require("./paths");
|
|
13
|
+
const config_1 = require("../types/config");
|
|
14
|
+
const NUMBER_KEYS = ["default-slippage", "priority-fee"];
|
|
15
|
+
const EXPLORER_VALUES = ["solscan", "solanaFm", "solanaExplorer"];
|
|
16
|
+
async function ensureConfigDir() {
|
|
17
|
+
await promises_1.default.mkdir(paths_1.CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
async function loadConfig(options) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await promises_1.default.readFile(paths_1.CONFIG_PATH, "utf8");
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return { ...config_1.DEFAULT_CONFIG, ...parsed };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === "ENOENT") {
|
|
27
|
+
const config = { ...config_1.DEFAULT_CONFIG };
|
|
28
|
+
if (options?.createIfMissing) {
|
|
29
|
+
await saveConfig(config);
|
|
30
|
+
}
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function saveConfig(config) {
|
|
37
|
+
await ensureConfigDir();
|
|
38
|
+
await promises_1.default.writeFile(paths_1.CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
39
|
+
}
|
|
40
|
+
function isValidConfigKey(key) {
|
|
41
|
+
return Object.prototype.hasOwnProperty.call(config_1.DEFAULT_CONFIG, key);
|
|
42
|
+
}
|
|
43
|
+
function parseConfigValue(key, value) {
|
|
44
|
+
if (NUMBER_KEYS.includes(key)) {
|
|
45
|
+
const num = Number(value);
|
|
46
|
+
if (!Number.isFinite(num))
|
|
47
|
+
throw new Error(`Invalid number for ${key}: ${value}`);
|
|
48
|
+
return num;
|
|
49
|
+
}
|
|
50
|
+
if (key === "explorer") {
|
|
51
|
+
if (!EXPLORER_VALUES.includes(value)) {
|
|
52
|
+
throw new Error(`Invalid explorer value. Use one of: ${EXPLORER_VALUES.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
if (key === "activeWallet") {
|
|
57
|
+
if (value === "null")
|
|
58
|
+
return null;
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
if (key === "pinata-jwt") {
|
|
62
|
+
if (value === "null" || value === "")
|
|
63
|
+
return null;
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|