123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- // 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("@project-serum/anchor").web3;
- const Connection = web3.Connection;
- const anchor = require("@project-serum/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("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
- 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 [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_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, vaultSigner] = 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,
- });
- return {
- marketA: MARKET_A_USDC,
- vaultSigner,
- marketMaker,
- mintA: MINT_A,
- usdc: USDC,
- godA: GOD_A,
- godUsdc: GOD_USDC,
- };
- }
- // Creates everything needed for an orderbook to be running
- //
- // * Mints for both the base and quote currencies.
- // * Lists the market.
- // * Provides resting orders on the market.
- //
- // Returns a client that can be used to interact with the market
- // (and some other data, e.g., the mints and market maker account).
- async function initOrderbook({ provider, bids, asks }) {
- if (!bids || !asks) {
- 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],
- ];
- 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],
- ];
- }
- // Create base and quote currency mints.
- const decimals = 6;
- const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
- provider,
- new BN(1000000000000000),
- undefined,
- decimals
- );
- const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
- provider,
- new BN(1000000000000000),
- undefined,
- decimals
- );
- // Create a funded account to act as market maker.
- const amount = 100000 * 10 ** decimals;
- const marketMaker = await fundAccount({
- provider,
- mints: [
- { god: GOD_A, mint: MINT_A, amount, decimals },
- { god: GOD_USDC, mint: USDC, amount, decimals },
- ],
- });
- [marketClient, vaultSigner] = 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,
- });
- return {
- marketClient,
- baseMint: MINT_A,
- quoteMint: USDC,
- marketMaker,
- vaultSigner,
- };
- }
- async function fundAccount({ provider, mints }) {
- const marketMaker = {
- tokens: {},
- account: MARKET_MAKER,
- };
- // Transfer lamports to market maker.
- await provider.send(
- (() => {
- 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.send(
- (() => {
- 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: "recent" },
- 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.send(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.send(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.getRecentBlockhash("max")).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 = "recent"
- ) {
- 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: "recent" },
- 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.send(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.send(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,
- initOrderbook,
- setupMarket,
- DEX_PID,
- getVaultOwnerAndNonce,
- runTradeBot,
- };
|