use super::IntentRecord;
use super::read::{record, record_from};
use crate::evm::ChainConfig;
use crate::order_types::{IntentLensV3, IntentSettlerV3};
use crate::service::intentscan::{self, Origins};
use alloy::primitives::{Address, B256};
use alloy::providers::DynProvider;
use eyre::{Result, ensure};
const PREVIEW_BATCH: usize = 100;
pub(super) async fn status(origins: &Origins<'_>, provider: &DynProvider, config: ChainConfig, id: B256) -> Result<Option<IntentRecord>> {
let Some(intent) = intentscan::intent(origins, config.id, id).await? else { return Ok(None) };
Ok(Some(record(provider, config, intent.order, intent.agent.map(|(agent, _)| agent)).await?))
}
pub(super) async fn list(origins: &Origins<'_>, provider: &DynProvider, config: ChainConfig, owner: Address, agent: Option<Address>) -> Result<Vec<IntentRecord>> {
let intents: Vec<_> = intentscan::history(origins, owner, config.id, 0).await?.into_iter()
.filter(|intent| agent.is_none() || intent.agent.map(|(signer, _)| signer) == agent).collect();
let lens = IntentLensV3::new(config.lens, provider.clone());
let mut out = Vec::new();
for chunk in intents.chunks(PREVIEW_BATCH) {
let views = lens.previewMany(chunk.iter().map(|intent| intent.order.clone()).collect()).call().await?;
ensure!(views.len() == chunk.len(), "lens returned {} views for {} intents", views.len(), chunk.len());
out.extend(chunk.iter().zip(&views).map(|(intent, view)| record_from(view, &intent.order, intent.agent.map(|(a, _)| a))));
}
Ok(out)
}
pub(super) async fn settled(provider: &DynProvider, config: ChainConfig, id: B256) -> Result<IntentRecord> {
let settler = IntentSettlerV3::new(config.settler, provider.clone());
let (filled, cancelled) = (settler.filled(id).call().await?, settler.cancelled(id).call().await?);
ensure!(filled || cancelled, "intent {id:?} was not found in the intent index or IntentAnnounced logs, and the settler marks it neither filled nor cancelled");
let status = if filled { "filled" } else { "cancelled" };
Ok(IntentRecord { id: format!("{id:?}"), placed_by: String::new(), owner: String::new(), agent: None, pair: String::new(),
amount_in: String::new(), start_out: String::new(), end_out: String::new(), window: String::new(), exclusive_window: false,
floor_now: String::new(), fee_now: String::new(), required_now: String::new(), floor_for_outsider: String::new(),
required_for_outsider: String::new(), status: status.into(),
reason: format!("settler marked {status}; the order is unavailable without the intent index or an IntentAnnounced log") })
}
#[cfg(test)]
mod tests {
use super::super::{ListInput, StatusInput, read::{list_from, status_from}};
use crate::{evm, order_types::{self, IntentLensV3, IntentSettlerV3}, service::{intentscan::{Origins, fixture}, test_http::TestHttp, test_rpc::{TestRpc, ok}}};
use alloy::{primitives::{Address, U256}, sol_types::{SolCall, SolValue}};
use serde_json::json;
fn chain(view: IntentLensV3::IntentView, filled: bool) -> TestRpc {
TestRpc::start(move |body| Some(ok(body, match body["method"].as_str().unwrap() {
"eth_blockNumber" => json!("0x100"),
"eth_getLogs" => json!([]),
_ => {
let data = hex::decode(body["params"][0]["input"].as_str().or(body["params"][0]["data"].as_str()).unwrap().trim_start_matches("0x")).unwrap();
let reply = match &data[..4] {
s if s == IntentLensV3::PREVIEW_LAYOUTCall::SELECTOR => U256::from(3).abi_encode(),
s if s == IntentLensV3::previewCall::SELECTOR => IntentLensV3::previewCall::abi_encode_returns(&view),
s if s == IntentLensV3::previewManyCall::SELECTOR =>
IntentLensV3::previewManyCall::abi_encode_returns(&vec![view.clone(); IntentLensV3::previewManyCall::abi_decode(&data).unwrap().o.len()]),
s if s == IntentSettlerV3::filledCall::SELECTOR => filled.abi_encode(),
s if s == IntentSettlerV3::cancelledCall::SELECTOR => false.abi_encode(),
other => panic!("unexpected selector {}", hex::encode(other)),
};
json!(format!("0x{}", hex::encode(reply)))
}
})))
}
fn order(nonce: u64) -> order_types::Order { fixture::order(Address::repeat_byte(4), Address::repeat_byte(1), 10, Address::repeat_byte(3), 5, nonce) }
fn index(orders: Vec<order_types::Order>) -> TestHttp {
TestHttp::start(move |target| {
let items: Vec<_> = orders.iter().map(|o| fixture::item(8453, o, Address::repeat_byte(2), 1, "open", 1_000, 2_000)).collect();
if target.starts_with("/v1/intents?") { return (200, fixture::page(items, None)); }
match orders.iter().position(|o| target == format!("/v1/intent/{:?}", order_types::order_id(o))) {
Some(i) => (200, json!({"intents": [items[i]]}).to_string()),
None => (404, r#"{"error":"not_found"}"#.into()),
}
})
}
fn input(order: &order_types::Order) -> StatusInput {
StatusInput { chain_id: "8453".into(), id: format!("{:?}", order_types::order_id(order)), lookback_blocks: Some(10) }
}
#[tokio::test]
async fn status_of_a_stream_only_intent_comes_from_the_index_and_the_lens() {
let config = evm::chain_config("8453").unwrap();
let server = index(vec![order(1)]);
let origins = Origins { stream: &server.url, data: &server.url };
for (view, expected) in [(fixture::view(true, false, true), "filled"), (fixture::view(false, false, false), "expired")] {
let rpc = chain(view, false);
let record = status_from(&origins, &evm::read_provider(&rpc.url).unwrap(), config, &input(&order(1))).await.unwrap();
assert_eq!((record.status.as_str(), record.agent.clone(), record.amount_in.as_str()), (expected, Some(format!("{:?}", Address::repeat_byte(2))), "10"));
assert_eq!(rpc.called("eth_getLogs"), 0, "no IntentAnnounced scan");
}
}
#[tokio::test]
async fn status_without_index_or_announce_log_falls_back_to_the_settler() {
let config = evm::chain_config("8453").unwrap();
let down = Origins { stream: "http://127.0.0.1:1", data: "http://127.0.0.1:1" };
let rpc = chain(fixture::view(true, false, true), true);
let record = status_from(&down, &evm::read_provider(&rpc.url).unwrap(), config, &input(&order(1))).await.unwrap();
assert_eq!((record.status.as_str(), record.owner.as_str()), ("filled", ""));
assert!(record.reason.contains("settler marked filled"), "{}", record.reason);
let unknown = chain(fixture::view(true, false, true), false);
let error = status_from(&down, &evm::read_provider(&unknown.url).unwrap(), config, &input(&order(1))).await.unwrap_err();
assert!(error.to_string().contains("neither filled nor cancelled"), "{error}");
}
#[tokio::test]
async fn owner_list_reads_the_index_and_one_batched_lens_call() {
let config = evm::chain_config("8453").unwrap();
let server = index(vec![order(1), order(2)]);
let origins = Origins { stream: &server.url, data: &server.url };
let rpc = chain(fixture::view(false, false, true), false);
let input = ListInput { chain_id: "8453".into(), owner: Some(Address::repeat_byte(4).to_string()), agent: None, lookback_blocks: None };
let records = list_from(&origins, &evm::read_provider(&rpc.url).unwrap(), config, &input).await.unwrap();
assert_eq!(records.iter().map(|r| r.status.as_str()).collect::<Vec<_>>(), ["open", "open"]);
assert_eq!((rpc.called("eth_call"), rpc.called("eth_getLogs")), (2, 0), "layout plus one previewMany");
}
}