メインコンテンツへスキップ

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.

このページは AI による自動翻訳です。すべての内容は英語版を正とします。英語版を表示 →
このスクリプトの役割。 選択したフィー階層で新しいCLMMプールを作成し、初期集約ポジションをオープンします。2つのトランザクション、1つのスクリプトで完了です。コードはraydium-sdk-V2-demo/src/clmmの公式デモから取得し、単一のNode実行可能ファイルに適合させています。

セットアップ

クイックスタートの前提条件を読んでから、RPC_URLKEYPAIR、および依存関係がインストールされていることを確認してください。 CLMMプール作成には、初回フィーと初期ポジションに対するティック配列のレントが必要です。また、両方のシードミントをウォレットに用意する必要があります。選択した範囲内に価格がある場合、ポジションをオープンするには両側にリクイディティが必要です。

ステップ1 — config.ts

config.tsとして保存してください。これはデモリポジトリのsrc/config.ts.templateと同じ形式です。disableFeatureChecktrueに強制されています(非自明な統合では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 は以下を初期化しました:
  • (mint1, mint2, ammConfig)の正規PDAsのプール状態、
  • token_0_vaulttoken_1_vault(ミントバイト順でソート)、
  • observationリングバッファ、
  • インラインティック配列ビットマップ、
そしてinitialPriceから初期sqrt_price_x64を設定しました。 トランザクション2 — raydium.clmm.openPositionFromBase は集約ポジションをオープンしました:
  • ウォレットにポジションNFTをミント(NFTはポジションそのもの。転送するとポジションが転送される)、
  • 下限と上限でティック配列を割り当て(最初のポジションがそれらの範囲内では1回のレント。プログラムはティック配列を閉じる方法を公開していないため、そのレントは永続的。同じ配列内の後続ポジションはレントを追加で支払わない)、
  • mint1inputAmountと対応するmint2の金額をデポジット(PoolUtils.getLiquidityAmountOutFromAmountInで計算)、
  • 範囲幅に比例したリクイディティでポジションにクレジット。
範囲が狭いほど、TVLあたりの資本効率は高くなり、価格が範囲外に移動したときのインパーマネント損失はより大きくなります。上記で使用した範囲([0.000001, 100000])は実質的にフルレンジです。現在のスポット付近のフィーに集約するために範囲を狭めます。

フィー階層の選択

clmmConfigs[0]は最も低いフィー階層です。完全なセットはGET https://api-v3.raydium.io/main/clmm-configで公開されています:
インデックスtradeFeeRateティック間隔使用する場合
0100(1bp)1ステーブル・ステーブル、インパーマネント損失が非常に低い場合
1500(5bp)10高度に相関したアセット(例:液体ステーキング対基礎)
22_500(25bp)60標準的なトークンペア、ブルーチップ+ステーブル
310_000(1.00%)120IL リスクが高い変動性またはシン ペア
完全な決定マトリックスについては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 rangelowerPriceまたはupperPriceが表現可能な価格範囲の外にあります。現在の価格に対してより合理的な範囲を使用してください。
  • 古い価格 — APIからの引用は5~60秒古い可能性があります。executePositionがスリッページで失敗する場合は、createPosition.tsgetRpcClmmPoolInfoブロックのコメントを解除して、署名直前にライブ価格を再取得してください。

注意事項

  • ポジションNFTは唯一のハンドルです。 NFTを失うか転送すると、ポジションにアクセスできなくなります。キーのように扱ってください。
  • 範囲外のポジションはフィーを獲得しません。 価格が[lowerPrice, upperPrice]の外に移動すると、ポジションは完全に1つのアセットに駐車され、リバランスするまで何も獲得しません。
  • ティック配列のレントは一方通行です。 最初のポジションが初期化されたことがないティック配列に触れると、そのレントを支払います。プログラムはティック配列を閉じるパスを公開していないため、そのレントは永続的です。同じ配列内の後続ポジションは無料です。

次のステップ

ソース: