use serde_json::Value;
use tail_fin_common::TailFinError;
use crate::types::{AddressEnriched, SearchResults, TransfersPage};
pub fn parse_search_results(v: &Value) -> Result<SearchResults, TailFinError> {
serde_json::from_value::<SearchResults>(v.clone())
.map_err(|e| TailFinError::Api(format!("failed to parse Arkham search results: {e}")))
}
pub fn parse_address_enriched(v: &Value) -> Result<AddressEnriched, TailFinError> {
serde_json::from_value::<AddressEnriched>(v.clone())
.map_err(|e| TailFinError::Api(format!("failed to parse Arkham address_enriched: {e}")))
}
pub fn parse_transfers(v: &Value) -> Result<TransfersPage, TailFinError> {
serde_json::from_value::<TransfersPage>(v.clone())
.map_err(|e| TailFinError::Api(format!("failed to parse Arkham transfers: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn binance_search_fixture() -> Value {
json!({
"arkhamEntities": [
{"name": "Binance", "note": "", "id": "binance", "type": "cex", "service": true, "addresses": null, "twitter": "binance"},
{"name": "Binance Pool", "note": "", "id": "binance-pool", "type": "miner-validator", "service": false, "addresses": null, "twitter": "binance"}
],
"tokens": [
{
"name": "Binance Coin",
"symbol": "bnb",
"price": 612.34,
"price24hAgo": 605.10,
"identifier": {"address": "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", "chain": "ethereum", "pricingID": "binancecoin"}
}
],
"pools": [],
"tags": [],
"twitter": [],
"services": []
})
}
#[test]
fn parse_extracts_entity_names_and_ids() {
let r = parse_search_results(&binance_search_fixture()).expect("parse OK");
let names: Vec<&str> = r
.arkham_entities
.iter()
.filter_map(|e| e.name.as_deref())
.collect();
assert_eq!(names, vec!["Binance", "Binance Pool"]);
assert_eq!(r.arkham_entities[0].id.as_deref(), Some("binance"));
assert_eq!(r.arkham_entities[0].r#type.as_deref(), Some("cex"));
assert_eq!(r.arkham_entities[0].service, Some(true));
}
#[test]
fn parse_extracts_token_with_renamed_fields() {
let r = parse_search_results(&binance_search_fixture()).expect("parse OK");
assert_eq!(r.tokens.len(), 1);
let t = &r.tokens[0];
assert_eq!(t.name.as_deref(), Some("Binance Coin"));
assert_eq!(t.symbol.as_deref(), Some("bnb"));
assert_eq!(t.price, Some(612.34));
assert_eq!(t.price_24h_ago, Some(605.10));
let ident = t.identifier.as_ref().expect("identifier");
assert_eq!(ident.chain.as_deref(), Some("ethereum"));
assert_eq!(ident.pricing_id.as_deref(), Some("binancecoin"));
}
#[test]
fn parse_handles_empty_response() {
let r = parse_search_results(&json!({})).expect("parse OK");
assert!(r.arkham_entities.is_empty());
assert!(r.tokens.is_empty());
assert!(r.pools.is_empty());
}
#[test]
fn parse_ignores_unknown_top_level_fields() {
let v = json!({"arkhamEntities": [], "newFutureField": {"whatever": 42}});
parse_search_results(&v).expect("parse OK despite unknown field");
}
#[test]
fn parse_errors_on_wrong_type() {
let v = json!({"arkhamEntities": {"not": "an array"}});
assert!(parse_search_results(&v).is_err());
}
fn enriched_fixture() -> Value {
json!({
"bsc": {
"address": "0x971435Fc38eeD5E0aAFF0dd717d0d16a02a4110e",
"chain": "bsc",
"isUserAddress": false,
"contract": true,
"populatedTags": [
{"id": "high-transacting", "label": "High Transacting", "rank": 160, "excludeEntities": false, "chain": "bsc", "disablePage": false}
]
},
"ethereum": {
"address": "0x971435Fc38eeD5E0aAFF0dd717d0d16a02a4110e",
"chain": "ethereum",
"isUserAddress": false,
"contract": false
}
})
}
#[test]
fn parse_address_enriched_indexes_by_chain() {
let r = parse_address_enriched(&enriched_fixture()).expect("parse OK");
let mut chains: Vec<&str> = r.known_chains();
chains.sort();
assert_eq!(chains, vec!["bsc", "ethereum"]);
}
#[test]
fn parse_address_enriched_extracts_tags() {
let r = parse_address_enriched(&enriched_fixture()).expect("parse OK");
let bsc = r.chains.get("bsc").expect("bsc");
assert!(bsc.contract);
assert_eq!(bsc.populated_tags.len(), 1);
assert_eq!(
bsc.populated_tags[0].label.as_deref(),
Some("High Transacting")
);
assert_eq!(bsc.populated_tags[0].rank, 160);
let eth = r.chains.get("ethereum").expect("ethereum");
assert!(eth.populated_tags.is_empty());
let all: Vec<&str> = r.all_tags().filter_map(|t| t.label.as_deref()).collect();
assert_eq!(all, vec!["High Transacting"]);
}
fn transfers_fixture() -> Value {
json!({
"count": 10000,
"transfers": [
{
"id": "0xabc_1",
"transactionHash": "0xabc",
"chain": "bsc",
"fromAddress": {"address": "0x111", "chain": "bsc", "isUserAddress": false, "contract": true},
"toAddress": {"address": "0x222", "chain": "bsc", "isUserAddress": false, "contract": false},
"tokenAddress": "0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d",
"tokenName": "World Liberty Financial USD",
"tokenSymbol": "USD1",
"tokenDecimals": 18,
"tokenId": "usd1-wlfi",
"unitValue": 38530.95,
"historicalUSD": 38530.95,
"blockNumber": 70789391,
"blockTimestamp": "2025-12-07T07:48:56Z",
"blockHash": "0x62c5",
"type": ""
}
]
})
}
#[test]
fn parse_transfers_extracts_count_and_items() {
let p = parse_transfers(&transfers_fixture()).expect("parse OK");
assert_eq!(p.count, 10000);
assert_eq!(p.transfers.len(), 1);
}
#[test]
fn parse_transfers_renames_camelcase_fields() {
let p = parse_transfers(&transfers_fixture()).expect("parse OK");
let t = &p.transfers[0];
assert_eq!(t.transaction_hash.as_deref(), Some("0xabc"));
assert_eq!(t.token_symbol.as_deref(), Some("USD1"));
assert_eq!(t.historical_usd, Some(38530.95));
assert_eq!(t.unit_value, Some(38530.95));
assert_eq!(t.token_decimals, Some(18));
let from = t.from_address.as_ref().expect("fromAddress");
let to = t.to_address.as_ref().expect("toAddress");
assert_eq!(from.address.as_deref(), Some("0x111"));
assert!(from.contract);
assert!(!to.contract);
}
#[test]
fn parse_transfers_handles_empty_page() {
let v = json!({"count": 0, "transfers": []});
let p = parse_transfers(&v).expect("parse OK");
assert_eq!(p.count, 0);
assert!(p.transfers.is_empty());
}
}