use anyhow::{anyhow, Result};
use bsv_wallet_toolbox::monitor::ArcadeEventsTask;
use bsv_wallet_toolbox::{MonitorStorage, ProofIngestOutcome, StorageSqlx};
use std::sync::atomic::AtomicBool;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IngestAction {
ProofIngested,
ProofRejected(String),
StatusApplied,
StatusIgnored,
}
pub async fn ingest_arc_payload(
storage: &StorageSqlx,
payload: &serde_json::Value,
) -> Result<IngestAction> {
let txid = payload
.get("txid")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("payload missing txid"))?;
if txid.len() != 64 || !txid.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(anyhow!("invalid txid"));
}
let tx_status = payload
.get("txStatus")
.and_then(|v| v.as_str())
.unwrap_or("");
let merkle_path_hex = payload
.get("merklePath")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
if let Some(mp_hex) = merkle_path_hex {
let merkle_path = hex::decode(mp_hex).map_err(|e| anyhow!("merklePath not hex: {}", e))?;
let block_height = payload
.get("blockHeight")
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow!("merklePath payload missing blockHeight"))?
as u32;
let block_hash = payload
.get("blockHash")
.and_then(|v| v.as_str())
.unwrap_or_default();
let _ = storage.mark_transaction_seen_on_network(txid).await;
match storage
.ingest_merkle_proof(txid, &merkle_path, block_height, block_hash, None)
.await
.map_err(|e| anyhow!("ingest_merkle_proof: {}", e))?
{
ProofIngestOutcome::Ingested(status) => {
tracing::info!(
txid = %txid,
block_height = ?status.block_height,
"arc-callback: merkle proof ingested"
);
Ok(IngestAction::ProofIngested)
}
ProofIngestOutcome::InvalidMerkleRoot { computed_root } => {
tracing::warn!(txid = %txid, computed_root = %computed_root, "arc-callback: proof rejected (invalid merkle root)");
Ok(IngestAction::ProofRejected("invalid merkle root".into()))
}
ProofIngestOutcome::InvalidProof(e) => {
tracing::warn!(txid = %txid, error = %e, "arc-callback: proof rejected (unparseable)");
Ok(IngestAction::ProofRejected(e))
}
ProofIngestOutcome::TrackerError(e) => {
tracing::warn!(txid = %txid, error = %e, "arc-callback: proof deferred (ChainTracker error) — polling sync will retry");
Ok(IngestAction::ProofRejected(format!("tracker error: {}", e)))
}
}
} else {
let trigger = AtomicBool::new(false);
let updated =
ArcadeEventsTask::<StorageSqlx>::apply_status_event(storage, txid, tx_status, &trigger)
.await
.map_err(|e| anyhow!("apply_status_event: {}", e))?;
tracing::info!(
txid = %txid,
status = %tx_status,
updated,
"arc-callback: status webhook received"
);
if updated {
Ok(IngestAction::StatusApplied)
} else {
Ok(IngestAction::StatusIgnored)
}
}
}