index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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 {
  247. transaction,
  248. signers,
  249. } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  250. owner: marketMaker.account,
  251. payer: marketMaker.baseToken,
  252. side: "sell",
  253. price: ask[0],
  254. size: ask[1],
  255. orderType: "postOnly",
  256. clientId: undefined,
  257. openOrdersAddressKey: undefined,
  258. openOrdersAccount: undefined,
  259. feeDiscountPubkey: null,
  260. selfTradeBehavior: "abortTransaction",
  261. });
  262. await provider.send(transaction, signers.concat(marketMaker.account));
  263. }
  264. for (let k = 0; k < bids.length; k += 1) {
  265. let bid = bids[k];
  266. const {
  267. transaction,
  268. signers,
  269. } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  270. owner: marketMaker.account,
  271. payer: marketMaker.quoteToken,
  272. side: "buy",
  273. price: bid[0],
  274. size: bid[1],
  275. orderType: "postOnly",
  276. clientId: undefined,
  277. openOrdersAddressKey: undefined,
  278. openOrdersAccount: undefined,
  279. feeDiscountPubkey: null,
  280. selfTradeBehavior: "abortTransaction",
  281. });
  282. await provider.send(transaction, signers.concat(marketMaker.account));
  283. }
  284. return [MARKET_A_USDC, vaultOwner];
  285. }
  286. async function listMarket({
  287. connection,
  288. wallet,
  289. baseMint,
  290. quoteMint,
  291. baseLotSize,
  292. quoteLotSize,
  293. dexProgramId,
  294. feeRateBps,
  295. }) {
  296. const market = new Account();
  297. const requestQueue = new Account();
  298. const eventQueue = new Account();
  299. const bids = new Account();
  300. const asks = new Account();
  301. const baseVault = new Account();
  302. const quoteVault = new Account();
  303. const quoteDustThreshold = new BN(100);
  304. const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
  305. market.publicKey,
  306. dexProgramId
  307. );
  308. const tx1 = new Transaction();
  309. tx1.add(
  310. SystemProgram.createAccount({
  311. fromPubkey: wallet.publicKey,
  312. newAccountPubkey: baseVault.publicKey,
  313. lamports: await connection.getMinimumBalanceForRentExemption(165),
  314. space: 165,
  315. programId: TOKEN_PROGRAM_ID,
  316. }),
  317. SystemProgram.createAccount({
  318. fromPubkey: wallet.publicKey,
  319. newAccountPubkey: quoteVault.publicKey,
  320. lamports: await connection.getMinimumBalanceForRentExemption(165),
  321. space: 165,
  322. programId: TOKEN_PROGRAM_ID,
  323. }),
  324. TokenInstructions.initializeAccount({
  325. account: baseVault.publicKey,
  326. mint: baseMint,
  327. owner: vaultOwner,
  328. }),
  329. TokenInstructions.initializeAccount({
  330. account: quoteVault.publicKey,
  331. mint: quoteMint,
  332. owner: vaultOwner,
  333. })
  334. );
  335. const tx2 = new Transaction();
  336. tx2.add(
  337. SystemProgram.createAccount({
  338. fromPubkey: wallet.publicKey,
  339. newAccountPubkey: market.publicKey,
  340. lamports: await connection.getMinimumBalanceForRentExemption(
  341. Market.getLayout(dexProgramId).span
  342. ),
  343. space: Market.getLayout(dexProgramId).span,
  344. programId: dexProgramId,
  345. }),
  346. SystemProgram.createAccount({
  347. fromPubkey: wallet.publicKey,
  348. newAccountPubkey: requestQueue.publicKey,
  349. lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
  350. space: 5120 + 12,
  351. programId: dexProgramId,
  352. }),
  353. SystemProgram.createAccount({
  354. fromPubkey: wallet.publicKey,
  355. newAccountPubkey: eventQueue.publicKey,
  356. lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
  357. space: 262144 + 12,
  358. programId: dexProgramId,
  359. }),
  360. SystemProgram.createAccount({
  361. fromPubkey: wallet.publicKey,
  362. newAccountPubkey: bids.publicKey,
  363. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  364. space: 65536 + 12,
  365. programId: dexProgramId,
  366. }),
  367. SystemProgram.createAccount({
  368. fromPubkey: wallet.publicKey,
  369. newAccountPubkey: asks.publicKey,
  370. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  371. space: 65536 + 12,
  372. programId: dexProgramId,
  373. }),
  374. DexInstructions.initializeMarket({
  375. market: market.publicKey,
  376. requestQueue: requestQueue.publicKey,
  377. eventQueue: eventQueue.publicKey,
  378. bids: bids.publicKey,
  379. asks: asks.publicKey,
  380. baseVault: baseVault.publicKey,
  381. quoteVault: quoteVault.publicKey,
  382. baseMint,
  383. quoteMint,
  384. baseLotSize: new BN(baseLotSize),
  385. quoteLotSize: new BN(quoteLotSize),
  386. feeRateBps,
  387. vaultSignerNonce,
  388. quoteDustThreshold,
  389. programId: dexProgramId,
  390. })
  391. );
  392. const signedTransactions = await signTransactions({
  393. transactionsAndSigners: [
  394. { transaction: tx1, signers: [baseVault, quoteVault] },
  395. {
  396. transaction: tx2,
  397. signers: [market, requestQueue, eventQueue, bids, asks],
  398. },
  399. ],
  400. wallet,
  401. connection,
  402. });
  403. for (let signedTransaction of signedTransactions) {
  404. await sendAndConfirmRawTransaction(
  405. connection,
  406. signedTransaction.serialize()
  407. );
  408. }
  409. const acc = await connection.getAccountInfo(market.publicKey);
  410. return [market.publicKey, vaultOwner];
  411. }
  412. async function signTransactions({
  413. transactionsAndSigners,
  414. wallet,
  415. connection,
  416. }) {
  417. const blockhash = (await connection.getRecentBlockhash("max")).blockhash;
  418. transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
  419. transaction.recentBlockhash = blockhash;
  420. transaction.setSigners(
  421. wallet.publicKey,
  422. ...signers.map((s) => s.publicKey)
  423. );
  424. if (signers?.length > 0) {
  425. transaction.partialSign(...signers);
  426. }
  427. });
  428. return await wallet.signAllTransactions(
  429. transactionsAndSigners.map(({ transaction }) => transaction)
  430. );
  431. }
  432. async function sendAndConfirmRawTransaction(
  433. connection,
  434. raw,
  435. commitment = "recent"
  436. ) {
  437. let tx = await connection.sendRawTransaction(raw, {
  438. skipPreflight: true,
  439. });
  440. return await connection.confirmTransaction(tx, commitment);
  441. }
  442. async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
  443. const nonce = new BN(0);
  444. while (nonce.toNumber() < 255) {
  445. try {
  446. const vaultOwner = await PublicKey.createProgramAddress(
  447. [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
  448. dexProgramId
  449. );
  450. return [vaultOwner, nonce];
  451. } catch (e) {
  452. nonce.iaddn(1);
  453. }
  454. }
  455. throw new Error("Unable to find nonce");
  456. }
  457. async function runTradeBot(market, provider, iterations = undefined) {
  458. let marketClient = await Market.load(
  459. provider.connection,
  460. market,
  461. { commitment: "recent" },
  462. DEX_PID
  463. );
  464. const baseTokenUser1 = (
  465. await marketClient.getTokenAccountsByOwnerForMint(
  466. provider.connection,
  467. MARKET_MAKER.publicKey,
  468. marketClient.baseMintAddress
  469. )
  470. )[0].pubkey;
  471. const quoteTokenUser1 = (
  472. await marketClient.getTokenAccountsByOwnerForMint(
  473. provider.connection,
  474. MARKET_MAKER.publicKey,
  475. marketClient.quoteMintAddress
  476. )
  477. )[0].pubkey;
  478. const baseTokenUser2 = (
  479. await marketClient.getTokenAccountsByOwnerForMint(
  480. provider.connection,
  481. provider.wallet.publicKey,
  482. marketClient.baseMintAddress
  483. )
  484. )[0].pubkey;
  485. const quoteTokenUser2 = (
  486. await marketClient.getTokenAccountsByOwnerForMint(
  487. provider.connection,
  488. provider.wallet.publicKey,
  489. marketClient.quoteMintAddress
  490. )
  491. )[0].pubkey;
  492. const makerOpenOrdersUser1 = (
  493. await OpenOrders.findForMarketAndOwner(
  494. provider.connection,
  495. market,
  496. MARKET_MAKER.publicKey,
  497. DEX_PID
  498. )
  499. )[0];
  500. makerOpenOrdersUser2 = (
  501. await OpenOrders.findForMarketAndOwner(
  502. provider.connection,
  503. market,
  504. provider.wallet.publicKey,
  505. DEX_PID
  506. )
  507. )[0];
  508. const price = 6.041;
  509. const size = 700000.8;
  510. let maker = MARKET_MAKER;
  511. let taker = provider.wallet.payer;
  512. let baseToken = baseTokenUser1;
  513. let quoteToken = quoteTokenUser2;
  514. let makerOpenOrders = makerOpenOrdersUser1;
  515. let k = 1;
  516. while (true) {
  517. if (iterations && k > iterations) {
  518. break;
  519. }
  520. const clientId = new anchor.BN(k);
  521. if (k % 5 === 0) {
  522. if (maker.publicKey.equals(MARKET_MAKER.publicKey)) {
  523. maker = provider.wallet.payer;
  524. makerOpenOrders = makerOpenOrdersUser2;
  525. taker = MARKET_MAKER;
  526. baseToken = baseTokenUser2;
  527. quoteToken = quoteTokenUser1;
  528. } else {
  529. maker = MARKET_MAKER;
  530. makerOpenOrders = makerOpenOrdersUser1;
  531. taker = provider.wallet.payer;
  532. baseToken = baseTokenUser1;
  533. quoteToken = quoteTokenUser2;
  534. }
  535. }
  536. // Post ask.
  537. const {
  538. transaction: tx_ask,
  539. signers: sigs_ask,
  540. } = await marketClient.makePlaceOrderTransaction(provider.connection, {
  541. owner: maker,
  542. payer: baseToken,
  543. side: "sell",
  544. price,
  545. size,
  546. orderType: "postOnly",
  547. clientId,
  548. openOrdersAddressKey: undefined,
  549. openOrdersAccount: undefined,
  550. feeDiscountPubkey: null,
  551. selfTradeBehavior: "abortTransaction",
  552. });
  553. let txSig = await provider.send(tx_ask, sigs_ask.concat(maker));
  554. console.log("Ask", txSig);
  555. // Take.
  556. const {
  557. transaction: tx_bid,
  558. signers: sigs_bid,
  559. } = await marketClient.makePlaceOrderTransaction(provider.connection, {
  560. owner: taker,
  561. payer: quoteToken,
  562. side: "buy",
  563. price,
  564. size,
  565. orderType: "ioc",
  566. clientId: undefined,
  567. openOrdersAddressKey: undefined,
  568. openOrdersAccount: undefined,
  569. feeDiscountPubkey: null,
  570. selfTradeBehavior: "abortTransaction",
  571. });
  572. txSig = await provider.send(tx_bid, sigs_bid.concat(taker));
  573. console.log("Bid", txSig);
  574. await sleep(1000);
  575. // Cancel anything remaining.
  576. try {
  577. txSig = await marketClient.cancelOrderByClientId(
  578. provider.connection,
  579. maker,
  580. makerOpenOrders.address,
  581. clientId
  582. );
  583. console.log("Cancelled the rest", txSig);
  584. await sleep(1000);
  585. } catch (e) {
  586. console.log("Unable to cancel order", e);
  587. }
  588. k += 1;
  589. // If the open orders account wasn't previously initialized, it is now.
  590. if (makerOpenOrdersUser2 === undefined) {
  591. makerOpenOrdersUser2 = (
  592. await OpenOrders.findForMarketAndOwner(
  593. provider.connection,
  594. market,
  595. provider.wallet.publicKey,
  596. DEX_PID
  597. )
  598. )[0];
  599. }
  600. }
  601. }
  602. function sleep(ms) {
  603. return new Promise((resolve) => setTimeout(resolve, ms));
  604. }
  605. module.exports = {
  606. fundAccount,
  607. initMarket,
  608. initOrderbook,
  609. setupMarket,
  610. DEX_PID,
  611. getVaultOwnerAndNonce,
  612. runTradeBot,
  613. };