swap.js 11 KB

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