index.js 16 KB

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