use super::*;
use alloy::sol_types::SolType;
mod response;
const BROADCAST_URL: &str = "https://broadcast.intentscan.net/v1/stream/publish";
const BROADCAST_ORIGIN: &str = "https://broadcast.intentscan.net";
pub(super) fn validate(config: ChainConfig, input: &PlaceInput, allow_trade: bool) -> Result<()> {
let modes = u8::from(input.relay) + u8::from(input.self_submit) + u8::from(input.broadcast);
if modes > 1 || (allow_trade && !input.dry_run && modes != 1) {
return Err(eyre!("choose exactly one of --relay, --self-submit or --broadcast"));
}
if config.id == 1 && input.relay {
return Err(eyre!("Ethereum has no sponsored relay; use --self-submit or --broadcast"));
}
Ok(())
}
pub(super) async fn announce(
announcer: Announcer<'_>, input: &PlaceInput, signer: Arc<dyn Signer>,
config: ChainConfig, order: &Order, envelope: &Bytes, outcome: &mut PlaceOutcome,
) -> Result<()> {
if input.broadcast {
outcome.broadcast = Some(publish(BROADCAST_URL, &broadcast_body(config, order, envelope)).await?);
} else if input.relay {
let body = announce_body(config, order, envelope);
outcome.relay = Some(match announcer.pacer {
Some(pacer) => pacer.publish(announcer.relay, &body).await?,
None => announcer.relay.announce_intent(&body).await?,
});
} else {
let call = IntentSettlerV3::announceCall { o: order.clone(), auth: envelope.clone() };
let result = submit::send(&evm::rpc_url(config), signer, config.settler,
call.abi_encode().into(), announcer.wait).await?;
outcome.record(result);
}
Ok(())
}
fn broadcast_body(config: ChainConfig, order: &Order, envelope: &Bytes) -> serde_json::Value {
serde_json::json!({
"chainId": config.id, "standard": "agentswap-v6", "settler": config.settler,
"order": hex_bytes(&<Order as SolType>::abi_encode(order)), "auth": hex_bytes(envelope),
})
}
async fn publish(url: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
let identity = publication_identity(url, body);
if let Some(identity) = &identity { eprintln!("publishing {identity}"); }
publish_request(url, body).await.map_err(|error| {
let recovery = identity.unwrap_or_else(|| "broadcast stream".into());
eyre!("{}; {recovery}; admission is unknown; re-running after an error signs a new intent and may duplicate a still-active intent; a missing history record does not prove rejection; intent status and intent list --owner read the intent index first; intent list --agent reads only IntentAnnounced logs", crate::redact::urls(&error.to_string()))
})
}
fn publication_identity(url: &str, body: &serde_json::Value) -> Option<String> {
let encoded = hex::decode(body["order"].as_str()?.strip_prefix("0x")?).ok()?;
let order = <Order as SolType>::abi_decode(&encoded).ok()?;
let chain = body["chainId"].as_u64()?;
let settler: Address = body["settler"].as_str()?.parse().ok()?;
let stream = if url == BROADCAST_URL { BROADCAST_ORIGIN.to_string() } else { crate::redact::urls(url) };
let lookup = if url == BROADCAST_URL {
format!("; lookup: GET /v1/intents with owner={:?} and chain_ids={chain}; match order_hash to this intent id and verify standard/settler; paginate using next_before_ms as before_ms until null", order.owner)
} else { String::new() };
Some(format!("intent {:?}; chain {chain}; standard agentswap-v6; settler {settler:?}; stream {stream}{lookup}",
order_types::order_id(&order)))
}
async fn publish_request(url: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
let response = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(std::time::Duration::from_secs(20)).build()?
.post(url).json(body).send().await?;
let status = response.status();
if !status.is_success() {
return Err(eyre!("broadcast returned HTTP {status}: {}", response::diagnostic(response).await));
}
let bytes = response.bytes().await?;
if bytes.is_empty() { return Ok(serde_json::json!({"accepted": true})); }
Ok(serde_json::from_slice(&bytes)?)
}
#[cfg(test)]
mod tests;