diff --git a/examples/swap/README.md b/examples/swap/README.md index 2edac7bcd5..60778c2750 100644 --- a/examples/swap/README.md +++ b/examples/swap/README.md @@ -6,7 +6,7 @@ for performing instantly settled token swaps. ## Usage This example requires building the Serum DEX from source, which is done using -git submodles. +git submodules. ### Install Submodules diff --git a/examples/swap/programs/swap/src/lib.rs b/examples/swap/programs/swap/src/lib.rs index 715f2279ee..6ba66f0885 100644 --- a/examples/swap/programs/swap/src/lib.rs +++ b/examples/swap/programs/swap/src/lib.rs @@ -24,6 +24,8 @@ pub mod swap { // // When side is "bid", then swaps B for A. When side is "ask", then swaps // A for B. + // + // Amount is the amount to swap *from*. #[access_control(is_valid_swap(&ctx))] pub fn swap<'info>( ctx: Context<'_, '_, '_, 'info, Swap<'info>>, @@ -65,7 +67,7 @@ pub mod swap { // Calculate change in balances, i.e. the amount actually swapped. let from_amount = from_amount_before.checked_sub(from_amount_after).unwrap(); let to_amount = to_amount_after.checked_sub(to_amount_before).unwrap(); - let spill_amount = 0; // TODO + let spill_amount = 0; apply_risk_checks( &ctx, @@ -293,11 +295,14 @@ struct OrderbookClient<'info> { impl<'info> OrderbookClient<'info> { // Executes the sell order portion of the swap. Instantly settles. - fn sell(&self, size: u64, referral: Option>) -> ProgramResult { - let market = MarketState::load(&self.market.market, &dex::ID)?; + fn sell(&self, base_amount: u64, referral: Option>) -> ProgramResult { let limit_price = 1; - let max_coin_qty = coin_lots(&market, size); - let max_native_pc_qty = 1; // Not used for asks. + let max_coin_qty = { + // The loaded market must be dropped before CPI. + let market = MarketState::load(&self.market.market, &dex::ID)?; + coin_lots(&market, base_amount) + }; + let max_native_pc_qty = u64::MAX; self.order_cpi( limit_price, max_coin_qty, @@ -312,7 +317,7 @@ impl<'info> OrderbookClient<'info> { // settles. fn buy(&self, quote_amount: u64, referral: Option>) -> ProgramResult { let limit_price = u64::MAX; - let max_coin_qty = 1; // Not used for bids. + let max_coin_qty = u64::MAX; let max_native_pc_qty = quote_amount; self.order_cpi( limit_price, diff --git a/examples/swap/tests/swap.js b/examples/swap/tests/swap.js index c6f1ceb1b1..0ace9a4466 100644 --- a/examples/swap/tests/swap.js +++ b/examples/swap/tests/swap.js @@ -5,14 +5,27 @@ const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID; const serumCmn = require("@project-serum/common"); const utils = require("./utils"); +// Taker fee rate (bps). +const TAKER_FEE = .0022; + describe("swap", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.Provider.env()); + // Swap program client. const program = anchor.workspace.Swap; - let ORDERBOOK_ENV; - const openOrdersA = new anchor.web3.Account(); + // Accounts used to setup the orderbook. + let ORDERBOOK_ENV, + // Accounts used for A <-> USDC swap transactions. + SWAP_A_USDC_ACCOUNTS, + // Serum DEX vault PDA for market A/USDC. + marketAVaultSigner, + // Serum DEX vault PDA for market B/USDC. + marketBVaultSigner; + + // Open orders accounts on the two markets for the provider. + const openOrdersA = new anchor.web3.Account(); const openOrdersB = new anchor.web3.Account(); it("BOILERPLATE: Sets up two markets with resting orders", async () => { @@ -21,74 +34,129 @@ describe("swap", () => { }); }); - it("Swaps from token USDC to A", async () => { + it("BOILERPLATE: Sets up reusable accounts", async () => { const marketA = ORDERBOOK_ENV.marketA; - const [vaultSigner] = await utils.getVaultOwnerAndNonce( + const marketB = ORDERBOOK_ENV.marketA; + + const [vaultSignerA] = await utils.getVaultOwnerAndNonce( marketA._decoded.ownAddress ); + const [vaultSignerB] = await utils.getVaultOwnerAndNonce( + marketB._decoded.ownAddress + ); + marketAVaultSigner = vaultSignerA; + marketBVaultSigner = vaultSignerB; - const tokenABefore = await serumCmn.getTokenAccount( - program.provider, - ORDERBOOK_ENV.godA + SWAP_A_USDC_ACCOUNTS = { + market: { + market: marketA._decoded.ownAddress, + requestQueue: marketA._decoded.requestQueue, + eventQueue: marketA._decoded.eventQueue, + bids: marketA._decoded.bids, + asks: marketA._decoded.asks, + coinVault: marketA._decoded.baseVault, + pcVault: marketA._decoded.quoteVault, + vaultSigner: marketAVaultSigner, + // User params. + openOrders: openOrdersA.publicKey, + orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc, + coinWallet: ORDERBOOK_ENV.godA, + }, + pcWallet: ORDERBOOK_ENV.godUsdc, + authority: program.provider.wallet.publicKey, + dexProgram: utils.DEX_PID, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }; + }); + + it("Swaps from USDC to Token A", async () => { + const marketA = ORDERBOOK_ENV.marketA; + + // Swap exactly enough USDC to get 1.2 A tokens (best offer price is 6.041 USDC). + // This amount includes the 22 bps taker fee. + const expectedResultantAmount = 7.2; + const bestOfferPrice = 6.041; + const amountToSpend = expectedResultantAmount * bestOfferPrice; + const swapAmount = new anchor.BN( + (amountToSpend / (1 - TAKER_FEE)) * 10 ** 6 ); - const usdcBefore = await serumCmn.getTokenAccount( + + const [tokenAChange, usdcChange] = await withBalanceChange( program.provider, - ORDERBOOK_ENV.godUsdc + ORDERBOOK_ENV.godA, + ORDERBOOK_ENV.godUsdc, + async () => { + await program.rpc.swap(Side.Bid, swapAmount, { + accounts: SWAP_A_USDC_ACCOUNTS, + instructions: [ + // First order to this market so one must create the open orders account. + await OpenOrders.makeCreateAccountTransaction( + program.provider.connection, + marketA._decoded.ownAddress, + program.provider.wallet.publicKey, + openOrdersA.publicKey, + utils.DEX_PID + ), + ], + signers: [openOrdersA], + }); + } ); - const tx = await program.rpc.swap(Side.Bid, new anchor.BN(2), { - accounts: { - market: { - market: marketA._decoded.ownAddress, - requestQueue: marketA._decoded.requestQueue, - eventQueue: marketA._decoded.eventQueue, - bids: marketA._decoded.bids, - asks: marketA._decoded.asks, - coinVault: marketA._decoded.baseVault, - pcVault: marketA._decoded.quoteVault, - vaultSigner, - // User params. - openOrders: openOrdersA.publicKey, - orderPayerTokenAccount: ORDERBOOK_ENV.godUsdc, - coinWallet: ORDERBOOK_ENV.godA, - }, - pcWallet: ORDERBOOK_ENV.godUsdc, - authority: program.provider.wallet.publicKey, - dexProgram: utils.DEX_PID, - tokenProgram: TOKEN_PROGRAM_ID, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, - }, - instructions: [ - // First order to this market so one must create the open orders account. - await OpenOrders.makeCreateAccountTransaction( - program.provider.connection, - marketA._decoded.ownAddress, - program.provider.wallet.publicKey, - openOrdersA.publicKey, - utils.DEX_PID - ), - ], - signers: [openOrdersA], - }); + assert.ok(tokenAChange === expectedResultantAmount); + assert.ok(usdcChange === -swapAmount.toNumber() / 10 ** 6); + }); - const tokenAAFter = await serumCmn.getTokenAccount( - program.provider, - ORDERBOOK_ENV.godA + it("Swaps from Token A to USDC", async () => { + const marketA = ORDERBOOK_ENV.marketA; + + // Swap out A tokens for USDC. + const swapAmount = 8.1; + const bestBidPrice = 6.004; + const amountToFill = swapAmount * bestBidPrice; + const takerFee = 0.0022; + const resultantAmount = new anchor.BN( + (amountToFill / (1 - TAKER_FEE)) * 10 ** 6 ); - const usdcAfter = await serumCmn.getTokenAccount( + + const [tokenAChange, usdcChange] = await withBalanceChange( program.provider, - ORDERBOOK_ENV.godUsdc + ORDERBOOK_ENV.godA, + ORDERBOOK_ENV.godUsdc, + async () => { + await program.rpc.swap(Side.Ask, new anchor.BN(swapAmount * 10 ** 6), { + accounts: SWAP_A_USDC_ACCOUNTS, + }); + } ); - console.log( - "Token A", - tokenAAFter.amount.sub(tokenABefore.amount).toNumber() - ); - console.log("usdc", usdcBefore.amount.sub(usdcAfter.amount).toNumber()); + assert.ok(tokenAChange === -swapAmount); + assert.ok(usdcChange === resultantAmount.toNumber() / 10 ** 6); }); }); +// Side rust enum used for the program's RPC API. const Side = { Bid: { bid: {} }, Ask: { ask: {} }, }; + +// Executes a closure. Returning the change in balances from before and after +// its execution. +async function withBalanceChange(provider, addr1, addr2, fn) { + const tokenABefore = await serumCmn.getTokenAccount(provider, addr1); + const usdcBefore = await serumCmn.getTokenAccount(provider, addr2); + + await fn(); + + const tokenAAFter = await serumCmn.getTokenAccount(provider, addr1); + const usdcAfter = await serumCmn.getTokenAccount(provider, addr2); + + const tokenAChange = + (tokenAAFter.amount.toNumber() - tokenABefore.amount.toNumber()) / 10 ** 6; + const usdcChange = + (usdcAfter.amount.toNumber() - usdcBefore.amount.toNumber()) / 10 ** 6; + + return [tokenAChange, usdcChange]; +} diff --git a/examples/swap/tests/utils/index.js b/examples/swap/tests/utils/index.js index 7e8366206f..6274443875 100644 --- a/examples/swap/tests/utils/index.js +++ b/examples/swap/tests/utils/index.js @@ -10,6 +10,7 @@ const TokenInstructions = require("@project-serum/serum").TokenInstructions; const Market = require("@project-serum/serum").Market; const DexInstructions = require("@project-serum/serum").DexInstructions; const web3 = require("@project-serum/anchor").web3; +const Connection = web3.Connection; const BN = require("@project-serum/anchor").BN; const serumCmn = require("@project-serum/common"); const Account = web3.Account; @@ -263,8 +264,8 @@ async function setupMarket({ wallet: provider.wallet, baseMint: baseMint, quoteMint: quoteMint, - baseLotSize: 1000000, - quoteLotSize: 10000, + baseLotSize: 100000, + quoteLotSize: 100, dexProgramId: DEX_PID, feeRateBps: 0, }); @@ -286,7 +287,7 @@ async function setupMarket({ price: ask[0], size: ask[1], orderType: "postOnly", - clientId: undefined, // todo? + clientId: undefined, openOrdersAddressKey: undefined, openOrdersAccount: undefined, feeDiscountPubkey: null, @@ -307,7 +308,7 @@ async function setupMarket({ price: bid[0], size: bid[1], orderType: "postOnly", - clientId: undefined, // todo? + clientId: undefined, openOrdersAddressKey: undefined, openOrdersAccount: undefined, feeDiscountPubkey: null,