anchor/tests/cfo/tests/utils/index.js

600 lines
16 KiB
JavaScript

// Boilerplate utils to bootstrap an orderbook for testing on a localnet.
// not super relevant to the point of the example, though may be useful to
// include into your own workspace for testing.
//
// TODO: Modernize all these apis. This is all quite clunky.
const Token = require("@solana/spl-token").Token;
const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
const TokenInstructions = require("@project-serum/serum").TokenInstructions;
const { Market, OpenOrders } = require("@project-serum/serum");
const DexInstructions = require("@project-serum/serum").DexInstructions;
const web3 = require("@coral-xyz/anchor").web3;
const Connection = web3.Connection;
const anchor = require("@coral-xyz/anchor");
const BN = anchor.BN;
const serumCmn = require("@project-serum/common");
const Account = web3.Account;
const Transaction = web3.Transaction;
const PublicKey = web3.PublicKey;
const SystemProgram = web3.SystemProgram;
const DEX_PID = new PublicKey("srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX");
const secret = JSON.parse(
require("fs").readFileSync("./scripts/market-maker.json")
);
const MARKET_MAKER = new Account(secret);
async function initMarket({ provider }) {
// Setup mints with initial tokens owned by the provider.
const decimals = 6;
const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
provider,
new BN("1000000000000000000"),
undefined,
decimals
);
const [MINT_B, GOD_B] = await serumCmn.createMintAndVault(
provider,
new BN("1000000000000000000"),
undefined,
decimals
);
const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
provider,
new BN("1000000000000000000"),
undefined,
decimals
);
// Create a funded account to act as market maker.
const amount = new BN("10000000000000").muln(10 ** decimals);
const marketMaker = await fundAccount({
provider,
mints: [
{ god: GOD_A, mint: MINT_A, amount, decimals },
{ god: GOD_B, mint: MINT_B, amount, decimals },
{ god: GOD_USDC, mint: USDC, amount, decimals },
],
});
// Setup A/USDC with resting orders.
const asks = [
[6.041, 7.8],
[6.051, 72.3],
[6.055, 5.4],
[6.067, 15.7],
[6.077, 390.0],
[6.09, 24.0],
[6.11, 36.3],
[6.133, 300.0],
[6.167, 687.8],
];
const bids = [
[6.004, 8.5],
[5.995, 12.9],
[5.987, 6.2],
[5.978, 15.3],
[5.965, 82.8],
[5.961, 25.4],
];
[MARKET_A_USDC, marketAVaultSigner] = await setupMarket({
baseMint: MINT_A,
quoteMint: USDC,
marketMaker: {
account: marketMaker.account,
baseToken: marketMaker.tokens[MINT_A.toString()],
quoteToken: marketMaker.tokens[USDC.toString()],
},
bids,
asks,
provider,
});
[MARKET_B_USDC, marketBVaultSigner] = await setupMarket({
baseMint: MINT_B,
quoteMint: USDC,
marketMaker: {
account: marketMaker.account,
baseToken: marketMaker.tokens[MINT_B.toString()],
quoteToken: marketMaker.tokens[USDC.toString()],
},
bids,
asks,
provider,
});
return {
marketA: MARKET_A_USDC,
marketAVaultSigner,
marketB: MARKET_B_USDC,
marketBVaultSigner,
marketMaker,
mintA: MINT_A,
mintB: MINT_B,
usdc: USDC,
godA: GOD_A,
godB: GOD_B,
godUsdc: GOD_USDC,
};
}
async function fundAccount({ provider, mints }) {
const marketMaker = {
tokens: {},
account: MARKET_MAKER,
};
// Transfer lamports to market maker.
await provider.sendAndConfirm(
(() => {
const tx = new Transaction();
tx.add(
SystemProgram.transfer({
fromPubkey: provider.wallet.publicKey,
toPubkey: MARKET_MAKER.publicKey,
lamports: 100000000000,
})
);
return tx;
})()
);
// Transfer SPL tokens to the market maker.
for (let k = 0; k < mints.length; k += 1) {
const { mint, god, amount, decimals } = mints[k];
let MINT_A = mint;
let GOD_A = god;
// Setup token accounts owned by the market maker.
const mintAClient = new Token(
provider.connection,
MINT_A,
TOKEN_PROGRAM_ID,
provider.wallet.payer // node only
);
const marketMakerTokenA = await mintAClient.createAccount(
MARKET_MAKER.publicKey
);
await provider.sendAndConfirm(
(() => {
const tx = new Transaction();
tx.add(
Token.createTransferCheckedInstruction(
TOKEN_PROGRAM_ID,
GOD_A,
MINT_A,
marketMakerTokenA,
provider.wallet.publicKey,
[],
amount,
decimals
)
);
return tx;
})()
);
marketMaker.tokens[mint.toString()] = marketMakerTokenA;
}
return marketMaker;
}
async function setupMarket({
provider,
marketMaker,
baseMint,
quoteMint,
bids,
asks,
}) {
const [marketAPublicKey, vaultOwner] = await listMarket({
connection: provider.connection,
wallet: provider.wallet,
baseMint: baseMint,
quoteMint: quoteMint,
baseLotSize: 100000,
quoteLotSize: 100,
dexProgramId: DEX_PID,
feeRateBps: 0,
});
const MARKET_A_USDC = await Market.load(
provider.connection,
marketAPublicKey,
{ commitment: "processed" },
DEX_PID
);
for (let k = 0; k < asks.length; k += 1) {
let ask = asks[k];
const { transaction, signers } =
await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
owner: marketMaker.account,
payer: marketMaker.baseToken,
side: "sell",
price: ask[0],
size: ask[1],
orderType: "postOnly",
clientId: undefined,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
});
await provider.sendAndConfirm(
transaction,
signers.concat(marketMaker.account)
);
}
for (let k = 0; k < bids.length; k += 1) {
let bid = bids[k];
const { transaction, signers } =
await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
owner: marketMaker.account,
payer: marketMaker.quoteToken,
side: "buy",
price: bid[0],
size: bid[1],
orderType: "postOnly",
clientId: undefined,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
});
await provider.sendAndConfirm(
transaction,
signers.concat(marketMaker.account)
);
}
return [MARKET_A_USDC, vaultOwner];
}
async function listMarket({
connection,
wallet,
baseMint,
quoteMint,
baseLotSize,
quoteLotSize,
dexProgramId,
feeRateBps,
}) {
const market = new Account();
const requestQueue = new Account();
const eventQueue = new Account();
const bids = new Account();
const asks = new Account();
const baseVault = new Account();
const quoteVault = new Account();
const quoteDustThreshold = new BN(100);
const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
market.publicKey,
dexProgramId
);
const tx1 = new Transaction();
tx1.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: baseVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: quoteVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
TokenInstructions.initializeAccount({
account: baseVault.publicKey,
mint: baseMint,
owner: vaultOwner,
}),
TokenInstructions.initializeAccount({
account: quoteVault.publicKey,
mint: quoteMint,
owner: vaultOwner,
})
);
const tx2 = new Transaction();
tx2.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: market.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
Market.getLayout(dexProgramId).span
),
space: Market.getLayout(dexProgramId).span,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: requestQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
space: 5120 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: eventQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
space: 262144 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: bids.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgramId,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: asks.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgramId,
}),
DexInstructions.initializeMarket({
market: market.publicKey,
requestQueue: requestQueue.publicKey,
eventQueue: eventQueue.publicKey,
bids: bids.publicKey,
asks: asks.publicKey,
baseVault: baseVault.publicKey,
quoteVault: quoteVault.publicKey,
baseMint,
quoteMint,
baseLotSize: new BN(baseLotSize),
quoteLotSize: new BN(quoteLotSize),
feeRateBps,
vaultSignerNonce,
quoteDustThreshold,
programId: dexProgramId,
})
);
const signedTransactions = await signTransactions({
transactionsAndSigners: [
{ transaction: tx1, signers: [baseVault, quoteVault] },
{
transaction: tx2,
signers: [market, requestQueue, eventQueue, bids, asks],
},
],
wallet,
connection,
});
for (let signedTransaction of signedTransactions) {
await sendAndConfirmRawTransaction(
connection,
signedTransaction.serialize()
);
}
const acc = await connection.getAccountInfo(market.publicKey);
return [market.publicKey, vaultOwner];
}
async function signTransactions({
transactionsAndSigners,
wallet,
connection,
}) {
const blockhash = (await connection.getLatestBlockhash("finalized"))
.blockhash;
transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
transaction.recentBlockhash = blockhash;
transaction.setSigners(
wallet.publicKey,
...signers.map((s) => s.publicKey)
);
if (signers?.length > 0) {
transaction.partialSign(...signers);
}
});
return await wallet.signAllTransactions(
transactionsAndSigners.map(({ transaction }) => transaction)
);
}
async function sendAndConfirmRawTransaction(
connection,
raw,
commitment = "processed"
) {
let tx = await connection.sendRawTransaction(raw, {
skipPreflight: true,
});
return await connection.confirmTransaction(tx, commitment);
}
async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
const nonce = new BN(0);
while (nonce.toNumber() < 255) {
try {
const vaultOwner = await PublicKey.createProgramAddress(
[marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
dexProgramId
);
return [vaultOwner, nonce];
} catch (e) {
nonce.iaddn(1);
}
}
throw new Error("Unable to find nonce");
}
async function runTradeBot(market, provider, iterations = undefined) {
let marketClient = await Market.load(
provider.connection,
market,
{ commitment: "processed" },
DEX_PID
);
const baseTokenUser1 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
MARKET_MAKER.publicKey,
marketClient.baseMintAddress
)
)[0].pubkey;
const quoteTokenUser1 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
MARKET_MAKER.publicKey,
marketClient.quoteMintAddress
)
)[0].pubkey;
const baseTokenUser2 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
provider.wallet.publicKey,
marketClient.baseMintAddress
)
)[0].pubkey;
const quoteTokenUser2 = (
await marketClient.getTokenAccountsByOwnerForMint(
provider.connection,
provider.wallet.publicKey,
marketClient.quoteMintAddress
)
)[0].pubkey;
const makerOpenOrdersUser1 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
MARKET_MAKER.publicKey,
DEX_PID
)
)[0];
makerOpenOrdersUser2 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
provider.wallet.publicKey,
DEX_PID
)
)[0];
const price = 6.041;
const size = 700000.8;
let maker = MARKET_MAKER;
let taker = provider.wallet.payer;
let baseToken = baseTokenUser1;
let quoteToken = quoteTokenUser2;
let makerOpenOrders = makerOpenOrdersUser1;
let k = 1;
while (true) {
if (iterations && k > iterations) {
break;
}
const clientId = new anchor.BN(k);
if (k % 5 === 0) {
if (maker.publicKey.equals(MARKET_MAKER.publicKey)) {
maker = provider.wallet.payer;
makerOpenOrders = makerOpenOrdersUser2;
taker = MARKET_MAKER;
baseToken = baseTokenUser2;
quoteToken = quoteTokenUser1;
} else {
maker = MARKET_MAKER;
makerOpenOrders = makerOpenOrdersUser1;
taker = provider.wallet.payer;
baseToken = baseTokenUser1;
quoteToken = quoteTokenUser2;
}
}
// Post ask.
const { transaction: tx_ask, signers: sigs_ask } =
await marketClient.makePlaceOrderTransaction(provider.connection, {
owner: maker,
payer: baseToken,
side: "sell",
price,
size,
orderType: "postOnly",
clientId,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
});
let txSig = await provider.sendAndConfirm(tx_ask, sigs_ask.concat(maker));
console.log("Ask", txSig);
// Take.
const { transaction: tx_bid, signers: sigs_bid } =
await marketClient.makePlaceOrderTransaction(provider.connection, {
owner: taker,
payer: quoteToken,
side: "buy",
price,
size,
orderType: "ioc",
clientId: undefined,
openOrdersAddressKey: undefined,
openOrdersAccount: undefined,
feeDiscountPubkey: null,
selfTradeBehavior: "abortTransaction",
});
txSig = await provider.sendAndConfirm(tx_bid, sigs_bid.concat(taker));
console.log("Bid", txSig);
await sleep(1000);
// Cancel anything remaining.
try {
txSig = await marketClient.cancelOrderByClientId(
provider.connection,
maker,
makerOpenOrders.address,
clientId
);
console.log("Cancelled the rest", txSig);
await sleep(1000);
} catch (e) {
console.log("Unable to cancel order", e);
}
k += 1;
// If the open orders account wasn't previously initialized, it is now.
if (makerOpenOrdersUser2 === undefined) {
makerOpenOrdersUser2 = (
await OpenOrders.findForMarketAndOwner(
provider.connection,
market,
provider.wallet.publicKey,
DEX_PID
)
)[0];
}
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {
fundAccount,
initMarket,
setupMarket,
DEX_PID,
getVaultOwnerAndNonce,
runTradeBot,
};