import { AccountLayout, MintInfo, MintLayout, Token, u64, } from '@solana/spl-token'; import { Account, Commitment, Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionSignature, } from '@solana/web3.js'; import { createInitSwapInstruction, DEFAULT_LIQUIDITY_TOKEN_PRECISION, depositInstruction, getProgramVersion, parseTokenAccount, SWAP_PROGRAM_OWNER_FEE_ADDRESS, swapInstruction, TOKEN_PROGRAM_ID, withdrawInstruction, WRAPPED_SOL_MINT, LATEST_VERSION, getLayoutForProgramId, deserializeMint, PROGRAM_ID, depositExactOneInstruction, withdrawExactOneInstruction, } from './instructions'; import { PoolConfig, PoolOptions, TokenAccount } from './types'; import { divideBnToNumber, timeMs } from './utils'; import assert from 'assert'; import BN from 'bn.js'; export class Pool { private _decoded: any; private _programId: PublicKey; private _poolAccount: PublicKey; private _tokenMints: PublicKey[]; private _holdingAccounts: PublicKey[]; private _poolTokenMint: PublicKey; private _feeAccount: PublicKey; private _skipPreflight: boolean; private _commitment: Commitment; private _mintAccountsCache: { [publickKey: string]: { value: MintInfo; ts: number }; }; private _tokenAccountsCache: { [publickKey: string]: { value: TokenAccount; ts: number }; }; constructor( decoded: any, // todo: remove any poolAccount: PublicKey, programId: PublicKey, options: PoolOptions = {}, ) { const { skipPreflight = false, commitment = 'recent' } = options; this._decoded = decoded; this._poolAccount = poolAccount; this._programId = programId; this._tokenMints = [decoded.mintA, decoded.mintB]; this._holdingAccounts = [decoded.tokenAccountA, decoded.tokenAccountB]; this._poolTokenMint = decoded.tokenPool; this._feeAccount = decoded.feeAccount; this._skipPreflight = skipPreflight; this._commitment = commitment; this._mintAccountsCache = {}; this._tokenAccountsCache = {}; } static async load( connection: Connection, address: PublicKey, programId: PublicKey, options: PoolOptions = {}, ): Promise<Pool> { const account = throwIfNull( await connection.getAccountInfo(address), 'Pool not found', ); const layout = getLayoutForProgramId(programId); const decoded = layout.decode(account.data); return new Pool(decoded, address, programId, options); } get address(): PublicKey { return this._poolAccount; } get publicKey(): PublicKey { return this.address; } get programVersion(): number { return getProgramVersion(this._programId); } get programId(): PublicKey { return this._programId; } get isLatest(): boolean { return getProgramVersion(this._programId) === LATEST_VERSION; } get poolTokenMint(): PublicKey { return this._poolTokenMint; } get holdingAccounts(): PublicKey[] { return this._holdingAccounts; } get tokenMints(): PublicKey[] { return this._tokenMints; } get feeAccount(): PublicKey { return this._feeAccount; } async cached<T>( callable: () => Promise<T>, cache: { [key: string]: { value: T; ts: number } }, key: string, cacheDurationMs: number, ): Promise<T> { const cachedItem = cache[key]; const now = timeMs(); if (cachedItem && now - cachedItem.ts < cacheDurationMs) { return cachedItem.value; } const value = await callable(); cache[key] = { value, ts: now, }; return value; } async getCachedMintAccount( connection: Connection, pubkey: PublicKey | string, cacheDurationMs = 0, ): Promise<MintInfo> { return this.cached<MintInfo>( () => getMintAccount(connection, pubkey), this._mintAccountsCache, typeof pubkey === 'string' ? pubkey : pubkey.toBase58(), cacheDurationMs, ); } async getCachedTokenAccount( connection: Connection, pubkey: PublicKey | string, cacheDurationMs = 0, ): Promise<TokenAccount> { return this.cached<TokenAccount>( () => getTokenAccount(connection, pubkey), this._tokenAccountsCache, typeof pubkey === 'string' ? pubkey : pubkey.toBase58(), cacheDurationMs, ); } async makeRemoveLiquidityTransaction<T extends PublicKey | Account>( connection: Connection, owner: T, liquidityAmount: number, poolAccount: TokenAccount, tokenAccounts: TokenAccount[], ): Promise<{ transaction: Transaction; signers: Account[]; payer: T }> { // @ts-ignore const ownerAddress: PublicKey = owner.publicKey ?? owner; // TODO get min amounts based on total supply and liquidity const minAmount0 = 0; const minAmount1 = 0; const poolMint = await this.getCachedMintAccount( connection, this._poolTokenMint, 3600000, ); const accountA = await this.getCachedTokenAccount( connection, this._holdingAccounts[0], 3600000, ); const accountB = await this.getCachedTokenAccount( connection, this._holdingAccounts[1], 3600000, ); if (!poolMint.mintAuthority) { throw new Error('Mint doesnt have authority'); } const authority = poolMint.mintAuthority; const signers: Account[] = []; const instructions: TransactionInstruction[] = []; const cleanUpInstructions: TransactionInstruction[] = []; let tokenAccountA: PublicKey | undefined; let tokenAccountB: PublicKey | undefined; if (accountA.info.mint.equals(WRAPPED_SOL_MINT)) { const accountRentExempt = await connection.getMinimumBalanceForRentExemption( AccountLayout.span, ); const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, accountRentExempt, ); tokenAccountA = account.publicKey; signers.push(account); instructions.push(...createWrappedSolInstructions); cleanUpInstructions.push(...removeWrappedSolInstructions) } else { tokenAccountA = tokenAccounts.find(a => a.info.mint.equals(accountA.info.mint), )?.pubkey; } if (accountB.info.mint.equals(WRAPPED_SOL_MINT)) { const accountRentExempt = await connection.getMinimumBalanceForRentExemption( AccountLayout.span, ); const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, accountRentExempt, ); tokenAccountB = account.publicKey; signers.push(account); instructions.push(...createWrappedSolInstructions); cleanUpInstructions.push(...removeWrappedSolInstructions) } else { tokenAccountB = tokenAccounts.find(a => a.info.mint.equals(accountB.info.mint), )?.pubkey; } assert( !!tokenAccountA, `Token account for mint ${accountA.info.mint.toBase58()} not provided`, ); assert( !!tokenAccountB, `Token account for mint ${accountB.info.mint.toBase58()} not provided`, ); const transferAuthority = approveTransfer( instructions, cleanUpInstructions, poolAccount.pubkey, ownerAddress, liquidityAmount, this.isLatest ? undefined : authority, ); if (this.isLatest) { signers.push(transferAuthority); } instructions.push( withdrawInstruction( this._poolAccount, authority, transferAuthority.publicKey, this._poolTokenMint, this._feeAccount, poolAccount.pubkey, this._holdingAccounts[0], this._holdingAccounts[1], tokenAccountA, tokenAccountB, this._programId, TOKEN_PROGRAM_ID, liquidityAmount, minAmount0, minAmount1, ), ); const transaction = new Transaction(); transaction.add(...instructions, ...cleanUpInstructions); return { transaction, signers, payer: owner }; } async makeAddLiquidityTransaction<T extends PublicKey | Account>( connection: Connection, owner: T, sourceTokenAccounts: { mint: PublicKey; tokenAccount: PublicKey; amount: number; // note this is raw amount, not decimal }[], poolTokenAccount?: PublicKey, slippageTolerance = 0.005, // allow slippage of this much between setting input amounts and on chain transaction ): Promise<{ transaction: Transaction; signers: Account[]; payer: T }> { // @ts-ignore const ownerAddress: PublicKey = owner.publicKey ?? owner; const poolMint = await this.getCachedMintAccount( connection, this._poolTokenMint, 360000, ); if (!poolMint.mintAuthority) { throw new Error('Mint doesnt have authority'); } if (!this._feeAccount) { throw new Error('Invald fee account'); } const accountA = await this.getCachedTokenAccount( connection, this._holdingAccounts[0], ); const accountB = await this.getCachedTokenAccount( connection, this._holdingAccounts[1], ); const reserve0 = accountA.info.amount.toNumber(); const reserve1 = accountB.info.amount.toNumber(); const [fromA, fromB] = accountA.info.mint.equals( sourceTokenAccounts[0].mint, ) ? [sourceTokenAccounts[0], sourceTokenAccounts[1]] : [sourceTokenAccounts[1], sourceTokenAccounts[0]]; if (!fromA.tokenAccount || !fromB.tokenAccount) { throw new Error('Missing account info.'); } const supply = poolMint.supply.toNumber(); const authority = poolMint.mintAuthority; // Uniswap whitepaper: https://uniswap.org/whitepaper.pdf // see: https://uniswap.org/docs/v2/advanced-topics/pricing/ // as well as native uniswap v2 oracle: https://uniswap.org/docs/v2/core-concepts/oracles/ const amount0 = fromA.amount; const amount1 = fromB.amount; const liquidity = Math.min( (amount0 * (1 - slippageTolerance) * supply) / reserve0, (amount1 * (1 - slippageTolerance) * supply) / reserve1, ); const instructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = []; const signers: Account[] = []; const accountRentExempt = await connection.getMinimumBalanceForRentExemption( AccountLayout.span, ); let fromKeyA: PublicKey; if (fromA.mint.equals(WRAPPED_SOL_MINT)) { const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, fromA.amount + accountRentExempt, ); fromKeyA = account.publicKey; signers.push(account); instructions.push(...createWrappedSolInstructions); cleanupInstructions.push(...removeWrappedSolInstructions); } else { fromKeyA = fromA.tokenAccount; } let fromKeyB: PublicKey; if (fromB.mint.equals(WRAPPED_SOL_MINT)) { const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, fromB.amount + accountRentExempt, ); fromKeyB = account.publicKey; signers.push(account); instructions.push(...createWrappedSolInstructions); cleanupInstructions.push(...removeWrappedSolInstructions); } else { fromKeyB = fromB.tokenAccount; } let toAccount: PublicKey; if (!poolTokenAccount) { const { account, instructions: createToAccountInstructions, cleanUpInstructions: cleanupCreateToAccountInstructions, } = createTokenAccount( ownerAddress, ownerAddress, this._poolTokenMint, accountRentExempt, ); toAccount = account.publicKey; signers.push(account); instructions.push(...createToAccountInstructions); cleanupInstructions.push(...cleanupCreateToAccountInstructions); } else { toAccount = poolTokenAccount; } // create approval for transfer transactions const transferAuthority = approveTransfer( instructions, cleanupInstructions, fromKeyA, ownerAddress, amount0, this.isLatest ? undefined : authority, ); if (this.isLatest) { signers.push(transferAuthority); } approveTransfer( instructions, cleanupInstructions, fromKeyB, ownerAddress, amount1, this.isLatest ? transferAuthority.publicKey : authority, ); instructions.push( depositInstruction( this._poolAccount, authority, transferAuthority.publicKey, fromKeyA, fromKeyB, this._holdingAccounts[0], this._holdingAccounts[1], this._poolTokenMint, toAccount, this._programId, TOKEN_PROGRAM_ID, liquidity, amount0, amount1, ), ); const transaction = new Transaction(); transaction.add(...instructions); transaction.add(...cleanupInstructions); return { transaction, signers, payer: owner }; } async makeAddSingleSidedLiquidityTransaction<T extends PublicKey>( connection: Connection, owner: T, sourceTokenAccount: { mint: PublicKey; tokenAccount: PublicKey; amount: number; // note this is raw amount, not decimal }, poolTokenAccount?: PublicKey, ): Promise<{ transaction: Transaction; signers: Account[]; payer: T }> { assert(this._decoded.curve.constantPrice, 'Only implemented for constant price pools'); // @ts-ignore const ownerAddress: PublicKey = owner.publicKey ?? owner; const instructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = []; const signers: Account[] = []; const poolMint = await this.getCachedMintAccount( connection, this._poolTokenMint, 0, ); if (!poolMint.mintAuthority) { throw new Error('Mint doesnt have authority'); } const authority = poolMint.mintAuthority; const accountRentExempt = await connection.getMinimumBalanceForRentExemption( AccountLayout.span, ); const accountA = await this.getCachedTokenAccount( connection, this._holdingAccounts[0], ); const accountB = await this.getCachedTokenAccount( connection, this._holdingAccounts[1], ); const reserve0 = accountA.info.amount.toNumber(); const reserve1 = accountB.info.amount.toNumber(); const supply = poolMint.supply.toNumber(); const tokenBPrice = this._decoded.curve.constantPrice.token_b_price let price; if (sourceTokenAccount.mint.equals(this.tokenMints[1])) { price = tokenBPrice; } else { price = 1; } const sourceAmountPostFees = sourceTokenAccount.amount - ( Math.max(1, sourceTokenAccount.amount / 2) * this._decoded.fees.tradeFeeNumerator / this._decoded.fees.tradeFeeDenominator ) const liquidity = Math.floor((sourceAmountPostFees * price * supply) / (reserve0 + reserve1 * tokenBPrice)); let fromKey: PublicKey; if (sourceTokenAccount.mint.equals(WRAPPED_SOL_MINT)) { const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, sourceTokenAccount.amount + accountRentExempt, ); fromKey = account.publicKey; signers.push(account); instructions.push(...createWrappedSolInstructions); cleanupInstructions.push(...removeWrappedSolInstructions); } else { fromKey = sourceTokenAccount.tokenAccount; } let toAccount: PublicKey; if (!poolTokenAccount) { const { account, instructions: createToAccountInstructions, cleanUpInstructions: cleanupCreateToAccountInstructions, } = createTokenAccount( ownerAddress, ownerAddress, this._poolTokenMint, accountRentExempt, ); toAccount = account.publicKey; signers.push(account); instructions.push(...createToAccountInstructions); cleanupInstructions.push(...cleanupCreateToAccountInstructions); } else { toAccount = poolTokenAccount; } const transferAuthority = approveTransfer( instructions, cleanupInstructions, fromKey, ownerAddress, sourceTokenAccount.amount, this.isLatest ? undefined : authority, ); if (this.isLatest) { signers.push(transferAuthority); } instructions.push( depositExactOneInstruction( this._poolAccount, authority, transferAuthority.publicKey, sourceTokenAccount.tokenAccount, this._holdingAccounts[0], this._holdingAccounts[1], this._poolTokenMint, toAccount, this._programId, TOKEN_PROGRAM_ID, sourceTokenAccount.amount, liquidity, this.isLatest, ), ); const transaction = new Transaction(); transaction.add(...instructions); transaction.add(...cleanupInstructions); return { transaction, signers, payer: owner }; } async makeWithdrawSingleSidedLiquidityTransaction<T extends PublicKey>( connection: Connection, owner: T, destinationTokenAccount: { mint: PublicKey; tokenAccount: PublicKey; amount: number; // note this is raw amount, not decimal }, poolTokenAccount: PublicKey, ): Promise<{ transaction: Transaction; signers: Account[]; payer: T }> { assert(this._decoded.curve.constantPrice, 'Only implemented for constant price pools'); // @ts-ignore const ownerAddress: PublicKey = owner.publicKey ?? owner; const instructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = []; const signers: Account[] = []; const poolMint = await this.getCachedMintAccount( connection, this._poolTokenMint, 0, ); if (!poolMint.mintAuthority) { throw new Error('Mint doesnt have authority'); } const authority = poolMint.mintAuthority; const accountA = await this.getCachedTokenAccount( connection, this._holdingAccounts[0], ); const accountB = await this.getCachedTokenAccount( connection, this._holdingAccounts[1], ); const reserve0 = accountA.info.mint.equals(destinationTokenAccount.mint) ? accountA.info.amount.toNumber() - destinationTokenAccount.amount : accountA.info.amount.toNumber(); const reserve1 = accountB.info.mint.equals(destinationTokenAccount.mint) ? accountB.info.amount.toNumber() - destinationTokenAccount.amount : accountB.info.amount.toNumber(); const supply = poolMint.supply.toNumber(); const tokenBPrice = this._decoded.curve.constantPrice.token_b_price let price; if (destinationTokenAccount.mint.equals(this.tokenMints[1])) { price = tokenBPrice; } else { price = 1; } const destinationAmountPostFees = destinationTokenAccount.amount - ( Math.max(1, destinationTokenAccount.amount / 2) * this._decoded.fees.tradeFeeNumerator / this._decoded.fees.tradeFeeDenominator ) const liquidityPreWithdrawalFee = Math.ceil( (destinationAmountPostFees * price * supply) / (reserve0 + reserve1 * tokenBPrice) ); let liquidity = liquidityPreWithdrawalFee; if (this._decoded.fees.ownerWithdrawFeeDenominator > 0) { liquidity += liquidityPreWithdrawalFee * ( this._decoded.fees.ownerWithdrawFeeNumerator / this._decoded.fees.ownerWithdrawFeeDenominator ) } else { liquidity += 1 } const transferAuthority = approveTransfer( instructions, cleanupInstructions, poolTokenAccount, ownerAddress, liquidity, this.isLatest ? undefined : authority, ); if (this.isLatest) { signers.push(transferAuthority); } instructions.push( withdrawExactOneInstruction( this._poolAccount, authority, transferAuthority.publicKey, this._poolTokenMint, poolTokenAccount, this._holdingAccounts[0], this._holdingAccounts[1], destinationTokenAccount.tokenAccount, this.feeAccount, this._programId, TOKEN_PROGRAM_ID, destinationTokenAccount.amount, liquidity, this.isLatest, ), ); const transaction = new Transaction(); transaction.add(...instructions); transaction.add(...cleanupInstructions); return { transaction, signers, payer: owner }; } async makeSwapTransaction<T extends PublicKey | Account>( connection: Connection, owner: T, tokenIn: { mint: PublicKey; tokenAccount: PublicKey; amount: number; }, tokenOut: { mint: PublicKey; tokenAccount: PublicKey; amount: number; }, slippage: number, hostFeeAccount?: PublicKey, ): Promise<{ transaction: Transaction; signers: Account[]; payer: T }> { // @ts-ignore const ownerAddress: PublicKey = owner.publicKey ?? owner; const [poolMint, inMint, outMint] = await Promise.all([ this.getCachedMintAccount(connection, this._poolTokenMint, 3600_000), this.getCachedMintAccount(connection, tokenIn.mint, 3600_000), this.getCachedMintAccount(connection, tokenOut.mint, 3600_000), ]); const amountIn = Math.floor(tokenIn.amount * Math.pow(10, inMint.decimals)); const minAmountOut = Math.floor( tokenOut.amount * Math.pow(10, outMint.decimals) * (1 - slippage), ); const holdingA = this._tokenMints[0].toBase58() === tokenIn.mint.toBase58() ? this._holdingAccounts[0] : this._holdingAccounts[1]; const holdingB = holdingA.equals(this._holdingAccounts[0]) ? this._holdingAccounts[1] : this._holdingAccounts[0]; if (!poolMint.mintAuthority || !this._feeAccount) { throw new Error('Mint doesnt have authority'); } const authority = poolMint.mintAuthority; const instructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = []; const signers: Account[] = []; const accountRentExempt = await connection.getMinimumBalanceForRentExemption( AccountLayout.span, ); let fromAccount: PublicKey; if (tokenIn.mint.equals(WRAPPED_SOL_MINT)) { const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, amountIn + accountRentExempt, ); fromAccount = account.publicKey; signers.push(account); instructions.push(...createWrappedSolInstructions); cleanupInstructions.push(...removeWrappedSolInstructions); } else { fromAccount = tokenIn.tokenAccount; } let toAccount: PublicKey; if (tokenOut.mint.equals(WRAPPED_SOL_MINT)) { const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, accountRentExempt, ); toAccount = account.publicKey; signers.push(account); instructions.push(...createWrappedSolInstructions); cleanupInstructions.push(...removeWrappedSolInstructions); } else { toAccount = tokenOut.tokenAccount; } // create approval for transfer transactions const transferAuthority = approveTransfer( instructions, cleanupInstructions, fromAccount, ownerAddress, amountIn, this.isLatest ? undefined : authority, ); if (this.isLatest) { signers.push(transferAuthority); } // swap instructions.push( swapInstruction( this._poolAccount, authority, transferAuthority.publicKey, fromAccount, holdingA, holdingB, toAccount, this._poolTokenMint, this._feeAccount, this._programId, TOKEN_PROGRAM_ID, amountIn, minAmountOut, hostFeeAccount, ), ); instructions.push(...cleanupInstructions); const transaction = new Transaction(); transaction.add(...instructions); return { transaction, signers, payer: owner }; } async swap( connection: Connection, owner: Account, tokenIn: { mint: PublicKey; tokenAccount: PublicKey; amount: number; }, tokenOut: { mint: PublicKey; tokenAccount: PublicKey; amount: number; }, slippage: number, hostFeeAccount?: PublicKey, skipPreflight = true, commitment: Commitment = 'single', ): Promise<string> { const { transaction, signers, payer } = await this.makeSwapTransaction( connection, owner, tokenIn, tokenOut, slippage, hostFeeAccount, ); return await sendTransaction( connection, transaction, [payer, ...signers], skipPreflight, commitment, ); } /** * Note: for seed param, this must be <= 32 characters for the txn to succeed */ static async makeInitializePoolTransaction<T extends PublicKey | Account>( connection: Connection, tokenSwapProgram: PublicKey, owner: T, componentMints: PublicKey[], sourceTokenAccounts: { mint: PublicKey; tokenAccount: PublicKey; amount: number; // note this is raw amount, not decimal }[], options: PoolConfig, liquidityTokenPrecision = DEFAULT_LIQUIDITY_TOKEN_PRECISION, accounts?: { liquidityTokenMint?: Account; tokenSwapPoolAddress?: Account; }, seed?: string, ): Promise<{ initializeAccountsTransaction: Transaction; initializeAccountsSigners: Account[]; initializePoolTransaction: Transaction; initializePoolSigners: Account[]; }> { // @ts-ignore const ownerAddress: PublicKey = owner.publicKey ?? owner; const initializeAccountsInstructions: TransactionInstruction[] = []; const initializeAccountsSigners: Account[] = []; const liquidityTokenMintAccount = accounts?.liquidityTokenMint ? accounts.liquidityTokenMint : new Account(); initializeAccountsInstructions.push( SystemProgram.createAccount({ fromPubkey: ownerAddress, newAccountPubkey: liquidityTokenMintAccount.publicKey, lamports: await connection.getMinimumBalanceForRentExemption( MintLayout.span, ), space: MintLayout.span, programId: TOKEN_PROGRAM_ID, }), ); initializeAccountsSigners.push(liquidityTokenMintAccount); let tokenSwapAccountPubkey; let tokenSwapAccountSigner; if (accounts?.tokenSwapPoolAddress) { tokenSwapAccountPubkey = accounts.tokenSwapPoolAddress.publicKey; tokenSwapAccountSigner = accounts.tokenSwapPoolAddress; } else if (seed) { // Only works when owner is of type Account tokenSwapAccountSigner = owner; tokenSwapAccountPubkey = await PublicKey.createWithSeed(ownerAddress, seed, tokenSwapProgram); } else { tokenSwapAccountSigner = new Account(); tokenSwapAccountPubkey = tokenSwapAccountSigner.pubkey; } const [authority, nonce] = await PublicKey.findProgramAddress( [tokenSwapAccountPubkey.toBuffer()], tokenSwapProgram, ); // create mint for pool liquidity token initializeAccountsInstructions.push( Token.createInitMintInstruction( TOKEN_PROGRAM_ID, liquidityTokenMintAccount.publicKey, liquidityTokenPrecision, // pass control of liquidity mint to swap program authority, // swap program can freeze liquidity token mint null, ), ); const accountRentExempt = await connection.getMinimumBalanceForRentExemption( AccountLayout.span, ); const holdingAccounts: { [mint: string]: Account } = {}; componentMints.forEach(mint => { const { account, instructions: createHoldingTokenAccountInstructions, } = createTokenAccount(authority, ownerAddress, mint, accountRentExempt); initializeAccountsInstructions.push( ...createHoldingTokenAccountInstructions, ); initializeAccountsSigners.push(account); holdingAccounts[mint.toBase58()] = account; }); const { account: depositorAccount, instructions: createLPTokenAccountInstructions, } = createTokenAccount( ownerAddress, ownerAddress, liquidityTokenMintAccount.publicKey, accountRentExempt, ); initializeAccountsSigners.push(depositorAccount); initializeAccountsInstructions.push(...createLPTokenAccountInstructions); const { account: feeAccount, instructions: createFeeAccountInstructions, } = createTokenAccount( SWAP_PROGRAM_OWNER_FEE_ADDRESS, ownerAddress, liquidityTokenMintAccount.publicKey, accountRentExempt, ); initializeAccountsSigners.push(feeAccount); initializeAccountsInstructions.push(...createFeeAccountInstructions); const initializeAccountsTransaction = new Transaction(); initializeAccountsTransaction.add(...initializeAccountsInstructions); // break up these into two transactions because it does not fit in a single transaction const initializePoolSigners: Account[] = []; const initializePoolInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = []; let initializeTokenSwapAccountInstruction; if (seed) { initializeTokenSwapAccountInstruction = SystemProgram.createAccountWithSeed({ fromPubkey: ownerAddress, basePubkey: ownerAddress, newAccountPubkey: tokenSwapAccountPubkey, seed: seed, lamports: await connection.getMinimumBalanceForRentExemption( getLayoutForProgramId(tokenSwapProgram).span ), space: getLayoutForProgramId(tokenSwapProgram).span, programId: tokenSwapProgram, }); } else { initializeTokenSwapAccountInstruction = SystemProgram.createAccount({ fromPubkey: ownerAddress, newAccountPubkey: tokenSwapAccountPubkey, lamports: await connection.getMinimumBalanceForRentExemption( getLayoutForProgramId(tokenSwapProgram).span, ), space: getLayoutForProgramId(tokenSwapProgram).span, programId: tokenSwapProgram, }) } initializePoolInstructions.push(initializeTokenSwapAccountInstruction); sourceTokenAccounts.forEach(({ mint, tokenAccount, amount }) => { let wrappedAccount: PublicKey; if (mint.equals(WRAPPED_SOL_MINT)) { const { account, instructions: createWrappedSolInstructions, cleanUpInstructions: removeWrappedSolInstructions, } = createTokenAccount( ownerAddress, ownerAddress, WRAPPED_SOL_MINT, amount + accountRentExempt, ); wrappedAccount = account.publicKey; initializePoolSigners.push(account); initializePoolInstructions.push(...createWrappedSolInstructions); cleanupInstructions.push(...removeWrappedSolInstructions); } else { wrappedAccount = tokenAccount; } initializePoolInstructions.push( Token.createTransferInstruction( TOKEN_PROGRAM_ID, wrappedAccount, holdingAccounts[mint.toBase58()].publicKey, ownerAddress, [], amount, ), ); }); initializePoolInstructions.push( createInitSwapInstruction( tokenSwapAccountPubkey, authority, holdingAccounts[sourceTokenAccounts[0].mint.toBase58()].publicKey, holdingAccounts[sourceTokenAccounts[1].mint.toBase58()].publicKey, liquidityTokenMintAccount.publicKey, feeAccount.publicKey, depositorAccount.publicKey, TOKEN_PROGRAM_ID, tokenSwapProgram, nonce, options, ), ); initializePoolSigners.push(tokenSwapAccountSigner); const initializePoolTransaction = new Transaction(); initializePoolTransaction.add( ...initializePoolInstructions, ...cleanupInstructions, ); return { initializeAccountsTransaction, initializeAccountsSigners, initializePoolTransaction, initializePoolSigners, }; } static async initializePool( connection: Connection, tokenSwapProgram: PublicKey, owner: Account, componentMints: PublicKey[], sourceTokenAccounts: { mint: PublicKey; tokenAccount: PublicKey; amount: number; }[], options: PoolConfig, liquidityTokenPrecision = DEFAULT_LIQUIDITY_TOKEN_PRECISION, skipPreflight = true, commitment: Commitment = 'single', ): Promise<string> { const { initializeAccountsTransaction, initializeAccountsSigners, initializePoolTransaction, initializePoolSigners, } = await Pool.makeInitializePoolTransaction( connection, tokenSwapProgram, owner, componentMints, sourceTokenAccounts, options, liquidityTokenPrecision, ); const createAccountsTxid = await sendTransaction( connection, initializeAccountsTransaction, [owner, ...initializeAccountsSigners], skipPreflight, commitment, ); const status = (await connection.confirmTransaction(createAccountsTxid)) .value; assert( !status.err, `Received error awaiting create accounts transaction ${createAccountsTxid}`, ); return await sendTransaction( connection, initializePoolTransaction, [owner, ...initializePoolSigners], skipPreflight, commitment, ); } async removeLiquidity( connection: Connection, owner: Account, liquidityAmount: number, poolAccount: TokenAccount, tokenAccounts: TokenAccount[], skipPreflight = true, commitment: Commitment = 'single', ): Promise<string> { const { transaction, signers } = await this.makeRemoveLiquidityTransaction( connection, owner, liquidityAmount, poolAccount, tokenAccounts, ); return await sendTransaction( connection, transaction, [owner, ...signers], skipPreflight, commitment, ); } async addLiquidity( connection: Connection, owner: Account, sourceTokenAccounts: { mint: PublicKey; tokenAccount: PublicKey; amount: number; // note this is raw amount, not decimal }[], poolTokenAccount?: PublicKey, slippageTolerance?: number, skipPreflight = true, commitment: Commitment = 'single', ): Promise<string> { const { transaction, signers } = await this.makeAddLiquidityTransaction( connection, owner, sourceTokenAccounts, poolTokenAccount, slippageTolerance, ); return await sendTransaction( connection, transaction, [owner, ...signers], skipPreflight, commitment, ); } async getHoldings( connection: Connection, ): Promise<{ account: PublicKey; mint: PublicKey; holding: u64 }[]> { const accounts = await Promise.all([ this.getCachedTokenAccount(connection, this._holdingAccounts[0]), this.getCachedTokenAccount(connection, this._holdingAccounts[1]), ]); return accounts.map(account => { return { account: account.pubkey, mint: account.info.mint, holding: account.info.amount, }; }); } get fees(): { tradeFee: number; ownerFee: number; withdrawFee: number; hostFee?: number; } { if (this.programVersion !== 2) { return { tradeFee: divideBnToNumber( new BN(this._decoded.tradeFeeNumerator, 'le'), new BN(this._decoded.tradeFeeDenominator, 'le'), ), ownerFee: divideBnToNumber( new BN(this._decoded.ownerTradeFeeNumerator, 'le'), new BN(this._decoded.ownerTradeFeeDenominator, 'le'), ), withdrawFee: divideBnToNumber( new BN(this._decoded.ownerWithdrawFeeNumerator, 'le'), new BN(this._decoded.ownerWithdrawFeeDenominator, 'le'), ), }; } else { let withdrawalFee; if ( this._decoded.fees.ownerWithdrawFeeNumerator != 0 && this._decoded.fees.ownerWithdrawFeeDenominator != 0 ) { withdrawalFee = divideBnToNumber( new BN(this._decoded.fees.ownerWithdrawFeeNumerator, 'le'), new BN(this._decoded.fees.ownerWithdrawFeeDenominator, 'le'), ); } else { withdrawalFee = 0; } return { tradeFee: divideBnToNumber( new BN(this._decoded.fees.tradeFeeNumerator, 'le'), new BN(this._decoded.fees.tradeFeeDenominator, 'le'), ), ownerFee: divideBnToNumber( new BN(this._decoded.fees.ownerTradeFeeNumerator, 'le'), new BN(this._decoded.fees.ownerTradeFeeDenominator, 'le'), ), withdrawFee: withdrawalFee, hostFee: divideBnToNumber( new BN(this._decoded.fees.hostFeeNumerator, 'le'), new BN(this._decoded.fees.hostFeeDenominator, 'le'), ), }; } } } async function sendTransaction( connection: Connection, transaction: Transaction, signers: Array<Account>, skipPreflight = true, commitment: Commitment = 'single', ): Promise<TransactionSignature> { const signature = await connection.sendTransaction(transaction, signers, { skipPreflight: skipPreflight, }); const { value } = await connection.confirmTransaction(signature, commitment); if (value?.err) { throw new Error(JSON.stringify(value.err)); } return signature; } export const getMintAccount = async ( connection: Connection, pubKey: PublicKey | string, ): Promise<MintInfo> => { const address = typeof pubKey === 'string' ? new PublicKey(pubKey) : pubKey; const info = await connection.getAccountInfo(address); if (info === null) { throw new Error('Failed to find mint account'); } return deserializeMint(info.data); }; export const getTokenAccount = async ( connection: Connection, pubKey: PublicKey | string, ): Promise<TokenAccount> => { const address = typeof pubKey === 'string' ? new PublicKey(pubKey) : pubKey; const info = await connection.getAccountInfo(address); if (info === null) { throw new Error('Failed to find token account'); } const accountInfo = parseTokenAccount(info.data); return { pubkey: address, account: info, info: accountInfo, }; }; export const approveTransfer = ( instructions: TransactionInstruction[], cleanupInstructions: TransactionInstruction[], account: PublicKey, owner: PublicKey, amount: number, // if delegate is not passed ephemeral transfer authority is used delegate?: PublicKey, ) => { const transferAuthority = new Account(); instructions.push( Token.createApproveInstruction( TOKEN_PROGRAM_ID, account, delegate ?? transferAuthority.publicKey, owner, [], amount, ), ); cleanupInstructions.push( Token.createRevokeInstruction(TOKEN_PROGRAM_ID, account, owner, []), ); return transferAuthority; }; export const createTokenAccount = ( owner: PublicKey, payer: PublicKey, mint: PublicKey, lamports: number, ) => { const account = new Account(); const instructions: TransactionInstruction[] = []; const cleanUpInstructions: TransactionInstruction[] = []; const space = AccountLayout.span as number; instructions.push( SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: account.publicKey, lamports, space, programId: TOKEN_PROGRAM_ID, }), ); instructions.push( Token.createInitAccountInstruction( TOKEN_PROGRAM_ID, mint, account.publicKey, owner, ), ); if (mint.equals(WRAPPED_SOL_MINT)) { cleanUpInstructions.push( Token.createCloseAccountInstruction( TOKEN_PROGRAM_ID, account.publicKey, payer, owner, [], ), ); } return { account, instructions, cleanUpInstructions }; }; function throwIfNull<T>(value: T | null, message = 'account not found'): T { if (value === null) { throw new Error(message); } return value; }