index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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 = require("@project-serum/serum").Market;
  10. const DexInstructions = require("@project-serum/serum").DexInstructions;
  11. const web3 = require("@project-serum/anchor").web3;
  12. const Connection = web3.Connection;
  13. const BN = require("@project-serum/anchor").BN;
  14. const serumCmn = require("@project-serum/common");
  15. const Account = web3.Account;
  16. const Transaction = web3.Transaction;
  17. const PublicKey = web3.PublicKey;
  18. const SystemProgram = web3.SystemProgram;
  19. const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
  20. async function setupTwoMarkets({ provider }) {
  21. // Setup mints with initial tokens owned by the provider.
  22. const decimals = 6;
  23. const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
  24. provider,
  25. new BN(1000000000000000),
  26. undefined,
  27. decimals
  28. );
  29. const [MINT_B, GOD_B] = await serumCmn.createMintAndVault(
  30. provider,
  31. new BN(1000000000000000),
  32. undefined,
  33. decimals
  34. );
  35. const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
  36. provider,
  37. new BN(1000000000000000),
  38. undefined,
  39. decimals
  40. );
  41. // Create a funded account to act as market maker.
  42. const amount = 100000 * 10 ** decimals;
  43. const marketMaker = await fundAccount({
  44. provider,
  45. mints: [
  46. { god: GOD_A, mint: MINT_A, amount, decimals },
  47. { god: GOD_B, mint: MINT_B, amount, decimals },
  48. { god: GOD_USDC, mint: USDC, amount, decimals },
  49. ],
  50. });
  51. // Setup A/USDC and B/USDC markets with resting orders.
  52. const asks = [
  53. [6.041, 7.8],
  54. [6.051, 72.3],
  55. [6.055, 5.4],
  56. [6.067, 15.7],
  57. [6.077, 390.0],
  58. [6.09, 24.0],
  59. [6.11, 36.3],
  60. [6.133, 300.0],
  61. [6.167, 687.8],
  62. ];
  63. const bids = [
  64. [6.004, 8.5],
  65. [5.995, 12.9],
  66. [5.987, 6.2],
  67. [5.978, 15.3],
  68. [5.965, 82.8],
  69. [5.961, 25.4],
  70. ];
  71. MARKET_A_USDC = await setupMarket({
  72. baseMint: MINT_A,
  73. quoteMint: USDC,
  74. marketMaker: {
  75. account: marketMaker.account,
  76. baseToken: marketMaker.tokens[MINT_A.toString()],
  77. quoteToken: marketMaker.tokens[USDC.toString()],
  78. },
  79. bids,
  80. asks,
  81. provider,
  82. });
  83. MARKET_B_USDC = await setupMarket({
  84. baseMint: MINT_B,
  85. quoteMint: USDC,
  86. marketMaker: {
  87. account: marketMaker.account,
  88. baseToken: marketMaker.tokens[MINT_B.toString()],
  89. quoteToken: marketMaker.tokens[USDC.toString()],
  90. },
  91. bids,
  92. asks,
  93. provider,
  94. });
  95. return {
  96. marketA: MARKET_A_USDC,
  97. marketB: MARKET_B_USDC,
  98. marketMaker,
  99. mintA: MINT_A,
  100. mintB: MINT_B,
  101. usdc: USDC,
  102. godA: GOD_A,
  103. godB: GOD_B,
  104. godUsdc: GOD_USDC,
  105. };
  106. }
  107. // Creates everything needed for an orderbook to be running
  108. //
  109. // * Mints for both the base and quote currencies.
  110. // * Lists the market.
  111. // * Provides resting orders on the market.
  112. //
  113. // Returns a client that can be used to interact with the market
  114. // (and some other data, e.g., the mints and market maker account).
  115. async function initOrderbook({ provider, bids, asks }) {
  116. if (!bids || !asks) {
  117. asks = [
  118. [6.041, 7.8],
  119. [6.051, 72.3],
  120. [6.055, 5.4],
  121. [6.067, 15.7],
  122. [6.077, 390.0],
  123. [6.09, 24.0],
  124. [6.11, 36.3],
  125. [6.133, 300.0],
  126. [6.167, 687.8],
  127. ];
  128. bids = [
  129. [6.004, 8.5],
  130. [5.995, 12.9],
  131. [5.987, 6.2],
  132. [5.978, 15.3],
  133. [5.965, 82.8],
  134. [5.961, 25.4],
  135. ];
  136. }
  137. // Create base and quote currency mints.
  138. const decimals = 6;
  139. const [MINT_A, GOD_A] = await serumCmn.createMintAndVault(
  140. provider,
  141. new BN(1000000000000000),
  142. undefined,
  143. decimals
  144. );
  145. const [USDC, GOD_USDC] = await serumCmn.createMintAndVault(
  146. provider,
  147. new BN(1000000000000000),
  148. undefined,
  149. decimals
  150. );
  151. // Create a funded account to act as market maker.
  152. const amount = 100000 * 10 ** decimals;
  153. const marketMaker = await fundAccount({
  154. provider,
  155. mints: [
  156. { god: GOD_A, mint: MINT_A, amount, decimals },
  157. { god: GOD_USDC, mint: USDC, amount, decimals },
  158. ],
  159. });
  160. marketClient = await setupMarket({
  161. baseMint: MINT_A,
  162. quoteMint: USDC,
  163. marketMaker: {
  164. account: marketMaker.account,
  165. baseToken: marketMaker.tokens[MINT_A.toString()],
  166. quoteToken: marketMaker.tokens[USDC.toString()],
  167. },
  168. bids,
  169. asks,
  170. provider,
  171. });
  172. return {
  173. marketClient,
  174. baseMint: MINT_A,
  175. quoteMint: USDC,
  176. marketMaker,
  177. };
  178. }
  179. async function fundAccount({ provider, mints }) {
  180. const MARKET_MAKER = new Account();
  181. const marketMaker = {
  182. tokens: {},
  183. account: MARKET_MAKER,
  184. };
  185. // Transfer lamports to market maker.
  186. await provider.sendAndConfirm(
  187. (() => {
  188. const tx = new Transaction();
  189. tx.add(
  190. SystemProgram.transfer({
  191. fromPubkey: provider.wallet.publicKey,
  192. toPubkey: MARKET_MAKER.publicKey,
  193. lamports: 100000000000,
  194. })
  195. );
  196. return tx;
  197. })()
  198. );
  199. // Transfer SPL tokens to the market maker.
  200. for (let k = 0; k < mints.length; k += 1) {
  201. const { mint, god, amount, decimals } = mints[k];
  202. let MINT_A = mint;
  203. let GOD_A = god;
  204. // Setup token accounts owned by the market maker.
  205. const mintAClient = new Token(
  206. provider.connection,
  207. MINT_A,
  208. TOKEN_PROGRAM_ID,
  209. provider.wallet.payer // node only
  210. );
  211. const marketMakerTokenA = await mintAClient.createAccount(
  212. MARKET_MAKER.publicKey
  213. );
  214. await provider.sendAndConfirm(
  215. (() => {
  216. const tx = new Transaction();
  217. tx.add(
  218. Token.createTransferCheckedInstruction(
  219. TOKEN_PROGRAM_ID,
  220. GOD_A,
  221. MINT_A,
  222. marketMakerTokenA,
  223. provider.wallet.publicKey,
  224. [],
  225. amount,
  226. decimals
  227. )
  228. );
  229. return tx;
  230. })()
  231. );
  232. marketMaker.tokens[mint.toString()] = marketMakerTokenA;
  233. }
  234. return marketMaker;
  235. }
  236. async function setupMarket({
  237. provider,
  238. marketMaker,
  239. baseMint,
  240. quoteMint,
  241. bids,
  242. asks,
  243. }) {
  244. const marketAPublicKey = await listMarket({
  245. connection: provider.connection,
  246. wallet: provider.wallet,
  247. baseMint: baseMint,
  248. quoteMint: quoteMint,
  249. baseLotSize: 100000,
  250. quoteLotSize: 100,
  251. dexProgramId: DEX_PID,
  252. feeRateBps: 0,
  253. });
  254. const MARKET_A_USDC = await Market.load(
  255. provider.connection,
  256. marketAPublicKey,
  257. { commitment: "processed" },
  258. DEX_PID
  259. );
  260. for (let k = 0; k < asks.length; k += 1) {
  261. let ask = asks[k];
  262. const { transaction, signers } =
  263. await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  264. owner: marketMaker.account,
  265. payer: marketMaker.baseToken,
  266. side: "sell",
  267. price: ask[0],
  268. size: ask[1],
  269. orderType: "postOnly",
  270. clientId: undefined,
  271. openOrdersAddressKey: undefined,
  272. openOrdersAccount: undefined,
  273. feeDiscountPubkey: null,
  274. selfTradeBehavior: "abortTransaction",
  275. });
  276. await provider.sendAndConfirm(
  277. transaction,
  278. signers.concat(marketMaker.account)
  279. );
  280. }
  281. for (let k = 0; k < bids.length; k += 1) {
  282. let bid = bids[k];
  283. const { transaction, signers } =
  284. await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  285. owner: marketMaker.account,
  286. payer: marketMaker.quoteToken,
  287. side: "buy",
  288. price: bid[0],
  289. size: bid[1],
  290. orderType: "postOnly",
  291. clientId: undefined,
  292. openOrdersAddressKey: undefined,
  293. openOrdersAccount: undefined,
  294. feeDiscountPubkey: null,
  295. selfTradeBehavior: "abortTransaction",
  296. });
  297. await provider.sendAndConfirm(
  298. transaction,
  299. signers.concat(marketMaker.account)
  300. );
  301. }
  302. return MARKET_A_USDC;
  303. }
  304. async function listMarket({
  305. connection,
  306. wallet,
  307. baseMint,
  308. quoteMint,
  309. baseLotSize,
  310. quoteLotSize,
  311. dexProgramId,
  312. feeRateBps,
  313. }) {
  314. const market = new Account();
  315. const requestQueue = new Account();
  316. const eventQueue = new Account();
  317. const bids = new Account();
  318. const asks = new Account();
  319. const baseVault = new Account();
  320. const quoteVault = new Account();
  321. const quoteDustThreshold = new BN(100);
  322. const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
  323. market.publicKey,
  324. dexProgramId
  325. );
  326. const tx1 = new Transaction();
  327. tx1.add(
  328. SystemProgram.createAccount({
  329. fromPubkey: wallet.publicKey,
  330. newAccountPubkey: baseVault.publicKey,
  331. lamports: await connection.getMinimumBalanceForRentExemption(165),
  332. space: 165,
  333. programId: TOKEN_PROGRAM_ID,
  334. }),
  335. SystemProgram.createAccount({
  336. fromPubkey: wallet.publicKey,
  337. newAccountPubkey: quoteVault.publicKey,
  338. lamports: await connection.getMinimumBalanceForRentExemption(165),
  339. space: 165,
  340. programId: TOKEN_PROGRAM_ID,
  341. }),
  342. TokenInstructions.initializeAccount({
  343. account: baseVault.publicKey,
  344. mint: baseMint,
  345. owner: vaultOwner,
  346. }),
  347. TokenInstructions.initializeAccount({
  348. account: quoteVault.publicKey,
  349. mint: quoteMint,
  350. owner: vaultOwner,
  351. })
  352. );
  353. const tx2 = new Transaction();
  354. tx2.add(
  355. SystemProgram.createAccount({
  356. fromPubkey: wallet.publicKey,
  357. newAccountPubkey: market.publicKey,
  358. lamports: await connection.getMinimumBalanceForRentExemption(
  359. Market.getLayout(dexProgramId).span
  360. ),
  361. space: Market.getLayout(dexProgramId).span,
  362. programId: dexProgramId,
  363. }),
  364. SystemProgram.createAccount({
  365. fromPubkey: wallet.publicKey,
  366. newAccountPubkey: requestQueue.publicKey,
  367. lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
  368. space: 5120 + 12,
  369. programId: dexProgramId,
  370. }),
  371. SystemProgram.createAccount({
  372. fromPubkey: wallet.publicKey,
  373. newAccountPubkey: eventQueue.publicKey,
  374. lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
  375. space: 262144 + 12,
  376. programId: dexProgramId,
  377. }),
  378. SystemProgram.createAccount({
  379. fromPubkey: wallet.publicKey,
  380. newAccountPubkey: bids.publicKey,
  381. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  382. space: 65536 + 12,
  383. programId: dexProgramId,
  384. }),
  385. SystemProgram.createAccount({
  386. fromPubkey: wallet.publicKey,
  387. newAccountPubkey: asks.publicKey,
  388. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  389. space: 65536 + 12,
  390. programId: dexProgramId,
  391. }),
  392. DexInstructions.initializeMarket({
  393. market: market.publicKey,
  394. requestQueue: requestQueue.publicKey,
  395. eventQueue: eventQueue.publicKey,
  396. bids: bids.publicKey,
  397. asks: asks.publicKey,
  398. baseVault: baseVault.publicKey,
  399. quoteVault: quoteVault.publicKey,
  400. baseMint,
  401. quoteMint,
  402. baseLotSize: new BN(baseLotSize),
  403. quoteLotSize: new BN(quoteLotSize),
  404. feeRateBps,
  405. vaultSignerNonce,
  406. quoteDustThreshold,
  407. programId: dexProgramId,
  408. })
  409. );
  410. const signedTransactions = await signTransactions({
  411. transactionsAndSigners: [
  412. { transaction: tx1, signers: [baseVault, quoteVault] },
  413. {
  414. transaction: tx2,
  415. signers: [market, requestQueue, eventQueue, bids, asks],
  416. },
  417. ],
  418. wallet,
  419. connection,
  420. });
  421. for (let signedTransaction of signedTransactions) {
  422. await sendAndConfirmRawTransaction(
  423. connection,
  424. signedTransaction.serialize()
  425. );
  426. }
  427. const acc = await connection.getAccountInfo(market.publicKey);
  428. return market.publicKey;
  429. }
  430. async function signTransactions({
  431. transactionsAndSigners,
  432. wallet,
  433. connection,
  434. }) {
  435. const blockhash = (await connection.getRecentBlockhash("finalized"))
  436. .blockhash;
  437. transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
  438. transaction.recentBlockhash = blockhash;
  439. transaction.setSigners(
  440. wallet.publicKey,
  441. ...signers.map((s) => s.publicKey)
  442. );
  443. if (signers.length > 0) {
  444. transaction.partialSign(...signers);
  445. }
  446. });
  447. return await wallet.signAllTransactions(
  448. transactionsAndSigners.map(({ transaction }) => transaction)
  449. );
  450. }
  451. async function sendAndConfirmRawTransaction(
  452. connection,
  453. raw,
  454. commitment = "processed"
  455. ) {
  456. let tx = await connection.sendRawTransaction(raw, {
  457. skipPreflight: true,
  458. });
  459. return await connection.confirmTransaction(tx, commitment);
  460. }
  461. async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
  462. const nonce = new BN(0);
  463. while (nonce.toNumber() < 255) {
  464. try {
  465. const vaultOwner = await PublicKey.createProgramAddress(
  466. [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
  467. dexProgramId
  468. );
  469. return [vaultOwner, nonce];
  470. } catch (e) {
  471. nonce.iaddn(1);
  472. }
  473. }
  474. throw new Error("Unable to find nonce");
  475. }
  476. module.exports = {
  477. fundAccount,
  478. setupMarket,
  479. initOrderbook,
  480. setupTwoMarkets,
  481. DEX_PID,
  482. getVaultOwnerAndNonce,
  483. };