Documentation Index
Fetch the complete documentation index at: https://docs.raydium.io/llms.txt
Use this file to discover all available pages before exploring further.
Что это делает. Создаёт новый CLMM-пул с выбранным уровнем комиссии и открывает на нём начальную концентрированную позицию. Две транзакции, один скрипт. Код адаптирован из официальных примеров в raydium-sdk-V2-demo/src/clmm в один файл, готовый к запуску в Node.
Подготовка
Убедитесь, что вы прочитали требования Quick start и установили переменные окружения RPC_URL, KEYPAIR и зависимости.
Создание CLMM-пула требует единовременной комиссии и платежей за аренду tick-массивов под начальную позицию. Также нужно иметь оба seed-токена в кошельке — открытие позиции, когда цена находится в выбранном диапазоне, требует ликвидности с обеих сторон.
Шаг 1 — config.ts
Сохраните как config.ts. Структура совпадает с src/config.ts.template из демо-репо — disableFeatureCheck принудительно установлена в true (рекомендуется для любой нетривиальной интеграции, чтобы SDK не блокировался на проверке функций при старте):
// config.ts
import { Raydium, TxVersion, parseTokenAccountResp } from "@raydium-io/raydium-sdk-v2";
import { Connection, Keypair, clusterApiUrl } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
import bs58 from "bs58";
import fs from "node:fs";
export const owner: Keypair = Keypair.fromSecretKey(
// accept either a JSON-array keypair file (same shape Solana CLI writes) or a bs58 secret in env
process.env.KEYPAIR_BS58
? bs58.decode(process.env.KEYPAIR_BS58)
: new Uint8Array(JSON.parse(fs.readFileSync(process.env.KEYPAIR!, "utf8"))),
);
export const connection = new Connection(
process.env.RPC_URL ?? clusterApiUrl("mainnet-beta"),
"confirmed",
);
export const txVersion = TxVersion.V0;
const cluster = "mainnet" as "mainnet" | "devnet";
let raydium: Raydium | undefined;
export const initSdk = async (params?: { loadToken?: boolean }) => {
if (raydium) return raydium;
raydium = await Raydium.load({
owner,
connection,
cluster,
disableFeatureCheck: true,
disableLoadToken: !params?.loadToken,
blockhashCommitment: "finalized",
});
return raydium;
};
export const fetchTokenAccountData = async () => {
const solAccountResp = await connection.getAccountInfo(owner.publicKey);
const tokenAccountResp = await connection.getTokenAccountsByOwner(owner.publicKey, {
programId: TOKEN_PROGRAM_ID,
});
const token2022Req = await connection.getTokenAccountsByOwner(owner.publicKey, {
programId: TOKEN_2022_PROGRAM_ID,
});
return parseTokenAccountResp({
owner: owner.publicKey,
solAccountResp,
tokenAccountResp: {
context: tokenAccountResp.context,
value: [...tokenAccountResp.value, ...token2022Req.value],
},
});
};
Шаг 2 — createPool.ts
Сохраните рядом с config.ts. Источник: src/clmm/createPool.ts.
// createPool.ts
import { CLMM_PROGRAM_ID, DEVNET_PROGRAM_ID } from "@raydium-io/raydium-sdk-v2";
import { PublicKey } from "@solana/web3.js";
import Decimal from "decimal.js";
import { initSdk, txVersion } from "./config";
export const createPool = async () => {
const raydium = await initSdk({ loadToken: true });
// RAY
const mint1 = await raydium.token.getTokenInfo("4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R");
// USDT
const mint2 = await raydium.token.getTokenInfo("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");
// Fee tiers come from the live API. On devnet the published `id` field is wrong;
// re-derive the PDA before passing it to the SDK.
const clmmConfigs = await raydium.api.getClmmConfigs();
const { execute } = await raydium.clmm.createPool({
programId: CLMM_PROGRAM_ID,
// programId: DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID,
mint1,
mint2,
ammConfig: {
...clmmConfigs[0],
id: new PublicKey(clmmConfigs[0].id),
fundOwner: "",
description: "",
},
initialPrice: new Decimal(1),
txVersion,
// optional: set up priority fee here
// computeBudgetConfig: { units: 600000, microLamports: 46591500 },
});
const { txId } = await execute({ sendAndConfirm: true });
console.log("clmm pool created:", { txId: `https://explorer.solana.com/tx/${txId}` });
process.exit();
};
createPool();
Шаг 3 — createPosition.ts
Источник: src/clmm/createPosition.ts.
// createPosition.ts
import {
ApiV3PoolInfoConcentratedItem,
TickUtils,
PoolUtils,
ClmmKeys,
} from "@raydium-io/raydium-sdk-v2";
import BN from "bn.js";
import Decimal from "decimal.js";
import { initSdk, txVersion } from "./config";
import { isValidClmm } from "./utils";
export const createPosition = async () => {
const raydium = await initSdk();
let poolInfo: ApiV3PoolInfoConcentratedItem;
// RAY-USDC pool
const poolId = "61R1ndXxvsWXXkWSyNkCxnzwd3zUNB8Q2ibmkiLPC8ht";
let poolKeys: ClmmKeys | undefined;
if (raydium.cluster === "mainnet") {
const data = await raydium.api.fetchPoolById({ ids: poolId });
poolInfo = data[0] as ApiV3PoolInfoConcentratedItem;
if (!isValidClmm(poolInfo.programId)) throw new Error("target pool is not CLMM pool");
} else {
const data = await raydium.clmm.getPoolInfoFromRpc(poolId);
poolInfo = data.poolInfo;
poolKeys = data.poolKeys;
}
// Optional: pull on-chain real-time price to avoid slippage errors from a stale API quote.
// const rpcData = await raydium.clmm.getRpcClmmPoolInfo({ poolId: poolInfo.id });
// poolInfo.price = rpcData.currentPrice;
const inputAmount = 0.000001; // RAY amount
const [startPrice, endPrice] = [0.000001, 100000];
const { tick: lowerTick } = TickUtils.getPriceAndTick({
poolInfo,
price: new Decimal(startPrice),
baseIn: true,
});
const { tick: upperTick } = TickUtils.getPriceAndTick({
poolInfo,
price: new Decimal(endPrice),
baseIn: true,
});
const epochInfo = await raydium.fetchEpochInfo();
const res = await PoolUtils.getLiquidityAmountOutFromAmountIn({
poolInfo,
slippage: 0,
inputA: true,
tickUpper: Math.max(lowerTick, upperTick),
tickLower: Math.min(lowerTick, upperTick),
amount: new BN(new Decimal(inputAmount || "0").mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
add: true,
amountHasFee: true,
epochInfo,
});
const { execute, extInfo } = await raydium.clmm.openPositionFromBase({
poolInfo,
poolKeys,
tickUpper: Math.max(lowerTick, upperTick),
tickLower: Math.min(lowerTick, upperTick),
base: "MintA",
ownerInfo: { useSOLBalance: true },
baseAmount: new BN(new Decimal(inputAmount || "0").mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
otherAmountMax: res.amountSlippageB.amount,
txVersion,
computeBudgetConfig: { units: 600000, microLamports: 100000 },
});
const { txId } = await execute({ sendAndConfirm: true });
console.log("clmm position opened:", { txId, nft: extInfo.nftMint.toBase58() });
process.exit();
};
createPosition();
Шаг 4 — utils.ts
Источник: src/clmm/utils.ts.
// utils.ts
import { CLMM_PROGRAM_ID, DEVNET_PROGRAM_ID } from "@raydium-io/raydium-sdk-v2";
const VALID_PROGRAM_IDS = new Set<string>([
CLMM_PROGRAM_ID.toBase58(),
DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID.toBase58(),
]);
export const isValidClmm = (programId: string) => VALID_PROGRAM_IDS.has(programId);
Запуск
# create the pool first
RPC_URL="https://api.mainnet-beta.solana.com" \
KEYPAIR="$HOME/.config/solana/id.json" \
npx tsx createPool.ts
# then open an initial position against the new pool id
# (edit poolId at the top of createPosition.ts to point at your new pool)
RPC_URL="https://api.mainnet-beta.solana.com" \
KEYPAIR="$HOME/.config/solana/id.json" \
npx tsx createPosition.ts
Что произошло
Транзакция 1 — raydium.clmm.createPool инициализировала:
- состояние пула по каноническому PDA для
(mint1, mint2, ammConfig),
token_0_vault и token_1_vault (отсортированы по порядку байтов mint),
- кольцевой буфер наблюдений,
- встроенное битовое отображение tick-массивов,
и установила начальную sqrt_price_x64 из указанной initialPrice.
Транзакция 2 — raydium.clmm.openPositionFromBase открыла концентрированную позицию:
- отчеканила NFT позиции в ваш кошелёк (NFT — это сама позиция; её передача переводит позицию),
- выделила tick-массивы на нижней и верхней границах (плата за аренду один раз, если это первая позиция в этих диапазонах; программа не предоставляет способ закрывать tick-массивы, поэтому эта аренда постоянна),
- внесла
inputAmount в mint1 и соответствующий размер mint2 (рассчитано PoolUtils.getLiquidityAmountOutFromAmountIn),
- закредитовала позицию ликвидностью, пропорциональной ширине диапазона.
Чем уже диапазон, тем выше эффективность капитала на доллар TVL — и тем болезненнее непостоянный убыток при смещении цены за границы диапазона. Диапазон выше ([0.000001, 100000]) практически полный; сузьте его, чтобы сконцентрировать комиссии рядом с текущей спотовой ценой.
Выбор уровня комиссии
clmmConfigs[0] — уровень с минимальной комиссией. Полный набор опубликован на GET https://api-v3.raydium.io/main/clmm-config:
| Индекс | tradeFeeRate | Tick spacing | Используйте когда |
|---|
| 0 | 100 (1bp) | 1 | Стейбл/стейбл, очень низкий непостоянный убыток |
| 1 | 500 (5bp) | 10 | Высоко коррелирующие активы (например, стейкированный vs базовый) |
| 2 | 2_500 (25bp) | 60 | Стандартная торговая пара, blue-chip + стейбл |
| 3 | 10_000 (1.00%) | 120 | Волатильная или тонкая пара с высоким риском IL |
Полную матрицу решений см. в user-flows/choosing-a-pool-type.
Частые ошибки
Pool already exists for this config — CLMM-пул уже существует для этой тройки (mint1, mint2, ammConfig). Найдите существующий ID пула и пропустите Шаг 2.
Insufficient funds for amount B — Ваш кошелёк имеет запрашиваемое количество mintA, но не хватает mintB. Открытие позиции, когда цена находится в диапазоне, требует ликвидности с обеих сторон.
Tick out of range — Ваша lowerPrice или upperPrice выходит за пределы представимого диапазона цен. Используйте более разумный диапазон относительно текущей цены.
- Устаревшая цена — Котировка из API может быть устаревшей на 5–60 секунд. Если
executePosition не пройдёт по slippage, раскомментируйте блок getRpcClmmPoolInfo в createPosition.ts, чтобы переполучить свежую цену прямо перед подписанием.
Замечания
- Position NFT — ваш единственный способ доступа. Потеряйте NFT или передайте его — потеряете доступ к позиции. Относитесь к нему как к ключу.
- Позиции вне диапазона не приносят комиссии. Если цена выходит за пределы
[lowerPrice, upperPrice], ваша позиция полностью припаркована в один актив и не приносит ничего, пока вы не перебалансируете.
- Аренда tick-массива — односторонняя. Первая позиция, которая касается никогда не инициализированного tick-массива, платит его аренду; программа не предоставляет пути закрытия tick-массивов, поэтому эта аренда постоянна. Последующие позиции в том же массиве — бесплатны.
Дальше
Источники: