TypeScript client
@pina-rs/amm is generated from the program’s IDL for @solana/kit. It ships ESM and CommonJS builds with bundled type declarations.
pnpm add @pina-rs/amm @solana/kit# Optional, for token program addresses and associated token accounts:pnpm add @solana-program/tokenWhat is exported
| Kind | Examples |
|---|---|
| Program | PINA_AMM_PROGRAM_ADDRESS, PinaAmmInstruction, identifyPinaAmmInstruction |
| Instructions | getSwapExactInInstruction, getCreatePoolInstructionAsync, getDepositInstruction, … |
| Data codecs | getSwapExactInInstructionDataEncoder, getPoolDecoder, … |
| Accounts | fetchPool, fetchMaybePool, fetchAmmConfig, decodePool, … |
| PDAs | findAmmConfigPda, findPoolPda, findPoolVaultPda, findPoolLpMintPda |
| Events | parsePinaAmmEventsFromLogs, decodeSwappedEvent, … |
| Errors | PINA_AMM_ERROR__SLIPPAGE_EXCEEDED, getPinaAmmErrorMessage, isPinaAmmError |
Find and read a pool
import { fetchPool, findAmmConfigPda, findPoolPda } from "@pina-rs/amm";import { type Address, createSolanaRpc, getAddressEncoder } from "@solana/kit";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
/** Sort two mints by their 32 address bytes, the order the program uses. */function sortMints(a: Address, b: Address): [Address, Address] { const encoder = getAddressEncoder(); const left = encoder.encode(a); const right = encoder.encode(b); for (let index = 0; index < 32; index += 1) { if (left[index] !== right[index]) { return left[index]! < right[index]! ? [a, b] : [b, a]; } } throw new Error("a pool needs two different mints");}
const [ammConfig] = await findAmmConfigPda({ index: 0 });const [mint0, mint1] = sortMints(usdcMint, solMint);const [poolAddress] = await findPoolPda({ ammConfig, mint0, mint1 });
const pool = await fetchPool(rpc, poolAddress);console.log(pool.data.lpSupply, pool.data.tradeFeeRate);fetchPool throws when the account is missing and checks the discriminator and schema version. It does not check the owner; fetchPool reads the address you derived from the program, which is enough. If you decode an account from an untrusted address, compare account.programAddress with PINA_AMM_PROGRAM_ADDRESS first.
Quote a swap off chain
Reserves are the vault balances minus the fees the pool has accrued:
import { fetchToken } from "@solana-program/token";
const [vault0, vault1] = await Promise.all([ fetchToken(rpc, pool.data.vault0), fetchToken(rpc, pool.data.vault1),]);const reserve0 = vault0.data.amount - pool.data.protocolFees0 - pool.data.creatorFees0;const reserve1 = vault1.data.amount - pool.data.protocolFees1 - pool.data.creatorFees1;
const D = 1_000_000n;const ceilDiv = (a: bigint, b: bigint) => (a + b - 1n) / b;
/** Exact-input quote when the creator fee is taken from the input. */function quoteExactIn( amountIn: bigint, reserveIn: bigint, reserveOut: bigint,): bigint { const t = BigInt(pool.data.tradeFeeRate); const c = BigInt(pool.data.creatorFeeRate); const fee = ceilDiv(amountIn * (t + c), D); const net = amountIn - fee; return (net * reserveOut) / (reserveIn + net);}Math has the formula for every creator fee mode and for exact-output swaps. When in doubt, simulate: the program’s own Swapped event is the authoritative quote.
Swap
import { getSwapExactInInstruction } from "@pina-rs/amm";import { findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstructionAsync, TOKEN_PROGRAM_ADDRESS,} from "@solana-program/token";import { appendTransactionMessageInstructions, assertIsTransactionWithBlockhashLifetime, createSolanaRpcSubscriptions, createTransactionMessage, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners,} from "@solana/kit";
const amountIn = 1_000_000n;const quote = quoteExactIn(amountIn, reserve0, reserve1);const minimumAmountOut = (quote * (10_000n - 50n)) / 10_000n; // 0.5%
const [inputToken] = await findAssociatedTokenPda({ owner: wallet.address, mint: pool.data.mint0, tokenProgram: TOKEN_PROGRAM_ADDRESS,});const [outputToken] = await findAssociatedTokenPda({ owner: wallet.address, mint: pool.data.mint1, tokenProgram: TOKEN_PROGRAM_ADDRESS,});
const instructions = [ await getCreateAssociatedTokenIdempotentInstructionAsync({ payer: wallet, owner: wallet.address, mint: pool.data.mint1, }), getSwapExactInInstruction({ trader: wallet, pool: poolAddress, inputToken, outputToken, inputVault: pool.data.vault0, outputVault: pool.data.vault1, inputTokenProgram: TOKEN_PROGRAM_ADDRESS, outputTokenProgram: TOKEN_PROGRAM_ADDRESS, amountIn, minimumAmountOut, }),];
const rpcSubscriptions = createSolanaRpcSubscriptions( "wss://api.mainnet-beta.solana.com",);const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions,});
const { value: blockhash } = await rpc.getLatestBlockhash().send();const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (message) => setTransactionMessageFeePayerSigner(wallet, message), (message) => setTransactionMessageLifetimeUsingBlockhash(blockhash, message), (message) => appendTransactionMessageInstructions(instructions, message),);const transaction = await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithBlockhashLifetime(transaction);await sendAndConfirm(transaction, { commitment: "confirmed" });To sell token 1 instead, reverse every input/output pair: swap inputToken/outputToken, inputVault/outputVault, and the token programs, create the token-0 account instead of the token-1 account, and quote with the reserves reversed, because quoteExactIn takes the input reserve first:
const quote = quoteExactIn(amountIn, reserve1, reserve0);const minimumAmountOut = (quote * (10_000n - 50n)) / 10_000n; // 0.5%The helper assumes the creator fee comes from the input, which for a token-1 sale holds in creator fee modes 0 and 2; mode 1 takes it from the output (see Math).
Token-2022 mints. The token program is part of an associated token account’s address, so a Token-2022 side needs TOKEN_2022_PROGRAM_ADDRESS from @solana-program/token-2022 in three places: its findAssociatedTokenPda call, the tokenProgram of its getCreateAssociatedTokenIdempotentInstructionAsync call, and its inputTokenProgram or outputTokenProgram. Read each mint’s owner to choose:
import { fetchEncodedAccount } from "@solana/kit";
const mint1Account = await fetchEncodedAccount(rpc, pool.data.mint1);const tokenProgram1 = mint1Account.exists ? mint1Account.programAddress : TOKEN_PROGRAM_ADDRESS;const [outputToken] = await findAssociatedTokenPda({ owner: wallet.address, mint: pool.data.mint1, tokenProgram: tokenProgram1,});getSwapExactOutInstruction takes amountOut and maximumAmountIn instead.
Create a pool
import { findPoolLpMintPda, findPoolVaultPda, getCreatePoolInstructionAsync,} from "@pina-rs/amm";
const [lpMint] = await findPoolLpMintPda({ pool: poolAddress });const [vault0] = await findPoolVaultPda({ pool: poolAddress, mint: mint0 });const [vault1] = await findPoolVaultPda({ pool: poolAddress, mint: mint1 });const [lpOwnerToken] = await findAssociatedTokenPda({ owner: wallet.address, mint: lpMint, tokenProgram: TOKEN_PROGRAM_ADDRESS,});
const createPool = await getCreatePoolInstructionAsync({ payer: wallet, depositor: wallet, poolCreatorAuthority: wallet, // any signer for an open tier ammConfig, mint0, mint1, lpMint, vault0, vault1, depositorToken0, depositorToken1, lpOwner: wallet.address, lpOwnerToken, tokenProgram0: TOKEN_PROGRAM_ADDRESS, tokenProgram1: TOKEN_PROGRAM_ADDRESS, amount0: 1_000_000_000n, amount1: 2_000_000_000n, creator: wallet.address, creatorFeeMode: 0,});The async builder derives pool for you; the LP program, associated token program, and system program have defaults.
Liquidity
import { getDepositInstruction, getWithdrawInstruction } from "@pina-rs/amm";
const deposit = getDepositInstruction({ owner: wallet, pool: poolAddress, vault0: pool.data.vault0, vault1: pool.data.vault1, lpMint: pool.data.lpMint, ownerToken0, ownerToken1, ownerLpToken, tokenProgram0: TOKEN_PROGRAM_ADDRESS, tokenProgram1: TOKEN_PROGRAM_ADDRESS, lpAmount: 1_000_000n, maximumAmount0: maxIn0, maximumAmount1: maxIn1,});Depositing lp shares costs ceil(reserve * lp / lpSupply) of each token, so compute maximumAmount* from the reserves plus your tolerance. getWithdrawInstruction mirrors it with minimumAmount0 and minimumAmount1.
Events
import { parsePinaAmmEventsFromLogs } from "@pina-rs/amm";
const response = await rpc.getTransaction(signature, { encoding: "json", maxSupportedTransactionVersion: 0,}).send();for ( const event of parsePinaAmmEventsFromLogs(response?.meta?.logMessages ?? [])) { if (event.name === "swapped") { console.log(event.data.amountIn, event.data.amountOut, event.data.tradeFee); }}Always pass every log line of one transaction, in order.
Errors
import { getPinaAmmErrorMessage, isPinaAmmError, PINA_AMM_ERROR__SLIPPAGE_EXCEEDED,} from "@pina-rs/amm";import { unwrapSimulationError } from "@solana/kit";
try { await sendAndConfirm(transaction, { commitment: "confirmed" });} catch (error) { const cause = unwrapSimulationError(error); if ( isPinaAmmError(cause, transactionMessage, PINA_AMM_ERROR__SLIPPAGE_EXCEEDED) ) { // re-quote and retry } else if (isPinaAmmError(cause, transactionMessage)) { console.error(getPinaAmmErrorMessage(cause.context.code)); } else { throw error; }}A rejected preflight simulation wraps the program error, so unwrap it with unwrapSimulationError before checking it.