1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//! `userNonFundingLedgerUpdates`: every movement of an account's money other than funding. A row
//! type this build does not read, or one that no longer reads as modelled, is [`Delta::Other`],
//! kept as written.
use serde::{Deserialize, Deserializer, de};
use serde_json::value::RawValue;
use crate::utils::address::Address;
use crate::utils::dec::Dec;
use crate::utils::json::Json;
/// One ledger row.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct LedgerUpdate {
/// Milliseconds.
pub time: u64,
/// The L1 transaction hash, for the explorer.
pub hash: String,
#[serde(deserialize_with = "delta")]
pub delta: Delta,
}
impl LedgerUpdate {
/// The row's `type`: `deposit`, `send`, `withdraw`, `accountClassTransfer` and the rest.
#[must_use]
pub fn kind(&self) -> &str {
self.delta.kind()
}
}
/// What a row did. USDC amounts named `usdc` are dollars; `amount` is in `token`, the token's name
/// (`"USDC"`, `"HYPE"`). A field not every row of its type carries is an `Option`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Delta {
/// `sendAsset`, from `user` to `destination`, possibly between balances.
#[serde(rename_all = "camelCase")]
Send {
user: Address,
destination: Address,
/// `""` the main perp dex, `spot` spot.
source_dex: String,
destination_dex: String,
token: String,
amount: Dec,
usdc_value: Dec,
fee: Dec,
/// Not always sent.
#[serde(default)]
native_token_fee: Option<Dec>,
#[serde(default)]
nonce: Option<u64>,
/// `""` where no fee was charged.
fee_token: String,
},
/// `usdSend`: USDC between perp balances.
#[serde(rename_all = "camelCase")]
InternalTransfer {
usdc: Dec,
user: Address,
destination: Address,
fee: Dec,
},
/// `spotSend`.
#[serde(rename_all = "camelCase")]
SpotTransfer {
token: String,
amount: Dec,
usdc_value: Dec,
user: Address,
destination: Address,
fee: Dec,
/// Not always sent.
#[serde(default)]
native_token_fee: Option<Dec>,
#[serde(default)]
nonce: Option<u64>,
fee_token: String,
},
/// In from the Arbitrum bridge.
Deposit {
usdc: Dec,
},
/// Out to Arbitrum, with the venue's fee.
#[serde(rename_all = "camelCase")]
Withdraw {
usdc: Dec,
nonce: u64,
fee: Dec,
},
/// Between the account's own spot and perp balances.
#[serde(rename_all = "camelCase")]
AccountClassTransfer {
usdc: Dec,
to_perp: bool,
},
/// The fee a first transfer to a fresh address paid.
AccountActivationGas {
amount: Dec,
token: String,
},
VaultDeposit {
vault: Address,
usdc: Dec,
},
#[serde(rename_all = "camelCase")]
VaultWithdraw {
vault: Address,
user: Address,
requested_usd: Dec,
commission: Dec,
closing_cost: Dec,
basis: Dec,
net_withdrawn_usd: Dec,
},
/// HYPE between spot and the staking balance: `cDeposit` in, `cWithdraw` out once its queue
/// has passed.
#[serde(rename_all = "camelCase")]
CStakingTransfer {
token: String,
amount: Dec,
is_deposit: bool,
},
/// A deployer's share of a token it launched.
SpotGenesis {
token: String,
amount: Dec,
},
/// A bid in a deploy gas auction.
DeployGasAuction {
token: String,
amount: Dec,
},
/// Referral or builder rewards claimed: an amount of `token`, not `usdc`.
RewardsClaim {
amount: Dec,
token: String,
},
/// USDC between a master and one of its subaccounts, `user` to `destination`.
SubAccountTransfer {
usdc: Dec,
user: Address,
destination: Address,
},
/// A vault made: the USDC its leader put in, and the creation fee charged beside it.
VaultCreate {
vault: Address,
usdc: Dec,
fee: Dec,
},
/// A type this build does not read, or a known one that no longer reads as modelled: its
/// `type` (empty when it names none) and the delta as written, every key and digit.
#[serde(skip_deserializing)]
Other {
kind: String,
fields: Json,
},
}
impl Delta {
/// The row's `type`, as the venue spells it.
#[must_use]
pub fn kind(&self) -> &str {
match self {
Delta::Send { .. } => "send",
Delta::InternalTransfer { .. } => "internalTransfer",
Delta::SpotTransfer { .. } => "spotTransfer",
Delta::Deposit { .. } => "deposit",
Delta::Withdraw { .. } => "withdraw",
Delta::AccountClassTransfer { .. } => "accountClassTransfer",
Delta::AccountActivationGas { .. } => "accountActivationGas",
Delta::VaultDeposit { .. } => "vaultDeposit",
Delta::VaultWithdraw { .. } => "vaultWithdraw",
Delta::CStakingTransfer { .. } => "cStakingTransfer",
Delta::SpotGenesis { .. } => "spotGenesis",
Delta::DeployGasAuction { .. } => "deployGasAuction",
Delta::RewardsClaim { .. } => "rewardsClaim",
Delta::SubAccountTransfer { .. } => "subAccountTransfer",
Delta::VaultCreate { .. } => "vaultCreate",
Delta::Other { kind, .. } => kind,
}
}
/// The nonce the row carries, as the venue writes it. A send's is its action's; a withdrawal's
/// runs to sixteen digits (`1759909832650000`) and is not.
#[must_use]
pub const fn nonce(&self) -> Option<u64> {
match self {
Delta::Send { nonce, .. } | Delta::SpotTransfer { nonce, .. } => *nonce,
Delta::Withdraw { nonce, .. } => Some(*nonce),
_ => None,
}
}
}
/// Read where it lies, never through a float: a known type as modelled, anything else kept as
/// written. One changed row must not blank the whole history.
fn delta<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Delta, D::Error> {
let raw = Box::<RawValue>::deserialize(deserializer)?;
if let Ok(delta) = serde_json::from_str::<Delta>(raw.get()) {
return Ok(delta);
}
let fields = Json::parse(raw.get()).map_err(de::Error::custom)?;
let kind = fields.get("type").and_then(Json::as_str).unwrap_or_default().to_owned();
Ok(Delta::Other { kind, fields })
}