index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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.send(
  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.send(
  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: "recent" },
  258. DEX_PID
  259. );
  260. for (let k = 0; k < asks.length; k += 1) {
  261. let ask = asks[k];
  262. const {
  263. transaction,
  264. signers,
  265. } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  266. owner: marketMaker.account,
  267. payer: marketMaker.baseToken,
  268. side: "sell",
  269. price: ask[0],
  270. size: ask[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. for (let k = 0; k < bids.length; k += 1) {
  281. let bid = bids[k];
  282. const {
  283. transaction,
  284. signers,
  285. } = await MARKET_A_USDC.makePlaceOrderTransaction(provider.connection, {
  286. owner: marketMaker.account,
  287. payer: marketMaker.quoteToken,
  288. side: "buy",
  289. price: bid[0],
  290. size: bid[1],
  291. orderType: "postOnly",
  292. clientId: undefined,
  293. openOrdersAddressKey: undefined,
  294. openOrdersAccount: undefined,
  295. feeDiscountPubkey: null,
  296. selfTradeBehavior: "abortTransaction",
  297. });
  298. await provider.send(transaction, signers.concat(marketMaker.account));
  299. }
  300. return MARKET_A_USDC;
  301. }
  302. async function listMarket({
  303. connection,
  304. wallet,
  305. baseMint,
  306. quoteMint,
  307. baseLotSize,
  308. quoteLotSize,
  309. dexProgramId,
  310. feeRateBps,
  311. }) {
  312. const market = new Account();
  313. const requestQueue = new Account();
  314. const eventQueue = new Account();
  315. const bids = new Account();
  316. const asks = new Account();
  317. const baseVault = new Account();
  318. const quoteVault = new Account();
  319. const quoteDustThreshold = new BN(100);
  320. const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(
  321. market.publicKey,
  322. dexProgramId
  323. );
  324. const tx1 = new Transaction();
  325. tx1.add(
  326. SystemProgram.createAccount({
  327. fromPubkey: wallet.publicKey,
  328. newAccountPubkey: baseVault.publicKey,
  329. lamports: await connection.getMinimumBalanceForRentExemption(165),
  330. space: 165,
  331. programId: TOKEN_PROGRAM_ID,
  332. }),
  333. SystemProgram.createAccount({
  334. fromPubkey: wallet.publicKey,
  335. newAccountPubkey: quoteVault.publicKey,
  336. lamports: await connection.getMinimumBalanceForRentExemption(165),
  337. space: 165,
  338. programId: TOKEN_PROGRAM_ID,
  339. }),
  340. TokenInstructions.initializeAccount({
  341. account: baseVault.publicKey,
  342. mint: baseMint,
  343. owner: vaultOwner,
  344. }),
  345. TokenInstructions.initializeAccount({
  346. account: quoteVault.publicKey,
  347. mint: quoteMint,
  348. owner: vaultOwner,
  349. })
  350. );
  351. const tx2 = new Transaction();
  352. tx2.add(
  353. SystemProgram.createAccount({
  354. fromPubkey: wallet.publicKey,
  355. newAccountPubkey: market.publicKey,
  356. lamports: await connection.getMinimumBalanceForRentExemption(
  357. Market.getLayout(dexProgramId).span
  358. ),
  359. space: Market.getLayout(dexProgramId).span,
  360. programId: dexProgramId,
  361. }),
  362. SystemProgram.createAccount({
  363. fromPubkey: wallet.publicKey,
  364. newAccountPubkey: requestQueue.publicKey,
  365. lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
  366. space: 5120 + 12,
  367. programId: dexProgramId,
  368. }),
  369. SystemProgram.createAccount({
  370. fromPubkey: wallet.publicKey,
  371. newAccountPubkey: eventQueue.publicKey,
  372. lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
  373. space: 262144 + 12,
  374. programId: dexProgramId,
  375. }),
  376. SystemProgram.createAccount({
  377. fromPubkey: wallet.publicKey,
  378. newAccountPubkey: bids.publicKey,
  379. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  380. space: 65536 + 12,
  381. programId: dexProgramId,
  382. }),
  383. SystemProgram.createAccount({
  384. fromPubkey: wallet.publicKey,
  385. newAccountPubkey: asks.publicKey,
  386. lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
  387. space: 65536 + 12,
  388. programId: dexProgramId,
  389. }),
  390. DexInstructions.initializeMarket({
  391. market: market.publicKey,
  392. requestQueue: requestQueue.publicKey,
  393. eventQueue: eventQueue.publicKey,
  394. bids: bids.publicKey,
  395. asks: asks.publicKey,
  396. baseVault: baseVault.publicKey,
  397. quoteVault: quoteVault.publicKey,
  398. baseMint,
  399. quoteMint,
  400. baseLotSize: new BN(baseLotSize),
  401. quoteLotSize: new BN(quoteLotSize),
  402. feeRateBps,
  403. vaultSignerNonce,
  404. quoteDustThreshold,
  405. programId: dexProgramId,
  406. })
  407. );
  408. const signedTransactions = await signTransactions({
  409. transactionsAndSigners: [
  410. { transaction: tx1, signers: [baseVault, quoteVault] },
  411. {
  412. transaction: tx2,
  413. signers: [market, requestQueue, eventQueue, bids, asks],
  414. },
  415. ],
  416. wallet,
  417. connection,
  418. });
  419. for (let signedTransaction of signedTransactions) {
  420. await sendAndConfirmRawTransaction(
  421. connection,
  422. signedTransaction.serialize()
  423. );
  424. }
  425. const acc = await connection.getAccountInfo(market.publicKey);
  426. return market.publicKey;
  427. }
  428. async function signTransactions({
  429. transactionsAndSigners,
  430. wallet,
  431. connection,
  432. }) {
  433. const blockhash = (await connection.getRecentBlockhash("max")).blockhash;
  434. transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
  435. transaction.recentBlockhash = blockhash;
  436. transaction.setSigners(
  437. wallet.publicKey,
  438. ...signers.map((s) => s.publicKey)
  439. );
  440. if (signers?.length > 0) {
  441. transaction.partialSign(...signers);
  442. }
  443. });
  444. return await wallet.signAllTransactions(
  445. transactionsAndSigners.map(({ transaction }) => transaction)
  446. );
  447. }
  448. async function sendAndConfirmRawTransaction(
  449. connection,
  450. raw,
  451. commitment = "recent"
  452. ) {
  453. let tx = await connection.sendRawTransaction(raw, {
  454. skipPreflight: true,
  455. });
  456. return await connection.confirmTransaction(tx, commitment);
  457. }
  458. async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) {
  459. const nonce = new BN(0);
  460. while (nonce.toNumber() < 255) {
  461. try {
  462. const vaultOwner = await PublicKey.createProgramAddress(
  463. [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)],
  464. dexProgramId
  465. );
  466. return [vaultOwner, nonce];
  467. } catch (e) {
  468. nonce.iaddn(1);
  469. }
  470. }
  471. throw new Error("Unable to find nonce");
  472. }
  473. module.exports = {
  474. fundAccount,
  475. setupMarket,
  476. initOrderbook,
  477. setupTwoMarkets,
  478. DEX_PID,
  479. getVaultOwnerAndNonce,
  480. };