diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e920b5..350dd929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +## @mercurial-finance/dynamic-amm-sdk [1.1.6] - PR[#161](https://github.com/mercurial-finance/mercurial-dynamic-amm-sdk/pull/163) + +### Added + +- Added `poolFees` in `getFeeConfigurations` function + +## @mercurial-finance/dynamic-amm-sdk [1.1.5] - PR[#161](https://github.com/mercurial-finance/mercurial-dynamic-amm-sdk/pull/163) + +### Added + +- Swap options on create pool config +- Added `calculateSwapQuoteForGoingToCreateMemecoinPool` to allow swap quote on going to be created pool. + ## @mercurial-finance/dynamic-amm-sdk [1.1.4] - PR[#161](https://github.com/mercurial-finance/mercurial-dynamic-amm-sdk/pull/164) ### Changed diff --git a/ts-client/index.ts b/ts-client/index.ts index ca0799b7..f83ddae4 100644 --- a/ts-client/index.ts +++ b/ts-client/index.ts @@ -24,6 +24,7 @@ import { Amm, IDL as AmmIdl } from './src/amm/idl'; export default AmmImpl; export { + AmmImpl, // Classes ConstantProductSwap, StableSwap, diff --git a/ts-client/package.json b/ts-client/package.json index af81075b..5ce28fd4 100644 --- a/ts-client/package.json +++ b/ts-client/package.json @@ -1,6 +1,6 @@ { "name": "@mercurial-finance/dynamic-amm-sdk", - "version": "1.1.4", + "version": "1.1.5", "description": "Mercurial Vaults SDK is a typescript library that allows you to interact with Mercurial v2's AMM.", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/ts-client/src/amm/index.ts b/ts-client/src/amm/index.ts index 852c7da6..fb0e74ea 100644 --- a/ts-client/src/amm/index.ts +++ b/ts-client/src/amm/index.ts @@ -67,6 +67,7 @@ import { calculateUnclaimedLockEscrowFee, derivePoolAddressWithConfig as deriveConstantProductPoolAddressWithConfig, deriveConfigPda, + deriveProtocolTokenFee, } from './utils'; import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; @@ -262,6 +263,11 @@ export default class AmmImpl implements AmmImplementation { cluster?: Cluster; programId?: string; lockLiquidity?: boolean; + swapLiquidity?: { + // always swap B to A + inAmount: BN; + minAmountOut: BN; + }; activationPoint?: BN; }, ) { @@ -337,6 +343,45 @@ export default class AmmImpl implements AmmImplementation { const [mintMetadata, _mintMetadataBump] = deriveMintMetadata(lpMint); const activationPoint = opt?.activationPoint || null; + const createPoolPostInstructions: TransactionInstruction[] = []; + if (opt?.lockLiquidity) { + const [lockEscrowPK] = deriveLockEscrowPda(poolPubkey, payer, ammProgram.programId); + const createLockEscrowIx = await ammProgram.methods + .createLockEscrow() + .accounts({ + pool: poolPubkey, + lockEscrow: lockEscrowPK, + owner: payer, + lpMint, + payer, + systemProgram: SystemProgram.programId, + }) + .instruction(); + createPoolPostInstructions.push(createLockEscrowIx); + const [escrowAta, createEscrowAtaIx] = await getOrCreateATAInstruction(lpMint, lockEscrowPK, connection, payer); + + createEscrowAtaIx && createPoolPostInstructions.push(createEscrowAtaIx); + const lockIx = await ammProgram.methods + .lock(U64_MAX) + .accounts({ + pool: poolPubkey, + lockEscrow: lockEscrowPK, + owner: payer, + lpMint, + sourceTokens: payerPoolLp, + escrowVault: escrowAta, + tokenProgram: TOKEN_PROGRAM_ID, + aVault, + bVault, + aVaultLp, + bVaultLp, + aVaultLpMint, + bVaultLpMint, + }) + .instruction(); + createPoolPostInstructions.push(lockIx); + } + const createPermissionlessPoolTx = await ammProgram.methods .initializePermissionlessConstantProductPoolWithConfig2(tokenAAmount, tokenBAmount, activationPoint) .accounts({ @@ -367,13 +412,17 @@ export default class AmmImpl implements AmmImplementation { systemProgram: SystemProgram.programId, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, }) + .postInstructions(createPoolPostInstructions) .transaction(); + const latestBlockHash = await ammProgram.provider.connection.getLatestBlockhash( + ammProgram.provider.connection.commitment, + ); const resultTx: Transaction[] = []; if (preInstructions.length) { const preInstructionTx = new Transaction({ feePayer: payer, - ...(await ammProgram.provider.connection.getLatestBlockhash(ammProgram.provider.connection.commitment)), + ...latestBlockHash, }).add(...preInstructions); resultTx.push(preInstructionTx); } @@ -388,48 +437,37 @@ export default class AmmImpl implements AmmImplementation { .add(setComputeUnitLimitIx) .add(createPermissionlessPoolTx); - if (opt?.lockLiquidity) { - const preLockLiquidityIx: TransactionInstruction[] = []; - const [lockEscrowPK] = deriveLockEscrowPda(poolPubkey, payer, ammProgram.programId); - const createLockEscrowIx = await ammProgram.methods - .createLockEscrow() - .accounts({ - pool: poolPubkey, - lockEscrow: lockEscrowPK, - owner: payer, - lpMint, - payer, - systemProgram: SystemProgram.programId, - }) - .instruction(); - preLockLiquidityIx.push(createLockEscrowIx); - const [escrowAta, createEscrowAtaIx] = await getOrCreateATAInstruction(lpMint, lockEscrowPK, connection, payer); + resultTx.push(mainTx); - createEscrowAtaIx && preLockLiquidityIx.push(createEscrowAtaIx); - const lockTx = await ammProgram.methods - .lock(U64_MAX) + if (opt?.swapLiquidity) { + const protocolTokenFee = deriveProtocolTokenFee(poolPubkey, tokenBMint, ammProgram.programId); + const swapTx = await ammProgram.methods + .swap(opt.swapLiquidity.inAmount, opt.swapLiquidity.minAmountOut) .accounts({ - pool: poolPubkey, - lockEscrow: lockEscrowPK, - owner: payer, - lpMint, - sourceTokens: payerPoolLp, - escrowVault: escrowAta, - tokenProgram: TOKEN_PROGRAM_ID, + aTokenVault, + bTokenVault, aVault, bVault, aVaultLp, bVaultLp, aVaultLpMint, bVaultLpMint, + userSourceToken: payerTokenB, + userDestinationToken: payerTokenA, + user: payer, + protocolTokenFee, + pool: poolPubkey, + tokenProgram: TOKEN_PROGRAM_ID, + vaultProgram: vaultProgram.programId, }) - .preInstructions(preLockLiquidityIx) .transaction(); - mainTx.add(lockTx); + const newSwapTx = new Transaction({ + feePayer: payer, + ...latestBlockHash, + }).add(swapTx); + resultTx.push(newSwapTx); } - resultTx.push(mainTx); - return resultTx; } @@ -445,6 +483,10 @@ export default class AmmImpl implements AmmImplementation { cluster?: Cluster; programId?: string; lockLiquidity?: boolean; + swapLiquidity?: { + inAmount: BN; + minAmountOut: BN; + }; skipAAta?: boolean; skipBAta?: boolean; }, @@ -508,18 +550,63 @@ export default class AmmImpl implements AmmImplementation { ), ]; - const payerPoolLp = await getAssociatedTokenAccount(lpMint, payer); + const payerPoolLp = getAssociatedTokenAccount(lpMint, payer); if (tokenAMint.equals(NATIVE_MINT)) { preInstructions = preInstructions.concat(wrapSOLInstruction(payer, payerTokenA, BigInt(tokenAAmount.toString()))); } if (tokenBMint.equals(NATIVE_MINT)) { - preInstructions = preInstructions.concat(wrapSOLInstruction(payer, payerTokenB, BigInt(tokenBAmount.toString()))); + preInstructions = preInstructions.concat( + wrapSOLInstruction( + payer, + payerTokenB, + BigInt(tokenBAmount.add(opt?.swapLiquidity?.inAmount ?? new BN(0)).toString()), + ), + ); } const [mintMetadata, _mintMetadataBump] = deriveMintMetadata(lpMint); + const createPoolPostInstructions: TransactionInstruction[] = []; + if (opt?.lockLiquidity) { + const [lockEscrowPK] = deriveLockEscrowPda(poolPubkey, payer, ammProgram.programId); + const createLockEscrowIx = await ammProgram.methods + .createLockEscrow() + .accounts({ + pool: poolPubkey, + lockEscrow: lockEscrowPK, + owner: payer, + lpMint, + payer, + systemProgram: SystemProgram.programId, + }) + .instruction(); + createPoolPostInstructions.push(createLockEscrowIx); + const [escrowAta, createEscrowAtaIx] = await getOrCreateATAInstruction(lpMint, lockEscrowPK, connection, payer); + + createEscrowAtaIx && createPoolPostInstructions.push(createEscrowAtaIx); + const lockIx = await ammProgram.methods + .lock(U64_MAX) + .accounts({ + pool: poolPubkey, + lockEscrow: lockEscrowPK, + owner: payer, + lpMint, + sourceTokens: payerPoolLp, + escrowVault: escrowAta, + tokenProgram: TOKEN_PROGRAM_ID, + aVault, + bVault, + aVaultLp, + bVaultLp, + aVaultLpMint, + bVaultLpMint, + }) + .instruction(); + createPoolPostInstructions.push(lockIx); + } + const createPermissionlessPoolTx = await ammProgram.methods .initializePermissionlessConstantProductPoolWithConfig(tokenAAmount, tokenBAmount) .accounts({ @@ -550,69 +637,62 @@ export default class AmmImpl implements AmmImplementation { systemProgram: SystemProgram.programId, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, }) + .postInstructions(createPoolPostInstructions) .transaction(); const resultTx: Transaction[] = []; + const latestBlockHash = await ammProgram.provider.connection.getLatestBlockhash( + ammProgram.provider.connection.commitment, + ); if (preInstructions.length) { const preInstructionTx = new Transaction({ feePayer: payer, - ...(await ammProgram.provider.connection.getLatestBlockhash(ammProgram.provider.connection.commitment)), + ...latestBlockHash, }).add(...preInstructions); resultTx.push(preInstructionTx); } const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ - units: 600_000, + units: 1_400_000, }); - const mainTx = new Transaction({ + const createPoolTx = new Transaction({ feePayer: payer, - ...(await ammProgram.provider.connection.getLatestBlockhash(ammProgram.provider.connection.commitment)), + ...latestBlockHash, }) .add(setComputeUnitLimitIx) .add(createPermissionlessPoolTx); - if (opt?.lockLiquidity) { - const preLockLiquidityIx: TransactionInstruction[] = []; - const [lockEscrowPK] = deriveLockEscrowPda(poolPubkey, payer, ammProgram.programId); - const createLockEscrowIx = await ammProgram.methods - .createLockEscrow() - .accounts({ - pool: poolPubkey, - lockEscrow: lockEscrowPK, - owner: payer, - lpMint, - payer, - systemProgram: SystemProgram.programId, - }) - .instruction(); - preLockLiquidityIx.push(createLockEscrowIx); - const [escrowAta, createEscrowAtaIx] = await getOrCreateATAInstruction(lpMint, lockEscrowPK, connection, payer); + resultTx.push(createPoolTx); - createEscrowAtaIx && preLockLiquidityIx.push(createEscrowAtaIx); - const lockTx = await ammProgram.methods - .lock(U64_MAX) + if (opt?.swapLiquidity) { + const protocolTokenFee = deriveProtocolTokenFee(poolPubkey, tokenBMint, ammProgram.programId); + const swapTx = await ammProgram.methods + .swap(opt.swapLiquidity.inAmount, opt.swapLiquidity.minAmountOut) .accounts({ - pool: poolPubkey, - lockEscrow: lockEscrowPK, - owner: payer, - lpMint, - sourceTokens: payerPoolLp, - escrowVault: escrowAta, - tokenProgram: TOKEN_PROGRAM_ID, + aTokenVault, + bTokenVault, aVault, bVault, aVaultLp, bVaultLp, aVaultLpMint, bVaultLpMint, + userSourceToken: payerTokenB, + userDestinationToken: payerTokenA, + user: payer, + protocolTokenFee, + pool: poolPubkey, + tokenProgram: TOKEN_PROGRAM_ID, + vaultProgram: vaultProgram.programId, }) - .preInstructions(preLockLiquidityIx) .transaction(); - mainTx.add(lockTx); + const newSwapTx = new Transaction({ + feePayer: payer, + ...latestBlockHash, + }).add(swapTx); + resultTx.push(newSwapTx); } - resultTx.push(mainTx); - return resultTx; } @@ -970,6 +1050,7 @@ export default class AmmImpl implements AmmImplementation { return { publicKey: configAccount.publicKey, + poolFees, tradeFeeBps: poolFees.tradeFeeNumerator.mul(new BN(10000)).div(poolFees.tradeFeeDenominator), protocolTradeFeeBps: poolFees.protocolTradeFeeNumerator .mul(new BN(10000)) diff --git a/ts-client/src/amm/tests/bundlePoolQuote.test.ts b/ts-client/src/amm/tests/bundlePoolQuote.test.ts new file mode 100644 index 00000000..3c580624 --- /dev/null +++ b/ts-client/src/amm/tests/bundlePoolQuote.test.ts @@ -0,0 +1,59 @@ +import VaultImpl from '@mercurial-finance/vault-sdk'; +import { AccountLayout, MintLayout, NATIVE_MINT, RawAccount, RawMint } from '@solana/spl-token'; +import { clusterApiUrl, Connection, PublicKey, SYSVAR_CLOCK_PUBKEY } from '@solana/web3.js'; +import { calculateSwapQuoteForGoingToCreateMemecoinPool } from '../utils'; +import { BN } from 'bn.js'; +import { Clock, ClockLayout } from '../types'; +import AmmImpl from '..'; + +describe('Bundle pool quote', () => { + const connection = new Connection(clusterApiUrl('mainnet-beta'), 'confirmed'); + const quoteMint = NATIVE_MINT; + + it('Able to quote', async () => { + const quoteDynamicVault = await VaultImpl.create(connection, quoteMint); + const inAmount = new BN('50000'); // 0.00005 SOL + const tokenAAmount = new BN('1000000000000000'); // 1000000000 memecoin + const tokenBAmount = new BN('50000'); // 0.00005 SOL + + // Memecoin config + const config = await AmmImpl.getPoolConfig( + connection, + new PublicKey('FiENCCbPi3rFh5pW2AJ59HC53yM32eLaCjMKxRqanKFJ'), + ); + + const [vaultReserveAccount, vaultLpMintAccount, clockAccount] = await connection.getMultipleAccountsInfo([ + quoteDynamicVault.vaultState.tokenVault, + quoteDynamicVault.vaultState.lpMint, + SYSVAR_CLOCK_PUBKEY, + ]); + const vaultReserve: RawAccount = AccountLayout.decode(vaultReserveAccount!.data); + const vaultMint: RawMint = MintLayout.decode(vaultLpMintAccount!.data); + const clock: Clock = ClockLayout.decode(clockAccount!.data); + + const { amountOut } = calculateSwapQuoteForGoingToCreateMemecoinPool( + inAmount, + tokenAAmount, + tokenBAmount, + false, + { + tradeFeeNumerator: config.poolFees.tradeFeeNumerator, + tradeFeeDenominator: config.poolFees.tradeFeeDenominator, + protocolTradeFeeNumerator: config.poolFees.protocolTradeFeeNumerator, + protocolTradeFeeDenominator: config.poolFees.protocolTradeFeeDenominator, + }, + { + // Memecoin vault doesn't exists until we create the pool for it + vaultA: undefined, + vaultB: { + vault: quoteDynamicVault.vaultState, + reserve: new BN(vaultReserve.amount.toString()), + lpSupply: new BN(vaultMint.supply.toString()), + }, + currentTime: clock.unixTimestamp.toNumber(), + }, + ); + + console.log('Out amount', amountOut.toString()); + }); +}); diff --git a/ts-client/src/amm/utils.ts b/ts-client/src/amm/utils.ts index d0f941a5..83b4971a 100644 --- a/ts-client/src/amm/utils.ts +++ b/ts-client/src/amm/utils.ts @@ -49,6 +49,7 @@ import { DepegNone, DepegSplStake, ParsedClockState, + PoolFees, PoolInformation, PoolState, StableSwapCurve, @@ -355,6 +356,119 @@ export const getDepegAccounts = async ( return depegAccounts; }; +export interface VaultAssociatedAccountStates { + vault: VaultState; + reserve: BN; + lpSupply: BN; +} + +export type SwapQuoteParams2 = { + vaultA?: VaultAssociatedAccountStates; + vaultB?: VaultAssociatedAccountStates; + currentTime: number; +}; + +export const calculateSwapQuoteForGoingToCreateMemecoinPool = ( + inAmountLamport: BN, + tokenADepositAmount: BN, + tokenBDepositAmount: BN, + aToB: boolean, + fees: PoolFees, + params: SwapQuoteParams2, +) => { + interface LocalStates { + vaultStates: VaultAssociatedAccountStates; + poolVaultLp: BN; + } + + const { currentTime } = params; + const vaultA: LocalStates | undefined = params.vaultA + ? { vaultStates: params.vaultA, poolVaultLp: new BN(0) } + : undefined; + const vaultB: LocalStates | undefined = params.vaultB + ? { vaultStates: params.vaultB, poolVaultLp: new BN(0) } + : undefined; + + invariant(vaultA || vaultB, 'Must one side have vault'); + invariant(!vaultA || !vaultB, 'Must one side have vault'); + + const getTokenAmountAfterDepositVault = (amount: BN, states?: LocalStates) => { + // No vault + if (!states) { + return amount; + } + + const vaultWithdrawableAmount = calculateWithdrawableAmount(currentTime, states.vaultStates.vault); + const lpMinted = getUnmintAmount(amount, vaultWithdrawableAmount, states.vaultStates.lpSupply); + + states.vaultStates.lpSupply = states.vaultStates.lpSupply.add(lpMinted); + states.vaultStates.vault.totalAmount = states.vaultStates.vault.totalAmount.add(amount); + states.poolVaultLp = states.poolVaultLp.add(lpMinted); + + return getAmountByShare( + states.poolVaultLp, + calculateWithdrawableAmount(currentTime, states.vaultStates.vault), + states.vaultStates.lpSupply, + ); + }; + + const getTokenAmountAfterWithdrawVault = (amount: BN, states?: LocalStates) => { + // No vault + if (!states) { + return amount; + } + + const vaultWithdrawableAmount = calculateWithdrawableAmount(currentTime, states.vaultStates.vault); + const lpBurned = getUnmintAmount(amount, vaultWithdrawableAmount, states.vaultStates.lpSupply); + + states.vaultStates.lpSupply = states.vaultStates.lpSupply.sub(lpBurned); + states.vaultStates.vault.totalAmount = states.vaultStates.vault.totalAmount.sub(amount); + states.poolVaultLp = states.poolVaultLp.sub(lpBurned); + + return getAmountByShare( + states.poolVaultLp, + calculateWithdrawableAmount(currentTime, states.vaultStates.vault), + states.vaultStates.lpSupply, + ); + }; + + const tokenAAmount = getTokenAmountAfterDepositVault(tokenADepositAmount, vaultA); + const tokenBAmount = getTokenAmountAfterDepositVault(tokenBDepositAmount, vaultB); + + const [sourceAmount, swapSourceAmount, swapDestinationAmount, sourceVault, destinationVault] = aToB + ? [inAmountLamport, tokenAAmount, tokenBAmount, vaultA, vaultB] + : [inAmountLamport, tokenBAmount, tokenAAmount, vaultB, vaultA]; + + const tradeFee = sourceAmount.mul(fees.tradeFeeNumerator).div(fees.tradeFeeDenominator); + const protocolFee = tradeFee.mul(fees.protocolTradeFeeNumerator).div(fees.protocolTradeFeeDenominator); + const sourceAmountLessProtocolFee = sourceAmount.sub(protocolFee); + + const beforeSwapSourceAmount = swapSourceAmount; + const afterSwapSourceAmount = sourceVault + ? getTokenAmountAfterDepositVault(sourceAmountLessProtocolFee, sourceVault) + : sourceAmountLessProtocolFee; + + const actualSourceAmount = afterSwapSourceAmount.sub(beforeSwapSourceAmount); + const sourceAmountLessFee = actualSourceAmount.sub(tradeFee.sub(protocolFee)); + + const curve = new ConstantProductSwap(); + const { outAmount: destinationAmount } = curve.computeOutAmount( + sourceAmountLessFee, + swapSourceAmount, + swapDestinationAmount, + aToB ? TradeDirection.AToB : TradeDirection.BToA, + ); + + const afterDestinationAmount = destinationVault + ? getTokenAmountAfterWithdrawVault(destinationAmount, destinationVault) + : destinationAmount; + + return { + amountOut: afterDestinationAmount, + fee: sourceAmountLessProtocolFee, + }; +}; + /** * It calculates the amount of tokens you will receive after swapping your tokens * @param {PublicKey} inTokenMint - The mint of the token you're swapping in. @@ -603,6 +717,15 @@ export const deriveConfigPda = (index: BN, programId: PublicKey) => { return configPda; }; +export const deriveProtocolTokenFee = (poolAddress: PublicKey, tokenMint: PublicKey, programId: PublicKey) => { + const [protocolTokenFee] = PublicKey.findProgramAddressSync( + [Buffer.from('fee'), tokenMint.toBuffer(), poolAddress.toBuffer()], + programId, + ); + + return protocolTokenFee; +}; + export function derivePoolAddress( connection: Connection, tokenInfoA: TokenInfo,