From 35714ca2d603ffc60aa6e6840ff2591347924f31 Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Sun, 25 Apr 2021 15:12:58 -0700 Subject: [PATCH] Update --- examples/swap/Anchor.toml | 2 +- examples/swap/programs/swap/src/lib.rs | 70 ++++++------- examples/swap/tests/swap.js | 131 ++++++++++++++----------- examples/swap/tests/utils/index.js | 39 +++++--- spl/Cargo.toml | 6 +- spl/src/dex.rs | 18 ++-- 6 files changed, 149 insertions(+), 117 deletions(-) diff --git a/examples/swap/Anchor.toml b/examples/swap/Anchor.toml index 7ace3db019..171d813417 100644 --- a/examples/swap/Anchor.toml +++ b/examples/swap/Anchor.toml @@ -3,4 +3,4 @@ wallet = "~/.config/solana/id.json" [[test.genesis]] address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" -program = "./deps/serum-dex/dex/target/bpfel-unknown-unknown/release/serum_dex.so" +program = "./deps/serum-dex/dex/target/deploy/serum_dex.so" diff --git a/examples/swap/programs/swap/src/lib.rs b/examples/swap/programs/swap/src/lib.rs index 23176915e5..715f2279ee 100644 --- a/examples/swap/programs/swap/src/lib.rs +++ b/examples/swap/programs/swap/src/lib.rs @@ -1,10 +1,16 @@ //! Program to perform instantly settled token swaps on the Serum DEX. +//! +//! Before using any method here, a user must first create an open orders +//! account on all markets being used. This only needs to be done once. As a +//! convention established by the DEX, this should be done via the system +//! program create account instruction in the same transaction as the user's +//! first trade. The DEX will lazily initialize the account. use anchor_lang::prelude::*; use anchor_spl::dex::serum_dex::instruction::SelfTradeBehavior; use anchor_spl::dex::serum_dex::matching::{OrderType, Side as SerumSide}; use anchor_spl::dex::serum_dex::state::MarketState; -use anchor_spl::dex::{self, DEX_PROGRAM_ID}; +use anchor_spl::dex::{self}; use anchor_spl::token::TokenAccount; use std::num::NonZeroU64; @@ -12,16 +18,12 @@ use std::num::NonZeroU64; pub mod swap { use super::*; - // Swaps two tokens on a single A/B market. This is just a direct IOC trade - // that instantly settles. + // Swaps two tokens on a single A/B market, where A is the base currency + // and B is the quote currency. This is just a direct IOC trade that + // instantly settles. // // When side is "bid", then swaps B for A. When side is "ask", then swaps // A for B. - // - // Before using this method, a user must first create an open orders account - // on the market being used. This should be done via the system program - // create account instruction in the same transaction as the user's first - // trade. The DEX will lazily initialize the account. #[access_control(is_valid_swap(&ctx))] pub fn swap<'info>( ctx: Context<'_, '_, '_, 'info, Swap<'info>>, @@ -61,9 +63,9 @@ pub mod swap { (from_token.reload()?.amount, to_token.reload()?.amount); // Calculate change in balances, i.e. the amount actually swapped. - let from_amount = from_amount_after - from_amount_before; - let to_amount = to_amount_after - to_amount_before; - let spill_amount = 0; + 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 apply_risk_checks( &ctx, @@ -94,11 +96,6 @@ pub mod swap { // * Settle open orders to get USD(x). // * IOC buy order on B/USD(x) market to convert USD(x) to token B. // * Settle open orders to get token B. - // - // Before using this method, a user must first create open orders accounts - // on both markets being used. This should be done via the system program - // create account instruction in the same transaction as the user's first - // trade. The DEX will lazily initialize the account. #[access_control(is_valid_swap_transitive(&ctx))] pub fn swap_transitive<'info>( ctx: Context<'_, '_, '_, 'info, SwapTransitive<'info>>, @@ -139,7 +136,10 @@ pub mod swap { }; // Report the delta. - (base_after - base_before, quote_after - quote_before) + ( + base_after.checked_sub(base_before).unwrap(), + quote_after.checked_sub(quote_before).unwrap(), + ) }; // The amount of the base token gained from the buy trade. @@ -166,7 +166,10 @@ pub mod swap { }; // Report the delta. - (base_after - base_before, quote_after - quote_before) + ( + base_after.checked_sub(base_before).unwrap(), + quote_after.checked_sub(quote_before).unwrap(), + ) }; apply_risk_checks_transitive( @@ -188,6 +191,7 @@ pub mod swap { } fn apply_risk_checks(ctx: &Context, event: DidSwap) -> Result<()> { + // msg!("event: {:?}", event); // todo // // TODO: check that the executed within some % from mid/fair. @@ -196,6 +200,7 @@ fn apply_risk_checks(ctx: &Context, event: DidSwap) -> Result<()> { } fn apply_risk_checks_transitive(ctx: &Context, event: DidSwap) -> Result<()> { + // msg!("event: {:?}", event); // todo // // TODO: check that the executed within some % from mid/fair. @@ -276,6 +281,7 @@ impl<'info> SwapTransitive<'info> { } } +// CPI client for sending orders to the Serum DEX. struct OrderbookClient<'info> { market: MarketAccounts<'info>, authority: AccountInfo<'info>, @@ -288,16 +294,15 @@ 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_PROGRAM_ID)?; + let market = MarketState::load(&self.market.market, &dex::ID)?; let limit_price = 1; let max_coin_qty = coin_lots(&market, size); - let max_native_pc_qty = 0; // Not used for asks. - //pc_native(&market, size, limit_price); + let max_native_pc_qty = 1; // Not used for asks. self.order_cpi( limit_price, max_coin_qty, max_native_pc_qty, - SerumSide::Ask, + Side::Ask, referral, ) } @@ -306,15 +311,14 @@ impl<'info> OrderbookClient<'info> { // base currency as possible, for the given `quote_amount`. Instantly // settles. fn buy(&self, quote_amount: u64, referral: Option>) -> ProgramResult { - let market = MarketState::load(&self.market.market, &*DEX_PROGRAM_ID)?; let limit_price = u64::MAX; - let max_coin_qty = 0; // Not used for bids. + let max_coin_qty = 1; // Not used for bids. let max_native_pc_qty = quote_amount; self.order_cpi( limit_price, max_coin_qty, max_native_pc_qty, - SerumSide::Bid, + Side::Bid, referral, ) } @@ -326,7 +330,7 @@ impl<'info> OrderbookClient<'info> { limit_price: u64, max_coin_qty: u64, max_native_pc_qty: u64, - side: SerumSide, + side: Side, referral: Option>, ) -> ProgramResult { // Client order id is only used for cancels. Not used here so hardcode. @@ -343,7 +347,7 @@ impl<'info> OrderbookClient<'info> { event_queue: self.market.event_queue.clone(), market_bids: self.market.bids.clone(), market_asks: self.market.asks.clone(), - order_payer: self.market.order_payer.clone(), + order_payer_token_account: self.market.order_payer_token_account.clone(), open_orders_authority: self.authority.clone(), coin_vault: self.market.coin_vault.clone(), pc_vault: self.market.pc_vault.clone(), @@ -356,7 +360,7 @@ impl<'info> OrderbookClient<'info> { } dex::new_order_v3( ctx, - side, + side.into(), NonZeroU64::new(limit_price).unwrap(), NonZeroU64::new(max_coin_qty).unwrap(), NonZeroU64::new(max_native_pc_qty).unwrap(), @@ -432,7 +436,7 @@ pub struct MarketAccounts<'info> { asks: AccountInfo<'info>, // The `spl_token::Account` that funds will be taken from. #[account(mut)] - order_payer: AccountInfo<'info>, + order_payer_token_account: AccountInfo<'info>, // Also known as the "base" currency. For a given A/B market, // this is the vault for the A mint. #[account(mut)] @@ -453,11 +457,11 @@ pub enum Side { Ask, } -impl From for Side { - fn from(side: SerumSide) -> Side { +impl From for SerumSide { + fn from(side: Side) -> SerumSide { match side { - SerumSide::Bid => Side::Bid, - SerumSide::Ask => Side::Ask, + Side::Bid => SerumSide::Bid, + Side::Ask => SerumSide::Ask, } } } diff --git a/examples/swap/tests/swap.js b/examples/swap/tests/swap.js index e901a8f6d0..c6f1ceb1b1 100644 --- a/examples/swap/tests/swap.js +++ b/examples/swap/tests/swap.js @@ -1,5 +1,8 @@ const assert = require("assert"); const anchor = require("@project-serum/anchor"); +const OpenOrders = require("@project-serum/serum").OpenOrders; +const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID; +const serumCmn = require("@project-serum/common"); const utils = require("./utils"); describe("swap", () => { @@ -8,68 +11,84 @@ describe("swap", () => { const program = anchor.workspace.Swap; - // The markets. - let MARKET_A_USDC, MARKET_B_USDC; + let ORDERBOOK_ENV; + const openOrdersA = new anchor.web3.Account(); + const openOrdersB = new anchor.web3.Account(); - // Account placing orders on the orderbook. - - it("Sets up two markets with resting orders", async () => { - const { marketA, marketB, marketMaker } = await utils.setupTwoMarkets({ + it("BOILERPLATE: Sets up two markets with resting orders", async () => { + ORDERBOOK_ENV = await utils.setupTwoMarkets({ provider: program.provider, }); + }); + + it("Swaps from token USDC to A", async () => { + const marketA = ORDERBOOK_ENV.marketA; + const [vaultSigner] = await utils.getVaultOwnerAndNonce( + marketA._decoded.ownAddress + ); - let myOrders = await marketA.loadOrdersForOwner( - program.provider.connection, - marketMaker.account.publicKey + const tokenABefore = await serumCmn.getTokenAccount( + program.provider, + ORDERBOOK_ENV.godA ); - console.log("orders", myOrders); - myOrders = await marketB.loadOrdersForOwner( - program.provider.connection, - marketMaker.account.publicKey + const usdcBefore = await serumCmn.getTokenAccount( + program.provider, + ORDERBOOK_ENV.godUsdc + ); + + 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], + }); + + const tokenAAFter = await serumCmn.getTokenAccount( + program.provider, + ORDERBOOK_ENV.godA + ); + const usdcAfter = await serumCmn.getTokenAccount( + program.provider, + ORDERBOOK_ENV.godUsdc ); - console.log("orders", myOrders); - }); - it("Is initialized!", async () => { - // await program.rpc.createUserAccount(); - /* - const tx = await program.rpc.swapBase(new BN(10), { - accounts: { - from: { - market: anchor.web3.SYSVAR_RENT_PUBKEY, - openOrders: anchor.web3.SYSVAR_RENT_PUBKEY, - openOrdersAuthority: program.provider.wallet.publicKey, - requestQueue: anchor.web3.SYSVAR_RENT_PUBKEY, - eventQueue: anchor.web3.SYSVAR_RENT_PUBKEY, - bids: anchor.web3.SYSVAR_RENT_PUBKEY, - asks: anchor.web3.SYSVAR_RENT_PUBKEY, - orderPayer: anchor.web3.SYSVAR_RENT_PUBKEY, - coinVault: anchor.web3.SYSVAR_RENT_PUBKEY, - pcVault: anchor.web3.SYSVAR_RENT_PUBKEY, - vaultSigner: anchor.web3.SYSVAR_RENT_PUBKEY, - coinWallet: anchor.web3.SYSVAR_RENT_PUBKEY, - }, - to: { - market: anchor.web3.SYSVAR_RENT_PUBKEY, - openOrders: anchor.web3.SYSVAR_RENT_PUBKEY, - openOrdersAuthority: program.provider.wallet.publicKey, - requestQueue: anchor.web3.SYSVAR_RENT_PUBKEY, - eventQueue: anchor.web3.SYSVAR_RENT_PUBKEY, - bids: anchor.web3.SYSVAR_RENT_PUBKEY, - asks: anchor.web3.SYSVAR_RENT_PUBKEY, - orderPayer: anchor.web3.SYSVAR_RENT_PUBKEY, - coinVault: anchor.web3.SYSVAR_RENT_PUBKEY, - pcVault: anchor.web3.SYSVAR_RENT_PUBKEY, - vaultSigner: anchor.web3.SYSVAR_RENT_PUBKEY, - coinWallet: anchor.web3.SYSVAR_RENT_PUBKEY, - }, - pcWallet: anchor.web3.SYSVAR_RENT_PUBKEY, - dexProgram: anchor.web3.SYSVAR_RENT_PUBKEY, - tokenProgram: anchor.web3.SYSVAR_RENT_PUBKEY, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, - } - }); - console.log("Your transaction signature", tx); - */ + console.log( + "Token A", + tokenAAFter.amount.sub(tokenABefore.amount).toNumber() + ); + console.log("usdc", usdcBefore.amount.sub(usdcAfter.amount).toNumber()); }); }); + +const Side = { + Bid: { bid: {} }, + Ask: { ask: {} }, +}; diff --git a/examples/swap/tests/utils/index.js b/examples/swap/tests/utils/index.js index 1ea4898d09..7e8366206f 100644 --- a/examples/swap/tests/utils/index.js +++ b/examples/swap/tests/utils/index.js @@ -103,8 +103,10 @@ async function setupTwoMarkets({ provider }) { marketMaker, mintA: MINT_A, mintB: MINT_B, + usdc: USDC, godA: GOD_A, godB: GOD_B, + godUsdc: GOD_USDC, }; } @@ -336,21 +338,10 @@ async function listMarket({ const quoteVault = new Account(); const quoteDustThreshold = new BN(100); - async function getVaultOwnerAndNonce() { - const nonce = new BN(0); - while (true) { - try { - const vaultOwner = await PublicKey.createProgramAddress( - [market.publicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)], - dexProgramId - ); - return [vaultOwner, nonce]; - } catch (e) { - nonce.iaddn(1); - } - } - } - const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce(); + const [vaultOwner, vaultSignerNonce] = await getVaultOwnerAndNonce( + market.publicKey, + dexProgramId + ); const tx1 = new Transaction(); tx1.add( @@ -492,9 +483,27 @@ async function sendAndConfirmRawTransaction( return await connection.confirmTransaction(tx, commitment); } +async function getVaultOwnerAndNonce(marketPublicKey, dexProgramId = DEX_PID) { + const nonce = new BN(0); + while (nonce.toNumber() < 255) { + try { + const vaultOwner = await PublicKey.createProgramAddress( + [marketPublicKey.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)], + dexProgramId + ); + return [vaultOwner, nonce]; + } catch (e) { + nonce.iaddn(1); + } + } + throw new Error("Unable to find nonce"); +} + module.exports = { fundAccount, setupMarket, initOrderbook, setupTwoMarkets, + DEX_PID, + getVaultOwnerAndNonce, }; diff --git a/spl/Cargo.toml b/spl/Cargo.toml index 4eae2e8fec..75f324ab46 100644 --- a/spl/Cargo.toml +++ b/spl/Cargo.toml @@ -8,7 +8,7 @@ description = "CPI clients for SPL programs" [dependencies] anchor-lang = { path = "../lang", version = "0.4.4", features = ["derive"] } -spl-token = { version = "3.0.1", features = ["no-entrypoint"] } -solana-program = "1.6.6" -serum_dex = { git = "https://github.com/project-serum/serum-dex", features = ["no-entrypoint"] } lazy_static = "1.4.0" +serum_dex = { git = "https://github.com/project-serum/serum-dex", features = ["no-entrypoint"] } +solana-program = "1.6.6" +spl-token = { version = "3.0.1", features = ["no-entrypoint"] } diff --git a/spl/src/dex.rs b/spl/src/dex.rs index da1d9de7d0..f5068eb41c 100644 --- a/spl/src/dex.rs +++ b/spl/src/dex.rs @@ -12,9 +12,7 @@ use std::num::NonZeroU64; pub use serum_dex; -lazy_static::lazy_static! { - pub static ref DEX_PROGRAM_ID: Pubkey = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin".parse().unwrap(); -} +anchor_lang::solana_program::declare_id!("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"); pub fn new_order_v3<'info>( ctx: CpiContext<'_, '_, '_, 'info, NewOrderV3<'info>>, @@ -35,14 +33,14 @@ pub fn new_order_v3<'info>( ctx.accounts.event_queue.key, ctx.accounts.market_bids.key, ctx.accounts.market_asks.key, - ctx.accounts.order_payer.key, + ctx.accounts.order_payer_token_account.key, ctx.accounts.open_orders_authority.key, ctx.accounts.coin_vault.key, ctx.accounts.pc_vault.key, ctx.accounts.token_program.key, ctx.accounts.rent.key, referral.map(|r| r.key), - &DEX_PROGRAM_ID, + &ID, side, limit_price, max_coin_qty, @@ -65,7 +63,7 @@ pub fn settle_funds<'info>( ) -> ProgramResult { let referral = ctx.remaining_accounts.iter().next(); let ix = serum_dex::instruction::settle_funds( - &DEX_PROGRAM_ID, + &ID, ctx.accounts.market.key, ctx.accounts.token_program.key, ctx.accounts.open_orders.key, @@ -93,7 +91,9 @@ pub struct NewOrderV3<'info> { pub event_queue: AccountInfo<'info>, pub market_bids: AccountInfo<'info>, pub market_asks: AccountInfo<'info>, - pub order_payer: AccountInfo<'info>, + // Token account where funds are transferred from for the order. If + // posting a bid market A/B, then this is the SPL token account for B. + pub order_payer_token_account: AccountInfo<'info>, pub open_orders_authority: AccountInfo<'info>, // Also known as the "base" currency. For a given A/B market, // this is the vault for the A mint. @@ -128,7 +128,7 @@ impl<'info> Market<'info> { } pub fn load_mut(&self) -> Result, ProgramError> { - MarketState::load(&self.acc_info, &DEX_PROGRAM_ID).map_err(Into::into) + MarketState::load(&self.acc_info, &ID).map_err(Into::into) } } @@ -144,7 +144,7 @@ impl<'info> anchor_lang::Accounts<'info> for Market<'info> { let account = &accounts[0]; *accounts = &accounts[1..]; let l = Market::new(account.clone()); - if l.acc_info.owner != &*DEX_PROGRAM_ID { + if l.acc_info.owner != &ID { return Err(ProgramError::Custom(1)); // todo: proper error } Ok(l)