// @flow import fs from 'mz/fs'; import { Account, Connection, BpfLoader, PublicKey, SystemProgram, Transaction, BPF_LOADER_PROGRAM_ID, } from '@solana/web3.js'; import {AccountLayout, Token} from '../../../token/js/client/token'; import {TokenSwap, CurveType} from '../client/token-swap'; import {sendAndConfirmTransaction} from '../client/util/send-and-confirm-transaction'; import {Store} from '../client/util/store'; import {newAccountWithLamports} from '../client/util/new-account-with-lamports'; import {url} from '../url'; import {sleep} from '../client/util/sleep'; // The following globals are created by `createTokenSwap` and used by subsequent tests // Token swap let tokenSwap: TokenSwap; // authority of the token and accounts let authority: PublicKey; // nonce used to generate the authority public key let nonce: number; // owner of the user accounts let owner: Account; // Token pool let tokenPool: Token; let tokenAccountPool: PublicKey; let feeAccount: PublicKey; // Tokens swapped let mintA: Token; let mintB: Token; let tokenAccountA: PublicKey; let tokenAccountB: PublicKey; // Hard-coded fee address, for testing production mode const SWAP_PROGRAM_OWNER_FEE_ADDRESS = process.env.SWAP_PROGRAM_OWNER_FEE_ADDRESS; // Pool fees const TRADING_FEE_NUMERATOR = 25; const TRADING_FEE_DENOMINATOR = 10000; const OWNER_TRADING_FEE_NUMERATOR = 5; const OWNER_TRADING_FEE_DENOMINATOR = 10000; const OWNER_WITHDRAW_FEE_NUMERATOR = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 0 : 1; const OWNER_WITHDRAW_FEE_DENOMINATOR = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 0 : 6; const HOST_FEE_NUMERATOR = 20; const HOST_FEE_DENOMINATOR = 100; // curve type used to calculate swaps and deposits const CURVE_TYPE = CurveType.ConstantProduct; // Initial amount in each swap token let currentSwapTokenA = 1000000; let currentSwapTokenB = 1000000; let currentFeeAmount = 0; // Swap instruction constants // Because there is no withdraw fee in the production version, these numbers // need to get slightly tweaked in the two cases. const SWAP_AMOUNT_IN = 100000; const SWAP_AMOUNT_OUT = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 90662 : 90675; const SWAP_FEE = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 22272 : 22276; const HOST_SWAP_FEE = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? Math.floor((SWAP_FEE * HOST_FEE_NUMERATOR) / HOST_FEE_DENOMINATOR) : 0; const OWNER_SWAP_FEE = SWAP_FEE - HOST_SWAP_FEE; // Pool token amount minted on init const DEFAULT_POOL_TOKEN_AMOUNT = 1000000000; // Pool token amount to withdraw / deposit const POOL_TOKEN_AMOUNT = 10000000; function assert(condition, message) { if (!condition) { console.log(Error().stack + ':token-test.js'); throw message || 'Assertion failed'; } } let connection; async function getConnection(): Promise { if (connection) return connection; connection = new Connection(url, 'recent'); const version = await connection.getVersion(); console.log('Connection to cluster established:', url, version); return connection; } async function loadProgram( connection: Connection, path: string, ): Promise { const NUM_RETRIES = 500; /* allow some number of retries */ const data = await fs.readFile(path); const {feeCalculator} = await connection.getRecentBlockhash(); const balanceNeeded = feeCalculator.lamportsPerSignature * (BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES) + (await connection.getMinimumBalanceForRentExemption(data.length)); const from = await newAccountWithLamports(connection, balanceNeeded); const program_account = new Account(); console.log('Loading program:', path); await BpfLoader.load( connection, from, program_account, data, BPF_LOADER_PROGRAM_ID, ); return program_account.publicKey; } async function GetPrograms( connection: Connection, ): Promise<[PublicKey, PublicKey]> { const store = new Store(); let tokenProgramId = null; let tokenSwapProgramId = null; try { const config = await store.load('config.json'); console.log('Using pre-loaded Token and Token-swap programs'); console.log( ' Note: To reload programs remove client/util/store/config.json', ); tokenProgramId = new PublicKey(config.tokenProgramId); tokenSwapProgramId = new PublicKey(config.tokenSwapProgramId); } catch (err) { tokenProgramId = await loadProgram( connection, '../../target/bpfel-unknown-unknown/release/spl_token.so', ); tokenSwapProgramId = await loadProgram( connection, '../../target/bpfel-unknown-unknown/release/spl_token_swap.so', ); await store.save('config.json', { tokenProgramId: tokenProgramId.toString(), tokenSwapProgramId: tokenSwapProgramId.toString(), }); } return [tokenProgramId, tokenSwapProgramId]; } export async function loadPrograms(): Promise { const connection = await getConnection(); const [tokenProgramId, tokenSwapProgramId] = await GetPrograms(connection); console.log('Token Program ID', tokenProgramId.toString()); console.log('Token-swap Program ID', tokenSwapProgramId.toString()); } export async function createTokenSwap(): Promise { const connection = await getConnection(); const [tokenProgramId, tokenSwapProgramId] = await GetPrograms(connection); const payer = await newAccountWithLamports(connection, 1000000000); owner = await newAccountWithLamports(connection, 1000000000); const tokenSwapAccount = new Account(); [authority, nonce] = await PublicKey.findProgramAddress( [tokenSwapAccount.publicKey.toBuffer()], tokenSwapProgramId, ); console.log('creating pool mint'); tokenPool = await Token.createMint( connection, payer, authority, null, 2, tokenProgramId, ); console.log('creating pool account'); tokenAccountPool = await tokenPool.createAccount(owner.publicKey); const ownerKey = SWAP_PROGRAM_OWNER_FEE_ADDRESS || owner.publicKey.toString(); feeAccount = await tokenPool.createAccount(new PublicKey(ownerKey)); console.log('creating token A'); mintA = await Token.createMint( connection, payer, owner.publicKey, null, 2, tokenProgramId, ); console.log('creating token A account'); tokenAccountA = await mintA.createAccount(authority); console.log('minting token A to swap'); await mintA.mintTo(tokenAccountA, owner, [], currentSwapTokenA); console.log('creating token B'); mintB = await Token.createMint( connection, payer, owner.publicKey, null, 2, tokenProgramId, ); console.log('creating token B account'); tokenAccountB = await mintB.createAccount(authority); console.log('minting token B to swap'); await mintB.mintTo(tokenAccountB, owner, [], currentSwapTokenB); console.log('creating token swap'); const swapPayer = await newAccountWithLamports(connection, 10000000000); tokenSwap = await TokenSwap.createTokenSwap( connection, swapPayer, tokenSwapAccount, authority, tokenAccountA, tokenAccountB, tokenPool.publicKey, mintA.publicKey, mintB.publicKey, feeAccount, tokenAccountPool, tokenSwapProgramId, tokenProgramId, nonce, CURVE_TYPE, TRADING_FEE_NUMERATOR, TRADING_FEE_DENOMINATOR, OWNER_TRADING_FEE_NUMERATOR, OWNER_TRADING_FEE_DENOMINATOR, OWNER_WITHDRAW_FEE_NUMERATOR, OWNER_WITHDRAW_FEE_DENOMINATOR, HOST_FEE_NUMERATOR, HOST_FEE_DENOMINATOR, ); console.log('loading token swap'); const fetchedTokenSwap = await TokenSwap.loadTokenSwap( connection, tokenSwapAccount.publicKey, tokenSwapProgramId, swapPayer, ); assert(fetchedTokenSwap.tokenProgramId.equals(tokenProgramId)); assert(fetchedTokenSwap.tokenAccountA.equals(tokenAccountA)); assert(fetchedTokenSwap.tokenAccountB.equals(tokenAccountB)); assert(fetchedTokenSwap.mintA.equals(mintA.publicKey)); assert(fetchedTokenSwap.mintB.equals(mintB.publicKey)); assert(fetchedTokenSwap.poolToken.equals(tokenPool.publicKey)); assert(fetchedTokenSwap.feeAccount.equals(feeAccount)); assert(CURVE_TYPE == fetchedTokenSwap.curveType); assert( TRADING_FEE_NUMERATOR == fetchedTokenSwap.tradeFeeNumerator.toNumber(), ); assert( TRADING_FEE_DENOMINATOR == fetchedTokenSwap.tradeFeeDenominator.toNumber(), ); assert( OWNER_TRADING_FEE_NUMERATOR == fetchedTokenSwap.ownerTradeFeeNumerator.toNumber(), ); assert( OWNER_TRADING_FEE_DENOMINATOR == fetchedTokenSwap.ownerTradeFeeDenominator.toNumber(), ); assert( OWNER_WITHDRAW_FEE_NUMERATOR == fetchedTokenSwap.ownerWithdrawFeeNumerator.toNumber(), ); assert( OWNER_WITHDRAW_FEE_DENOMINATOR == fetchedTokenSwap.ownerWithdrawFeeDenominator.toNumber(), ); assert(HOST_FEE_NUMERATOR == fetchedTokenSwap.hostFeeNumerator.toNumber()); assert( HOST_FEE_DENOMINATOR == fetchedTokenSwap.hostFeeDenominator.toNumber(), ); } export async function deposit(): Promise { const poolMintInfo = await tokenPool.getMintInfo(); const supply = poolMintInfo.supply.toNumber(); const swapTokenA = await mintA.getAccountInfo(tokenAccountA); const tokenA = (swapTokenA.amount.toNumber() * POOL_TOKEN_AMOUNT) / supply; const swapTokenB = await mintB.getAccountInfo(tokenAccountB); const tokenB = (swapTokenB.amount.toNumber() * POOL_TOKEN_AMOUNT) / supply; console.log('Creating depositor token a account'); const userAccountA = await mintA.createAccount(owner.publicKey); await mintA.mintTo(userAccountA, owner, [], tokenA); await mintA.approve(userAccountA, authority, owner, [], tokenA); console.log('Creating depositor token b account'); const userAccountB = await mintB.createAccount(owner.publicKey); await mintB.mintTo(userAccountB, owner, [], tokenB); await mintB.approve(userAccountB, authority, owner, [], tokenB); console.log('Creating depositor pool token account'); const newAccountPool = await tokenPool.createAccount(owner.publicKey); console.log('Depositing into swap'); await tokenSwap.deposit( userAccountA, userAccountB, newAccountPool, POOL_TOKEN_AMOUNT, tokenA, tokenB, ); let info; info = await mintA.getAccountInfo(userAccountA); assert(info.amount.toNumber() == 0); info = await mintB.getAccountInfo(userAccountB); assert(info.amount.toNumber() == 0); info = await mintA.getAccountInfo(tokenAccountA); assert(info.amount.toNumber() == currentSwapTokenA + tokenA); currentSwapTokenA += tokenA; info = await mintB.getAccountInfo(tokenAccountB); assert(info.amount.toNumber() == currentSwapTokenB + tokenB); currentSwapTokenB += tokenB; info = await tokenPool.getAccountInfo(newAccountPool); assert(info.amount.toNumber() == POOL_TOKEN_AMOUNT); } export async function withdraw(): Promise { const poolMintInfo = await tokenPool.getMintInfo(); const supply = poolMintInfo.supply.toNumber(); let swapTokenA = await mintA.getAccountInfo(tokenAccountA); let swapTokenB = await mintB.getAccountInfo(tokenAccountB); let feeAmount = 0; if (OWNER_WITHDRAW_FEE_NUMERATOR !== 0) { feeAmount = Math.floor( (POOL_TOKEN_AMOUNT * OWNER_WITHDRAW_FEE_NUMERATOR) / OWNER_WITHDRAW_FEE_DENOMINATOR, ); } const poolTokenAmount = POOL_TOKEN_AMOUNT - feeAmount; const tokenA = Math.floor( (swapTokenA.amount.toNumber() * poolTokenAmount) / supply, ); const tokenB = Math.floor( (swapTokenB.amount.toNumber() * poolTokenAmount) / supply, ); console.log('Creating withdraw token A account'); let userAccountA = await mintA.createAccount(owner.publicKey); console.log('Creating withdraw token B account'); let userAccountB = await mintB.createAccount(owner.publicKey); console.log('Approving withdrawal from pool account'); await tokenPool.approve( tokenAccountPool, authority, owner, [], POOL_TOKEN_AMOUNT, ); console.log('Withdrawing pool tokens for A and B tokens'); await tokenSwap.withdraw( userAccountA, userAccountB, tokenAccountPool, POOL_TOKEN_AMOUNT, tokenA, tokenB, ); //const poolMintInfo = await tokenPool.getMintInfo(); swapTokenA = await mintA.getAccountInfo(tokenAccountA); swapTokenB = await mintB.getAccountInfo(tokenAccountB); let info = await tokenPool.getAccountInfo(tokenAccountPool); assert( info.amount.toNumber() == DEFAULT_POOL_TOKEN_AMOUNT - POOL_TOKEN_AMOUNT, ); assert(swapTokenA.amount.toNumber() == currentSwapTokenA - tokenA); currentSwapTokenA -= tokenA; assert(swapTokenB.amount.toNumber() == currentSwapTokenB - tokenB); currentSwapTokenB -= tokenB; info = await mintA.getAccountInfo(userAccountA); assert(info.amount.toNumber() == tokenA); info = await mintB.getAccountInfo(userAccountB); assert(info.amount.toNumber() == tokenB); info = await tokenPool.getAccountInfo(feeAccount); assert(info.amount.toNumber() == feeAmount); currentFeeAmount = feeAmount; } export async function createAccountAndSwapAtomic(): Promise { console.log('Creating swap token a account'); let userAccountA = await mintA.createAccount(owner.publicKey); await mintA.mintTo(userAccountA, owner, [], SWAP_AMOUNT_IN); const balanceNeeded = await Token.getMinBalanceRentForExemptAccount( connection, ); const newAccount = new Account(); const transaction = new Transaction(); transaction.add( SystemProgram.createAccount({ fromPubkey: owner.publicKey, newAccountPubkey: newAccount.publicKey, lamports: balanceNeeded, space: AccountLayout.span, programId: mintB.programId, }), ); transaction.add( Token.createInitAccountInstruction( mintB.programId, mintB.publicKey, newAccount.publicKey, owner.publicKey, ), ); transaction.add( Token.createApproveInstruction( mintA.programId, userAccountA, authority, owner.publicKey, [owner], SWAP_AMOUNT_IN, ), ); transaction.add( TokenSwap.swapInstruction( tokenSwap.tokenSwap, tokenSwap.authority, userAccountA, tokenSwap.tokenAccountA, tokenSwap.tokenAccountB, newAccount.publicKey, tokenSwap.poolToken, tokenSwap.feeAccount, null, tokenSwap.swapProgramId, tokenSwap.tokenProgramId, SWAP_AMOUNT_IN, 0, ), ); // Send the instructions console.log('sending big instruction'); await sendAndConfirmTransaction( 'create account, approve transfer, swap', connection, transaction, owner, newAccount, ); } export async function swap(): Promise { console.log('Creating swap token a account'); let userAccountA = await mintA.createAccount(owner.publicKey); await mintA.mintTo(userAccountA, owner, [], SWAP_AMOUNT_IN); await mintA.approve(userAccountA, authority, owner, [], SWAP_AMOUNT_IN); console.log('Creating swap token b account'); let userAccountB = await mintB.createAccount(owner.publicKey); let poolAccount = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? await tokenPool.createAccount(owner.publicKey) : null; console.log('Swapping'); await tokenSwap.swap( userAccountA, tokenAccountA, tokenAccountB, userAccountB, poolAccount, SWAP_AMOUNT_IN, SWAP_AMOUNT_OUT, ); await sleep(500); let info; info = await mintA.getAccountInfo(userAccountA); assert(info.amount.toNumber() == 0); info = await mintB.getAccountInfo(userAccountB); assert(info.amount.toNumber() == SWAP_AMOUNT_OUT); info = await mintA.getAccountInfo(tokenAccountA); assert(info.amount.toNumber() == currentSwapTokenA + SWAP_AMOUNT_IN); currentSwapTokenA -= SWAP_AMOUNT_IN; info = await mintB.getAccountInfo(tokenAccountB); assert(info.amount.toNumber() == currentSwapTokenB - SWAP_AMOUNT_OUT); currentSwapTokenB -= SWAP_AMOUNT_OUT; info = await tokenPool.getAccountInfo(tokenAccountPool); assert( info.amount.toNumber() == DEFAULT_POOL_TOKEN_AMOUNT - POOL_TOKEN_AMOUNT, ); info = await tokenPool.getAccountInfo(feeAccount); assert(info.amount.toNumber() == currentFeeAmount + OWNER_SWAP_FEE); if (poolAccount != null) { info = await tokenPool.getAccountInfo(poolAccount); assert(info.amount.toNumber() == HOST_SWAP_FEE); } }