solana-program-library/token-swap/js/cli/token-swap-test.js

518 lines
16 KiB
JavaScript

// @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<Connection> {
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<PublicKey> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}