diff --git a/ts-client/README.md b/ts-client/README.md index 44208f76..678c4699 100644 --- a/ts-client/README.md +++ b/ts-client/README.md @@ -5,6 +5,189 @@


+## Getting started + +NPM: https://www.npmjs.com/package/@meteora-ag/dlmm-sdk-public + +SDK: https://github.com/MeteoraAg/dlmm-sdk + + + +Discord: https://discord.com/channels/841152225564950528/864859354335412224 + +## Install + +1. Install deps + +``` +npm i @meteora-ag/dlmm-sdk-public @coral-xyz/anchor @solana/web3.js +``` + +2. Initialize DLMM instance + +```ts +const USDC_USDT_POOL = new PublicKey('ARwi1S4DaiTG5DX7S4M4ZsrXqpMD1MrTmbu9ue2tpmEq') // You can get your desired pool address from the API https://dlmm-api.meteora.ag/pair/all +const dlmmPool = await DLMM.create(connection, USDC_USDT_POOL, { + cluster: "devnet", +}); + +// If you need to create multiple, can consider using `createMultiple` +const dlmmPool = await DLMM.create(connection, [USDC_USDT_POOL, ...], { + cluster: "devnet", +}); + +``` + +3. To interact with the AmmImpl + +- Get Active Bin + +```ts +const activeBin = await dlmmPool.getActiveBin(); +const activeBinPriceLamport = activeBin.price; +const activeBinPricePerToken = dlmmPool.fromPricePerLamport( + Number(activeBin.price) +); +``` + +- Create Position + +```ts +const TOTAL_RANGE_INTERVAL = 10; // 10 bins on each side of the active bin +const bins = [activeBin.binId]; // Make sure bins is less than 70, as currently only support up to 70 bins for 1 position +for ( + let i = activeBin.binId; + i < activeBin.binId + TOTAL_RANGE_INTERVAL / 2; + i++ +) { + const rightNextBinId = i + 1; + const leftPrevBinId = activeBin.binId - (rightNextBinId - activeBin.binId); + bins.push(rightNextBinId); + bins.unshift(leftPrevBinId); +} + +const activeBinPricePerToken = dlmmPool.fromPricePerLamport( + Number(activeBin.price) +); +const totalXAmount = new BN(100); +const totalYAmount = totalXAmount.mul(new BN(Number(activeBinPricePerToken))); + +// Get spot distribution (You can calculate with other strategy `calculateSpotDistribution`, `calculateNormalDistribution`) +const spotXYAmountDistribution = calculateSpotDistribution( + activeBin.binId, + bins +); +const newPosition = new Keypair(); +const createPositionTx = + await dlmmPool.initializePositionAndAddLiquidityByWeight({ + positionPubKey: newPosition.publicKey, + lbPairPubKey: dlmmPool.pubkey, + user: user.publicKey, + totalXAmount, + totalYAmount, + xYAmountDistribution: spotXYAmountDistribution, + }); + +try { + for (let tx of Array.isArray(createPositionTx) + ? createPositionTx + : [createPositionTx]) { + const createPositionTxHash = await sendAndConfirmTransaction( + connection, + tx, + [user, newPosition] + ); + } +} catch (error) {} +``` + +- Get list of positions + +```ts +const { userPositions } = await dlmmPool.getPositionsByUserAndLbPair( + user.publicKey +); +const binData = userPositions[0].positionData.positionBinData; +``` + +- Add liquidity to existing position + +```ts +const addLiquidityTx = await dlmmPool.addLiquidityByWeight({ + positionPubKey: userPositions[0].publicKey, + lbPairPubKey: dlmmPool.pubkey, + user: user.publicKey, + totalXAmount, + totalYAmount, + xYAmountDistribution: spotXYAmountDistribution, +}); + +try { + for (let tx of Array.isArray(addLiquidityTx) + ? addLiquidityTx + : [addLiquidityTx]) { + const addLiquidityTxHash = await sendAndConfirmTransaction(connection, tx, [ + user, + newPosition, + ]); + } +} catch (error) {} +``` + +- Remove Liquidity + +```ts +const binIdsToRemove = userPositions[0].positionData.positionBinData.map( + (bin) => bin.binId +); +const removeLiquidityTx = await dlmmPool.removeLiquidity({ + position: userPositions[0].publicKey, + user: user.publicKey, + binIds: binIdsToRemove, + liquiditiesBpsToRemove: new Array(binIdsToRemove.length).fill( + new BN(100 * 100) + ), // 100% (range from 0 to 100) + shouldClaimAndClose: true, // should claim swap fee and close position together +}); + +try { + for (let tx of Array.isArray(removeLiquidityTx) + ? removeLiquidityTx + : [removeLiquidityTx]) { + const removeLiquidityTxHash = await sendAndConfirmTransaction( + connection, + tx, + [user, newPosition] + ); + } +} catch (error) {} +``` + +- Swap + +```ts +const swapAmount = new BN(100); +// Swap quote +const swapQuote = await dlmmPool.swapQuote(swapAmount, true, new BN(10)); + +// Swap +const swapTx = await dlmmPool.swap({ + inToken: dlmmPool.tokenX.publicKey, + binArraysPubkey: swapQuote.binArraysPubkey, + inAmount: swapAmount, + lbPair: dlmmPool.pubkey, + user: user.publicKey, + minOutAmount: swapQuote.minOutAmount, + outToken: dlmmPool.tokenY.publicKey, +}); + +try { + const swapTxHash = await sendAndConfirmTransaction(connection, swapTx, [ + user, + ]); +} catch (error) {} +``` + ## Static functions | Function | Description | Return | @@ -41,3 +224,7 @@ | `claimSwapFee` | Claim swap fees for a specific position owned by a specific owner | `Promise` | | `claimAllSwapFee` | Claim swap fees for multiple positions owned by a specific owner | `Promise` | | `claimAllRewards` | Claim swap fees and LM rewards for multiple positions owned by a specific owner | `Promise` | + +``` + +``` diff --git a/ts-client/package.json b/ts-client/package.json index 0fb6f05b..3ca2d9cf 100644 --- a/ts-client/package.json +++ b/ts-client/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "tsup", "start": "npm run build -- --watch", - "test": "jest src/test/sdk.test.ts" + "test": "jest src/test/sdk.test.ts", + "example": "dotenv -e .env npx ts-node src/example.ts" }, "devDependencies": { "@babel/preset-env": "^7.22.5", diff --git a/ts-client/src/dlmm/helpers/index.ts b/ts-client/src/dlmm/helpers/index.ts index 249d69d8..9f66b2a1 100644 --- a/ts-client/src/dlmm/helpers/index.ts +++ b/ts-client/src/dlmm/helpers/index.ts @@ -11,6 +11,7 @@ import { } from "@solana/spl-token"; import { SCALE_OFFSET } from "../constants"; import { + ComputeBudgetProgram, Connection, PublicKey, SystemProgram, @@ -179,3 +180,9 @@ export async function chunkedGetMultipleAccountInfos( return accountInfos; } + +export const computeBudgetIx = () => { + return ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000, + }); +}; diff --git a/ts-client/src/dlmm/helpers/strategy.ts b/ts-client/src/dlmm/helpers/strategy.ts index bfc20df7..9a0f40d3 100644 --- a/ts-client/src/dlmm/helpers/strategy.ts +++ b/ts-client/src/dlmm/helpers/strategy.ts @@ -9,8 +9,7 @@ import { } from "../types"; import Decimal from "decimal.js"; -/** private */ -function getPriceOfBinByBinId(binId: number, binStep: number): Decimal { +export function getPriceOfBinByBinId(binId: number, binStep: number): Decimal { const binStepNum = new Decimal(binStep).div(new Decimal(BASIS_POINT_MAX)); return new Decimal(1).add(new Decimal(binStepNum)).pow(new Decimal(binId)); } diff --git a/ts-client/src/dlmm/index.ts b/ts-client/src/dlmm/index.ts index f66a3a9d..37d6e716 100644 --- a/ts-client/src/dlmm/index.ts +++ b/ts-client/src/dlmm/index.ts @@ -1,6 +1,5 @@ import { Cluster, - ComputeBudgetProgram, Connection, PublicKey, TransactionInstruction, @@ -79,6 +78,7 @@ import { deriveLbPair, deriveOracle, derivePresetParameter, + computeBudgetIx, } from "./helpers"; import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes"; import Decimal from "decimal.js"; @@ -1453,9 +1453,7 @@ export class DLMM { closeWrappedSOLIx && postInstructions.push(closeWrappedSOLIx); } - const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_400_000, - }); + const setComputeUnitLimitIx = computeBudgetIx(); const minBinId = Math.min(...binIds); const maxBinId = Math.max(...binIds); @@ -1745,9 +1743,7 @@ export class DLMM { closeWrappedSOLIx && postInstructions.push(closeWrappedSOLIx); } - const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_400_000, - }); + const setComputeUnitLimitIx = computeBudgetIx(); const liquidityParams: LiquidityParameterByWeight = { amountX: totalXAmount, @@ -1909,9 +1905,7 @@ export class DLMM { ); const preInstructions: Array = []; - const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_400_000, - }); + const setComputeUnitLimitIx = computeBudgetIx(); preInstructions.push(setComputeUnitLimitIx); const [ @@ -2093,7 +2087,7 @@ export class DLMM { position, }: { owner: PublicKey; - position: { publicKey: PublicKey; positionData: PositionData }; + position: Position; }): Promise { const { lowerBinId } = await this.program.account.positionV2.fetch( position.publicKey @@ -2287,11 +2281,7 @@ export class DLMM { const { tokenXMint, tokenYMint, reserveX, reserveY, activeId, oracle } = await this.program.account.lbPair.fetch(lbPair); - const preInstructions: TransactionInstruction[] = [ - ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_400_000, - }), - ]; + const preInstructions: TransactionInstruction[] = [computeBudgetIx()]; const [ { ataPubKey: userTokenIn, ix: createInTokenAccountIx }, @@ -2422,16 +2412,17 @@ export class DLMM { const chunkedClaimAllTx = chunks(claimAllTxs, MAX_CLAIM_ALL_ALLOWED); + const { blockhash, lastValidBlockHeight } = + await this.program.provider.connection.getLatestBlockhash("confirmed"); return Promise.all( chunkedClaimAllTx.map(async (claimAllTx) => { return new Transaction({ feePayer: owner, - ...(await this.program.provider.connection.getLatestBlockhash( - "finalized" - )), + blockhash, + lastValidBlockHeight, }) - .add(...claimAllTx) - .add(ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 })); + .add(computeBudgetIx()) + .add(...claimAllTx); }) ); } @@ -2466,7 +2457,7 @@ export class DLMM { * @param * - `owner`: The public key of the owner of the positions. * - `positions`: An array of objects of type `PositionData` that represents the positions to claim swap fees from. - * @returns a Promise that resolves to an array of Transaction objects. + * @returns {Promise} */ public async claimAllSwapFee({ owner, @@ -2492,12 +2483,58 @@ export class DLMM { return Promise.all( chunkedClaimAllTx.map(async (claimAllTx) => { + const { recentBlockhash, lastValidBlockHeight } = claimAllTx[0]; + return new Transaction({ + feePayer: owner, + blockhash: recentBlockhash, + lastValidBlockHeight, + }) + .add(computeBudgetIx()) + .add(...claimAllTx); + }) + ); + } + + /** + * The function `claimAllRewardsByPosition` allows a user to claim all rewards for a specific + * position. + * @param + * - `owner`: The public key of the owner of the position. + * - `position`: The public key of the position account. + * @returns {Promise} + */ + public async claimAllRewardsByPosition({ + owner, + position, + }: { + owner: PublicKey; + position: Position; + }): Promise { + const claimAllSwapFeeTxs = await this.createClaimSwapFeeMethod({ + owner, + position, + }); + const claimAllLMTxs = await this.createClaimBuildMethod({ + owner, + position, + }); + + const claimAllTxs = chunks( + [claimAllSwapFeeTxs, ...claimAllLMTxs], + MAX_CLAIM_ALL_ALLOWED + ); + + const { blockhash, lastValidBlockHeight } = + await this.program.provider.connection.getLatestBlockhash("confirmed"); + return Promise.all( + claimAllTxs.map(async (claimAllTx) => { return new Transaction({ feePayer: owner, - ...(await this.program.provider.connection.getLatestBlockhash( - "finalized" - )), - }).add(...claimAllTx); + blockhash, + lastValidBlockHeight, + }) + .add(computeBudgetIx()) + .add(...claimAllTx); }) ); } @@ -2546,14 +2583,17 @@ export class DLMM { MAX_CLAIM_ALL_ALLOWED ); + const { blockhash, lastValidBlockHeight } = + await this.program.provider.connection.getLatestBlockhash("confirmed"); return Promise.all( chunkedClaimAllTx.map(async (claimAllTx) => { return new Transaction({ feePayer: owner, - ...(await this.program.provider.connection.getLatestBlockhash( - "finalized" - )), - }).add(...claimAllTx); + blockhash, + lastValidBlockHeight, + }) + .add(computeBudgetIx()) + .add(...claimAllTx); }) ); } @@ -3163,7 +3203,7 @@ export class DLMM { const rewardInfo = this.lbPair.rewardInfos[i]; if (!rewardInfo || rewardInfo.mint.equals(PublicKey.default)) continue; - const preInstructions: Array = []; + const preInstructions = []; const { ataPubKey, ix } = await getOrCreateATAInstruction( this.program.provider.connection, rewardInfo.mint, @@ -3230,10 +3270,6 @@ export class DLMM { ); const preInstructions: TransactionInstruction[] = []; - const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_400_000, - }); - preInstructions.push(computeBudgetIx); const [ { ataPubKey: userTokenX, ix: createInTokenAccountIx }, { ataPubKey: userTokenY, ix: createOutTokenAccountIx }, diff --git a/ts-client/src/example.ts b/ts-client/src/example.ts index e69de29b..54ecdd32 100644 --- a/ts-client/src/example.ts +++ b/ts-client/src/example.ts @@ -0,0 +1,170 @@ +import { + Connection, + Keypair, + PublicKey, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes"; +import { DLMM } from "./dlmm"; +import { calculateSpotDistribution } from "./dlmm/helpers"; +import BN from "bn.js"; + +const user = Keypair.fromSecretKey( + new Uint8Array(bs58.decode(process.env.USER_PRIVATE_KEY)) +); +const RPC = process.env.RPC || "https://api.devnet.solana.com"; +const connection = new Connection(RPC, "finalized"); + +const devnetPool = new PublicKey( + "3W2HKgUa96Z69zzG3LK1g8KdcRAWzAttiLiHfYnKuPw5" +); + +async function main() { + const dlmmPool = await DLMM.create(connection, devnetPool, { + cluster: "devnet", + }); + + // Get pool state + const activeBin = await dlmmPool.getActiveBin(); + console.log("🚀 ~ activeBin:", activeBin); + + const TOTAL_RANGE_INTERVAL = 10; // 10 bins on each side of the active bin + const bins = [activeBin.binId]; // Make sure bins is less than 70, as currently only support up to 70 bins for 1 position + for ( + let i = activeBin.binId; + i < activeBin.binId + TOTAL_RANGE_INTERVAL / 2; + i++ + ) { + const rightNextBinId = i + 1; + const leftPrevBinId = activeBin.binId - (rightNextBinId - activeBin.binId); + bins.push(rightNextBinId); + bins.unshift(leftPrevBinId); + } + + const activeBinPricePerToken = dlmmPool.fromPricePerLamport( + Number(activeBin.price) + ); + const totalXAmount = new BN(100); + const totalYAmount = totalXAmount.mul(new BN(Number(activeBinPricePerToken))); + + // Get spot distribution + const spotXYAmountDistribution = calculateSpotDistribution( + activeBin.binId, + bins + ); + + // Create Position + const newPosition = new Keypair(); + const createPositionTx = + await dlmmPool.initializePositionAndAddLiquidityByWeight({ + positionPubKey: newPosition.publicKey, + lbPairPubKey: dlmmPool.pubkey, + user: user.publicKey, + totalXAmount, + totalYAmount, + xYAmountDistribution: spotXYAmountDistribution, + }); + + try { + for (let tx of Array.isArray(createPositionTx) + ? createPositionTx + : [createPositionTx]) { + const createPositionTxHash = await sendAndConfirmTransaction( + connection, + tx, + [user, newPosition] + ); + console.log("🚀 ~ createPositionTxHash:", createPositionTxHash); + } + } catch (error) { + console.log("🚀 ~ error:", JSON.parse(JSON.stringify(error))); + } + + // Get position state + const { userPositions } = await dlmmPool.getPositionsByUserAndLbPair( + user.publicKey + ); + console.log("🚀 ~ userPositions:", userPositions); + + // Add Liquidity to existing position + const addLiquidityTx = await dlmmPool.addLiquidityByWeight({ + positionPubKey: userPositions[0].publicKey, + lbPairPubKey: dlmmPool.pubkey, + user: user.publicKey, + totalXAmount, + totalYAmount, + xYAmountDistribution: spotXYAmountDistribution, + }); + + try { + for (let tx of Array.isArray(addLiquidityTx) + ? addLiquidityTx + : [addLiquidityTx]) { + const addLiquidityTxHash = await sendAndConfirmTransaction( + connection, + tx, + [user, newPosition] + ); + console.log("🚀 ~ addLiquidityTxHash:", addLiquidityTxHash); + } + } catch (error) { + console.log("🚀 ~ error:", JSON.parse(JSON.stringify(error))); + } + + // Remove Liquidity + const binIdsToRemove = userPositions[0].positionData.positionBinData.map( + (bin) => bin.binId + ); + const removeLiquidityTx = await dlmmPool.removeLiquidity({ + position: userPositions[0].publicKey, + user: user.publicKey, + binIds: binIdsToRemove, + liquiditiesBpsToRemove: new Array(binIdsToRemove.length).fill( + new BN(100 * 100) + ), // 100% (range from 0 to 100) + shouldClaimAndClose: true, // should claim swap fee and close position together + }); + + try { + for (let tx of Array.isArray(removeLiquidityTx) + ? removeLiquidityTx + : [removeLiquidityTx]) { + const removeLiquidityTxHash = await sendAndConfirmTransaction( + connection, + tx, + [user, newPosition], + { skipPreflight: false, preflightCommitment: "singleGossip" } + ); + console.log("🚀 ~ removeLiquidityTxHash:", removeLiquidityTxHash); + } + } catch (error) { + console.log("🚀 ~ error:", JSON.parse(JSON.stringify(error))); + } + + const swapAmount = new BN(100); + // Swap quote + const swapQuote = await dlmmPool.swapQuote(swapAmount, true, new BN(10)); + console.log("🚀 ~ swapQuote:", swapQuote); + + // Swap + const swapTx = await dlmmPool.swap({ + inToken: dlmmPool.tokenX.publicKey, + binArraysPubkey: swapQuote.binArraysPubkey, + inAmount: swapAmount, + lbPair: dlmmPool.pubkey, + user: user.publicKey, + minOutAmount: swapQuote.minOutAmount, + outToken: dlmmPool.tokenY.publicKey, + }); + + try { + const swapTxHash = await sendAndConfirmTransaction(connection, swapTx, [ + user, + ]); + console.log("🚀 ~ swapTxHash:", swapTxHash); + } catch (error) { + console.log("🚀 ~ error:", JSON.parse(JSON.stringify(error))); + } +} + +main();