Skip to content

Commit 64b01da

Browse files
committed
refactor: Implement multi-oracle architecture with route validation
1 parent a65572c commit 64b01da

9 files changed

Lines changed: 618 additions & 294 deletions

File tree

crates/solver-service/src/apis/quote/generation.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl QuoteGenerator {
161161
) -> Result<QuoteOrder, QuoteError> {
162162
// Standard determined by business logic context
163163
// Currently we only support EIP7683
164-
let standard = "eip7683";
164+
let _standard = "eip7683"; // Currently only supporting eip7683
165165

166166
// Extract chain from first output to find appropriate settlement
167167
// TODO: Implement support for multiple destination chains
@@ -173,14 +173,16 @@ impl QuoteGenerator {
173173
.ethereum_chain_id()
174174
.map_err(|e| QuoteError::InvalidRequest(format!("Invalid chain ID: {}", e)))?;
175175

176-
// Lookup settlement for exact standard and network
177-
let settlement = self
176+
// Get settlement AND selected oracle for consistency
177+
let (settlement, selected_oracle) = self
178178
.settlement_service
179-
.find_settlement_for_standard_and_network(standard, chain_id)
180-
.map_err(|e| QuoteError::InvalidRequest(e.to_string()))?;
179+
.get_any_settlement_for_chain(chain_id)
180+
.ok_or_else(|| QuoteError::InvalidRequest(
181+
format!("No settlement available for chain {}", chain_id)
182+
))?;
181183

182184
match escrow_kind {
183-
EscrowKind::Permit2 => self.generate_permit2_order(request, config, settlement),
185+
EscrowKind::Permit2 => self.generate_permit2_order(request, config, settlement, selected_oracle),
184186
EscrowKind::Erc3009 => self.generate_erc3009_order(request, config),
185187
}
186188
}
@@ -190,6 +192,7 @@ impl QuoteGenerator {
190192
request: &GetQuoteRequest,
191193
config: &Config,
192194
settlement: &dyn SettlementInterface,
195+
selected_oracle: solver_types::Address,
193196
) -> Result<QuoteOrder, QuoteError> {
194197
use alloy_primitives::hex;
195198

@@ -201,7 +204,7 @@ impl QuoteGenerator {
201204
})?;
202205
let domain_address = permit2_domain_address_from_config(config, chain_id)?;
203206
let (final_digest, message_obj) =
204-
build_permit2_batch_witness_digest(request, config, settlement)?;
207+
build_permit2_batch_witness_digest(request, config, settlement, selected_oracle)?;
205208
let message = serde_json::json!({ "digest": with_0x_prefix(&hex::encode(final_digest)), "eip712": message_obj });
206209
Ok(QuoteOrder {
207210
signature_type: SignatureType::Eip712,

crates/solver-service/src/apis/quote/signing/payloads/permit2.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ use solver_types::{
3535
pub fn build_permit2_batch_witness_digest(
3636
request: &GetQuoteRequest,
3737
config: &Config,
38-
settlement: &dyn SettlementInterface,
38+
_settlement: &dyn SettlementInterface, // Kept for potential future use
39+
selected_oracle: solver_types::Address,
3940
) -> Result<(B256, serde_json::Value), QuoteError> {
4041
// TODO: Implement support for multi-input/outputs
4142
let input = &request.available_inputs[0];
@@ -92,16 +93,8 @@ pub fn build_permit2_batch_witness_digest(
9293
QuoteError::InvalidRequest(format!("Permit2 not deployed on chain {}", origin_chain_id))
9394
})?;
9495

95-
// Oracle address (per origin chain) from the settlement implementation
96-
let input_oracle = settlement
97-
.get_oracle_address(origin_chain_id)
98-
.ok_or_else(|| {
99-
QuoteError::InvalidRequest(format!(
100-
"No oracle configured for chain {}",
101-
origin_chain_id
102-
))
103-
})?;
104-
let input_oracle = bytes20_to_alloy_address(&input_oracle.0)
96+
// Use the pre-selected oracle address
97+
let input_oracle = bytes20_to_alloy_address(&selected_oracle.0)
10598
.map_err(|e| QuoteError::InvalidRequest(format!("Invalid oracle address: {}", e)))?;
10699

107100
// Nonce and deadlines

crates/solver-settlement/src/implementations/direct.rs

Lines changed: 62 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
//! readiness checks using simple transaction receipt verification without
66
//! complex attestation mechanisms.
77
8-
use crate::{SettlementError, SettlementInterface};
9-
use alloy_primitives::{hex, Address as AlloyAddress, FixedBytes};
8+
use crate::{
9+
utils::parse_oracle_config, OracleConfig, SettlementError,
10+
SettlementInterface,
11+
};
12+
use alloy_primitives::{hex, FixedBytes};
1013
use alloy_provider::{Provider, RootProvider};
1114
use alloy_rpc_types::BlockTransactionsKind;
1215
use alloy_transport_http::Http;
1316
use async_trait::async_trait;
1417
use solver_types::{
15-
without_0x_prefix, ConfigSchema, Eip7683OrderData, Field, FieldType, FillProof, NetworksConfig,
18+
with_0x_prefix, ConfigSchema, Eip7683OrderData, Field, FieldType, FillProof, NetworksConfig,
1619
Order, Schema, TransactionHash,
1720
};
1821
use std::collections::HashMap;
@@ -22,35 +25,38 @@ use std::collections::HashMap;
2225
/// This implementation validates fills by checking transaction receipts
2326
/// and manages dispute periods before allowing claims.
2427
pub struct DirectSettlement {
25-
/// The order type this implementation handles
26-
order: String,
27-
/// Supported network IDs
28-
network_ids: Vec<u64>,
2928
/// RPC providers for each supported network.
3029
providers: HashMap<u64, RootProvider<Http<reqwest::Client>>>,
31-
/// Oracle addresses for each network (network_id -> oracle_address).
32-
oracle_addresses: HashMap<u64, String>,
30+
/// Oracle configuration including addresses and routes
31+
oracle_config: OracleConfig,
3332
/// Dispute period duration in seconds.
3433
dispute_period_seconds: u64,
3534
}
3635

3736
impl DirectSettlement {
3837
/// Creates a new DirectSettlement instance.
3938
///
40-
/// Configures settlement validation with multiple networks, oracle addresses,
39+
/// Configures settlement validation with oracle configuration
4140
/// and dispute period.
4241
pub async fn new(
43-
order: String,
44-
network_ids: Vec<u64>,
4542
networks: &NetworksConfig,
46-
oracle_addresses: HashMap<u64, String>,
43+
oracle_config: OracleConfig,
4744
dispute_period_seconds: u64,
4845
) -> Result<Self, SettlementError> {
49-
// Create RPC providers for each supported network
46+
// Create RPC providers for each network that has oracles configured
5047
let mut providers = HashMap::new();
48+
let mut all_network_ids = std::collections::HashSet::new();
5149

52-
for network_id in &network_ids {
53-
let network = networks.get(network_id).ok_or_else(|| {
50+
// Collect all network IDs from input and output oracles
51+
for network_id in oracle_config.input_oracles.keys() {
52+
all_network_ids.insert(*network_id);
53+
}
54+
for network_id in oracle_config.output_oracles.keys() {
55+
all_network_ids.insert(*network_id);
56+
}
57+
58+
for network_id in all_network_ids {
59+
let network = networks.get(&network_id).ok_or_else(|| {
5460
SettlementError::ValidationFailed(format!(
5561
"Network {} not found in configuration",
5662
network_id
@@ -70,26 +76,12 @@ impl DirectSettlement {
7076
))
7177
})?);
7278

73-
providers.insert(*network_id, provider);
74-
}
75-
76-
// Validate oracle addresses
77-
let mut validated_oracle_addresses = HashMap::new();
78-
for (network_id, oracle_address) in oracle_addresses {
79-
let oracle = oracle_address.parse::<AlloyAddress>().map_err(|e| {
80-
SettlementError::ValidationFailed(format!(
81-
"Invalid oracle address for network {}: {}",
82-
network_id, e
83-
))
84-
})?;
85-
validated_oracle_addresses.insert(network_id, oracle.to_string());
79+
providers.insert(network_id, provider);
8680
}
8781

8882
Ok(Self {
89-
order,
90-
network_ids: network_ids.clone(),
9183
providers,
92-
oracle_addresses: validated_oracle_addresses,
84+
oracle_config,
9385
dispute_period_seconds,
9486
})
9587
}
@@ -111,53 +103,27 @@ impl ConfigSchema for DirectSettlementSchema {
111103
let schema = Schema::new(
112104
// Required fields
113105
vec![
114-
Field::new("order", FieldType::String),
115106
Field::new(
116-
"network_ids",
117-
FieldType::Array(Box::new(FieldType::Integer {
118-
min: Some(1),
119-
max: None,
120-
})),
107+
"dispute_period_seconds",
108+
FieldType::Integer {
109+
min: Some(0),
110+
max: Some(86400),
111+
},
121112
),
122113
Field::new(
123-
"oracle_addresses",
114+
"oracles",
124115
FieldType::Table(Schema::new(
125-
vec![], // No required fields - network IDs are dynamic
126-
vec![], // No optional fields - all entries should be valid addresses
116+
vec![
117+
Field::new("input", FieldType::Table(Schema::new(vec![], vec![]))),
118+
Field::new("output", FieldType::Table(Schema::new(vec![], vec![]))),
119+
],
120+
vec![],
127121
)),
128-
)
129-
.with_validator(|value| {
130-
// Validate that all values in the table are valid Ethereum addresses
131-
if let Some(table) = value.as_table() {
132-
for (network_id, address_value) in table {
133-
if let Some(addr) = address_value.as_str() {
134-
if addr.len() != 42 || !addr.starts_with("0x") {
135-
return Err(format!(
136-
"oracle_addresses.{} must be a valid Ethereum address",
137-
network_id
138-
));
139-
}
140-
} else {
141-
return Err(format!(
142-
"oracle_addresses.{} must be a string",
143-
network_id
144-
));
145-
}
146-
}
147-
Ok(())
148-
} else {
149-
Err("oracle_addresses must be a table".to_string())
150-
}
151-
}),
122+
),
123+
Field::new("routes", FieldType::Table(Schema::new(vec![], vec![]))),
152124
],
153125
// Optional fields
154-
vec![Field::new(
155-
"dispute_period_seconds",
156-
FieldType::Integer {
157-
min: Some(0),
158-
max: Some(86400),
159-
},
160-
)],
126+
vec![Field::new("oracle_selection_strategy", FieldType::String)],
161127
);
162128

163129
schema.validate(config)
@@ -166,39 +132,14 @@ impl ConfigSchema for DirectSettlementSchema {
166132

167133
#[async_trait]
168134
impl SettlementInterface for DirectSettlement {
169-
fn supported_order(&self) -> &str {
170-
&self.order
171-
}
172-
173-
fn supported_networks(&self) -> &[u64] {
174-
&self.network_ids
135+
fn oracle_config(&self) -> &OracleConfig {
136+
&self.oracle_config
175137
}
176138

177139
fn config_schema(&self) -> Box<dyn ConfigSchema> {
178140
Box::new(DirectSettlementSchema)
179141
}
180142

181-
/// Returns the oracle address configured for a specific chain.
182-
///
183-
/// This implementation stores oracle addresses per chain in its configuration.
184-
/// The addresses are stored as hex strings and converted to the Address type on demand.
185-
///
186-
/// # Arguments
187-
/// * `chain_id` - The chain ID to get the oracle address for
188-
///
189-
/// # Returns
190-
/// * `Some(Address)` if an oracle is configured for this chain and the address is valid
191-
/// * `None` if no oracle is configured or the address format is invalid
192-
fn get_oracle_address(&self, chain_id: u64) -> Option<solver_types::Address> {
193-
self.oracle_addresses.get(&chain_id).and_then(|addr_str| {
194-
let hex_str = without_0x_prefix(addr_str);
195-
hex::decode(hex_str)
196-
.ok()
197-
.filter(|bytes| bytes.len() == 20)
198-
.map(solver_types::Address)
199-
})
200-
}
201-
202143
/// Gets attestation data for a filled order and generates a fill proof.
203144
///
204145
/// Since the transaction is already confirmed by the delivery service,
@@ -233,13 +174,25 @@ impl SettlementInterface for DirectSettlement {
233174
))
234175
})?;
235176

236-
// Get the oracle address for this chain
237-
let oracle_address = self.oracle_addresses.get(&origin_chain_id).ok_or_else(|| {
238-
SettlementError::ValidationFailed(format!(
239-
"No oracle address configured for chain {}",
177+
// Get the oracle address for this chain using the selection strategy
178+
let oracle_addresses = self.get_input_oracles(origin_chain_id);
179+
if oracle_addresses.is_empty() {
180+
return Err(SettlementError::ValidationFailed(format!(
181+
"No input oracle configured for chain {}",
240182
origin_chain_id
241-
))
242-
})?;
183+
)));
184+
}
185+
186+
// Use selection strategy with order nonce as context for deterministic selection
187+
let selection_context = order_data.nonce.to::<u64>();
188+
let oracle_address = self
189+
.select_oracle(&oracle_addresses, Some(selection_context))
190+
.ok_or_else(|| {
191+
SettlementError::ValidationFailed(format!(
192+
"Failed to select oracle for chain {}",
193+
origin_chain_id
194+
))
195+
})?;
243196

244197
// Convert tx hash
245198
let hash = FixedBytes::<32>::from_slice(&tx_hash.0);
@@ -283,7 +236,7 @@ impl SettlementInterface for DirectSettlement {
283236
Ok(FillProof {
284237
tx_hash: tx_hash.clone(),
285238
block_number: tx_block,
286-
oracle_address: oracle_address.clone(),
239+
oracle_address: with_0x_prefix(&hex::encode(&oracle_address.0)),
287240
attestation_data: Some(order_data.order_id.to_vec()),
288241
filled_timestamp: block_timestamp,
289242
})
@@ -361,51 +314,8 @@ pub fn create_settlement(
361314
DirectSettlementSchema::validate_config(config)
362315
.map_err(|e| SettlementError::ValidationFailed(format!("Invalid configuration: {}", e)))?;
363316

364-
// Get order type
365-
let order_standard = config
366-
.get("order")
367-
.and_then(|v| v.as_str())
368-
.ok_or_else(|| SettlementError::ValidationFailed("order is required".to_string()))?
369-
.to_string();
370-
371-
// Get network IDs
372-
let network_ids = config
373-
.get("network_ids")
374-
.and_then(|v| v.as_array())
375-
.ok_or_else(|| SettlementError::ValidationFailed("network_ids is required".to_string()))?
376-
.iter()
377-
.filter_map(|v| v.as_integer().map(|i| i as u64))
378-
.collect::<Vec<_>>();
379-
380-
if network_ids.is_empty() {
381-
return Err(SettlementError::ValidationFailed(
382-
"network_ids cannot be empty".to_string(),
383-
));
384-
}
385-
386-
// Get oracle addresses table
387-
let addresses_table = config
388-
.get("oracle_addresses")
389-
.and_then(|v| v.as_table())
390-
.ok_or_else(|| {
391-
SettlementError::ValidationFailed("oracle_addresses is required".to_string())
392-
})?;
393-
394-
// Build oracle addresses map
395-
let mut oracle_addresses = HashMap::new();
396-
for network_id in &network_ids {
397-
let network_id_str = network_id.to_string();
398-
let address = addresses_table
399-
.get(&network_id_str)
400-
.and_then(|v| v.as_str())
401-
.ok_or_else(|| {
402-
SettlementError::ValidationFailed(format!(
403-
"oracle_addresses missing entry for network {}",
404-
network_id
405-
))
406-
})?;
407-
oracle_addresses.insert(*network_id, address.to_string());
408-
}
317+
// Parse oracle configuration using common utilities
318+
let oracle_config = parse_oracle_config(config)?;
409319

410320
let dispute_period_seconds = config
411321
.get("dispute_period_seconds")
@@ -415,14 +325,7 @@ pub fn create_settlement(
415325
// Create settlement service synchronously
416326
let settlement = tokio::task::block_in_place(|| {
417327
tokio::runtime::Handle::current().block_on(async {
418-
DirectSettlement::new(
419-
order_standard,
420-
network_ids,
421-
networks,
422-
oracle_addresses,
423-
dispute_period_seconds,
424-
)
425-
.await
328+
DirectSettlement::new(networks, oracle_config, dispute_period_seconds).await
426329
})
427330
})?;
428331

0 commit comments

Comments
 (0)