1use crate::codecs::scalars::{format_uint64_id, u128_to_str};
4use crate::models::{
5 AssetBalance, BalanceHistory, BalanceHistorySeries, BalancesList, Hold, HoldsList,
6 LedgerTransfer, TransfersList,
7};
8use crate::proto::ledger::read::v1::{
9 AssetBalance as ProtoAssetBalance, BalanceRange, GetBalanceHistoryResponse,
10 GetBalancesResponse, HoldRow, ListHoldsResponse, ListTransfersResponse, TransferRow,
11};
12use crate::proto::polyester::r#type::v1::U128;
13
14fn u128_field(msg: Option<&U128>) -> String {
15 match msg {
16 Some(u) => u128_to_str(u.hi, u.lo),
17 None => "0".to_owned(),
18 }
19}
20
21fn balance_range_label(range: buffa::EnumValue<BalanceRange>) -> String {
22 match range.as_known() {
23 Some(BalanceRange::Day1) => "1d".to_owned(),
24 Some(BalanceRange::Day7) => "7d".to_owned(),
25 Some(BalanceRange::Day30) => "30d".to_owned(),
26 Some(BalanceRange::Day90) => "90d".to_owned(),
27 Some(BalanceRange::Day180) => "180d".to_owned(),
28 Some(BalanceRange::Day365) => "365d".to_owned(),
29 Some(_) => String::new(),
30 None => format!("UNKNOWN({})", range.to_i32()),
31 }
32}
33
34pub fn asset_balance_from_proto(msg: &ProtoAssetBalance) -> AssetBalance {
35 AssetBalance {
36 asset_id: msg.asset_id,
37 trading: u128_field(msg.trading.as_option()),
38 funding: u128_field(msg.funding.as_option()),
39 reserved: u128_field(msg.reserved.as_option()),
40 available: u128_field(msg.available.as_option()),
41 trading_revision: msg.trading_revision,
42 funding_revision: msg.funding_revision,
43 }
44}
45
46pub fn balances_list_from_proto(msg: &GetBalancesResponse) -> BalancesList {
47 BalancesList {
48 balances: msg.balances.iter().map(asset_balance_from_proto).collect(),
49 }
50}
51
52pub fn balance_history_from_proto(msg: &GetBalanceHistoryResponse) -> BalanceHistory {
53 BalanceHistory {
54 range: balance_range_label(msg.range),
55 bucket: msg.bucket.clone(),
56 start_ts_sec: msg.start_ts_sec as i64,
57 end_ts_sec: msg.end_ts_sec as i64,
58 points: msg.points,
59 series: msg
60 .series
61 .iter()
62 .map(|s| BalanceHistorySeries {
63 asset_id: s.asset_id,
64 account_code: s.account_code.to_i32(),
65 balance_q: s.balance_q.clone(),
66 })
67 .collect(),
68 }
69}
70
71pub fn equity_history_from_proto(
72 msg: &crate::proto::ledger::read::v1::GetEquityHistorySeriesResponse,
73) -> crate::models::EquityHistory {
74 use crate::models::{EquityHistory, EquityHistorySeries};
75 use crate::proto::ledger::read::v1::__buffa::oneof::equity_series::Grouping;
76 EquityHistory {
77 range: balance_range_label(msg.range),
78 bucket: msg.bucket.clone(),
79 start_ts_sec: msg.start_ts_sec as i64,
80 end_ts_sec: msg.end_ts_sec as i64,
81 quote_asset: msg.quote_asset.clone(),
82 points: msg.points,
83 series: msg
84 .series
85 .iter()
86 .map(|s| {
87 let mut row = EquityHistorySeries {
88 account_code: 0,
89 account_name: String::new(),
90 asset_id: 0,
91 asset_symbol: String::new(),
92 equity_q: s.equity_q.clone(),
93 };
94 match s.grouping.as_ref() {
95 Some(Grouping::Account(a)) => {
96 row.account_code = a.account_code;
97 row.account_name = a.name.clone();
98 }
99 Some(Grouping::Asset(a)) => {
100 row.asset_id = a.id;
101 row.asset_symbol = a.symbol.clone();
102 }
103 None => {}
104 }
105 row
106 })
107 .collect(),
108 }
109}
110
111pub fn hold_from_proto(msg: &HoldRow) -> Hold {
112 Hold {
113 hold_id: format_uint64_id(msg.hold_id),
114 asset_id: msg.asset_id,
115 amount_reserved: u128_field(msg.amount_reserved_e18.as_option()),
116 expires_at_ns: if msg.expires_at_ns == 0 {
117 String::new()
118 } else {
119 msg.expires_at_ns.to_string()
120 },
121 }
122}
123
124pub fn holds_list_from_proto(msg: &ListHoldsResponse) -> HoldsList {
125 HoldsList {
126 holds: msg.holds.iter().map(hold_from_proto).collect(),
127 }
128}
129
130pub fn transfer_row_from_proto(msg: &TransferRow) -> LedgerTransfer {
131 LedgerTransfer {
132 asset_id: msg.asset_id,
133 amount: u128_field(msg.amount_e18.as_option()),
134 transfer_type: msg.transfer_code.to_i32(),
135 account_code: msg.account_code.to_i32(),
136 timestamp: msg.ts_us as i64,
137 tx_id: msg.flow_id.clone(),
138 is_debit: msg.is_debit,
139 }
140}
141
142pub fn transfers_list_from_proto(msg: &ListTransfersResponse) -> TransfersList {
143 let next_cursor = if msg.next_page_token.is_empty() {
144 None
145 } else {
146 msg.next_page_token.parse::<i64>().ok().filter(|v| *v != 0)
147 };
148 TransfersList {
149 transfers: msg.transfers.iter().map(transfer_row_from_proto).collect(),
150 next_cursor,
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::proto::ledger::read::v1::__buffa::oneof::equity_series::Grouping;
158 use crate::proto::ledger::read::v1::{
159 AccountGrouping, BalanceSeries, GetBalanceHistoryResponse, GetEquityHistorySeriesResponse,
160 ListHoldsResponse, ListTransfersResponse,
161 };
162
163 #[test]
164 fn balances_list_maps_u128() {
165 let msg = GetBalancesResponse {
166 balances: vec![ProtoAssetBalance {
167 asset_id: 7,
168 trading: U128 {
169 hi: 0,
170 lo: 500,
171 ..Default::default()
172 }
173 .into(),
174 trading_revision: u64::MAX - 2,
175 funding_revision: u64::MAX - 1,
176 ..Default::default()
177 }],
178 ..Default::default()
179 };
180 let list = balances_list_from_proto(&msg);
181 assert_eq!(list.balances.len(), 1);
182 assert_eq!(list.balances[0].asset_id, 7);
183 assert_eq!(list.balances[0].trading, "500");
184 assert_eq!(list.balances[0].trading_revision, u64::MAX - 2);
185 assert_eq!(list.balances[0].funding_revision, u64::MAX - 1);
186
187 let one_e18 = GetBalancesResponse {
189 balances: vec![ProtoAssetBalance {
190 asset_id: 1,
191 trading: U128 {
192 hi: 0,
193 lo: 1_000_000_000_000_000_000,
194 ..Default::default()
195 }
196 .into(),
197 ..Default::default()
198 }],
199 ..Default::default()
200 };
201 let list = balances_list_from_proto(&one_e18);
202 assert_eq!(list.balances[0].trading, "1000000000000000000");
203 }
204
205 #[test]
206 fn balance_history_maps_range_and_series() {
207 let msg = GetBalanceHistoryResponse {
208 range: BalanceRange::Day7.into(),
209 bucket: "1h".into(),
210 start_ts_sec: 100,
211 end_ts_sec: 200,
212 points: 2,
213 series: vec![BalanceSeries {
214 asset_id: 1,
215 balance_q: vec![100, 200],
216 ..Default::default()
217 }],
218 ..Default::default()
219 };
220 let result = balance_history_from_proto(&msg);
221 assert_eq!(result.range, "7d");
222 assert_eq!(result.bucket, "1h");
223 assert_eq!(result.series.len(), 1);
224 assert_eq!(result.series[0].balance_q, vec![100, 200]);
225 }
226
227 #[test]
228 fn balance_history_preserves_full_u64_range() {
229 use crate::proto::ledger::read::v1::BalanceSeries;
230
231 let msg = GetBalanceHistoryResponse {
232 series: vec![BalanceSeries {
233 asset_id: 1,
234 account_code: buffa::EnumValue::from(-7),
235 balance_q: vec![i64::MAX as u64 + 1, u64::MAX],
236 ..Default::default()
237 }],
238 ..Default::default()
239 };
240
241 let result = balance_history_from_proto(&msg);
242 assert_eq!(
243 result.series[0].balance_q,
244 vec![i64::MAX as u64 + 1, u64::MAX]
245 );
246 assert_eq!(result.series[0].account_code, -7);
247 }
248
249 #[test]
250 fn equity_history_maps_account_grouping() {
251 let msg = GetEquityHistorySeriesResponse {
252 range: BalanceRange::Day30.into(),
253 bucket: "1d".into(),
254 start_ts_sec: 1,
255 end_ts_sec: 2,
256 quote_asset: "USD".into(),
257 points: 1,
258 series: vec![crate::proto::ledger::read::v1::EquitySeries {
259 equity_q: vec![999],
260 grouping: Some(Grouping::Account(Box::new(AccountGrouping {
261 account_code: 5,
262 name: "Trading".into(),
263 ..Default::default()
264 }))),
265 ..Default::default()
266 }],
267 ..Default::default()
268 };
269 let result = equity_history_from_proto(&msg);
270 assert_eq!(result.range, "30d");
271 assert_eq!(result.quote_asset, "USD");
272 assert_eq!(result.series[0].account_code, 5);
273 assert_eq!(result.series[0].account_name, "Trading");
274 assert_eq!(result.series[0].equity_q, vec![999]);
275 }
276
277 #[test]
278 fn history_point_counts_preserve_full_u32_range() {
279 let balance = balance_history_from_proto(&GetBalanceHistoryResponse {
280 points: u32::MAX,
281 ..Default::default()
282 });
283 let equity = equity_history_from_proto(&GetEquityHistorySeriesResponse {
284 points: u32::MAX,
285 ..Default::default()
286 });
287 assert_eq!(balance.points, u32::MAX);
288 assert_eq!(equity.points, u32::MAX);
289 }
290
291 #[test]
292 fn hold_formats_id_and_amount() {
293 let msg = HoldRow {
294 hold_id: 42,
295 asset_id: 1,
296 amount_reserved_e18: U128 {
297 hi: 0,
298 lo: 99,
299 ..Default::default()
300 }
301 .into(),
302 expires_at_ns: 123,
303 ..Default::default()
304 };
305 let hold = hold_from_proto(&msg);
306 assert_eq!(hold.hold_id, format_uint64_id(42));
307 assert_eq!(hold.amount_reserved, "99");
308 assert_eq!(hold.expires_at_ns, "123");
309 }
310
311 #[test]
312 fn holds_list_maps_rows() {
313 let msg = ListHoldsResponse {
314 holds: vec![HoldRow {
315 hold_id: 42,
316 asset_id: 1,
317 amount_reserved_e18: U128 {
318 hi: 0,
319 lo: 500,
320 ..Default::default()
321 }
322 .into(),
323 expires_at_ns: 1_700_000_000_000,
324 ..Default::default()
325 }],
326 ..Default::default()
327 };
328 let result = holds_list_from_proto(&msg);
329 assert_eq!(result.holds.len(), 1);
330 assert_eq!(result.holds[0].hold_id, format_uint64_id(42));
331 assert_eq!(result.holds[0].amount_reserved, "500");
332 }
333
334 #[test]
335 fn transfer_row_and_list_cursor() {
336 let row = TransferRow {
337 asset_id: 2,
338 amount_e18: U128 {
339 hi: 0,
340 lo: 1000,
341 ..Default::default()
342 }
343 .into(),
344 transfer_code: buffa::EnumValue::from(5),
345 account_code: buffa::EnumValue::from(1),
346 ts_us: 999,
347 is_debit: true,
348 flow_id: "flow-abc".into(),
349 ..Default::default()
350 };
351 let transfer = transfer_row_from_proto(&row);
352 assert_eq!(transfer.amount, "1000");
353 assert_eq!(transfer.transfer_type, 5);
354 assert_eq!(transfer.tx_id, "flow-abc");
355 assert!(transfer.is_debit);
356
357 let list = transfers_list_from_proto(&ListTransfersResponse {
358 transfers: vec![TransferRow {
359 asset_id: 1,
360 ..Default::default()
361 }],
362 next_page_token: "12345".into(),
363 ..Default::default()
364 });
365 assert_eq!(list.transfers.len(), 1);
366 assert_eq!(list.next_cursor, Some(12345));
367 }
368}