Add init and close open orders instructions (#1)

This commit is contained in:
Armani Ferrante 2021-05-31 13:45:24 -07:00 committed by GitHub
parent 2b9fea9dfe
commit 9f2f036c99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 268 additions and 52 deletions

View File

@ -5,8 +5,8 @@ rust:
env:
global:
- NODE_VERSION="v14.7.0"
- SOLANA_VERSION="v1.6.6"
- ANCHOR_VERSION="v0.4.5"
- SOLANA_VERSION="v1.6.9"
- ANCHOR_VERSION="v0.7.0"
git:
submodules: true

View File

@ -1,3 +1,4 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

50
Cargo.lock generated
View File

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.4.7"
@ -23,9 +25,9 @@ checksum = "6b2d54853319fd101b8dd81de382bcbf3e03410a64d8928bbee85a3e7dcde483"
[[package]]
name = "anchor-attribute-access-control"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5753b98b698915b2c102224c1cf319beb625ee9749655f9b36eef37f4bfba8f"
checksum = "59a5a5b14eaa4535945c4799a2121d935dea2aa990e89b892985434471ed05ae"
dependencies = [
"anchor-syn",
"anyhow",
@ -37,9 +39,9 @@ dependencies = [
[[package]]
name = "anchor-attribute-account"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "546290e4fd03da3e617a684d767fe92b7ad9dcf7d5862202211f147221167df6"
checksum = "e1076ad7bc578c864fa559f16c172c1463d8cb02b6443b8061e623c212652921"
dependencies = [
"anchor-syn",
"anyhow",
@ -50,9 +52,9 @@ dependencies = [
[[package]]
name = "anchor-attribute-error"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659e40c9ea5651254949b21e6e58087a930b1353d02315a6e9115d5e17aa569"
checksum = "7acb3a4f83627cce3912d9915acf7db78b5f7c3fbec8375ba766db2f58aa2054"
dependencies = [
"anchor-syn",
"proc-macro2",
@ -62,9 +64,9 @@ dependencies = [
[[package]]
name = "anchor-attribute-event"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e1e2f99012c3eb302e75665d4be3666e7258c476e9a7ab7c267d9a4ba0fa8d1"
checksum = "e396eb7941e35f545e11d499300d738feccdc920e7dbcea30adcce349669e545"
dependencies = [
"anchor-syn",
"anyhow",
@ -75,9 +77,9 @@ dependencies = [
[[package]]
name = "anchor-attribute-interface"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c6aa4ad4cb1538d20797c8f6919ca8a8c6edd227526a6f9d1892a2121632f1"
checksum = "848ddeeeffdf3e3c2ea006bd8b00716bebae6ba92aeb34bdb7db16bc7744fb7e"
dependencies = [
"anchor-syn",
"anyhow",
@ -89,9 +91,9 @@ dependencies = [
[[package]]
name = "anchor-attribute-program"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afed272c6e1da83141f58ad793c965a939bf661991a97d18e36ecf8417ba9490"
checksum = "fc72e379b34a4f975f02dd6b500126e381000fd526e4388072fa2ef5e4aef2db"
dependencies = [
"anchor-syn",
"anyhow",
@ -102,9 +104,9 @@ dependencies = [
[[package]]
name = "anchor-attribute-state"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1224797a32c8a2888afc6c62b8476506ce1d7ca984ec9da8c027b1f56fcaf057"
checksum = "75d9cfa1d15e665f6c67e3bfb3d95069edfaaf140bf0c2c3f9f78455bed45ef4"
dependencies = [
"anchor-syn",
"anyhow",
@ -115,9 +117,9 @@ dependencies = [
[[package]]
name = "anchor-derive-accounts"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3318d2fe412eda4fe64d424ded7b5cb706cac7e20b3524d7618a727ed33c51af"
checksum = "89e0562d6e20af401acc334db975adf2f4243096102188d53ba767ebd47da93c"
dependencies = [
"anchor-syn",
"anyhow",
@ -128,9 +130,9 @@ dependencies = [
[[package]]
name = "anchor-lang"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97c07ac8ab867440e446ed30f20114baaa203ce68b25f4c889dd31604ef84565"
checksum = "8eab80a68813ec31bd9068e7e16665aae7d0bf1a96ef032155b837d66ed91620"
dependencies = [
"anchor-attribute-access-control",
"anchor-attribute-account",
@ -149,9 +151,9 @@ dependencies = [
[[package]]
name = "anchor-spl"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e23e56970600fc71f346ec42c5e5943f36f936272a4717ca55368e189ece9c4"
checksum = "dc7645f58767121a6e04681a444af5358c5780d211cffb2793b4e6c3d1de3c72"
dependencies = [
"anchor-lang",
"lazy_static",
@ -162,9 +164,9 @@ dependencies = [
[[package]]
name = "anchor-syn"
version = "0.4.5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a19a89111e400bb27aad8a57c70b43e8d9442a18a10880fce1fa3af166d4139"
checksum = "dbf72c2970b8dd3c81aa7da780357d24d535f11d878cbf186662a60c09771f6d"
dependencies = [
"anyhow",
"bs58",
@ -851,9 +853,9 @@ dependencies = [
[[package]]
name = "serum_dex"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5614c9e8e72610b17a51f024da2634a03252581689a2efda061190797372c2ef"
checksum = "dafc59d6c9502642898caafe8879ff7383c811cb0d6ca3a9e9b96feba9955465"
dependencies = [
"arrayref",
"bincode",

2
deps/serum-dex vendored

@ -1 +1 @@
Subproject commit 66904088599c1a8d42623f6a6d157cec46c8da62
Subproject commit 7d1d41538417aa8721aabea9503bf9d99eab7cc4

View File

@ -15,5 +15,5 @@ cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = "0.4.5"
anchor-spl = "0.4.5"
anchor-lang = "0.7.0"
anchor-spl = "0.7.0"

View File

@ -1,10 +1,11 @@
//! Program to perform instantly settled token swaps on the Serum DEX.
//!
//! Before using any instruction 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. Then, the DEX will lazily initialize the open orders account.
//! account on all markets being used. This only needs to be done once, either
//! via the system program create account instruction in the same transaction
//! as the user's first trade or via the explicit `init_account` and
//! `close_account` instructions provided here, which can be included in
//! transactions.
use anchor_lang::prelude::*;
use anchor_spl::dex;
@ -18,6 +19,22 @@ use std::num::NonZeroU64;
pub mod swap {
use super::*;
/// Convenience API to initialize an open orders account on the Serum DEX.
pub fn init_account<'info>(ctx: Context<'_, '_, '_, 'info, InitAccount<'info>>) -> Result<()> {
let ctx = CpiContext::new(ctx.accounts.dex_program.clone(), ctx.accounts.into());
dex::init_open_orders(ctx)?;
Ok(())
}
/// Convenience API to close an open orders account on the Serum DEX.
pub fn close_account<'info>(
ctx: Context<'_, '_, '_, 'info, CloseAccount<'info>>,
) -> Result<()> {
let ctx = CpiContext::new(ctx.accounts.dex_program.clone(), ctx.accounts.into());
dex::close_open_orders(ctx)?;
Ok(())
}
/// 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.
@ -213,6 +230,51 @@ fn apply_risk_checks<'info>(event: DidSwap) -> Result<()> {
Ok(())
}
#[derive(Accounts)]
pub struct InitAccount<'info> {
#[account(mut)]
open_orders: AccountInfo<'info>,
#[account(signer)]
authority: AccountInfo<'info>,
market: AccountInfo<'info>,
dex_program: AccountInfo<'info>,
rent: AccountInfo<'info>,
}
impl<'info> From<&mut InitAccount<'info>> for dex::InitOpenOrders<'info> {
fn from(accs: &mut InitAccount<'info>) -> dex::InitOpenOrders<'info> {
dex::InitOpenOrders {
open_orders: accs.open_orders.clone(),
authority: accs.authority.clone(),
market: accs.market.clone(),
rent: accs.rent.clone(),
}
}
}
#[derive(Accounts)]
pub struct CloseAccount<'info> {
#[account(mut)]
open_orders: AccountInfo<'info>,
#[account(signer)]
authority: AccountInfo<'info>,
#[account(mut)]
destination: AccountInfo<'info>,
market: AccountInfo<'info>,
dex_program: AccountInfo<'info>,
}
impl<'info> From<&mut CloseAccount<'info>> for dex::CloseOpenOrders<'info> {
fn from(accs: &mut CloseAccount<'info>) -> dex::CloseOpenOrders<'info> {
dex::CloseOpenOrders {
open_orders: accs.open_orders.clone(),
authority: accs.authority.clone(),
destination: accs.destination.clone(),
market: accs.market.clone(),
}
}
}
// The only constraint imposed on these accounts is that the market's base
// currency mint is not equal to the quote currency's. All other checks are
// done by the DEX on CPI.
@ -287,6 +349,7 @@ impl<'info> SwapTransitive<'info> {
}
// Client for sending orders to the Serum DEX.
#[derive(Clone)]
struct OrderbookClient<'info> {
market: MarketAccounts<'info>,
authority: AccountInfo<'info>,
@ -368,21 +431,7 @@ impl<'info> OrderbookClient<'info> {
// before giving up and posting the remaining unmatched order.
let limit = 65535;
let dex_accs = dex::NewOrderV3 {
market: self.market.market.clone(),
open_orders: self.market.open_orders.clone(),
request_queue: self.market.request_queue.clone(),
event_queue: self.market.event_queue.clone(),
market_bids: self.market.bids.clone(),
market_asks: self.market.asks.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(),
token_program: self.token_program.clone(),
rent: self.rent.clone(),
};
let mut ctx = CpiContext::new(self.dex_program.clone(), dex_accs);
let mut ctx = CpiContext::new(self.dex_program.clone(), self.clone().into());
if let Some(srm_msrm_discount) = srm_msrm_discount {
ctx = ctx.with_remaining_accounts(vec![srm_msrm_discount]);
}
@ -419,6 +468,25 @@ impl<'info> OrderbookClient<'info> {
}
}
impl<'info> From<OrderbookClient<'info>> for dex::NewOrderV3<'info> {
fn from(c: OrderbookClient<'info>) -> dex::NewOrderV3<'info> {
dex::NewOrderV3 {
market: c.market.market.clone(),
open_orders: c.market.open_orders.clone(),
request_queue: c.market.request_queue.clone(),
event_queue: c.market.event_queue.clone(),
market_bids: c.market.bids.clone(),
market_asks: c.market.asks.clone(),
order_payer_token_account: c.market.order_payer_token_account.clone(),
open_orders_authority: c.authority.clone(),
coin_vault: c.market.coin_vault.clone(),
pc_vault: c.market.pc_vault.clone(),
token_program: c.token_program.clone(),
rent: c.rent.clone(),
}
}
}
// Returns the amount of lots for the base currency of a trade with `size`.
fn coin_lots(market: &MarketState, size: u64) -> u64 {
size.checked_div(market.coin_lot_size).unwrap()

View File

@ -1,5 +1,7 @@
const assert = require("assert");
const anchor = require("@project-serum/anchor");
const Account = anchor.web3.Account;
const Transaction = anchor.web3.Transaction;
const BN = anchor.BN;
const OpenOrders = require("@project-serum/serum").OpenOrders;
const TOKEN_PROGRAM_ID = require("@solana/spl-token").TOKEN_PROGRAM_ID;
@ -80,6 +82,150 @@ describe("swap", () => {
};
});
// For testing the initialization and closing of the open orders account.
const ooAccount = new Account();
it("Initializes an open orders account", async () => {
// Balance before the tx.
const beforeAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
const marketA = ORDERBOOK_ENV.marketA;
const openOrders = ooAccount;
await program.rpc.initAccount({
accounts: {
openOrders: openOrders.publicKey,
authority: program.provider.wallet.publicKey,
market: marketA._decoded.ownAddress,
dexProgram: utils.DEX_PID,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
instructions: [
await OpenOrders.makeCreateAccountTransaction(
program.provider.connection,
marketA._decoded.ownAddress,
program.provider.wallet.publicKey,
openOrders.publicKey,
utils.DEX_PID
),
],
signers: [openOrders],
});
const accountInfo = await program.provider.connection.getAccountInfo(
openOrders.publicKey
);
const serumPadding = accountInfo.data.slice(0, 5);
const accountFlags = accountInfo.data[5];
// b"serum".
assert.ok(serumPadding.equals(Buffer.from([115, 101, 114, 117, 109])));
// Initialized | OpenOrders.
assert.ok(accountFlags === 5);
// Balance after the tx.
const afterAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
const solChange = beforeAccount.lamports - afterAccount.lamports;
// The fee to create and initialize the account toggles between these
// to for some reason? 64 lamports.
assert.ok(solChange === 23367808 || solChange === 23367744);
});
it("Closes an open orders account", async () => {
// Balance before the tx.
const beforeAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
const marketA = ORDERBOOK_ENV.marketA;
const openOrders = ooAccount;
await program.rpc.closeAccount({
accounts: {
openOrders: openOrders.publicKey,
authority: program.provider.wallet.publicKey,
destination: program.provider.wallet.publicKey,
market: marketA._decoded.ownAddress,
dexProgram: utils.DEX_PID,
},
});
// Check the account was garbage collected.
const accountInfo = await program.provider.connection.getAccountInfo(
openOrders.publicKey
);
assert.ok(accountInfo === null);
// Balance after the tx.
const afterAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
// Should get the rent exemption sol back.
const solChange = afterAccount.lamports - beforeAccount.lamports;
assert.ok(solChange === 23352768);
});
it("Does not pay rent exemption sol in a single transaction", async () => {
// Balance before the tx.
const beforeAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
// Build the tx.
const openOrders = new Account();
const marketA = ORDERBOOK_ENV.marketA;
const tx = new Transaction();
tx.add(
await OpenOrders.makeCreateAccountTransaction(
program.provider.connection,
marketA._decoded.ownAddress,
program.provider.wallet.publicKey,
openOrders.publicKey,
utils.DEX_PID
)
);
tx.add(
program.instruction.initAccount({
accounts: {
openOrders: openOrders.publicKey,
authority: program.provider.wallet.publicKey,
market: marketA._decoded.ownAddress,
dexProgram: utils.DEX_PID,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
})
);
tx.add(
program.instruction.closeAccount({
accounts: {
openOrders: openOrders.publicKey,
authority: program.provider.wallet.publicKey,
destination: program.provider.wallet.publicKey,
market: marketA._decoded.ownAddress,
dexProgram: utils.DEX_PID,
},
})
);
// Send it.
await program.provider.send(tx, [openOrders]);
// Balance after the transaction.
const afterAccount = await program.provider.connection.getAccountInfo(
program.provider.wallet.publicKey
);
// Only paid transaction fees. No rent exemption sol.
const solChange = beforeAccount.lamports - afterAccount.lamports;
// The fee to create the account toggles between +- 64 lamports.
// So we must adjust for that here.
assert.ok(solChange === 10048 || solChange === 9984);
});
it("Swaps from USDC to Token A", async () => {
const marketA = ORDERBOOK_ENV.marketA;
@ -124,9 +270,8 @@ describe("swap", () => {
);
}
);
assert.ok(tokenAChange === expectedResultantAmount);
assert.ok(usdcChange === -swapAmount.toNumber() / 10 ** 6);
assert.ok(-usdcChange <= swapAmount.toNumber() / 10 ** 6);
});
it("Swaps from Token A to USDC", async () => {
@ -216,7 +361,7 @@ describe("swap", () => {
assert.ok(tokenAChange === -swapAmount);
// TODO: calculate this dynamically from the swap amount.
assert.ok(tokenBChange === 9.8);
assert.ok(usdcChange === 0);
assert.ok(usdcChange >= 0);
});
it("Swaps from Token B to Token A", async () => {
@ -277,7 +422,7 @@ describe("swap", () => {
// TODO: calculate this dynamically from the swap amount.
assert.ok(tokenAChange === 22.6);
assert.ok(tokenBChange === -swapAmount);
assert.ok(usdcChange === 0);
assert.ok(usdcChange >= 0);
});
});