/** * @flow */ import assert from 'assert'; import BN from 'bn.js'; import * as BufferLayout from 'buffer-layout'; import type {Connection, TransactionSignature} from '@solana/web3.js'; import { Account, PublicKey, SystemProgram, Transaction, TransactionInstruction, } from '@solana/web3.js'; import * as Layout from './layout'; import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction'; import {loadAccount} from './util/account'; /** * Some amount of tokens */ export class Numberu64 extends BN { /** * Convert to Buffer representation */ toBuffer(): typeof Buffer { const a = super.toArray().reverse(); const b = Buffer.from(a); if (b.length === 8) { return b; } assert(b.length < 8, 'Numberu64 too large'); const zeroPad = Buffer.alloc(8); b.copy(zeroPad); return zeroPad; } /** * Construct a Numberu64 from Buffer representation */ static fromBuffer(buffer: typeof Buffer): Numberu64 { assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`); return new BN( [...buffer] .reverse() .map(i => `00${i.toString(16)}`.slice(-2)) .join(''), 16, ); } } /** * @private */ export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct( [ BufferLayout.u8('isInitialized'), BufferLayout.u8('nonce'), Layout.publicKey('tokenProgramId'), Layout.publicKey('tokenAccountA'), Layout.publicKey('tokenAccountB'), Layout.publicKey('tokenPool'), Layout.publicKey('mintA'), Layout.publicKey('mintB'), Layout.publicKey('feeAccount'), BufferLayout.u8('curveType'), Layout.uint64('tradeFeeNumerator'), Layout.uint64('tradeFeeDenominator'), Layout.uint64('ownerTradeFeeNumerator'), Layout.uint64('ownerTradeFeeDenominator'), Layout.uint64('ownerWithdrawFeeNumerator'), Layout.uint64('ownerWithdrawFeeDenominator'), BufferLayout.blob(16, 'padding'), ], ); export const CurveType = Object.freeze({ ConstantProduct: 0, // Constant product curve, Uniswap-style Flat: 1, // Flat curve, always 1:1 trades }); /** * A program to exchange tokens against a pool of liquidity */ export class TokenSwap { /** * @private */ connection: Connection; /** * Program Identifier for the Swap program */ swapProgramId: PublicKey; /** * Program Identifier for the Token program */ tokenProgramId: PublicKey; /** * The public key identifying this swap program */ tokenSwap: PublicKey; /** * The public key for the liquidity pool token mint */ poolToken: PublicKey; /** * The public key for the fee account receiving trade and/or withdrawal fees */ feeAccount: PublicKey; /** * Authority */ authority: PublicKey; /** * The public key for the first token account of the trading pair */ tokenAccountA: PublicKey; /** * The public key for the second token account of the trading pair */ tokenAccountB: PublicKey; /** * The public key for the mint of the first token account of the trading pair */ mintA: PublicKey; /** * The public key for the mint of the second token account of the trading pair */ mintB: PublicKey; /** * Trading fee numerator */ tradeFeeNumerator: Numberu64; /** * Trading fee denominator */ tradeFeeDenominator: Numberu64; /** * Owner trading fee numerator */ ownerTradeFeeNumerator: Numberu64; /** * Owner trading fee denominator */ ownerTradeFeeDenominator: Numberu64; /** * Owner withdraw fee numerator */ ownerWithdrawFeeNumerator: Numberu64; /** * Owner withdraw fee denominator */ ownerWithdrawFeeDenominator: Numberu64; /** * CurveType, current options are: */ curveType: number; /** * Fee payer */ payer: Account; /** * Create a Token object attached to the specific token * * @param connection The connection to use * @param tokenSwap The token swap account * @param swapProgramId The program ID of the token-swap program * @param tokenProgramId The program ID of the token program * @param poolToken The pool token * @param authority The authority over the swap and accounts * @param tokenAccountA: The token swap's Token A account * @param tokenAccountB: The token swap's Token B account * @param payer Pays for the transaction */ constructor( connection: Connection, tokenSwap: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, poolToken: PublicKey, feeAccount: PublicKey, authority: PublicKey, tokenAccountA: PublicKey, tokenAccountB: PublicKey, mintA: PublicKey, mintB: PublicKey, curveType: number, tradeFeeNumerator: Numberu64, tradeFeeDenominator: Numberu64, ownerTradeFeeNumerator: Numberu64, ownerTradeFeeDenominator: Numberu64, ownerWithdrawFeeNumerator: Numberu64, ownerWithdrawFeeDenominator: Numberu64, payer: Account, ) { Object.assign(this, { connection, tokenSwap, swapProgramId, tokenProgramId, poolToken, feeAccount, authority, tokenAccountA, tokenAccountB, mintA, mintB, curveType, tradeFeeNumerator, tradeFeeDenominator, ownerTradeFeeNumerator, ownerTradeFeeDenominator, ownerWithdrawFeeNumerator, ownerWithdrawFeeDenominator, payer, }); } /** * Get the minimum balance for the token swap account to be rent exempt * * @return Number of lamports required */ static async getMinBalanceRentForExemptTokenSwap( connection: Connection, ): Promise { return await connection.getMinimumBalanceForRentExemption( TokenSwapLayout.span, ); } static createInitSwapInstruction( tokenSwapAccount: Account, authority: PublicKey, tokenAccountA: PublicKey, tokenAccountB: PublicKey, tokenPool: PublicKey, feeAccount: PublicKey, tokenAccountPool: PublicKey, tokenProgramId: PublicKey, swapProgramId: PublicKey, nonce: number, curveType: number, tradeFeeNumerator: number, tradeFeeDenominator: number, ownerTradeFeeNumerator: number, ownerTradeFeeDenominator: number, ownerWithdrawFeeNumerator: number, ownerWithdrawFeeDenominator: number, ): TransactionInstruction { const keys = [ {pubkey: tokenSwapAccount.publicKey, isSigner: false, isWritable: true}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: tokenAccountA, isSigner: false, isWritable: false}, {pubkey: tokenAccountB, isSigner: false, isWritable: false}, {pubkey: tokenPool, isSigner: false, isWritable: true}, {pubkey: feeAccount, isSigner: false, isWritable: false}, {pubkey: tokenAccountPool, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; const commandDataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), BufferLayout.u8('nonce'), BufferLayout.u8('curveType'), BufferLayout.nu64('tradeFeeNumerator'), BufferLayout.nu64('tradeFeeDenominator'), BufferLayout.nu64('ownerTradeFeeNumerator'), BufferLayout.nu64('ownerTradeFeeDenominator'), BufferLayout.nu64('ownerWithdrawFeeNumerator'), BufferLayout.nu64('ownerWithdrawFeeDenominator'), BufferLayout.blob(16, 'padding'), ]); let data = Buffer.alloc(1024); { const encodeLength = commandDataLayout.encode( { instruction: 0, // InitializeSwap instruction nonce, curveType, tradeFeeNumerator, tradeFeeDenominator, ownerTradeFeeNumerator, ownerTradeFeeDenominator, ownerWithdrawFeeNumerator, ownerWithdrawFeeDenominator, }, data, ); data = data.slice(0, encodeLength); } return new TransactionInstruction({ keys, programId: swapProgramId, data, }); } static async loadTokenSwap( connection: Connection, address: PublicKey, programId: PublicKey, payer: Account, ): Promise { const data = await loadAccount(connection, address, programId); const tokenSwapData = TokenSwapLayout.decode(data); if (!tokenSwapData.isInitialized) { throw new Error(`Invalid token swap state`); } const [authority] = await PublicKey.findProgramAddress( [address.toBuffer()], programId, ); const poolToken = new PublicKey(tokenSwapData.tokenPool); const feeAccount = new PublicKey(tokenSwapData.feeAccount); const tokenAccountA = new PublicKey(tokenSwapData.tokenAccountA); const tokenAccountB = new PublicKey(tokenSwapData.tokenAccountB); const mintA = new PublicKey(tokenSwapData.mintA); const mintB = new PublicKey(tokenSwapData.mintB); const tokenProgramId = new PublicKey(tokenSwapData.tokenProgramId); const tradeFeeNumerator = Numberu64.fromBuffer( tokenSwapData.tradeFeeNumerator, ); const tradeFeeDenominator = Numberu64.fromBuffer( tokenSwapData.tradeFeeDenominator, ); const ownerTradeFeeNumerator = Numberu64.fromBuffer( tokenSwapData.ownerTradeFeeNumerator, ); const ownerTradeFeeDenominator = Numberu64.fromBuffer( tokenSwapData.ownerTradeFeeDenominator, ); const ownerWithdrawFeeNumerator = Numberu64.fromBuffer( tokenSwapData.ownerWithdrawFeeNumerator, ); const ownerWithdrawFeeDenominator = Numberu64.fromBuffer( tokenSwapData.ownerWithdrawFeeDenominator, ); const curveType = tokenSwapData.curveType; return new TokenSwap( connection, address, programId, tokenProgramId, poolToken, feeAccount, authority, tokenAccountA, tokenAccountB, mintA, mintB, curveType, tradeFeeNumerator, tradeFeeDenominator, ownerTradeFeeNumerator, ownerTradeFeeDenominator, ownerWithdrawFeeNumerator, ownerWithdrawFeeDenominator, payer, ); } /** * Create a new Token Swap * * @param connection The connection to use * @param payer Pays for the transaction * @param tokenSwapAccount The token swap account * @param authority The authority over the swap and accounts * @param nonce The nonce used to generate the authority * @param tokenAccountA: The token swap's Token A account * @param tokenAccountB: The token swap's Token B account * @param poolToken The pool token * @param tokenAccountPool The token swap's pool token account * @param tokenProgramId The program ID of the token program * @param swapProgramId The program ID of the token-swap program * @param feeNumerator Numerator of the fee ratio * @param feeDenominator Denominator of the fee ratio * @return Token object for the newly minted token, Public key of the account holding the total supply of new tokens */ static async createTokenSwap( connection: Connection, payer: Account, tokenSwapAccount: Account, authority: PublicKey, tokenAccountA: PublicKey, tokenAccountB: PublicKey, poolToken: PublicKey, mintA: PublicKey, mintB: PublicKey, feeAccount: PublicKey, tokenAccountPool: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, nonce: number, curveType: number, tradeFeeNumerator: number, tradeFeeDenominator: number, ownerTradeFeeNumerator: number, ownerTradeFeeDenominator: number, ownerWithdrawFeeNumerator: number, ownerWithdrawFeeDenominator: number, ): Promise { let transaction; const tokenSwap = new TokenSwap( connection, tokenSwapAccount.publicKey, swapProgramId, tokenProgramId, poolToken, feeAccount, authority, tokenAccountA, tokenAccountB, mintA, mintB, curveType, new Numberu64(tradeFeeNumerator), new Numberu64(tradeFeeDenominator), new Numberu64(ownerTradeFeeNumerator), new Numberu64(ownerTradeFeeDenominator), new Numberu64(ownerWithdrawFeeNumerator), new Numberu64(ownerWithdrawFeeDenominator), payer, ); // Allocate memory for the account const balanceNeeded = await TokenSwap.getMinBalanceRentForExemptTokenSwap( connection, ); transaction = new Transaction(); transaction.add( SystemProgram.createAccount({ fromPubkey: payer.publicKey, newAccountPubkey: tokenSwapAccount.publicKey, lamports: balanceNeeded, space: TokenSwapLayout.span, programId: swapProgramId, }), ); const instruction = TokenSwap.createInitSwapInstruction( tokenSwapAccount, authority, tokenAccountA, tokenAccountB, poolToken, feeAccount, tokenAccountPool, tokenProgramId, swapProgramId, nonce, curveType, tradeFeeNumerator, tradeFeeDenominator, ownerTradeFeeNumerator, ownerTradeFeeDenominator, ownerWithdrawFeeNumerator, ownerWithdrawFeeDenominator, ); transaction.add(instruction); await sendAndConfirmTransaction( 'createAccount and InitializeSwap', connection, transaction, payer, tokenSwapAccount, ); return tokenSwap; } /** * Swap token A for token B * * @param userSource User's source token account * @param poolSource Pool's source token account * @param poolDestination Pool's destination token account * @param userDestination User's destination token account * @param amountIn Amount to transfer from source account * @param minimumAmountOut Minimum amount of tokens the user will receive */ async swap( userSource: PublicKey, poolSource: PublicKey, poolDestination: PublicKey, userDestination: PublicKey, amountIn: number | Numberu64, minimumAmountOut: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'swap', this.connection, new Transaction().add( TokenSwap.swapInstruction( this.tokenSwap, this.authority, userSource, poolSource, poolDestination, userDestination, this.poolToken, this.feeAccount, this.swapProgramId, this.tokenProgramId, amountIn, minimumAmountOut, ), ), this.payer, ); } static swapInstruction( tokenSwap: PublicKey, authority: PublicKey, userSource: PublicKey, poolSource: PublicKey, poolDestination: PublicKey, userDestination: PublicKey, poolMint: PublicKey, feeAccount: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, amountIn: number | Numberu64, minimumAmountOut: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), Layout.uint64('amountIn'), Layout.uint64('minimumAmountOut'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 1, // Swap instruction amountIn: new Numberu64(amountIn).toBuffer(), minimumAmountOut: new Numberu64(minimumAmountOut).toBuffer(), }, data, ); const keys = [ {pubkey: tokenSwap, isSigner: false, isWritable: false}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: userSource, isSigner: false, isWritable: true}, {pubkey: poolSource, isSigner: false, isWritable: true}, {pubkey: poolDestination, isSigner: false, isWritable: true}, {pubkey: userDestination, isSigner: false, isWritable: true}, {pubkey: poolMint, isSigner: false, isWritable: true}, {pubkey: feeAccount, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; return new TransactionInstruction({ keys, programId: swapProgramId, data, }); } /** * Deposit tokens into the pool * @param userAccountA User account for token A * @param userAccountB User account for token B * @param poolAccount User account for pool token * @param poolTokenAmount Amount of pool tokens to mint * @param maximumTokenA The maximum amount of token A to deposit * @param maximumTokenB The maximum amount of token B to deposit */ async deposit( userAccountA: PublicKey, userAccountB: PublicKey, poolAccount: PublicKey, poolTokenAmount: number | Numberu64, maximumTokenA: number | Numberu64, maximumTokenB: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'deposit', this.connection, new Transaction().add( TokenSwap.depositInstruction( this.tokenSwap, this.authority, userAccountA, userAccountB, this.tokenAccountA, this.tokenAccountB, this.poolToken, poolAccount, this.swapProgramId, this.tokenProgramId, poolTokenAmount, maximumTokenA, maximumTokenB, ), ), this.payer, ); } static depositInstruction( tokenSwap: PublicKey, authority: PublicKey, sourceA: PublicKey, sourceB: PublicKey, intoA: PublicKey, intoB: PublicKey, poolToken: PublicKey, poolAccount: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, poolTokenAmount: number | Numberu64, maximumTokenA: number | Numberu64, maximumTokenB: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), Layout.uint64('poolTokenAmount'), Layout.uint64('maximumTokenA'), Layout.uint64('maximumTokenB'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 2, // Deposit instruction poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(), maximumTokenA: new Numberu64(maximumTokenA).toBuffer(), maximumTokenB: new Numberu64(maximumTokenB).toBuffer(), }, data, ); const keys = [ {pubkey: tokenSwap, isSigner: false, isWritable: false}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: sourceA, isSigner: false, isWritable: true}, {pubkey: sourceB, isSigner: false, isWritable: true}, {pubkey: intoA, isSigner: false, isWritable: true}, {pubkey: intoB, isSigner: false, isWritable: true}, {pubkey: poolToken, isSigner: false, isWritable: true}, {pubkey: poolAccount, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; return new TransactionInstruction({ keys, programId: swapProgramId, data, }); } /** * Withdraw tokens from the pool * * @param userAccountA User account for token A * @param userAccountB User account for token B * @param poolAccount User account for pool token * @param poolTokenAmount Amount of pool tokens to burn * @param minimumTokenA The minimum amount of token A to withdraw * @param minimumTokenB The minimum amount of token B to withdraw */ async withdraw( userAccountA: PublicKey, userAccountB: PublicKey, poolAccount: PublicKey, poolTokenAmount: number | Numberu64, minimumTokenA: number | Numberu64, minimumTokenB: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'withdraw', this.connection, new Transaction().add( TokenSwap.withdrawInstruction( this.tokenSwap, this.authority, this.poolToken, this.feeAccount, poolAccount, this.tokenAccountA, this.tokenAccountB, userAccountA, userAccountB, this.swapProgramId, this.tokenProgramId, poolTokenAmount, minimumTokenA, minimumTokenB, ), ), this.payer, ); } static withdrawInstruction( tokenSwap: PublicKey, authority: PublicKey, poolMint: PublicKey, feeAccount: PublicKey, sourcePoolAccount: PublicKey, fromA: PublicKey, fromB: PublicKey, userAccountA: PublicKey, userAccountB: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, poolTokenAmount: number | Numberu64, minimumTokenA: number | Numberu64, minimumTokenB: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), Layout.uint64('poolTokenAmount'), Layout.uint64('minimumTokenA'), Layout.uint64('minimumTokenB'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 3, // Withdraw instruction poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(), minimumTokenA: new Numberu64(minimumTokenA).toBuffer(), minimumTokenB: new Numberu64(minimumTokenB).toBuffer(), }, data, ); const keys = [ {pubkey: tokenSwap, isSigner: false, isWritable: false}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: poolMint, isSigner: false, isWritable: true}, {pubkey: sourcePoolAccount, isSigner: false, isWritable: true}, {pubkey: fromA, isSigner: false, isWritable: true}, {pubkey: fromB, isSigner: false, isWritable: true}, {pubkey: userAccountA, isSigner: false, isWritable: true}, {pubkey: userAccountB, isSigner: false, isWritable: true}, {pubkey: feeAccount, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; return new TransactionInstruction({ keys, programId: swapProgramId, data, }); } }