swap.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. const assert = require("assert");
  2. const anchor = require("@project-serum/anchor");
  3. const BN = anchor.BN;
  4. const OpenOrders = require("@project-serum/serum").OpenOrders;
  5. const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
  6. const serumCmn = require("@project-serum/common");
  7. const utils = require("./utils");
  8. // Taker fee rate (bps).
  9. const TAKER_FEE = 0.0022;
  10. describe("swap", () => {
  11. // Configure the client to use the local cluster.
  12. anchor.setProvider(anchor.Provider.env());
  13. // Swap program client.
  14. const program = anchor.workspace.Swap;
  15. // Accounts used to setup the orderbook.
  16. let ORDERBOOK_ENV,
  17. // Accounts used for A -> USDC swap transactions.
  18. SWAP_A_USDC_ACCOUNTS,
  19. // Accounts used for USDC -> A swap transactions.
  20. SWAP_USDC_A_ACCOUNTS,
  21. // Serum DEX vault PDA for market A/USDC.
  22. marketAVaultSigner,
  23. // Serum DEX vault PDA for market B/USDC.
  24. marketBVaultSigner;
  25. // Open orders accounts on the two markets for the provider.
  26. const openOrdersA = new anchor.web3.Account();
  27. const openOrdersB = new anchor.web3.Account();
  28. it("BOILERPLATE: Sets up two markets with resting orders", async () => {
  29. ORDERBOOK_ENV = await utils.setupTwoMarkets({
  30. provider: program.provider,
  31. });
  32. });
  33. it("BOILERPLATE: Sets up reusable accounts", async () => {
  34. const marketA = ORDERBOOK_ENV.marketA;
  35. const marketB = ORDERBOOK_ENV.marketB;
  36. const [vaultSignerA] = await utils.getVaultOwnerAndNonce(
  37. marketA._decoded.ownAddress
  38. );
  39. const [vaultSignerB] = await utils.getVaultOwnerAndNonce(
  40. marketB._decoded.ownAddress
  41. );
  42. marketAVaultSigner = vaultSignerA;
  43. marketBVaultSigner = vaultSignerB;
  44. SWAP_USDC_A_ACCOUNTS = {
  45. market: {
  46. market: marketA._decoded.ownAddress,
  47. requestQueue: marketA._decoded.requestQueue,
  48. eventQueue: marketA._decoded.eventQueue,
  49. bids: marketA._decoded.bids,
  50. asks: marketA._decoded.asks,
  51. coinVault: marketA._decoded.baseVault,
  52. pcVault: marketA._decoded.quoteVault,
  53. vaultSigner: marketAVaultSigner,
  54. // User params.
  55. openOrders: openOrdersA.publicKey,
  56. orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
  57. coinWallet: ORDERBOOK_ENV.godA,
  58. },
  59. pcWallet: ORDERBOOK_ENV.godUsdc,
  60. authority: program.provider.wallet.publicKey,
  61. dexProgram: utils.DEX_PID,
  62. tokenProgram: TOKEN_PROGRAM_ID,
  63. rent: anchor.web3.SYSVAR_RENT_PUBKEY,
  64. };
  65. SWAP_A_USDC_ACCOUNTS = {
  66. ...SWAP_USDC_A_ACCOUNTS,
  67. market: {
  68. ...SWAP_USDC_A_ACCOUNTS.market,
  69. orderPayerTokenAccount: ORDERBOOK_ENV.godA,
  70. },
  71. };
  72. });
  73. it("Swaps from USDC to Token A", async () => {
  74. const marketA = ORDERBOOK_ENV.marketA;
  75. // Swap exactly enough USDC to get 1.2 A tokens (best offer price is 6.041 USDC).
  76. const expectedResultantAmount = 7.2;
  77. const bestOfferPrice = 6.041;
  78. const amountToSpend = expectedResultantAmount * bestOfferPrice;
  79. const swapAmount = new BN((amountToSpend / (1 - TAKER_FEE)) * 10 ** 6);
  80. const [tokenAChange, usdcChange] = await withBalanceChange(
  81. program.provider,
  82. [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc],
  83. async () => {
  84. await program.rpc.swap(Side.Bid, swapAmount, new BN(1.0), {
  85. accounts: SWAP_USDC_A_ACCOUNTS,
  86. instructions: [
  87. // First order to this market so one must create the open orders account.
  88. await OpenOrders.makeCreateAccountTransaction(
  89. program.provider.connection,
  90. marketA._decoded.ownAddress,
  91. program.provider.wallet.publicKey,
  92. openOrdersA.publicKey,
  93. utils.DEX_PID
  94. ),
  95. // Might as well create the second open orders account while we're here.
  96. // In prod, this should actually be done within the same tx as an
  97. // order to market B.
  98. await OpenOrders.makeCreateAccountTransaction(
  99. program.provider.connection,
  100. ORDERBOOK_ENV.marketB._decoded.ownAddress,
  101. program.provider.wallet.publicKey,
  102. openOrdersB.publicKey,
  103. utils.DEX_PID
  104. ),
  105. ],
  106. signers: [openOrdersA, openOrdersB],
  107. });
  108. }
  109. );
  110. assert.ok(tokenAChange === expectedResultantAmount);
  111. assert.ok(usdcChange === -swapAmount.toNumber() / 10 ** 6);
  112. });
  113. it("Swaps from Token A to USDC", async () => {
  114. const marketA = ORDERBOOK_ENV.marketA;
  115. // Swap out A tokens for USDC.
  116. const swapAmount = 8.1;
  117. const bestBidPrice = 6.004;
  118. const amountToFill = swapAmount * bestBidPrice;
  119. const takerFee = 0.0022;
  120. const resultantAmount = new BN(amountToFill * (1 - TAKER_FEE) * 10 ** 6);
  121. const [tokenAChange, usdcChange] = await withBalanceChange(
  122. program.provider,
  123. [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godUsdc],
  124. async () => {
  125. await program.rpc.swap(
  126. Side.Ask,
  127. new BN(swapAmount * 10 ** 6),
  128. new BN(swapAmount),
  129. {
  130. accounts: SWAP_A_USDC_ACCOUNTS,
  131. }
  132. );
  133. }
  134. );
  135. assert.ok(tokenAChange === -swapAmount);
  136. assert.ok(usdcChange === resultantAmount.toNumber() / 10 ** 6);
  137. });
  138. it("Swaps from Token A to Token B", async () => {
  139. const marketA = ORDERBOOK_ENV.marketA;
  140. const marketB = ORDERBOOK_ENV.marketB;
  141. const swapAmount = 10;
  142. const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange(
  143. program.provider,
  144. [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc],
  145. async () => {
  146. // Perform the actual swap.
  147. await program.rpc.swapTransitive(
  148. new BN(swapAmount * 10 ** 6),
  149. new BN(swapAmount - 1),
  150. {
  151. accounts: {
  152. from: {
  153. market: marketA._decoded.ownAddress,
  154. requestQueue: marketA._decoded.requestQueue,
  155. eventQueue: marketA._decoded.eventQueue,
  156. bids: marketA._decoded.bids,
  157. asks: marketA._decoded.asks,
  158. coinVault: marketA._decoded.baseVault,
  159. pcVault: marketA._decoded.quoteVault,
  160. vaultSigner: marketAVaultSigner,
  161. // User params.
  162. openOrders: openOrdersA.publicKey,
  163. // Swapping from A -> USDC.
  164. orderPayerTokenAccount: ORDERBOOK_ENV.godA,
  165. coinWallet: ORDERBOOK_ENV.godA,
  166. },
  167. to: {
  168. market: marketB._decoded.ownAddress,
  169. requestQueue: marketB._decoded.requestQueue,
  170. eventQueue: marketB._decoded.eventQueue,
  171. bids: marketB._decoded.bids,
  172. asks: marketB._decoded.asks,
  173. coinVault: marketB._decoded.baseVault,
  174. pcVault: marketB._decoded.quoteVault,
  175. vaultSigner: marketBVaultSigner,
  176. // User params.
  177. openOrders: openOrdersB.publicKey,
  178. // Swapping from USDC -> B.
  179. orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
  180. coinWallet: ORDERBOOK_ENV.godB,
  181. },
  182. pcWallet: ORDERBOOK_ENV.godUsdc,
  183. authority: program.provider.wallet.publicKey,
  184. dexProgram: utils.DEX_PID,
  185. tokenProgram: TOKEN_PROGRAM_ID,
  186. rent: anchor.web3.SYSVAR_RENT_PUBKEY,
  187. },
  188. }
  189. );
  190. }
  191. );
  192. assert.ok(tokenAChange === -swapAmount);
  193. // TODO: calculate this dynamically from the swap amount.
  194. assert.ok(tokenBChange === 9.8);
  195. assert.ok(usdcChange === 0);
  196. });
  197. it("Swaps from Token B to Token A", async () => {
  198. const marketA = ORDERBOOK_ENV.marketA;
  199. const marketB = ORDERBOOK_ENV.marketB;
  200. const swapAmount = 23;
  201. const [tokenAChange, tokenBChange, usdcChange] = await withBalanceChange(
  202. program.provider,
  203. [ORDERBOOK_ENV.godA, ORDERBOOK_ENV.godB, ORDERBOOK_ENV.godUsdc],
  204. async () => {
  205. // Perform the actual swap.
  206. await program.rpc.swapTransitive(
  207. new BN(swapAmount * 10 ** 6),
  208. new BN(swapAmount - 1),
  209. {
  210. accounts: {
  211. from: {
  212. market: marketB._decoded.ownAddress,
  213. requestQueue: marketB._decoded.requestQueue,
  214. eventQueue: marketB._decoded.eventQueue,
  215. bids: marketB._decoded.bids,
  216. asks: marketB._decoded.asks,
  217. coinVault: marketB._decoded.baseVault,
  218. pcVault: marketB._decoded.quoteVault,
  219. vaultSigner: marketBVaultSigner,
  220. // User params.
  221. openOrders: openOrdersB.publicKey,
  222. // Swapping from B -> USDC.
  223. orderPayerTokenAccount: ORDERBOOK_ENV.godB,
  224. coinWallet: ORDERBOOK_ENV.godB,
  225. },
  226. to: {
  227. market: marketA._decoded.ownAddress,
  228. requestQueue: marketA._decoded.requestQueue,
  229. eventQueue: marketA._decoded.eventQueue,
  230. bids: marketA._decoded.bids,
  231. asks: marketA._decoded.asks,
  232. coinVault: marketA._decoded.baseVault,
  233. pcVault: marketA._decoded.quoteVault,
  234. vaultSigner: marketAVaultSigner,
  235. // User params.
  236. openOrders: openOrdersA.publicKey,
  237. // Swapping from USDC -> A.
  238. orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc,
  239. coinWallet: ORDERBOOK_ENV.godA,
  240. },
  241. pcWallet: ORDERBOOK_ENV.godUsdc,
  242. authority: program.provider.wallet.publicKey,
  243. dexProgram: utils.DEX_PID,
  244. tokenProgram: TOKEN_PROGRAM_ID,
  245. rent: anchor.web3.SYSVAR_RENT_PUBKEY,
  246. },
  247. }
  248. );
  249. }
  250. );
  251. // TODO: calculate this dynamically from the swap amount.
  252. assert.ok(tokenAChange === 22.6);
  253. assert.ok(tokenBChange === -swapAmount);
  254. assert.ok(usdcChange === 0);
  255. });
  256. });
  257. // Side rust enum used for the program's RPC API.
  258. const Side = {
  259. Bid: { bid: {} },
  260. Ask: { ask: {} },
  261. };
  262. // Executes a closure. Returning the change in balances from before and after
  263. // its execution.
  264. async function withBalanceChange(provider, addrs, fn) {
  265. const beforeBalances = [];
  266. for (let k = 0; k < addrs.length; k += 1) {
  267. beforeBalances.push(
  268. (await serumCmn.getTokenAccount(provider, addrs[k])).amount
  269. );
  270. }
  271. await fn();
  272. const afterBalances = [];
  273. for (let k = 0; k < addrs.length; k += 1) {
  274. afterBalances.push(
  275. (await serumCmn.getTokenAccount(provider, addrs[k])).amount
  276. );
  277. }
  278. const deltas = [];
  279. for (let k = 0; k < addrs.length; k += 1) {
  280. deltas.push(
  281. (afterBalances[k].toNumber() - beforeBalances[k].toNumber()) / 10 ** 6
  282. );
  283. }
  284. return deltas;
  285. }