Swap
Easily swap Doppler created assets on Solana
/**
* Example: Swap tokens on a CPMM pool (Solana)
*
* Demonstrates:
* - Fetching pool state and computing an exact-in quote off-chain
* - Deriving user ATAs and building a swap_exact_in instruction
*/
import './env.js';
import {
address,
createKeyPairSignerFromBytes,
createSolanaRpc,
createSolanaRpcSubscriptions,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
getSignatureFromTransaction,
type Address,
} from '@solana/kit';
import {
TOKEN_PROGRAM_ADDRESS,
findAssociatedTokenPda,
} from '@solana-program/token';
import { cpmm } from '../src/solana/index.js';
// ============================================================================
// Environment
// ============================================================================
const keypairJson = process.env.SOLANA_KEYPAIR;
const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://api.devnet.solana.com';
const wsUrl = process.env.SOLANA_WS_URL ?? 'wss://api.devnet.solana.com';
if (!keypairJson) {
throw new Error('SOLANA_KEYPAIR must be set (JSON array of 64 bytes)');
}
if (!process.env.MINT_0 || !process.env.MINT_1) {
throw new Error(
'MINT_0 and MINT_1 must be set to the two token mints of the pool',
);
}
// The two token mints of the pool — order does not matter, they are sorted internally.
const MINT_0: Address = address(process.env.MINT_0);
const MINT_1: Address = address(process.env.MINT_1);
// ============================================================================
// Main
// ============================================================================
async function main() {
const payer = await createKeyPairSignerFromBytes(
new Uint8Array(JSON.parse(keypairJson as string)),
);
const rpc = createSolanaRpc(rpcUrl);
const rpcSubscriptions = createSolanaRpcSubscriptions(wsUrl);
// ── Fetch pool state ─────────────────────────────────────────────────────
console.log('Fetching pool state...');
const poolResult = await cpmm.getPoolByMints(rpc, MINT_0, MINT_1);
if (!poolResult) {
throw new Error(`No pool found for ${MINT_0} / ${MINT_1}`);
}
const { address: poolAddress, account: pool } = poolResult;
console.log(' Pool address: ', poolAddress);
console.log(' token0 mint: ', pool.token0Mint);
console.log(' token1 mint: ', pool.token1Mint);
console.log(' reserve0: ', pool.reserve0.toString());
console.log(' reserve1: ', pool.reserve1.toString());
console.log(' Swap fee: ', pool.swapFeeBps, 'bps');
console.log('');
// ── Quote the swap ───────────────────────────────────────────────────────
// direction 0 = token0→token1, direction 1 = token1→token0.
const direction = 0 as const; // token0 → token1
const AMOUNT_IN = 1_000_000n; // 1 token (assuming 6 decimals)
const quote = cpmm.getSwapQuote(pool, AMOUNT_IN, direction);
const SLIPPAGE_BPS = 50n; // 0.5%
const minAmountOut = (quote.amountOut * (10_000n - SLIPPAGE_BPS)) / 10_000n;
console.log('Swap quote (token0 → token1):');
console.log(' Amount in: ', AMOUNT_IN.toString(), '(token0 atoms)');
console.log(
' Amount out: ',
quote.amountOut.toString(),
'(token1 atoms, estimated)',
);
console.log(' Fee: ', quote.feeTotal.toString(), '(token0 atoms)');
console.log(' Price impact: ', (quote.priceImpact * 100).toFixed(4), '%');
console.log(' Min out (0.5%):', minAmountOut.toString());
console.log('');
// ── Derive PDAs and user token accounts ─────────────────────────────────
const [config] = await cpmm.getConfigAddress();
const [userIn] = await findAssociatedTokenPda({
owner: payer.address,
mint: pool.token0Mint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const [userOut] = await findAssociatedTokenPda({
owner: payer.address,
mint: pool.token1Mint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
console.log(' Config: ', config);
console.log(' User ATA (in): ', userIn);
console.log(' User ATA (out):', userOut);
console.log('');
// ── Build and send the swap instruction ──────────────────────────────────
console.log('Submitting swap...');
try {
const ix = cpmm.createSwapInstruction({
config,
pool: poolAddress,
authority: pool.authority,
vault0: pool.vault0,
vault1: pool.vault1,
token0Mint: pool.token0Mint,
token1Mint: pool.token1Mint,
userToken0: userIn,
userToken1: userOut,
user: payer.address,
amountIn: AMOUNT_IN,
minAmountOut,
direction,
});
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([ix], tx),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
await sendAndConfirmTransaction(signedTransaction, {
commitment: 'confirmed',
});
console.log('');
console.log('Swap confirmed!');
console.log(
' Transaction:',
getSignatureFromTransaction(signedTransaction),
);
console.log(' Sent: ', AMOUNT_IN.toString(), 'token0 atoms');
console.log(' Received: ~', quote.amountOut.toString(), 'token1 atoms');
} catch (error) {
console.error('Error executing swap:', error);
process.exit(1);
}
}
main();Last updated