index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. // Boilerplate utils to bootstrap an orderbook for testing on a localnet.
  2. // not super relevant to the point of the example, though may be useful to
  3. // include into your own workspace for testing.
  4. //
  5. // TODO: Modernize all these apis. This is all quite clunky.
  6. const Token = require("@solana/spl-token").Token;
  7. const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
  8. const TokenInstructions = require("@project-serum/serum").TokenInstructions;
  9. const { Market, OpenOrders } = require("@project-serum/serum");
  10. const DexInstructions = require("@project-serum/serum").DexInstructions;
  11. const web3 = require("@project-serum/anchor").web3;
  12. const Connection = web3.Connection;
  13. const anchor = require("@project-serum/anchor");
  14. const BN = anchor.BN;
  15. const serumCmn = require("@project-serum/common");
  16. const Account = web3.Account;
  17. const Transaction = web3.Transaction;
  18. const PublicKey = web3.PublicKey;
  19. const SystemProgram = web3.SystemProgram;
  20. const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
  21. const secret = JSON.parse(
  22. require("fs").readFileSync("./scripts/market-maker.json")
  23. );
  24. const MARKET_MAKER = new Account(secret);
  25. async function initMarket({ provider }) {
  26. // Setup mints with initial tokens owned by the provider.
  27. const decimals = 6;
  28. const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
  29. provider,
  30. new BN("1000000000000000000"),
  31. undefined,
  32. decimals
  33. );
  34. const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
  35. provider,
  36. new BN("1000000000000000000"),
  37. undefined,
  38. decimals
  39. );
  40. // Create a funded account to act as market maker.
  41. const amount = new BN("10000000000000").muln(10 ** decimals);
  42. const marketMaker = await fundAccount({
  43. provider,
  44. mints: [
  45. { god: GOD_A, mint: MINT_A, amount, decimals },
  46. { god: GOD_USDC, mint: USDC, amount, decimals },
  47. ],
  48. });
  49. // Setup A/USDC with resting orders.
  50. const asks = [
  51. [6.041, 7.8],
  52. [6.051, 72.3],
  53. [6.055, 5.4],
  54. [6.067, 15.7],
  55. [6.077, 390.0],
  56. [6.09, 24.0],
  57. [6.11, 36.3],
  58. [6.133, 300.0],
  59. [6.167, 687.8],
  60. ];
  61. const bids = [
  62. [6.004, 8.5],
  63. [5.995, 12.9],
  64. [5.987, 6.2],
  65. [5.978, 15.3],
  66. [5.965, 82.8],
  67. [5.961, 25.4],
  68. ];
  69. [MARKET_A_USDC, vaultSigner] = await setupMarket({
  70. baseMint: MINT_A,
  71. quoteMint: USDC,
  72. marketMaker: {
  73. account: marketMaker.account,
  74. baseToken: marketMaker.tokens[MINT_A.toString()],
  75. quoteToken: marketMaker.tokens[USDC.toString()],
  76. },
  77. bids,
  78. asks,
  79. provider,
  80. });
  81. return {
  82. marketA: MARKET_A_USDC,
  83. vaultSigner,
  84. marketMaker,
  85. mintA: MINT_A,
  86. usdc: USDC,
  87. godA: GOD_A,
  88. godUsdc: GOD_USDC,
  89. };
  90. }
  91. // Creates everything needed for an orderbook to be running
  92. //
  93. // * Mints for both the base and quote currencies.
  94. // * Lists the market.
  95. // * Provides resting orders on the market.
  96. //
  97. // Returns a client that can be used to interact with the market
  98. // (and some other data, e.g., the mints and market maker account).
  99. async function initOrderbook({ provider, bids, asks }) {
  100. if (!bids || !asks) {
  101. asks = [
  102. [6.041, 7.8],
  103. [6.051, 72.3],
  104. [6.055, 5.4],
  105. [6.067, 15.7],
  106. [6.077, 390.0],
  107. [6.09, 24.0],
  108. [6.11, 36.3],
  109. [6.133, 300.0],
  110. [6.167, 687.8],
  111. ];
  112. bids = [
  113. [6.004, 8.5],
  114. [5.995, 12.9],
  115. [5.987, 6.2],
  116. [5.978, 15.3],
  117. [5.965, 82.8],
  118. [5.961, 25.4],
  119. ];
  120. }
  121. // Create base and quote currency mints.
  122. const decimals = 6;
  123. const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
  124. provider,
  125. new BN(1000000000000000),
  126. undefined,
  127. decimals
  128. );
  129. const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
  130. provider,
  131. new BN(1000000000000000),
  132. undefined,
  133. decimals
  134. );
  135. // Create a funded account to act as market maker.
  136. const amount = 100000 * 10 ** decimals;
  137. const marketMaker = await fundAccount({
  138. provider,
  139. mints: [
  140. { god: GOD_A, mint: MINT_A, amount, decimals },
  141. { god: GOD_USDC, mint: USDC, amount, decimals },
  142. ],
  143. });
  144. [marketClient, vaultSigner] = await setupMarket({
  145. baseMint: MINT_A,
  146. quoteMint: USDC,
  147. marketMaker: {
  148. account: marketMaker.account,
  149. baseToken: marketMaker.tokens[MINT_A.toString()],
  150. quoteToken: marketMaker.tokens[USDC.toString()],
  151. },
  152. bids,
  153. asks,
  154. provider,
  155. });
  156. return {
  157. marketClient,
  158. baseMint: MINT_A,
  159. quoteMint: USDC,
  160. marketMaker,
  161. vaultSigner,
  162. };
  163. }
  164. async function fundAccount({ provider, mints }) {
  165. const marketMaker = {
  166. tokens: {},
  167. account: MARKET_MAKER,
  168. };
  169. // Transfer lamports to market maker.
  170. await provider.send(
  171. (() => {
  172. const tx = new Transaction();
  173. tx.add(
  174. SystemProgram.transfer({
  175. fromPubkey: provider.wallet.publicKey,
  176. toPubkey: MARKET_MAKER.publicKey,
  177. lamports: 100000000000,
  178. })
  179. );
  180. return tx;
  181. })()
  182. );
  183. // Transfer SPL tokens to the market maker.
  184. for (let k = 0; k < mints.length; k += 1) {
  185. const { mint, god, amount, decimals } = mints[k];
  186. let MINT_A = mint;
  187. let GOD_A = god;
  188. // Setup token accounts owned by the market maker.
  189. const mintAClient = new Token(
  190. provider.connection,
  191. MINT_A,
  192. TOKEN_PROGRAM_ID,
  193. provider.wallet.payer // node only
  194. );
  195. const marketMakerTokenA = await mintAClient.createAccount(
  196. MARKET_MAKER.publicKey
  197. );
  198. await provider.send(
  199. (() => {
  200. const tx = new Transaction();
  201. tx.add(
  202. Token.createTransferCheckedInstruction(
  203. TOKEN_PROGRAM_ID,
  204. GOD_A,
  205. MINT_A,
  206. marketMakerTokenA,
  207. provider.wallet.publicKey,
  208. [],
  209. amount,
  210. decimals
  211. )
  212. );
  213. return tx;
  214. })()
  215. );
  216. marketMaker.tokens[mint.toString()] = marketMakerTokenA;
  217. }
  218. return marketMaker;
  219. }
  220. async function setupMarket({
  221. provider,
  222. marketMaker,
  223. baseMint,
  224. quoteMint,
  225. bids,
  226. asks,
  227. }) {
  228. const [marketAPublicKey, vaultOwner] = await listMarket({
  229. connection: provider.connection,
  230. wallet: provider.wallet,
  231. baseMint: baseMint,
  232. quoteMint: quoteMint,
  233. baseLotSize: 100000,
  234. quoteLotSize: 100,
  235. dexProgramId: DEX_PID,
  236. feeRateBps: 0,
  237. });
  238. const MARKET_A_USDC = await Market.load(
  239. provider.connection,
  240. marketAPublicKey,
  241. { commitment: "recent" },
  242. DEX_PID
  243. );
  244. for (let k = 0; k < asks.length; k += 1) {
  245. let ask = asks[k];
  246. const { transaction, signers } =
  247. await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  248. owner: marketMaker.account,
  249. payer: marketMaker.baseToken,
  250. side: "sell",
  251. price: ask[0],
  252. size: ask[1],
  253. orderType: "postOnly",
  254. clientId: undefined,
  255. openOrdersAddressKey: undefined,
  256. openOrdersAccount: undefined,
  257. feeDiscountPubkey: null,
  258. selfTradeBehavior: "abortTransaction",
  259. });
  260. await provider.send(transaction, signers.concat(marketMaker.account));
  261. }
  262. for (let k = 0; k < bids.length; k += 1) {
  263. let bid = bids[k];
  264. const { transaction, signers } =
  265. await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  266. owner: marketMaker.account,
  267. payer: marketMaker.quoteToken,
  268. side: "buy",
  269. price: bid[0],
  270. size: bid[1],
  271. orderType: "postOnly",
  272. clientId: undefined,
  273. openOrdersAddressKey: undefined,
  274. openOrdersAccount: undefined,
  275. feeDiscountPubkey: null,
  276. selfTradeBehavior: "abortTransaction",
  277. });
  278. await provider.send(transaction, signers.concat(marketMaker.account));
  279. }
  280. return [MARKET_A_USDC, vaultOwner];
  281. }
  282. async function listMarket({
  283. connection,
  284. wallet,
  285. baseMint,
  286. quoteMint,
  287. baseLotSize,
  288. quoteLotSize,
  289. dexProgramId,
  290. feeRateBps,
  291. }) {
  292. const market = new Account();
  293. const requestQueue = new Account();
  294. const eventQueue = new Account();
  295. const bids = new Account();
  296. const asks = new Account();
  297. const baseVault = new Account();
  298. const quoteVault = new Account();
  299. const quoteDustThreshold = new BN(100);
  300. const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
  301. market.publicKey,
  302. dexProgramId
  303. );
  304. const tx1 = new Transaction();
  305. tx1.add(
  306. SystemProgram.createAccount({
  307. fromPubkey: wallet.publicKey,
  308. newAccountPubkey: baseVault.publicKey,
  309. lamports: await connection.getMinimumBalanceForRentExemption(165),
  310. space: 165,
  311. programId: TOKEN_PROGRAM_ID,
  312. }),
  313. SystemProgram.createAccount({
  314. fromPubkey: wallet.publicKey,
  315. newAccountPubkey: quoteVault.publicKey,
  316. lamports: await connection.getMinimumBalanceForRentExemption(165),
  317. space: 165,
  318. programId: TOKEN_PROGRAM_ID,
  319. }),
  320. TokenInstructions.initializeAccount({
  321. account: baseVault.publicKey,
  322. mint: baseMint,
  323. owner: vaultOwner,
  324. }),
  325. TokenInstructions.initializeAccount({
  326. account: quoteVault.publicKey,
  327. mint: quoteMint,
  328. owner: vaultOwner,
  329. })
  330. );
  331. const tx2 = new Transaction();
  332. tx2.add(
  333. SystemProgram.createAccount({
  334. fromPubkey: wallet.publicKey,
  335. newAccountPubkey: market.publicKey,
  336. lamports: await connection.getMinimumBalanceForRentExemption(
  337. Market.getLayout(dexProgramId).span
  338. ),
  339. space: Market.getLayout(dexProgramId).span,
  340. programId: dexProgramId,
  341. }),
  342. SystemProgram.createAccount({
  343. fromPubkey: wallet.publicKey,
  344. newAccountPubkey: requestQueue.publicKey,
  345. lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
  346. space: 5120 + 12,
  347. programId: dexProgramId,
  348. }),
  349. SystemProgram.createAccount({
  350. fromPubkey: wallet.publicKey,
  351. newAccountPubkey: eventQueue.publicKey,
  352. lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
  353. space: 262144 + 12,
  354. programId: dexProgramId,
  355. }),
  356. SystemProgram.createAccount({
  357. fromPubkey: wallet.publicKey,
  358. newAccountPubkey: bids.publicKey,
  359. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  360. space: 65536 + 12,
  361. programId: dexProgramId,
  362. }),
  363. SystemProgram.createAccount({
  364. fromPubkey: wallet.publicKey,
  365. newAccountPubkey: asks.publicKey,
  366. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  367. space: 65536 + 12,
  368. programId: dexProgramId,
  369. }),
  370. DexInstructions.initializeMarket({
  371. market: market.publicKey,
  372. requestQueue: requestQueue.publicKey,
  373. eventQueue: eventQueue.publicKey,
  374. bids: bids.publicKey,
  375. asks: asks.publicKey,
  376. baseVault: baseVault.publicKey,
  377. quoteVault: quoteVault.publicKey,
  378. baseMint,
  379. quoteMint,
  380. baseLotSize: new BN(baseLotSize),
  381. quoteLotSize: new BN(quoteLotSize),
  382. feeRateBps,
  383. vaultSignerNonce,
  384. quoteDustThreshold,
  385. programId: dexProgramId,
  386. })
  387. );
  388. const signedTransactions = await signTransactions({
  389. transactionsAndSigners: [
  390. { transaction: tx1, signers: [baseVault, quoteVault] },
  391. {
  392. transaction: tx2,
  393. signers: [market, requestQueue, eventQueue, bids, asks],
  394. },
  395. ],
  396. wallet,
  397. connection,
  398. });
  399. for (let signedTransaction of signedTransactions) {
  400. await sendAndConfirmRawTransaction(
  401. connection,
  402. signedTransaction.serialize()
  403. );
  404. }
  405. const acc = await connection.getAccountInfo(market.publicKey);
  406. return [market.publicKey, vaultOwner];
  407. }
  408. async function signTransactions({
  409. transactionsAndSigners,
  410. wallet,
  411. connection,
  412. }) {
  413. const blockhash = (await connection.getRecentBlockhash("max")).blockhash;
  414. transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
  415. transaction.recentBlockhash = blockhash;
  416. transaction.setSigners(
  417. wallet.publicKey,
  418. ...signers.map((s) => s.publicKey)
  419. );
  420. if (signers?.length > 0) {
  421. transaction.partialSign(...signers);
  422. }
  423. });
  424. return await wallet.signAllTransactions(
  425. transactionsAndSigners.map(({ transaction }) => transaction)
  426. );
  427. }
  428. async function sendAndConfirmRawTransaction(
  429. connection,
  430. raw,
  431. commitment = "recent"
  432. ) {
  433. let tx = await connection.sendRawTransaction(raw, {
  434. skipPreflight: true,
  435. });
  436. return await connection.confirmTransaction(tx, commitment);
  437. }
  438. async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
  439. const nonce = new BN(0);
  440. while (nonce.toNumber() < 255) {
  441. try {
  442. const vaultOwner = await PublicKey.createProgramAddress(
  443. [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
  444. dexProgramId
  445. );
  446. return [vaultOwner, nonce];
  447. } catch (e) {
  448. nonce.iaddn(1);
  449. }
  450. }
  451. throw new Error("Unable to find nonce");
  452. }
  453. async function runTradeBot(market, provider, iterations = undefined) {
  454. let marketClient = await Market.load(
  455. provider.connection,
  456. market,
  457. { commitment: "recent" },
  458. DEX_PID
  459. );
  460. const baseTokenUser1 = (
  461. await marketClient.getTokenAccountsByOwnerForMint(
  462. provider.connection,
  463. MARKET_MAKER.publicKey,
  464. marketClient.baseMintAddress
  465. )
  466. )[0].pubkey;
  467. const quoteTokenUser1 = (
  468. await marketClient.getTokenAccountsByOwnerForMint(
  469. provider.connection,
  470. MARKET_MAKER.publicKey,
  471. marketClient.quoteMintAddress
  472. )
  473. )[0].pubkey;
  474. const baseTokenUser2 = (
  475. await marketClient.getTokenAccountsByOwnerForMint(
  476. provider.connection,
  477. provider.wallet.publicKey,
  478. marketClient.baseMintAddress
  479. )
  480. )[0].pubkey;
  481. const quoteTokenUser2 = (
  482. await marketClient.getTokenAccountsByOwnerForMint(
  483. provider.connection,
  484. provider.wallet.publicKey,
  485. marketClient.quoteMintAddress
  486. )
  487. )[0].pubkey;
  488. const makerOpenOrdersUser1 = (
  489. await OpenOrders.findForMarketAndOwner(
  490. provider.connection,
  491. market,
  492. MARKET_MAKER.publicKey,
  493. DEX_PID
  494. )
  495. )[0];
  496. makerOpenOrdersUser2 = (
  497. await OpenOrders.findForMarketAndOwner(
  498. provider.connection,
  499. market,
  500. provider.wallet.publicKey,
  501. DEX_PID
  502. )
  503. )[0];
  504. const price = 6.041;
  505. const size = 700000.8;
  506. let maker = MARKET_MAKER;
  507. let taker = provider.wallet.payer;
  508. let baseToken = baseTokenUser1;
  509. let quoteToken = quoteTokenUser2;
  510. let makerOpenOrders = makerOpenOrdersUser1;
  511. let k = 1;
  512. while (true) {
  513. if (iterations && k > iterations) {
  514. break;
  515. }
  516. const clientId = new anchor.BN(k);
  517. if (k % 5 === 0) {
  518. if (maker.publicKey.equals(MARKET_MAKER.publicKey)) {
  519. maker = provider.wallet.payer;
  520. makerOpenOrders = makerOpenOrdersUser2;
  521. taker = MARKET_MAKER;
  522. baseToken = baseTokenUser2;
  523. quoteToken = quoteTokenUser1;
  524. } else {
  525. maker = MARKET_MAKER;
  526. makerOpenOrders = makerOpenOrdersUser1;
  527. taker = provider.wallet.payer;
  528. baseToken = baseTokenUser1;
  529. quoteToken = quoteTokenUser2;
  530. }
  531. }
  532. // Post ask.
  533. const { transaction: tx_ask, signers: sigs_ask } =
  534. await marketClient.makePlaceOrderTransaction(provider.connection, {
  535. owner: maker,
  536. payer: baseToken,
  537. side: "sell",
  538. price,
  539. size,
  540. orderType: "postOnly",
  541. clientId,
  542. openOrdersAddressKey: undefined,
  543. openOrdersAccount: undefined,
  544. feeDiscountPubkey: null,
  545. selfTradeBehavior: "abortTransaction",
  546. });
  547. let txSig = await provider.send(tx_ask, sigs_ask.concat(maker));
  548. console.log("Ask", txSig);
  549. // Take.
  550. const { transaction: tx_bid, signers: sigs_bid } =
  551. await marketClient.makePlaceOrderTransaction(provider.connection, {
  552. owner: taker,
  553. payer: quoteToken,
  554. side: "buy",
  555. price,
  556. size,
  557. orderType: "ioc",
  558. clientId: undefined,
  559. openOrdersAddressKey: undefined,
  560. openOrdersAccount: undefined,
  561. feeDiscountPubkey: null,
  562. selfTradeBehavior: "abortTransaction",
  563. });
  564. txSig = await provider.send(tx_bid, sigs_bid.concat(taker));
  565. console.log("Bid", txSig);
  566. await sleep(1000);
  567. // Cancel anything remaining.
  568. try {
  569. txSig = await marketClient.cancelOrderByClientId(
  570. provider.connection,
  571. maker,
  572. makerOpenOrders.address,
  573. clientId
  574. );
  575. console.log("Cancelled the rest", txSig);
  576. await sleep(1000);
  577. } catch (e) {
  578. console.log("Unable to cancel order", e);
  579. }
  580. k += 1;
  581. // If the open orders account wasn't previously initialized, it is now.
  582. if (makerOpenOrdersUser2 === undefined) {
  583. makerOpenOrdersUser2 = (
  584. await OpenOrders.findForMarketAndOwner(
  585. provider.connection,
  586. market,
  587. provider.wallet.publicKey,
  588. DEX_PID
  589. )
  590. )[0];
  591. }
  592. }
  593. }
  594. function sleep(ms) {
  595. return new Promise((resolve) => setTimeout(resolve, ms));
  596. }
  597. module.exports = {
  598. fundAccount,
  599. initMarket,
  600. initOrderbook,
  601. setupMarket,
  602. DEX_PID,
  603. getVaultOwnerAndNonce,
  604. runTradeBot,
  605. };