diff --git a/bots/crates/market-maker/examples/initialization_helper.rs b/bots/crates/market-maker/examples/initialization_helper.rs index 6af08730..3b41a478 100644 --- a/bots/crates/market-maker/examples/initialization_helper.rs +++ b/bots/crates/market-maker/examples/initialization_helper.rs @@ -64,7 +64,8 @@ async fn main() -> anyhow::Result<()> { .await?; let seat = e2e - .find_seat(&maker_address)? + .fetch_seat(&maker_address) + .await? .expect("Should have a seat") .index; @@ -80,7 +81,8 @@ async fn main() -> anyhow::Result<()> { maker_keypair: maker.insecure_clone().to_base58_string(), market: e2e.market.market, maker_seat: e2e - .view_market()? + .view_market() + .await? .seats .iter() .find(|s| s.user == maker_address) diff --git a/bots/crates/market-maker/src/cli.rs b/bots/crates/market-maker/src/cli.rs index dbd3a52c..f3081bc6 100644 --- a/bots/crates/market-maker/src/cli.rs +++ b/bots/crates/market-maker/src/cli.rs @@ -77,4 +77,5 @@ pub async fn initialize_context_from_cli( target_base, initial_price_feed_response, ) + .await } diff --git a/bots/crates/market-maker/src/main.rs b/bots/crates/market-maker/src/main.rs index 27b813c8..91a01d24 100644 --- a/bots/crates/market-maker/src/main.rs +++ b/bots/crates/market-maker/src/main.rs @@ -31,10 +31,7 @@ use solana_client::{ }, }; use strum_macros::Display; -use tokio::{ - sync::watch, - time::sleep, -}; +use tokio::sync::watch; use tokio_stream::StreamExt; use transaction_parser::views::try_market_view_all_from_owner_and_data; @@ -241,6 +238,6 @@ async fn throttled_order_update( // Sleep for the throttle window in milliseconds before doing work again. // This effectively means the loop only does the cancel/post work once every window of time. - sleep(Duration::from_millis(throttle_window_ms)).await; + tokio::time::sleep(Duration::from_millis(throttle_window_ms)).await; } } diff --git a/bots/crates/market-maker/src/maker_context/mod.rs b/bots/crates/market-maker/src/maker_context/mod.rs index 0c33684e..16f67a83 100644 --- a/bots/crates/market-maker/src/maker_context/mod.rs +++ b/bots/crates/market-maker/src/maker_context/mod.rs @@ -67,7 +67,7 @@ pub struct MakerContext { impl MakerContext { /// Creates a new maker context from a token pair. - pub fn init( + pub async fn init( rpc: &CustomRpcClient, maker: Keypair, base_mint: Address, @@ -77,8 +77,8 @@ impl MakerContext { initial_price_feed_response: OandaCandlestickResponse, ) -> anyhow::Result { let market_ctx = - MarketContext::new_from_token_pair(rpc, base_mint, quote_mint, None, None)?; - let market = market_ctx.view_market(rpc)?; + MarketContext::new_from_token_pair(rpc, base_mint, quote_mint, None, None).await?; + let market = market_ctx.view_market(rpc).await?; let latest_state = MakerState::new_from_market(maker.pubkey(), market)?; let mid_price = get_normalized_mid_price(initial_price_feed_response, &pair, &market_ctx)?; let maker_address = maker.pubkey(); diff --git a/client/examples/close_seat.rs b/client/examples/close_seat.rs index 261c6f9e..3d66e72d 100644 --- a/client/examples/close_seat.rs +++ b/client/examples/close_seat.rs @@ -38,11 +38,12 @@ async fn main() -> anyhow::Result<()> { .send_single_signer(&e2e.rpc, &trader) .await?; - let market = e2e.view_market()?; + let market = e2e.view_market().await?; print_kv!("Seats before", market.header.num_seats, LogColor::Info); let user_seat = e2e - .find_seat(&trader.pubkey())? + .fetch_seat(&trader.pubkey()) + .await? .expect("User should have been registered on deposit"); e2e.market @@ -50,7 +51,7 @@ async fn main() -> anyhow::Result<()> { .send_single_signer(&e2e.rpc, &trader) .await?; - let market = e2e.view_market()?; + let market = e2e.view_market().await?; print_kv!("Seats after", market.header.num_seats, LogColor::Info); Ok(()) diff --git a/client/examples/deposit_and_withdraw.rs b/client/examples/deposit_and_withdraw.rs index f504f5ec..632ae4da 100644 --- a/client/examples/deposit_and_withdraw.rs +++ b/client/examples/deposit_and_withdraw.rs @@ -18,10 +18,11 @@ async fn main() -> anyhow::Result<()> { .send_single_signer(&e2e.rpc, &trader) .await?; - println!("{:#?}", e2e.view_market()); + println!("{:#?}", e2e.view_market().await?); let user_seat = e2e - .find_seat(&trader.pubkey())? + .fetch_seat(&trader.pubkey()) + .await? .expect("User should have been registered on deposit"); let res = e2e diff --git a/client/examples/many_instructions.rs b/client/examples/many_instructions.rs index 1e17147c..ef6f158d 100644 --- a/client/examples/many_instructions.rs +++ b/client/examples/many_instructions.rs @@ -54,12 +54,11 @@ async fn main() -> anyhow::Result<()> { ) .await?; - let seats: Vec = traders + let market_seats = e2e.view_market().await?.seats; + let trader_seats: Vec = traders .iter() .map(|trader| { - e2e.find_seat(&trader.address()) - .ok() - .flatten() + e2e.find_seat(&market_seats, &trader.address()) .expect("Trader should have a seat") .index }) @@ -76,7 +75,7 @@ async fn main() -> anyhow::Result<()> { let (deposits, withdraws): (Vec, Vec) = traders .iter() - .zip(seats) + .zip(trader_seats) .map(|(trader, seat)| { let trader_addr = trader.address(); let (deposit, withdraw) = base_amounts.get(&trader_addr).unwrap(); @@ -108,7 +107,7 @@ async fn main() -> anyhow::Result<()> { .sorted_by_key(|v| v.0) .collect_vec(); - let market = e2e.view_market()?; + let market = e2e.view_market().await?; // Check that seats are ordered by address (ascending) and compare the final state of each // user's seat to the expected state. diff --git a/client/examples/market_order.rs b/client/examples/market_order.rs index 42ce8418..fa9ee256 100644 --- a/client/examples/market_order.rs +++ b/client/examples/market_order.rs @@ -78,10 +78,10 @@ impl BalanceDelta { } impl Balances { - pub fn get(e2e: &E2e, user: &Address) -> anyhow::Result { + pub async fn fetch(e2e: &E2e, user: &Address) -> anyhow::Result { Ok(Self { - base: e2e.get_base_balance(user)?, - quote: e2e.get_quote_balance(user)?, + base: e2e.get_base_balance(user).await?, + quote: e2e.get_quote_balance(user).await?, }) } @@ -109,7 +109,7 @@ struct MarketSnapshot { } impl MarketSnapshot { - pub fn new(e2e: &E2e, order_ctx: &OrderContext) -> anyhow::Result { + pub async fn new(e2e: &E2e, order_ctx: &OrderContext<'_>) -> anyhow::Result { let order_info = order_ctx.order_info()?; let OrderContext { maker, @@ -117,7 +117,7 @@ impl MarketSnapshot { taker, .. } = order_ctx; - let market = e2e.view_market()?; + let market = e2e.view_market().await?; let maker_seat = market .seats @@ -136,8 +136,8 @@ impl MarketSnapshot { }) .cloned(); - let taker_balances = Balances::get(e2e, &taker.pubkey())?; - let market_balances = Balances::get(e2e, &e2e.market.market)?; + let taker_balances = Balances::fetch(e2e, &taker.pubkey()).await?; + let market_balances = Balances::fetch(e2e, &e2e.market.market).await?; Ok(Self { maker_order, @@ -214,7 +214,7 @@ async fn post_maker_order( .await?; // The maker should have the first and only seat in the market. - let market = e2e.view_market()?; + let market = e2e.view_market().await?; let maker_seat = market.seats.first().expect("Should have one market seat"); assert_eq!(maker_seat.user, maker.pubkey()); @@ -258,8 +258,8 @@ async fn initialize_traders_and_market(ctx: &OrderContext<'_>) -> anyhow::Result ) .await?; - let maker_init = Balances::get(&e2e, &ctx.maker.pubkey())?; - let taker_init = Balances::get(&e2e, &ctx.taker.pubkey())?; + let maker_init = Balances::fetch(&e2e, &ctx.maker.pubkey()).await?; + let taker_init = Balances::fetch(&e2e, &ctx.taker.pubkey()).await?; maker_init.check(maker_base, maker_quote)?; taker_init.check(taker_base, taker_quote)?; @@ -361,7 +361,7 @@ async fn main() -> anyhow::Result<()> { let e2e = initialize_traders_and_market(&ctx).await?; // Local helpers. - let create_snapshot = || MarketSnapshot::new(&e2e, &ctx); + let create_snapshot = async || MarketSnapshot::new(&e2e, &ctx).await; let taker_base_size = ctx.taker_size_base()?; let taker_quote_size = ctx.taker_size_quote()?; @@ -376,9 +376,9 @@ async fn main() -> anyhow::Result<()> { // ----------------------------------------------------------------------------------------- // Send the first taker order. - let before_1 = create_snapshot()?; + let before_1 = create_snapshot().await?; let fill_1 = run_partial_fill(&e2e, &ctx, Denomination::Base).await?; - let after_1 = create_snapshot()?; + let after_1 = create_snapshot().await?; assert_fill_deltas(side, &before_1, &after_1, taker_base_size, taker_quote_size); @@ -386,7 +386,7 @@ async fn main() -> anyhow::Result<()> { // Send the second taker order. let before_2 = after_1; let fill_2 = run_partial_fill(&e2e, &ctx, Denomination::Quote).await?; - let after_2 = create_snapshot()?; + let after_2 = create_snapshot().await?; assert_fill_deltas(side, &before_2, &after_2, taker_base_size, taker_quote_size); diff --git a/client/examples/post_and_cancel.rs b/client/examples/post_and_cancel.rs index 3dc2b9f3..a5b57eea 100644 --- a/client/examples/post_and_cancel.rs +++ b/client/examples/post_and_cancel.rs @@ -44,10 +44,11 @@ async fn main() -> anyhow::Result<()> { .send_single_signer(&e2e.rpc, trader) .await?; - println!("Market after user deposit\n{:#?}", e2e.view_market()?); + println!("Market after user deposit\n{:#?}", e2e.view_market().await?); let user_seat = e2e - .find_seat(&trader.pubkey())? + .fetch_seat(&trader.pubkey()) + .await? .expect("User should have been registered on deposit"); let order_info_args = OrderInfoArgs::new( @@ -75,9 +76,12 @@ async fn main() -> anyhow::Result<()> { post_ask_res.parsed_transaction.signature ); - println!("Market after posting user ask:\n{:#?}", e2e.view_market()?); + println!( + "Market after posting user ask:\n{:#?}", + e2e.view_market().await? + ); - let user_seat = e2e.find_seat(&trader.pubkey())?.unwrap(); + let user_seat = e2e.fetch_seat(&trader.pubkey()).await?.unwrap(); println!("User seat after posting ask: {user_seat:#?}"); let cancel_ask_res = e2e @@ -98,10 +102,13 @@ async fn main() -> anyhow::Result<()> { cancel_ask_res.parsed_transaction.signature ); - let user_seat = e2e.find_seat(&trader.pubkey())?.unwrap(); + let user_seat = e2e.fetch_seat(&trader.pubkey()).await?.unwrap(); println!("User seat after canceling ask: {user_seat:#?}"); - println!("Market after canceling user ask:\n{:#?}", e2e.view_market()); + println!( + "Market after canceling user ask:\n{:#?}", + e2e.view_market().await? + ); Ok(()) } diff --git a/client/examples/post_asks.rs b/client/examples/post_asks.rs index bb34b08b..f1fe764a 100644 --- a/client/examples/post_asks.rs +++ b/client/examples/post_asks.rs @@ -41,10 +41,11 @@ async fn main() -> anyhow::Result<()> { .send_single_signer(&e2e.rpc, trader) .await?; - println!("Market after user deposit\n{:#?}", e2e.view_market()?); + println!("Market after user deposit\n{:#?}", e2e.view_market().await?); let user_seat = e2e - .find_seat(&trader.pubkey())? + .fetch_seat(&trader.pubkey()) + .await? .expect("User should have been registered on deposit"); let order_info_args = OrderInfoArgs::new( @@ -70,9 +71,12 @@ async fn main() -> anyhow::Result<()> { post_ask_res.parsed_transaction.signature ); - println!("Market after posting user ask:\n{:#?}", e2e.view_market()?); + println!( + "Market after posting user ask:\n{:#?}", + e2e.view_market().await? + ); - let user_seat = e2e.find_seat(&trader.pubkey())?.unwrap(); + let user_seat = e2e.fetch_seat(&trader.pubkey()).await?.unwrap(); println!("User seat after posting ask: {user_seat:#?}"); // Post an ask. The user provides base as collateral and receives quote when filled. @@ -104,10 +108,10 @@ async fn main() -> anyhow::Result<()> { println!( "Market after posting many user asks:\n{:#?}", - e2e.view_market()? + e2e.view_market().await? ); - let user_seat = e2e.find_seat(&trader.pubkey())?.unwrap(); + let user_seat = e2e.fetch_seat(&trader.pubkey()).await?.unwrap(); println!("User seat after posting many asks: {user_seat:#?}"); Ok(()) diff --git a/client/examples/two_seats.rs b/client/examples/two_seats.rs index 7d97d966..72f87807 100644 --- a/client/examples/two_seats.rs +++ b/client/examples/two_seats.rs @@ -31,7 +31,7 @@ async fn main() -> anyhow::Result<()> { .send_single_signer(&e2e.rpc, payer_1) .await?; - let market = e2e.view_market()?; + let market = e2e.view_market().await?; // Sanity check. assert!(payer_1.pubkey() != payer_2.pubkey()); diff --git a/client/src/context/market.rs b/client/src/context/market.rs index 8ca089ca..1145db3c 100644 --- a/client/src/context/market.rs +++ b/client/src/context/market.rs @@ -65,15 +65,15 @@ impl Denomination { impl MarketContext { /// Creates a new [`MarketContext`] from two mint token addresses. Verifies that the tokens /// exist on-chain but does not verify the market does. - pub fn new_from_token_pair( + pub async fn new_from_token_pair( rpc: &CustomRpcClient, base_mint: Address, quote_mint: Address, base_mint_authority: Option, quote_mint_authority: Option, ) -> anyhow::Result { - let base = TokenContext::new_from_existing(rpc, base_mint, base_mint_authority)?; - let quote = TokenContext::new_from_existing(rpc, quote_mint, quote_mint_authority)?; + let base = TokenContext::new_from_existing(rpc, base_mint, base_mint_authority).await?; + let quote = TokenContext::new_from_existing(rpc, quote_mint, quote_mint_authority).await?; let (market_address, _bump) = find_market_address(&base.mint_address, "e.mint_address); let base_market_ata = base.get_ata_for(&market_address); @@ -141,18 +141,22 @@ impl MarketContext { .expect("Should be a single signer instruction") } - pub fn view_market(&self, rpc: &CustomRpcClient) -> anyhow::Result { - let market_account = rpc.client.get_account(&self.market)?; + pub async fn view_market(&self, rpc: &CustomRpcClient) -> anyhow::Result { + let market_account = rpc.client.get_account(&self.market).await?; try_market_view_all_from_owner_and_data(market_account.owner, &market_account.data) } - pub fn find_seat( + pub async fn fetch_seat( &self, rpc: &CustomRpcClient, user: &Address, ) -> anyhow::Result> { - let market_seats = self.view_market(rpc)?.seats; - Ok(market_seats.into_iter().find(|seat| &seat.user == user)) + let market = self.view_market(rpc).await?; + Ok(self.find_seat(&market.seats, user)) + } + + pub fn find_seat(&self, seats: &[MarketSeatView], user: &Address) -> Option { + seats.iter().find(|seat| &seat.user == user).cloned() } pub fn close_seat(&self, user: Address, sector_index_hint: u32) -> SingleSignerInstruction { diff --git a/client/src/context/token.rs b/client/src/context/token.rs index da2eb270..7cc445d8 100644 --- a/client/src/context/token.rs +++ b/client/src/context/token.rs @@ -37,12 +37,12 @@ pub struct TokenContext { impl TokenContext { /// Creates a new [`TokenContext`] from an existing token. Checks that the token mint exists /// on-chain and is owned by a valid token program. - pub fn new_from_existing( + pub async fn new_from_existing( rpc: &CustomRpcClient, mint_token: Address, mint_authority: Option, ) -> anyhow::Result { - let mint_account = rpc.client.get_account(&mint_token)?; + let mint_account = rpc.client.get_account(&mint_token).await?; check_spl_token_program_account(&mint_account.owner)?; let mint = Mint::unpack(&mint_account.data)?; @@ -82,7 +82,8 @@ impl TokenContext { ) -> anyhow::Result { let mint_rent = rpc .client - .get_minimum_balance_for_rent_exemption(Mint::LEN)?; + .get_minimum_balance_for_rent_exemption(Mint::LEN) + .await?; let create_mint_account = solana_system_interface::instruction::create_account( &mint_authority.pubkey(), &mint.pubkey(), @@ -168,9 +169,13 @@ impl TokenContext { .map(|txn| txn.parsed_transaction.signature) } - pub fn get_balance_for(&self, rpc: &CustomRpcClient, owner: &Address) -> anyhow::Result { + pub async fn get_balance_for( + &self, + rpc: &CustomRpcClient, + owner: &Address, + ) -> anyhow::Result { let ata = self.get_ata_for(owner); - let account_data = rpc.client.get_account_data(&ata)?; + let account_data = rpc.client.get_account_data(&ata).await?; let account_data = Account::unpack(&account_data)?; Ok(account_data.amount) } diff --git a/client/src/e2e_helpers/mod.rs b/client/src/e2e_helpers/mod.rs index 87c5da47..243ddada 100644 --- a/client/src/e2e_helpers/mod.rs +++ b/client/src/e2e_helpers/mod.rs @@ -112,19 +112,24 @@ impl E2e { }) } - pub fn view_market(&self) -> anyhow::Result { - self.market.view_market(&self.rpc) + pub async fn view_market(&self) -> anyhow::Result { + self.market.view_market(&self.rpc).await } - pub fn find_seat(&self, user: &Address) -> anyhow::Result> { - self.market.find_seat(&self.rpc, user) + pub async fn fetch_seat(&self, user: &Address) -> anyhow::Result> { + let market = self.view_market().await?; + Ok(self.find_seat(&market.seats, user)) } - pub fn get_base_balance(&self, user: &Address) -> anyhow::Result { - self.market.base.get_balance_for(&self.rpc, user) + pub fn find_seat(&self, seats: &[MarketSeatView], user: &Address) -> Option { + self.market.find_seat(seats, user) } - pub fn get_quote_balance(&self, user: &Address) -> anyhow::Result { - self.market.quote.get_balance_for(&self.rpc, user) + pub async fn get_base_balance(&self, user: &Address) -> anyhow::Result { + self.market.base.get_balance_for(&self.rpc, user).await + } + + pub async fn get_quote_balance(&self, user: &Address) -> anyhow::Result { + self.market.quote.get_balance_for(&self.rpc, user).await } } diff --git a/client/src/transactions.rs b/client/src/transactions.rs index e5217a87..151d2025 100644 --- a/client/src/transactions.rs +++ b/client/src/transactions.rs @@ -1,4 +1,4 @@ -//! Lightweight RPC client utilities for funding accounts, sending transactions, +//! Lightweight, nonblocking RPC client utilities for funding accounts, sending transactions, //! and pretty-printing `dropset`-related transaction logs. use std::collections::HashSet; @@ -9,7 +9,7 @@ use anyhow::{ }; use itertools::Itertools; use solana_address::Address; -use solana_client::rpc_client::RpcClient; +use solana_client::nonblocking::rpc_client::RpcClient; use solana_commitment_config::CommitmentConfig; use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_sdk::{ @@ -55,7 +55,7 @@ impl Default for CustomRpcClient { fn default() -> Self { CustomRpcClient { client: RpcClient::new_with_commitment( - "http://localhost:8899", + "http://localhost:8899".into(), CommitmentConfig::confirmed(), ), config: Default::default(), @@ -82,7 +82,7 @@ impl CustomRpcClient { pub fn new_from_url(url: &str, config: SendTransactionConfig) -> Self { CustomRpcClient { - client: RpcClient::new_with_commitment(url, CommitmentConfig::confirmed()), + client: RpcClient::new_with_commitment(url.into(), CommitmentConfig::confirmed()), config, } } @@ -128,16 +128,18 @@ pub const DEFAULT_FUND_AMOUNT: u64 = 10_000_000_000; async fn fund(rpc: &RpcClient, address: &Address) -> anyhow::Result<()> { let airdrop_signature: Signature = rpc .request_airdrop(address, DEFAULT_FUND_AMOUNT) + .await .context("Failed to request airdrop")?; let mut i = 0; // Wait for airdrop confirmation. while !rpc .confirm_transaction(&airdrop_signature) + .await .context("Couldn't confirm transaction")? && i < MAX_TRIES { - std::thread::sleep(std::time::Duration::from_millis(500)); + tokio::time::sleep(std::time::Duration::from_millis(500)).await; i += 1; } @@ -186,6 +188,7 @@ async fn send_transaction_with_config( ) -> anyhow::Result { let bh = rpc .get_latest_blockhash() + .await .or(Err(())) .expect("Should be able to get blockhash."); @@ -212,7 +215,7 @@ async fn send_transaction_with_config( ) .expect("Should sign"); - let res = rpc.send_and_confirm_transaction(&tx); + let res = rpc.send_and_confirm_transaction(&tx).await; match res { Ok(signature) => { let encoded = fetch_transaction_json(rpc, signature).await?; @@ -262,7 +265,7 @@ async fn send_transaction_with_config( } async fn fetch_transaction_json( - rpc: &solana_client::rpc_client::RpcClient, + rpc: &RpcClient, sig: Signature, ) -> anyhow::Result { rpc.get_transaction_with_config( @@ -273,16 +276,15 @@ async fn fetch_transaction_json( max_supported_transaction_version: Some(0), }, ) + .await .context("Should be able to fetch transaction with config") } /// Checks if an account at the given address exists on-chain. -pub async fn account_exists( - rpc: &solana_client::rpc_client::RpcClient, - address: &Address, -) -> anyhow::Result { +pub async fn account_exists(rpc: &RpcClient, address: &Address) -> anyhow::Result { Ok(rpc .get_account_with_commitment(address, CommitmentConfig::confirmed()) + .await .context("Couldn't retrieve account data")? .value .is_some()) diff --git a/grpc-stream/src/main.rs b/grpc-stream/src/main.rs index b2f6449f..2df063db 100644 --- a/grpc-stream/src/main.rs +++ b/grpc-stream/src/main.rs @@ -12,10 +12,7 @@ use grpc_stream::parse_update::{ InstructionEventsWithIndices, ParsedUpdate, }; -use tokio::time::{ - sleep, - Duration, -}; +use tokio::time::Duration; use yellowstone_grpc_client::GeyserGrpcClient; use yellowstone_grpc_proto::{ geyser::{ @@ -112,7 +109,7 @@ async fn main() -> anyhow::Result<()> { } Err(error) => { eprintln!("❌ Stream error: {}", error); - sleep(Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_secs(1)).await; } } } diff --git a/instruction-macros/crates/instruction-macros-impl/src/render/instruction_data/pack_tagged_and_unpack/pack_tagged.rs b/instruction-macros/crates/instruction-macros-impl/src/render/instruction_data/pack_tagged_and_unpack/pack_tagged.rs index 18b48b90..e57fb79c 100644 --- a/instruction-macros/crates/instruction-macros-impl/src/render/instruction_data/pack_tagged_and_unpack/pack_tagged.rs +++ b/instruction-macros/crates/instruction-macros-impl/src/render/instruction_data/pack_tagged_and_unpack/pack_tagged.rs @@ -38,7 +38,7 @@ pub fn render( // # Safety: `dst` has sufficient writable bytes. unsafe { ::write_bytes_tagged(self, dst) }; - // All bytes are initialized during the construction above. + // # Safety: All bytes are initialized during the construction above. unsafe { *(data.as_ptr() as *const [u8; #size_with_tag]) } } } diff --git a/instruction-macros/crates/instruction-macros-traits/src/unpack.rs b/instruction-macros/crates/instruction-macros-traits/src/unpack.rs index 8b89aad0..12f04034 100644 --- a/instruction-macros/crates/instruction-macros-traits/src/unpack.rs +++ b/instruction-macros/crates/instruction-macros-traits/src/unpack.rs @@ -21,14 +21,19 @@ use crate::pack::Pack; /// Callers of [`read_bytes`](Unpack::read_bytes) must: /// - Ensure `src` points to at least [`Pack::LEN`] bytes of readable memory. pub unsafe trait Unpack: Pack + Sized { - /// Reads [`Pack::LEN`] bytes from `src`. + /// Reads [`Pack::LEN`] bytes from `src` and constructs `Self` with them. /// /// Returns an error if the bytes at `src` represent an invalid byte pattern; e.g., a `bool` /// with a value of 2. /// /// # Safety /// - /// `src` must point to at least [`Pack::LEN`] bytes of readable memory. + /// Implementor guarantees: + /// - At most [`Pack::LEN`] bytes are read from `src`. + /// - `src` is read from as an unaligned pointer. + /// + /// Caller guarantees: + /// - `src` points to at least [`Pack::LEN`] bytes of readable memory. unsafe fn read_bytes(src: *const u8) -> Result; /// Checks that the length of the passed slice is sufficient before reading its bytes, then diff --git a/interface/src/instructions.rs b/interface/src/instructions/mod.rs similarity index 94% rename from interface/src/instructions.rs rename to interface/src/instructions/mod.rs index dcf9c2de..76dcf6f0 100644 --- a/interface/src/instructions.rs +++ b/interface/src/instructions/mod.rs @@ -7,7 +7,10 @@ //! The `program` feature: [`crate::instructions::generated_program`] //! The `client` feature: [`crate::instructions::generated_client`] +mod orders; + use instruction_macros::ProgramInstruction; +pub use orders::*; use price::OrderInfoArgs; #[repr(u8)] @@ -89,6 +92,12 @@ pub enum DropsetInstruction { #[args(user_sector_index_hint: u32, "A hint indicating which sector the user's seat resides in.")] CancelOrder, + #[account(0, name = "event_authority", desc = "The event authority PDA signer.")] + #[account(1, signer, name = "user", desc = "The user canceling an order.")] + #[account(2, writable, name = "market_account", desc = "The market account PDA.")] + #[account(3, name = "dropset_program", desc = "The dropset program itself, used for the self-CPI.")] + #[args(new_bids: Orders, "The new bids to replace the user's current bids.")] + #[args(new_asks: Orders, "The new asks to replace the user's current asks.")] BatchReplace, #[account(0, name = "event_authority", desc = "The event authority PDA signer.")] diff --git a/interface/src/instructions/orders.rs b/interface/src/instructions/orders.rs new file mode 100644 index 00000000..ce94ed22 --- /dev/null +++ b/interface/src/instructions/orders.rs @@ -0,0 +1,147 @@ +use core::mem::MaybeUninit; + +use instruction_macros::{ + Pack, + Unpack, +}; +use price::OrderInfoArgs; +use solana_program_error::ProgramError; +use static_assertions::const_assert_eq; + +use crate::state::user_order_sectors::MAX_ORDERS; + +#[repr(C)] +#[derive(Debug, Clone, Pack, Unpack, PartialEq, Eq)] +pub struct Orders { + /// The number of elements representing real order arguments in [`NewOrders::order_args`]. + /// This value will always be less than or equal to the max number of orders [`MAX_ORDERS`]. + /// The remaining elements will be zero-initialized but cannot be accessed through the + /// public [`Orders`] interface. + pub num_orders: u8, + /// Instruction data that isn't read is free, so it's simpler to always use [`MAX_ORDERS`] + /// elements in the array and simply ignore elements with an index >= [`NewOrders::num_orders`] + /// than to use a slice with a dynamic length. + order_args: OrdersArray, +} + +impl Orders { + pub fn new(orders: [OrderInfoArgs; N]) -> Self { + const { + // Number of orders must be <= MAX_ORDERS. + assert!(N <= MAX_ORDERS as usize); + } + + let mut res: [MaybeUninit; MAX_ORDERS as usize] = + [const { MaybeUninit::uninit() }; MAX_ORDERS as usize]; + + unsafe { + // Copy the orders passed in. This initializes `res[0..N]`. + // + // Safety: + // - `orders` is valid for `N` reads. + // - `res` is valid for `MAX_ORDERS` writes, and `MAX_ORDERS` >= `N`. + // - Both pointers are aligned and do not overlap. + core::ptr::copy_nonoverlapping( + orders.as_ptr(), + res.as_mut_ptr() as *mut OrderInfoArgs, + N, + ); + + // Write zeros to the remaining elements. This initializes `res[N..MAX_ORDERS]`. + // + // Safety: + // - `res.as_mut_ptr().add(N)` is valid for up to `MAX_ORDERS - N` writes and `N` is + // guaranteed to be <= `MAX_ORDERS`. + // - Zero is a valid value for all field types. + core::ptr::write_bytes( + (res.as_mut_ptr() as *mut OrderInfoArgs).add(N), + 0u8, + MAX_ORDERS as usize - N, + ); + } + + Self { + num_orders: N as u8, + // Safety: The array has been fully initialized with `MAX_ORDERS` elements. + order_args: OrdersArray(unsafe { + core::mem::transmute::< + [MaybeUninit; MAX_ORDERS as usize], + [OrderInfoArgs; MAX_ORDERS as usize], + >(res) + }), + } + } + + /// Exposes the valid order args elements as a slice view from 0..[`Self::num_orders`]`. + pub fn order_args(&self) -> &[OrderInfoArgs] { + &self.order_args.0[..self.num_orders as usize] + } +} + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq)] +struct OrdersArray([OrderInfoArgs; MAX_ORDERS as usize]); + +unsafe impl Pack for OrdersArray { + type Packed = [u8; OrderInfoArgs::LEN * MAX_ORDERS as usize]; + + /// # Safety + /// + /// Writes [`OrderInfoArgs::LEN`] bytes to `dst` [`MAX_ORDERS`] times, with each write + /// destination offset increased by the amount written each time. + unsafe fn write_bytes(&self, dst: *mut u8) { + // This implementation was written with the expectation that the max number of orders is 5. + // If that changes, the implementation needs to change to account for the different size. + const_assert_eq!(MAX_ORDERS, 5); + + let array = &self.0; + array[0].write_bytes(dst); + array[1].write_bytes(dst.add(OrderInfoArgs::LEN)); + array[2].write_bytes(dst.add(OrderInfoArgs::LEN * 2)); + array[3].write_bytes(dst.add(OrderInfoArgs::LEN * 3)); + array[4].write_bytes(dst.add(OrderInfoArgs::LEN * 4)); + } + + fn pack(&self) -> Self::Packed { + let mut data: [MaybeUninit; Self::LEN] = [MaybeUninit::uninit(); Self::LEN]; + let dst = data.as_mut_ptr() as *mut u8; + // Safety: `dst` points to `Self::LEN` contiguous, writable bytes. + unsafe { self.write_bytes(dst) }; + + // Safety: All bytes are initialized during the construction above. + unsafe { *(data.as_ptr() as *const [u8; Self::LEN]) } + } +} + +unsafe impl Unpack for OrdersArray { + /// # Safety (implementor) + /// + /// - Exactly [`Orders::LEN`] bytes are read from `src`. + /// - `src` is read from as an unaligned pointer. + /// + /// # Safety (caller) + /// + /// Caller must guarantee `src` points to at least [`Self::LEN`] bytes of readable memory. + unsafe fn read_bytes(src: *const u8) -> Result { + // This implementation was written with the expectation that the max number of orders is 5. + // If that changes, the implementation needs to change to account for the different size. + const_assert_eq!(MAX_ORDERS, 5); + + Ok(Self([ + OrderInfoArgs::read_bytes(src)?, + OrderInfoArgs::read_bytes(src.add(OrderInfoArgs::LEN))?, + OrderInfoArgs::read_bytes(src.add(OrderInfoArgs::LEN * 2))?, + OrderInfoArgs::read_bytes(src.add(OrderInfoArgs::LEN * 3))?, + OrderInfoArgs::read_bytes(src.add(OrderInfoArgs::LEN * 4))?, + ])) + } + + fn unpack(data: &[u8]) -> Result { + if data.len() < Self::LEN { + return Err(ProgramError::InvalidInstructionData); + } + + // Safety: `data` has at least `Self::LEN` bytes. + unsafe { Self::read_bytes(data.as_ptr()) } + } +}