|
@@ -0,0 +1,427 @@
|
|
|
+// 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 MARKET_MAKER = new Account();
|
|
|
+
|
|
|
+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,
|
|
|
+ });
|
|
|
+
|
|
|
+ const marketMakerOpenOrders = (
|
|
|
+ await OpenOrders.findForMarketAndOwner(
|
|
|
+ provider.connection,
|
|
|
+ MARKET_A_USDC.address,
|
|
|
+ marketMaker.account.publicKey,
|
|
|
+ DEX_PID
|
|
|
+ )
|
|
|
+ )[0].address;
|
|
|
+
|
|
|
+ return {
|
|
|
+ marketA: MARKET_A_USDC,
|
|
|
+ vaultSigner,
|
|
|
+ marketMaker,
|
|
|
+ marketMakerOpenOrders,
|
|
|
+ mintA: MINT_A,
|
|
|
+ usdc: USDC,
|
|
|
+ godA: GOD_A,
|
|
|
+ godUsdc: GOD_USDC,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+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");
|
|
|
+}
|
|
|
+
|
|
|
+function sleep(ms) {
|
|
|
+ return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ fundAccount,
|
|
|
+ initMarket,
|
|
|
+ setupMarket,
|
|
|
+ DEX_PID,
|
|
|
+ getVaultOwnerAndNonce,
|
|
|
+ sleep,
|
|
|
+};
|