From 55731bd34437076beaf378275e735ad9de75354a Mon Sep 17 00:00:00 2001 From: Alastair Ong Date: Tue, 28 Apr 2026 21:08:49 +0100 Subject: [PATCH 1/4] bindings: switch IERC20 interface to ERC20 contract bindings The sol! macro for ERC20 now uses the actual contract artifact (`ERC20.sol/ERC20.json`) with `rpc` derives enabled, instead of the generic IERC20 interface. This unblocks call/send paths that need the RPC-aware instance type and matches what alloy now expects for the `approveCall` schema (field is `value`, not `amount`). Mechanical follow-on changes: - All `approveCall { amount: ... }` call sites and decoded-call assertions rename `amount` -> `value` to match the ERC20 ABI. - `IERC20Instance` -> `ERC20Instance`, `IERC20::symbolCall` -> `ERC20::symbolCall`, `IERC20::decimalsCall` -> `ERC20::decimalsCall` in the common ERC20 helper, fuzz impls, and vaults tests. No behavior change beyond the binding switch. --- crates/bindings/src/lib.rs | 2 +- crates/common/src/deposit.rs | 4 ++-- crates/common/src/erc20.rs | 6 +++--- crates/common/src/fuzz/impls.rs | 6 +++--- crates/common/src/raindex_client/take_orders/result.rs | 2 +- crates/common/src/raindex_client/vaults.rs | 6 +++--- crates/common/src/take_orders/preflight.rs | 7 +++++-- crates/js_api/src/gui/order_operations.rs | 2 +- 8 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index b3d232009c..1298ab67e3 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -18,7 +18,7 @@ sol!( ); sol!( - #![sol(all_derives = true)] + #![sol(all_derives = true, rpc)] ERC20, "../../out/ERC20.sol/ERC20.json" ); diff --git a/crates/common/src/deposit.rs b/crates/common/src/deposit.rs index abd395f938..9caa74d86e 100644 --- a/crates/common/src/deposit.rs +++ b/crates/common/src/deposit.rs @@ -96,7 +96,7 @@ impl DepositArgs { if !current_allowance_float.eq(self.amount)? { let approve_call = approveCall { spender: transaction_args.orderbook_address, - amount: self.amount.to_fixed_decimal(self.decimals)?, + value: self.amount.to_fixed_decimal(self.decimals)?, }; let params = transaction_args.try_into_write_contract_parameters(approve_call, self.token)?; @@ -232,7 +232,7 @@ mod tests { }; let approve_call = approveCall { spender: Address::ZERO, - amount: U256::from(100), + value: U256::from(100), }; let params = args .try_into_write_contract_parameters(approve_call.clone(), Address::ZERO) diff --git a/crates/common/src/erc20.rs b/crates/common/src/erc20.rs index bf32ca8aa6..65bbeea718 100644 --- a/crates/common/src/erc20.rs +++ b/crates/common/src/erc20.rs @@ -5,7 +5,7 @@ use alloy_ethers_typecast::ReadContractParametersBuilderError; use rain_error_decoding::{AbiDecodeFailedErrors, AbiDecodedErrorType}; use rain_orderbook_app_settings::token::TokenCfg; use rain_orderbook_bindings::provider::{mk_read_provider, ReadProvider, ReadProviderError}; -use rain_orderbook_bindings::IERC20::IERC20Instance; +use rain_orderbook_bindings::ERC20::ERC20Instance; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -87,9 +87,9 @@ impl ERC20 { Self { rpcs, address } } - fn get_instance(&self) -> Result, Error> { + fn get_instance(&self) -> Result, Error> { let provider = mk_read_provider(&self.rpcs)?; - let erc20 = IERC20Instance::new(self.address, provider); + let erc20 = ERC20Instance::new(self.address, provider); Ok(erc20) } diff --git a/crates/common/src/fuzz/impls.rs b/crates/common/src/fuzz/impls.rs index 7aca970328..54c1f93615 100644 --- a/crates/common/src/fuzz/impls.rs +++ b/crates/common/src/fuzz/impls.rs @@ -28,7 +28,7 @@ use rain_orderbook_app_settings::{ order::OrderIOCfg, yaml::{dotrain::DotrainYaml, YamlError, YamlParsable}, }; -use rain_orderbook_bindings::IERC20; +use rain_orderbook_bindings::ERC20; use std::collections::HashMap; use std::sync::Arc; use thiserror::Error; @@ -417,7 +417,7 @@ impl FuzzRunner { .alloy_call( deployer.address, input_token.address, - IERC20::symbolCall {}, + ERC20::symbolCall {}, false, ) .await?; @@ -426,7 +426,7 @@ impl FuzzRunner { .alloy_call( deployer.address, output_token.address, - IERC20::symbolCall {}, + ERC20::symbolCall {}, false, ) .await?; diff --git a/crates/common/src/raindex_client/take_orders/result.rs b/crates/common/src/raindex_client/take_orders/result.rs index 04b083f3b2..7b4724b87f 100644 --- a/crates/common/src/raindex_client/take_orders/result.rs +++ b/crates/common/src/raindex_client/take_orders/result.rs @@ -672,7 +672,7 @@ mod tests { .expect("Should decode approval calldata"); let expected_truncated = U256::from(22_446_685u64); assert_eq!( - decoded.amount, expected_truncated, + decoded.value, expected_truncated, "Approved amount should be 22.446685 truncated to 6 decimals = 22446685" ); diff --git a/crates/common/src/raindex_client/vaults.rs b/crates/common/src/raindex_client/vaults.rs index d96da88958..ce369901e8 100644 --- a/crates/common/src/raindex_client/vaults.rs +++ b/crates/common/src/raindex_client/vaults.rs @@ -517,7 +517,7 @@ impl RaindexVault { let calldata = approveCall { spender: transaction_args.orderbook_address, - amount: amount.to_fixed_decimal(self.token.decimals)?, + value: amount.to_fixed_decimal(self.token.decimals)?, } .abi_encode(); @@ -2154,7 +2154,7 @@ mod tests { use alloy::primitives::{address, b256}; use alloy::sol_types::SolCall; use httpmock::MockServer; - use rain_orderbook_bindings::IERC20::decimalsCall; + use rain_orderbook_bindings::ERC20::decimalsCall; use rain_orderbook_bindings::{ IOrderBookV6::{deposit4Call, withdraw4Call}, IERC20::approveCall, @@ -3160,7 +3160,7 @@ mod tests { Bytes::copy_from_slice( &approveCall { spender: Address::from_str(CHAIN_ID_1_ORDERBOOK_ADDRESS).unwrap(), - amount: U256::from(600000000000000000000u128), + value: U256::from(600000000000000000000u128), } .abi_encode(), ) diff --git a/crates/common/src/take_orders/preflight.rs b/crates/common/src/take_orders/preflight.rs index c824d6373e..03ac03bcc0 100644 --- a/crates/common/src/take_orders/preflight.rs +++ b/crates/common/src/take_orders/preflight.rs @@ -140,7 +140,10 @@ pub async fn check_taker_balance_and_allowance( } pub fn build_approval_calldata(spender: Address, amount: U256) -> Bytes { - let call = approveCall { spender, amount }; + let call = approveCall { + spender, + value: amount, + }; Bytes::from(call.abi_encode()) } @@ -286,7 +289,7 @@ mod tests { assert!(decoded.is_ok()); let decoded = decoded.unwrap(); assert_eq!(decoded.spender, spender); - assert_eq!(decoded.amount, amount); + assert_eq!(decoded.value, amount); } } diff --git a/crates/js_api/src/gui/order_operations.rs b/crates/js_api/src/gui/order_operations.rs index 3f92008fa3..0452e32137 100644 --- a/crates/js_api/src/gui/order_operations.rs +++ b/crates/js_api/src/gui/order_operations.rs @@ -357,7 +357,7 @@ impl DotrainOrderGui { if !allowance_float.eq(*deposit_amount)? { let calldata = approveCall { spender: tx_args.orderbook_address, - amount: deposit_amount.to_fixed_decimal(decimals)?, + value: deposit_amount.to_fixed_decimal(decimals)?, } .abi_encode(); From e0161eee68d09873bce1602cb9de8dbbed53659d Mon Sep 17 00:00:00 2001 From: Alastair Ong Date: Tue, 28 Apr 2026 21:08:58 +0100 Subject: [PATCH 2/4] bindings: avoid request amplification in mk_read_provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FallbackLayer::with_active_transport_count(rpcs.len())` dispatches every request to ALL configured transports in parallel — the opposite of what operators expect when supplying multiple RPCs for load sharing or burst absorption. With N RPCs, every read becomes N reads, and downstream rate limits fire N times sooner. Set `active_transport_count = 1` so the FallbackLayer health-routes a single transport per request and falls back to others on error/429, preserving load-sharing semantics. Also short-circuit the empty-rpcs case before constructing the layer to make the error path obvious. No behavior change for single-RPC setups; multi-RPC setups stop amplifying requests. --- crates/bindings/src/provider.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/bindings/src/provider.rs b/crates/bindings/src/provider.rs index 3f6d593337..3992ff1c77 100644 --- a/crates/bindings/src/provider.rs +++ b/crates/bindings/src/provider.rs @@ -22,10 +22,17 @@ pub enum ReadProviderError { } pub fn mk_read_provider(rpcs: &[Url]) -> Result { - let size = rpcs.len(); + if rpcs.is_empty() { + return Err(ReadProviderError::NoRpcs); + } + // Use one active transport per request: alloy's FallbackLayer health-routes + // to the best-scored transport and falls back to others on error/429. With + // `active_transport_count = rpcs.len()` it would dispatch every request to + // ALL transports in parallel (request amplification), defeating the purpose + // of providing multiple RPCs for load sharing. let fallback_layer = FallbackLayer::default() - .with_active_transport_count(NonZeroUsize::new(size).ok_or(ReadProviderError::NoRpcs)?); + .with_active_transport_count(NonZeroUsize::new(1).expect("1 is non-zero")); let transports = rpcs .iter() From 34bd830566adf9591a8ab7f9ca7b4fe6f64a68fd Mon Sep 17 00:00:00 2001 From: Alastair Ong Date: Tue, 28 Apr 2026 21:09:05 +0100 Subject: [PATCH 3/4] local_db: raise SQLite busy_timeout from 500ms to 10s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 500ms is below the writer-contention window that shows up under real ingest concurrency: parallel readers being held off by a single long write hit `SQLITE_BUSY` returns to the caller. 10s is conservative for WAL-mode reads and aligned with the upstream rusqlite default recommendation for high-concurrency workloads. No correctness change — just a longer wait window before surfacing SQLITE_BUSY to the application. --- crates/common/src/local_db/executor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/common/src/local_db/executor.rs b/crates/common/src/local_db/executor.rs index 182e4164e1..aeb8d29d6e 100644 --- a/crates/common/src/local_db/executor.rs +++ b/crates/common/src/local_db/executor.rs @@ -56,7 +56,7 @@ fn open_connection(db_path: &Path) -> Result { .map_err(|e| LocalDbQueryError::database(format!("Failed to open database: {e}")))?; conn.pragma_update(None, "journal_mode", "wal") .map_err(|e| LocalDbQueryError::database(format!("Failed to set WAL journal mode: {e}")))?; - conn.busy_timeout(Duration::from_millis(500)) + conn.busy_timeout(Duration::from_secs(10)) .map_err(|e| LocalDbQueryError::database(format!("Failed to set busy_timeout: {e}")))?; functions::register_all(&conn).map_err(|e| { LocalDbQueryError::database(format!("Failed to register sqlite functions: {e}")) From 76ca3b25d344f55c5c7797117ca3a0aa4d78d4fe Mon Sep 17 00:00:00 2001 From: Alastair Ong Date: Tue, 28 Apr 2026 21:09:16 +0100 Subject: [PATCH 4/4] quote: thread per-order signed_contexts through batch quoting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional `signed_contexts: Option<&[Vec]>` parameter to `get_order_quotes` and `get_order_quotes_batch`. When supplied, the per-order context (indexed by order position) is attached to each generated `QuoteV2` request as `signedContext`. When `None` (or out of range), behavior is unchanged: empty signedContext, matching the prior default. Why: callers gating order-fill on a signed context (e.g. an off-chain authorization passed via SignedContextInjector) need to quote with the same signed context they intend to take with — otherwise the gate check on the calculate-io side fails at simulation, producing misleading "no liquidity" results for orders that would actually fill. Backward compatible: every existing call site passes `None` and gets the previous behavior. Only one new parameter, no field renames. --- .../common/src/raindex_client/order_quotes.rs | 13 ++++++++----- crates/common/src/take_orders/candidates.rs | 2 +- crates/quote/src/order_quotes.rs | 18 ++++++++++++------ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/common/src/raindex_client/order_quotes.rs b/crates/common/src/raindex_client/order_quotes.rs index 9c4ae6d891..85814c5e48 100644 --- a/crates/common/src/raindex_client/order_quotes.rs +++ b/crates/common/src/raindex_client/order_quotes.rs @@ -2,7 +2,7 @@ use super::*; use crate::raindex_client::orders::RaindexOrder; use crate::raindex_client::orders_list::RaindexOrders; use rain_math_float::Float; -use rain_orderbook_bindings::IOrderBookV6::OrderV4; +use rain_orderbook_bindings::IOrderBookV6::{OrderV4, SignedContextV1}; use rain_orderbook_quote::{get_order_quotes, BatchOrderQuotesResponse, OrderQuoteValue, Pair}; use rain_orderbook_subgraph_client::utils::float::{F0, F1}; use std::ops::{Div, Mul}; @@ -127,6 +127,7 @@ impl RaindexOrder { block_number, rpcs.iter().map(|s| s.to_string()).collect(), chunk_size.map(|v| v as usize), + None, ) .await?; @@ -184,7 +185,7 @@ impl RaindexClient { )] chunk_size: Option, ) -> Result>, RaindexError> { - get_order_quotes_batch(orders.inner(), block_number, chunk_size).await + get_order_quotes_batch(orders.inner(), block_number, chunk_size, None).await } } @@ -192,6 +193,7 @@ pub async fn get_order_quotes_batch( orders: &[RaindexOrder], block_number: Option, chunk_size: Option, + signed_contexts: Option<&[Vec]>, ) -> Result>, RaindexError> { if orders.is_empty() { return Ok(vec![]); @@ -240,6 +242,7 @@ pub async fn get_order_quotes_batch( block_number, rpcs, chunk_size.map(|v| v as usize), + signed_contexts, ) .await?; @@ -446,7 +449,7 @@ mod tests { #[tokio::test] async fn test_get_order_quotes_batch_empty() { - let result = get_order_quotes_batch(&[], None, None).await; + let result = get_order_quotes_batch(&[], None, None, None).await; assert!(result.is_ok()); assert!(result.unwrap().is_empty()); } @@ -583,7 +586,7 @@ mod tests { .await .unwrap(); - let result = get_order_quotes_batch(&[order], None, None).await.unwrap(); + let result = get_order_quotes_batch(&[order], None, None, None).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].len(), 1); @@ -677,7 +680,7 @@ mod tests { .unwrap(); let orders = vec![order.clone(), order]; - let result = get_order_quotes_batch(&orders, None, None).await.unwrap(); + let result = get_order_quotes_batch(&orders, None, None, None).await.unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].len(), 1); diff --git a/crates/common/src/take_orders/candidates.rs b/crates/common/src/take_orders/candidates.rs index 62796bc6d0..ccff27f569 100644 --- a/crates/common/src/take_orders/candidates.rs +++ b/crates/common/src/take_orders/candidates.rs @@ -74,7 +74,7 @@ pub async fn build_take_order_candidates_for_pair( block_number: Option, chunk_size: Option, ) -> Result, RaindexError> { - let all_quotes = get_order_quotes_batch(orders, block_number, chunk_size).await?; + let all_quotes = get_order_quotes_batch(orders, block_number, chunk_size, None).await?; orders .iter() diff --git a/crates/quote/src/order_quotes.rs b/crates/quote/src/order_quotes.rs index 8aa70534f4..92a6d68245 100644 --- a/crates/quote/src/order_quotes.rs +++ b/crates/quote/src/order_quotes.rs @@ -5,7 +5,7 @@ use crate::{ }; use alloy::primitives::{Address, U256}; use alloy_ethers_typecast::ReadableClient; -use rain_orderbook_bindings::IOrderBookV6::{OrderV4, QuoteV2}; +use rain_orderbook_bindings::IOrderBookV6::{OrderV4, QuoteV2, SignedContextV1}; use rain_orderbook_subgraph_client::types::common::SgOrder; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -43,6 +43,7 @@ pub async fn get_order_quotes( block_number: Option, rpcs: Vec, chunk_size: Option, + signed_contexts: Option<&[Vec]>, ) -> Result, Error> { let req_block_number = match block_number { Some(block) => block, @@ -56,9 +57,13 @@ pub async fn get_order_quotes( let mut all_pairs: Vec = Vec::new(); let mut all_quote_targets: Vec = Vec::new(); - for order in &orders { + for (order_idx, order) in orders.iter().enumerate() { let order_struct: OrderV4 = order.clone().try_into()?; let orderbook = Address::from_str(&order.orderbook.id.0)?; + let order_signed_context = signed_contexts + .and_then(|ctxs| ctxs.get(order_idx)) + .cloned() + .unwrap_or_default(); for (input_index, input) in order_struct.validInputs.iter().enumerate() { for (output_index, output) in order_struct.validOutputs.iter().enumerate() { @@ -103,7 +108,7 @@ pub async fn get_order_quotes( order: order_struct.clone(), inputIOIndex: U256::from(input_index), outputIOIndex: U256::from(output_index), - signedContext: vec![], + signedContext: order_signed_context.clone(), }, }); } @@ -389,7 +394,7 @@ amount price: context<3 0>() context<4 0>(); let order = create_sg_order(&setup, order, inputs, outputs); - let result = get_order_quotes(vec![order], None, vec![setup.local_evm.url()], None) + let result = get_order_quotes(vec![order], None, vec![setup.local_evm.url()], None, None) .await .unwrap(); @@ -466,7 +471,7 @@ amount price: context<3 0>() context<4 0>(); let mut invalid_order = create_sg_order(&setup, order.clone(), vec![], vec![]); invalid_order.orderbook.id = SgBytes("invalid_address".to_string()); - let err = get_order_quotes(vec![invalid_order], None, vec![setup.local_evm.url()], None) + let err = get_order_quotes(vec![invalid_order], None, vec![setup.local_evm.url()], None, None) .await .unwrap_err(); @@ -475,7 +480,7 @@ amount price: context<3 0>() context<4 0>(); // Test invalid order bytes let invalid_order = create_sg_order(&setup, B256::random().to_string(), vec![], vec![]); - let err = get_order_quotes(vec![invalid_order], None, vec![setup.local_evm.url()], None) + let err = get_order_quotes(vec![invalid_order], None, vec![setup.local_evm.url()], None, None) .await .unwrap_err(); @@ -492,6 +497,7 @@ amount price: context<3 0>() context<4 0>(); None, vec!["invalid_rpc_url".to_string()], None, + None, ) .await .unwrap_err();