Passer au contenu principal

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.

Cette page est traduite automatiquement par IA. La version anglaise fait foi.Voir la version anglaise →
Ce que cela fait. Crée un nouveau pool CLMM au niveau de frais de votre choix, puis ouvre une position concentrée initiale. Deux transactions, un seul script. Le code provient des démos officielles dans raydium-sdk-V2-demo/src/clmm et a été adapté pour s’exécuter dans un seul fichier Node.

Configuration

Assurez-vous d’avoir lu les prérequis du démarrage rapide et d’avoir RPC_URL, KEYPAIR et les dépendances installées. La création d’un pool CLMM entraîne un frais unique plus un loyer par tick-array pour la position initiale. Vous aurez également besoin des deux mints de semence dans votre portefeuille — ouvrir une position quand le prix se situe dans la plage choisie nécessite de la liquidité des deux côtés.

Étape 1 — config.ts

Enregistrez sous config.ts. Cette structure est la même que celle du modèle src/config.ts.template du dépôt de démo — disableFeatureCheck est forcé à true (recommandé pour tout intégration non triviale afin que le SDK ne se bloque pas sur son appel de détection de fonctionnalités au démarrage) :
// 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],
    },
  });
};

Étape 2 — createPool.ts

Enregistrez aux côtés de config.ts. Source : 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();

Étape 3 — createPosition.ts

Source : 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();

Étape 4 — utils.ts

Source : 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);

Exécuter

# 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

Ce qui vient de se passer

Transaction 1 — raydium.clmm.createPool a initialisé :
  • l’état du pool à la PDA canonique pour (mint1, mint2, ammConfig),
  • les token_0_vault et token_1_vault (triés par ordre d’octet du mint),
  • le ring buffer observation,
  • la bitmap de tick-array en ligne,
et a défini le sqrt_price_x64 initial à partir de votre initialPrice. Transaction 2 — raydium.clmm.openPositionFromBase a ouvert une position concentrée :
  • créé un NFT de position dans votre portefeuille (le NFT est la position ; le transférer transfère la position),
  • alloué des tick arrays aux limites inférieure et supérieure (loyer unique si première position dans ces plages ; les tick arrays ne sont jamais fermés par le programme, donc les positions suivantes dans les mêmes arrays ne payent pas de loyer supplémentaire),
  • déposé inputAmount de mint1 et le montant de paire correspondant de mint2 (calculé par PoolUtils.getLiquidityAmountOutFromAmountIn),
  • crédité la position avec une liquidité proportionnelle à la largeur de la plage.
Plus la plage est étroite, plus l’efficacité du capital par dollar de TVL est élevée — et plus la perte impermanente est douloureuse quand le prix s’éloigne de la plage. La plage utilisée ci-dessus ([0.000001, 100000]) est effectivement pleine ; resserrez-la pour concentrer les frais près du prix spot actuel.

Choisir un niveau de frais

clmmConfigs[0] est le niveau de frais le plus bas. L’ensemble complet est publié sur GET https://api-v3.raydium.io/main/clmm-config :
IndextradeFeeRateEspacement des ticksÀ utiliser quand
0100 (1bp)1Stables / stables, perte impermanente très faible attendue
1500 (5bp)10Actifs hautement corrélés (p. ex. liquid-staking vs sous-jacent)
22_500 (25bp)60Paire de tokens standard, blue-chip + stable
310_000 (1,00%)120Paire volatile ou illiquide où le risque de IL est élevé
Consultez user-flows/choosing-a-pool-type pour une matrice de décision complète.

Erreurs courantes

  • Pool already exists for this config — Un pool CLMM existe déjà pour ce triplet (mint1, mint2, ammConfig). Recherchez l’ID de pool existant et ignorez l’étape 2.
  • Insufficient funds for amount B — Votre portefeuille a le montant demandé de mintA mais pas le mintB correspondant. Ouvrir une position quand le prix se situe dans la plage nécessite de la liquidité des deux côtés.
  • Tick out of range — Votre lowerPrice ou upperPrice sort de la plage de prix représentable. Utilisez une plage plus raisonnable par rapport au prix actuel.
  • Prix obsolète — Une cotation de l’API peut être obsolète de 5 à 60 secondes. Si executePosition échoue sur le slippage, décommentez le bloc getRpcClmmPoolInfo dans createPosition.ts pour récupérer le prix actuel juste avant de signer.

Avertissements

  • Le NFT de position est votre seul accès. Perdez le NFT ou transférez-le, vous perdez l’accès à la position. Traitez-le comme une clé.
  • Les positions hors plage ne gagnent pas de frais. Si le prix se déplace en dehors de [lowerPrice, upperPrice], votre position est entièrement parquée dans un actif et ne gagne rien jusqu’à ce que vous rééquilibriez.
  • Le loyer du tick array est unidirectionnel. La première position à toucher un tick array jamais initialisé paie son loyer ; le programme n’expose pas de voie pour fermer les tick arrays, donc ce loyer est permanent. Les positions suivantes dans le même array sont gratuites.

Suivant

Sources :