Raydium AMM: Trading SPL tokens on Raydium using Typescript
Overview
Raydium is a decentralized order book and automated market maker (AMM) platform built on the Solana blockchain. While the Raydium frontend provides a user-friendly interface for swapping tokens, not all users interact with the platform through this interface. Developers, in particular, may need to build applications or server-side scripts that interact directly with the Raydium smart contracts to facilitate token swaps or leverage other Raydium features.
In this article, we’ll provide a brief overview of the swap instructions in the Raydium smart contracts. Additionally, we’ll walk through a code example that demonstrates how to build a script for swapping tokens on the Raydium AMM using TypeScript.
Prerequisites:
- Basic understanding of smart contract development on Solana
- Node.js and ts-node are installed.
- A wallet with USDC and SOL on the mainnet.
- Solana CLI (link)
The Raydium AMM Program
The Raydium.io AMM program is implemented in native Rust. Its structure resembles a basic vanilla Rust program with additional Rust files for logging (log.rs), mathematical computations (math.rs), and cross-program invocations (invoker.rs).
── program
├── Cargo.lock
├── Cargo.toml
├── Xargo.toml
└── src
├── entrypoint.rs
├── error.rs
├── instruction.rs
├── invokers.rs
├── lib.rs
├── log.rs
├── math.rs
├── processor.rs
└── state.rs
- entrypoint.rs: Entry point to the program.
- instruction.rs: An enum of instructions that can be executed in the program as well as a description of accepted accounts.
- processor.rs: Contains the core logic for instruction execution and state modification.
- state.rs: Provides object definitions for the program and methods for serializing and deserializing these objects.
- error.rs: An enum for Raydium AMM program-specific errors.
For this article, we will be focusing on the SwapBaseIn
and SwapBaseOut
instructions.
Implementing SOL-USDC swap on Raydium
The Raydium AMM program uses two instructions to exchange tokens; SwapBaseIn
and SwapBaseOut
. These two instructions accept similar accounts but differ in functionality. If the output token is the base token, we use the SwapBaseOut
else we use the SwapBaseIn
. These instructions can be found at swap-instruction. We aim to write a script to exchange SOL for USDC on Raydium using typescript.
Project Set up
- Create a new directory for the project in your terminal using the following;
mkdir raydium-swap
cd raydium-swap
2. Create a tsconfig.json file and add the following;
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
3. Create a package.json file and add the following;
{
"name": "raydium-swap",
"version": "1.0.0",
"main": "main.js",
"license": "MIT",
"scripts": {
"swap": "ts-node src/main.ts"
},
"dependencies": {
"@project-serum/serum": "^0.13.65",
"@raydium-io/raydium-sdk": "^1.3.1-beta.47",
"@solana/spl-token": "^0.3.11",
"@solana/web3.js": "^1.89.1",
"buffer-layout": "^1.2.2"
},
"devDependencies": {
"typescript": "^4.4.3"
}
}
4. Install dependencies;npm install
5. Install ts-node;npm install -g ts-node
6. Create a src
folder and add the main.ts
file in the src folder.
Fetch pool keys
As described in the contract code, the swap instructions accept 18 accounts and two instruction data. To successfully invoke the instruction, we need to fetch these accounts using the AMM ID of the SOL-USDC pool.
In the main.ts
file, we’re going to be creating the function that gets the accounts needed to execute the swap instruction from the AMM ID. Populate the main.ts
file with the code below;
import {
LIQUIDITY_STATE_LAYOUT_V4,
LiquidityPoolKeys,
MAINNET_PROGRAM_ID,
MARKET_STATE_LAYOUT_V3
} from "@raydium-io/raydium-sdk";
import {
Connection,
PublicKey
} from "@solana/web3.js";
const getPoolKeys = async (ammId: string, connection: Connection) => {
const ammAccount = await connection.getAccountInfo(new PublicKey(ammId));
if (ammAccount) {
const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(ammAccount.data);
const marketAccount = await connection.getAccountInfo(poolState.marketId);
if (marketAccount) {
const marketState = MARKET_STATE_LAYOUT_V3.decode(marketAccount.data);
const marketAuthority = PublicKey.createProgramAddressSync(
[
marketState.ownAddress.toBuffer(),
marketState.vaultSignerNonce.toArrayLike(Buffer, "le", 8),
],
MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
);
return {
id: new PublicKey(ammId),
programId: MAINNET_PROGRAM_ID.AmmV4,
status: poolState.status,
baseDecimals: poolState.baseDecimal.toNumber(),
quoteDecimals: poolState.quoteDecimal.toNumber(),
lpDecimals: 9,
baseMint: poolState.baseMint,
quoteMint: poolState.quoteMint,
version: 4,
authority: new PublicKey(
"5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1",
),
openOrders: poolState.openOrders,
baseVault: poolState.baseVault,
quoteVault: poolState.quoteVault,
marketProgramId: MAINNET_PROGRAM_ID.OPENBOOK_MARKET,
marketId: marketState.ownAddress,
marketBids: marketState.bids,
marketAsks: marketState.asks,
marketEventQueue: marketState.eventQueue,
marketBaseVault: marketState.baseVault,
marketQuoteVault: marketState.quoteVault,
marketAuthority: marketAuthority,
targetOrders: poolState.targetOrders,
lpMint: poolState.lpMint,
} as unknown as LiquidityPoolKeys;
}
}
};
The function takes two arguments: the SOL-USDC pool address and a web3 connection object. It fetches and decodes the pool account data and the market account data associated with the provided pool address.
The market authority is a Program Derived Address (PDA) owned by the OpenBook market program. It is derived using the pool’s market ID and the market vault signer nonce, which were generated when the pool was initially created.
The hardcoded authority
address is the Raydium v4 liquidity pool authority.
Create swap instruction
To create the swap instruction, we need to calculate the expected amount of USDC/SOL we want to receive based on the set slippage. The following function is used to achieve that;
const calculateAmountOut = async (
poolKeys: LiquidityPoolKeys,
poolInfo: LiquidityPoolInfo,
tokenToBuy: string,
amountIn: number,
rawSlippage: number,
) => {
let tokenOutMint = new PublicKey(tokenToBuy);
let tokenOutDecimals = poolKeys.baseMint.equals(tokenOutMint)
? poolInfo.baseDecimals
: poolKeys.quoteDecimals;
let tokenInMint = poolKeys.baseMint.equals(tokenOutMint)
? poolKeys.quoteMint
: poolKeys.baseMint;
let tokenInDecimals = poolKeys.baseMint.equals(tokenOutMint)
? poolInfo.quoteDecimals
: poolInfo.baseDecimals;
const tokenIn = new Token(TOKEN_PROGRAM_ID, tokenInMint, tokenInDecimals);
const tknAmountIn = new TokenAmount(tokenIn, amountIn, false);
const tokenOut = new Token(TOKEN_PROGRAM_ID, tokenOutMint, tokenOutDecimals);
const slippage = new Percent(rawSlippage, 100);
return {
amountIn: tknAmountIn,
tokenIn: tokenInMint,
tokenOut: tokenOutMint,
...Liquidity.computeAmountOut({
poolKeys,
poolInfo,
amountIn: tknAmountIn,
currencyOut: tokenOut,
slippage,
}),
};
};
The function uses the Liquidity
class provided by the Raydium SDK to calculate the expected amount of tokens to receive. It accepts the pool keys, tokens to buy (USDC/SOL), amount of tokens to swap, and slippage tolerance.
Next, we add the function to create the swap instruction. The function accepts a connection object, a token to buy, the amount of SOL/USDC to exchange, the slippage, and the key pair of the signer. Before creating the swap instruction, the function gets token accounts for the input and output tokens, respectively. If the token accounts do not exist for the key pair, it creates the accounts before making the swap instruction. To learn more about Solana token accounts, visit this link. Also in the case of SOL input/output, the function creates a wrapped SOL token account for the key pair.
const makeSwapInstruction = async (
connection: Connection,
tokenToBuy: string,
rawAmountIn: number,
slippage: number,
poolKeys: LiquidityPoolKeys,
poolInfo: LiquidityPoolInfo,
keyPair: Keypair,
) => {
const { amountIn, tokenIn, tokenOut, minAmountOut } =
await calculateAmountOut(
poolKeys,
poolInfo,
tokenToBuy,
rawAmountIn,
slippage,
);
let tokenInAccount: PublicKey;
let tokenOutAccount: PublicKey;
if (tokenIn.toString() == WSOL.mint) {
tokenInAccount = (
await getOrCreateAssociatedTokenAccount(
connection,
keyPair,
NATIVE_MINT,
keyPair.publicKey,
)
).address;
tokenOutAccount = (
await getOrCreateAssociatedTokenAccount(
connection,
keyPair,
tokenOut,
keyPair.publicKey,
)
).address;
} else {
tokenOutAccount = (
await getOrCreateAssociatedTokenAccount(
connection,
keyPair,
NATIVE_MINT,
keyPair.publicKey
)
).address;
tokenInAccount = (
await getOrCreateAssociatedTokenAccount(
connection,
keyPair,
tokenIn,
keyPair.publicKey,
)
).address;
}
const ix = new TransactionInstruction({
programId: new PublicKey(poolKeys.programId),
keys: [
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: poolKeys.id, isSigner: false, isWritable: true },
{ pubkey: poolKeys.authority, isSigner: false, isWritable: false },
{ pubkey: poolKeys.openOrders, isSigner: false, isWritable: true },
{ pubkey: poolKeys.baseVault, isSigner: false, isWritable: true },
{ pubkey: poolKeys.quoteVault, isSigner: false, isWritable: true },
{ pubkey: poolKeys.marketProgramId, isSigner: false, isWritable: false },
{ pubkey: poolKeys.marketId, isSigner: false, isWritable: true },
{ pubkey: poolKeys.marketBids, isSigner: false, isWritable: true },
{ pubkey: poolKeys.marketAsks, isSigner: false, isWritable: true },
{ pubkey: poolKeys.marketEventQueue, isSigner: false, isWritable: true },
{ pubkey: poolKeys.marketBaseVault, isSigner: false, isWritable: true },
{ pubkey: poolKeys.marketQuoteVault, isSigner: false, isWritable: true },
{ pubkey: poolKeys.marketAuthority, isSigner: false, isWritable: false },
{ pubkey: tokenInAccount, isSigner: false, isWritable: true },
{ pubkey: tokenOutAccount, isSigner: false, isWritable: true },
{ pubkey: keyPair.publicKey, isSigner: true, isWritable: false },
],
data: Buffer.from(
Uint8Array.of(
9,
...new BN(amountIn.raw).toArray("le", 8),
...new BN(minAmountOut.raw).toArray("le", 8),
),
),
});
return {
swapIX: ix,
tokenInAccount: tokenInAccount,
tokenOutAccount: tokenOutAccount,
tokenIn,
tokenOut,
amountIn,
minAmountOut,
};
};
Execute transaction
This is the final part of the script. In this section, we create a function to make swap instructions and submit a transaction to swap SOL for USDC and USDC for SOL. Add the following code to the main.ts
file:
const executeTransaction = async (swapAmountIn: number, tokenToBuy: string) => {
const connection = new Connection("https://api.mainnet-beta.solana.com");
const secretKey = Uint8Array.from(
JSON.parse(fs.readFileSync(`./keypair.json`) as unknown as string),
);
const keyPair = Keypair.fromSecretKey(secretKey);
const ammId = "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2"; // Address of the SOL-USDC pool on mainnet
const slippage = 2; // 2% slippage tolerance
const poolKeys = await getPoolKeys(ammId, connection);
if (poolKeys) {
const poolInfo = await Liquidity.fetchInfo({ connection, poolKeys });
const txn = new Transaction();
const {
swapIX,
tokenInAccount,
tokenIn,
amountIn
} = await makeSwapInstruction(
connection,
tokenToBuy,
swapAmountIn,
slippage,
poolKeys,
poolInfo,
keyPair,
);
if (tokenIn.toString() == WSOL.mint) {
// Convert SOL to Wrapped SOL
txn.add(
SystemProgram.transfer({
fromPubkey: keyPair.publicKey,
toPubkey: tokenInAccount,
lamports: amountIn.raw.toNumber(),
}),
createSyncNativeInstruction(tokenInAccount, TOKEN_PROGRAM_ID),
);
}
txn.add(swapIX);
const hash = await sendAndConfirmTransaction(connection, txn, [keyPair], {
skipPreflight: false,
preflightCommitment: "confirmed",
});
console.log("Transaction Completed Successfully 🎉🚀.");
console.log(`Explorer URL: https://solscan.io/tx/${hash}`);
} else {
console.log(`Could not get PoolKeys for AMM: ${ammId}`);
}
};
To execute this function, we must generate a Solana keypair in the project directory and load it with 0.01 SOL. Run the following code in the project terminal to generate the key pair:
solana-keygen new -o keypair.json
You can install solana-cli tools using this link.
To exchange 0.01 SOL for USDC, add the following code to the main.ts file and run npm run swap
;
executeTransaction(
0.01,
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // USDC Address
)
On successful transaction execution, the terminal should display the following output:
This transaction can be viewed on Solscan using this link.
To exchange USDC back into SOL, replace the USDC address in the earlier call with the WSOL mint address as follows and run the script again:
executeTransaction(
1, // Convert 1 USDC to SOL
WSOL.mint // WSOL Address
)
Conclusion
In this article, we’ve had a brief description of the Raydium AMM program, swap instructions, and how to invoke these instructions to exchange tokens using Typescript. The full script for this article can be found in this GitHub gist. You can also drop a comment if you’d like to see this in any other programming languages (python
, Rust
).
Happy hacking 👍🚀🇳🇬