Skip to main content

tail_fin_arkham/
types.rs

1//! Typed responses for the public Arkham Intel API surface.
2//!
3//! Field coverage is intentionally minimal — the OpenAPI spec ships ~101
4//! schemas, but most are background detail. The structs here cover what the
5//! adapter actually surfaces; they use `serde(default)` and skip unknown
6//! fields so partial responses parse cleanly when Arkham adds keys.
7
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11
12/// Top-level response of `GET /intelligence/search`.
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct SearchResults {
15    /// Curated entities matching the query (exchanges, protocols, funds, …).
16    #[serde(rename = "arkhamEntities", default)]
17    pub arkham_entities: Vec<Entity>,
18
19    /// Tokens matching the query.
20    #[serde(default)]
21    pub tokens: Vec<Token>,
22
23    /// Solana DEX pools matching the query.
24    #[serde(default)]
25    pub pools: Vec<SolanaPool>,
26}
27
28/// An Arkham-curated entity (an exchange, protocol, person, fund, etc.).
29///
30/// All fields are `Option` even where the HAR samples always populated them.
31/// Arkham doesn't publish stability guarantees on these field shapes, and
32/// schema-drift panics are far worse than a missing field at the use site.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Entity {
35    #[serde(default)]
36    pub id: Option<String>,
37    #[serde(default)]
38    pub name: Option<String>,
39    /// Entity type slug — `cex`, `dex`, `protocol`, `fund`, `miner-validator`,
40    /// `individual`, etc. Always present in HAR samples but kept optional for
41    /// safety against future schema drift.
42    #[serde(default)]
43    pub r#type: Option<String>,
44    /// Free-form note, often empty.
45    #[serde(default)]
46    pub note: Option<String>,
47    /// Twitter handle (sometimes a bare handle, sometimes a full URL).
48    #[serde(default)]
49    pub twitter: Option<String>,
50    /// `true` if the entity represents a hosted service (CEX, custodian, …).
51    #[serde(default)]
52    pub service: Option<bool>,
53}
54
55/// A token entry in search results.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Token {
58    #[serde(default)]
59    pub name: Option<String>,
60    #[serde(default)]
61    pub symbol: Option<String>,
62    #[serde(default)]
63    pub price: Option<f64>,
64    #[serde(rename = "price24hAgo", default)]
65    pub price_24h_ago: Option<f64>,
66    #[serde(default)]
67    pub identifier: Option<TokenIdentifier>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct TokenIdentifier {
72    #[serde(default)]
73    pub address: Option<String>,
74    #[serde(default)]
75    pub chain: Option<String>,
76    #[serde(rename = "pricingID", default)]
77    pub pricing_id: Option<String>,
78}
79
80/// A Solana DEX pool entry in search results.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct SolanaPool {
83    #[serde(rename = "poolAddress", default)]
84    pub pool_address: Option<String>,
85    #[serde(rename = "tokenAddress", default)]
86    pub token_address: Option<String>,
87    #[serde(rename = "tokenName", default)]
88    pub token_name: Option<String>,
89    #[serde(rename = "tokenSymbol", default)]
90    pub token_symbol: Option<String>,
91    #[serde(rename = "priceUsd", default)]
92    pub price_usd: Option<f64>,
93    #[serde(rename = "liquidityUsd", default)]
94    pub liquidity_usd: Option<f64>,
95}
96
97// ─── /intelligence/address_enriched/{address}/all ────────────────────────────
98//
99// The endpoint returns a flat object keyed by chain slug
100// (`ethereum`, `bsc`, `polygon`, …). Each value is a per-chain enrichment
101// record. We represent it as a HashMap so callers can iterate or look up
102// a specific chain without having an enum exhaustively listing every chain
103// Arkham might add tomorrow.
104
105/// Per-chain enriched record returned for a single address.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ChainAddressInfo {
108    #[serde(default)]
109    pub address: Option<String>,
110    #[serde(default)]
111    pub chain: Option<String>,
112    #[serde(rename = "isUserAddress", default)]
113    pub is_user_address: bool,
114    #[serde(default)]
115    pub contract: bool,
116    #[serde(rename = "populatedTags", default)]
117    pub populated_tags: Vec<AddressTag>,
118}
119
120/// One Arkham label/tag attached to an address on a specific chain.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct AddressTag {
123    #[serde(default)]
124    pub id: Option<String>,
125    #[serde(default)]
126    pub label: Option<String>,
127    #[serde(default)]
128    pub rank: i64,
129    #[serde(default)]
130    pub chain: String,
131    #[serde(rename = "excludeEntities", default)]
132    pub exclude_entities: bool,
133    #[serde(rename = "disablePage", default)]
134    pub disable_page: bool,
135}
136
137/// Wrapper around the chain-keyed map. New-typed so callers can hang
138/// helper methods on it without orphan-rule issues.
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140#[serde(transparent)]
141pub struct AddressEnriched {
142    pub chains: HashMap<String, ChainAddressInfo>,
143}
144
145impl AddressEnriched {
146    /// Chains with non-empty data on this address — i.e. anywhere we found a
147    /// label, contract flag, or just a record. Useful for ranking which
148    /// chain to drill into first when an address shows up cross-chain.
149    pub fn known_chains(&self) -> Vec<&str> {
150        self.chains.keys().map(String::as_str).collect()
151    }
152
153    /// Convenience: every tag across every chain, flattened.
154    pub fn all_tags(&self) -> impl Iterator<Item = &AddressTag> {
155        self.chains.values().flat_map(|c| c.populated_tags.iter())
156    }
157}
158
159// ─── /transfers ─────────────────────────────────────────────────────────────
160
161/// Page of transfers returned by `/transfers`.
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
163pub struct TransfersPage {
164    /// Total matching transfers on the server side (NOT the page size). Used
165    /// for paging via `offset` + `limit`.
166    #[serde(default)]
167    pub count: u64,
168    #[serde(default)]
169    pub transfers: Vec<Transfer>,
170}
171
172/// A single token transfer. Identifying fields (`id`, `transaction_hash`,
173/// `chain`) are `Option` because Arkham doesn't formally guarantee they
174/// stay populated across all transfer types — better to surface a `None`
175/// than panic during deserialization.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct Transfer {
178    #[serde(default)]
179    pub id: Option<String>,
180    #[serde(rename = "transactionHash", default)]
181    pub transaction_hash: Option<String>,
182    #[serde(default)]
183    pub chain: Option<String>,
184
185    #[serde(rename = "fromAddress", default)]
186    pub from_address: Option<TransferParty>,
187    #[serde(rename = "toAddress", default)]
188    pub to_address: Option<TransferParty>,
189
190    #[serde(rename = "tokenAddress", default)]
191    pub token_address: Option<String>,
192    #[serde(rename = "tokenName", default)]
193    pub token_name: Option<String>,
194    #[serde(rename = "tokenSymbol", default)]
195    pub token_symbol: Option<String>,
196    #[serde(rename = "tokenDecimals", default)]
197    pub token_decimals: Option<i64>,
198    #[serde(rename = "tokenId", default)]
199    pub token_id: Option<String>,
200
201    /// Token amount in its native unit (already divided by 10^decimals).
202    #[serde(rename = "unitValue", default)]
203    pub unit_value: Option<f64>,
204    /// USD value at transfer time.
205    #[serde(rename = "historicalUSD", default)]
206    pub historical_usd: Option<f64>,
207
208    #[serde(rename = "blockNumber", default)]
209    pub block_number: Option<u64>,
210    #[serde(rename = "blockTimestamp", default)]
211    pub block_timestamp: Option<String>,
212    #[serde(rename = "blockHash", default)]
213    pub block_hash: Option<String>,
214
215    #[serde(rename = "type", default)]
216    pub r#type: Option<String>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct TransferParty {
221    #[serde(default)]
222    pub address: Option<String>,
223    #[serde(default)]
224    pub chain: Option<String>,
225    #[serde(rename = "isUserAddress", default)]
226    pub is_user_address: bool,
227    #[serde(default)]
228    pub contract: bool,
229}
230
231/// Filter parameters for the `/transfers` endpoint. Only the fields callers
232/// actually populate get serialized into the query string. All optional.
233#[derive(Debug, Clone, Default)]
234pub struct TransfersQuery<'a> {
235    /// `base=` repeats per address; `["user"]` returns flow involving the
236    /// caller's saved addresses (requires identity).
237    pub base: Option<&'a [&'a str]>,
238    /// `flow=in` / `out` / `all`. Server defaults if omitted.
239    pub flow: Option<&'a str>,
240    /// Lower-bound USD value (inclusive). Common: `"1"` to filter dust.
241    pub usd_gte: Option<&'a str>,
242    /// `time` / `usd` / etc. — see OpenAPI `TransferSortKey`.
243    pub sort_key: Option<&'a str>,
244    /// `asc` / `desc`.
245    pub sort_dir: Option<&'a str>,
246    pub limit: Option<u32>,
247    pub offset: Option<u32>,
248}