1use std::array::TryFromSliceError;
6
7use ahash::{HashMap, HashMapExt};
8use borderless::{
9 contracts::ledger::{Currency, LedgerEntry, Money},
10 http::{queries::Pagination, PaginatedElements},
11 prelude::{ledger::EntryType, TxCtx},
12 BorderlessId, Context, ContractId,
13};
14use borderless_kv_store::{Db, RawRead, RawWrite, RoCursor as _, RoTx};
15use serde::{Deserialize, Serialize};
16
17use crate::{Error, Result, LEDGER_SUB_DB};
18
19use crate::log_shim::debug;
20
21pub struct Ledger<'a, S: Db> {
23 db: &'a S,
24}
25
26impl<'a, S: Db> Ledger<'a, S> {
27 pub fn new(db: &'a S) -> Self {
28 Self { db }
29 }
30
31 pub(crate) fn commit_entry(
33 &self,
34 txn: &mut <S as Db>::RwTx<'_>,
35 entry: &LedgerEntry,
36 cid: ContractId,
37 tx_ctx: &TxCtx,
38 ) -> Result<()> {
39 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
40 let ledger_id = entry.creditor.merge_compact(&entry.debitor);
42 let meta_key = LedgerKey::meta(ledger_id);
43 let mut meta = match txn.read(&db_ptr, &meta_key)? {
44 Some(val) => postcard::from_bytes(val)?,
45 None => LedgerMeta::new(entry.creditor, entry.debitor),
46 };
47
48 let c_key = LedgerKey::new(ledger_id, meta.len, "creditor");
50 let d_key = LedgerKey::new(ledger_id, meta.len, "debitor");
51 let amount_key = LedgerKey::new(ledger_id, meta.len, "amount");
52 let tax_key = LedgerKey::new(ledger_id, meta.len, "tax");
53 let currency_key = LedgerKey::new(ledger_id, meta.len, "currency");
54 let kind_key = LedgerKey::new(ledger_id, meta.len, "kind");
55 let tag_key = LedgerKey::new(ledger_id, meta.len, "tag");
56 let cid_key = LedgerKey::new(ledger_id, meta.len, "contract_id");
57 let tx_ctx_key = LedgerKey::new(ledger_id, meta.len, "tx_ctx");
58 let tx_ctx_bytes = postcard::to_allocvec(&tx_ctx)?;
59 txn.write(&db_ptr, &c_key, entry.creditor.as_bytes())?;
60 txn.write(&db_ptr, &d_key, entry.debitor.as_bytes())?;
61 txn.write(&db_ptr, &amount_key, &entry.amount_milli.to_be_bytes())?;
62 txn.write(&db_ptr, &tax_key, &entry.tax_milli.to_be_bytes())?;
63 txn.write(&db_ptr, ¤cy_key, &entry.currency.to_be_bytes())?;
64 txn.write(&db_ptr, &kind_key, &entry.kind.to_be_bytes())?;
65 txn.write(&db_ptr, &tag_key, &entry.tag.as_bytes())?;
66 txn.write(&db_ptr, &cid_key, &cid.as_bytes())?;
67 txn.write(&db_ptr, &tx_ctx_key, &tx_ctx_bytes)?;
68
69 meta.update(entry)?;
71
72 let meta_bytes = postcard::to_allocvec(&meta)?;
74 txn.write(&db_ptr, &meta_key, &meta_bytes)?;
75 debug!(
76 "commited ledger entry: {entry}, ledger-id={ledger_id}, len={}",
77 meta.len
78 );
79 Ok(())
80 }
81
82 pub fn open(&self, p1: BorderlessId, p2: BorderlessId) -> SelectedLedger<'a, S> {
84 let ledger_id = p1.merge_compact(&p2);
85 SelectedLedger {
86 db: self.db,
87 ledger_id,
88 }
89 }
90
91 pub fn select(&self, ledger_id: u64) -> SelectedLedger<'a, S> {
93 SelectedLedger {
94 db: self.db,
95 ledger_id,
96 }
97 }
98
99 pub fn all(&self) -> Result<Vec<LedgerMeta>> {
101 let mut out = Vec::new();
102 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
103 let txn = self.db.begin_ro_txn()?;
104 let mut cursor = txn.ro_cursor(&db_ptr)?;
105
106 for (key, value) in cursor.iter() {
112 let key = LedgerKey::from_slice(key);
113 if key.is_meta() {
114 let ledger_meta = postcard::from_bytes(value)?;
115 out.push(ledger_meta);
116 }
117 }
118 Ok(out)
119 }
120
121 pub fn all_paginated(
123 &self,
124 pagination: Pagination,
125 ) -> Result<PaginatedElements<LedgerMetaDto>> {
126 let mut elements = Vec::new();
127 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
128 let txn = self.db.begin_ro_txn()?;
129 let mut cursor = txn.ro_cursor(&db_ptr)?;
130
131 let range = pagination.to_range();
132 let mut idx = 0;
133
134 for (key, value) in cursor.iter() {
140 let key = LedgerKey::from_slice(key);
141 if !key.is_meta() {
142 continue;
143 }
144 if range.start <= idx && idx < range.end {
145 let ledger_meta: LedgerMeta = postcard::from_bytes(value)?;
146 elements.push(ledger_meta.into_dto());
147 }
148 idx += 1;
149 }
150 let paginated = PaginatedElements {
151 elements,
152 total_elements: idx,
153 pagination,
154 };
155 Ok(paginated)
156 }
157
158 pub fn all_ids(&self) -> Result<Vec<LedgerIds>> {
159 let mut out = Vec::new();
160 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
161 let txn = self.db.begin_ro_txn()?;
162 let mut cursor = txn.ro_cursor(&db_ptr)?;
163 let mut last_ledger_id = 0;
164 for (key, _) in cursor.iter() {
165 let key = LedgerKey::from_slice(key);
166 let ledger_id = key.ledger_id();
167 if ledger_id == last_ledger_id {
168 continue;
169 }
170 last_ledger_id = ledger_id;
171 let elem = self.get_ledger_id(&txn, &db_ptr, ledger_id, key.line())?;
173 out.push(elem);
174 }
175 Ok(out)
176 }
177
178 pub fn all_ids_paginated(
179 &self,
180 pagination: Pagination,
181 ) -> Result<PaginatedElements<LedgerIds>> {
182 let mut elements = Vec::new();
183 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
184 let txn = self.db.begin_ro_txn()?;
185 let mut cursor = txn.ro_cursor(&db_ptr)?;
186 let mut last_ledger_id = 0;
187
188 let range = pagination.to_range();
189 let mut idx = 0;
190
191 for (key, _) in cursor.iter() {
197 let key = LedgerKey::from_slice(key);
198 let ledger_id = key.ledger_id();
199 if ledger_id == last_ledger_id {
200 continue;
201 }
202 last_ledger_id = ledger_id;
203
204 if range.start <= idx && idx < range.end {
205 let elem = self.get_ledger_id(&txn, &db_ptr, ledger_id, key.line())?;
207 elements.push(elem);
208 }
209 idx += 1;
210 }
211 let paginated = PaginatedElements {
212 elements,
213 total_elements: idx,
214 pagination,
215 };
216 Ok(paginated)
217 }
218
219 fn get_ledger_id(
222 &self,
223 txn: &<S as Db>::RoTx<'_>,
224 db_ptr: &<S as Db>::Handle,
225 ledger_id: u64,
226 line: u64,
227 ) -> Result<LedgerIds> {
228 let c_key = LedgerKey::new(ledger_id, line, "creditor");
230 let d_key = LedgerKey::new(ledger_id, line, "debitor");
231 let creditor = txn
232 .read(db_ptr, &c_key)?
233 .and_then(|b| BorderlessId::from_slice(b).ok())
234 .context("missing creditor")?;
235 let debitor = txn
236 .read(db_ptr, &d_key)?
237 .and_then(|b| BorderlessId::from_slice(b).ok())
238 .context("missing debitor")?;
239 Ok(LedgerIds {
240 creditor,
241 debitor,
242 ledger_id,
243 })
244 }
245}
246
247pub struct SelectedLedger<'a, S: Db> {
249 db: &'a S,
250 ledger_id: u64,
251}
252
253impl<'a, S: Db> SelectedLedger<'a, S> {
254 pub fn meta(&self) -> Result<Option<LedgerMeta>> {
256 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
257 let key = LedgerKey::meta(self.ledger_id);
258 let txn = self.db.begin_ro_txn()?;
259 match txn.read(&db_ptr, &key)? {
260 Some(val) => {
261 let out = postcard::from_bytes(val)?;
262 Ok(Some(out))
263 }
264 None => Ok(None),
265 }
266 }
267
268 pub fn meta_for_contract(&self, cid: ContractId) -> Result<Option<LedgerMetaDto>> {
270 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
271 let txn = self.db.begin_ro_txn()?;
272
273 let key = LedgerKey::meta(self.ledger_id);
275 let mut meta: LedgerMeta = match txn
276 .read(&db_ptr, &key)?
277 .and_then(|b| postcard::from_bytes(b).ok())
278 {
279 Some(m) => m,
280 None => return Ok(None),
281 };
282 meta.reset_balance();
284
285 let mut line = 0;
286 loop {
287 match self.check_line(&txn, &db_ptr, line as u64, cid)? {
289 Some(true) => { }
290 Some(false) => {
291 line += 1;
292 continue;
293 }
294 None => break,
295 }
296 let (entry, entry_cid, _tx_ctx) = self
297 .get(&txn, &db_ptr, line as u64)?
298 .context("line must exist")?;
299 debug_assert_eq!(entry_cid, cid);
300 meta.update(&entry)?;
302 line += 1;
303 }
304 let mut dto = meta.into_dto();
305 dto.contract_id = Some(cid);
306 Ok(Some(dto))
307 }
308
309 pub fn get_entries_paginated(
311 &self,
312 pagination: Pagination,
313 ) -> Result<PaginatedElements<LedgerEntryDto>> {
314 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
315 let txn = self.db.begin_ro_txn()?;
316
317 let meta_key = LedgerKey::meta(self.ledger_id);
319 let total_elements = match txn
320 .read(&db_ptr, &meta_key)?
321 .and_then(|b| postcard::from_bytes::<LedgerMeta>(b).ok())
322 {
323 Some(meta) => meta.len as usize,
324 None => return Ok(PaginatedElements::empty(pagination)),
325 };
326
327 let mut elements = Vec::new();
328 if !pagination.reverse {
329 for idx in pagination.to_range() {
330 match self.get(&txn, &db_ptr, idx as u64)? {
331 Some((entry, cid, tx_ctx)) => {
332 elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
333 }
334 None => break,
335 }
336 }
337 } else {
338 let range = pagination.to_range();
339 let mut idx = total_elements.saturating_sub(range.start);
340 while idx > 0 {
341 idx -= 1;
344 let (entry, cid, tx_ctx) = self
345 .get(&txn, &db_ptr, idx as u64)?
346 .context("entry idx < max_len must exist")?;
347 elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
348 if range.end - range.start <= elements.len() {
349 break;
350 }
351 }
352 }
353 Ok(PaginatedElements {
354 elements,
355 total_elements,
356 pagination,
357 })
358 }
359
360 pub fn get_contract_paginated(
361 &self,
362 cid: ContractId,
363 pagination: Pagination,
364 ) -> Result<PaginatedElements<LedgerEntryDto>> {
365 let db_ptr = self.db.open_sub_db(LEDGER_SUB_DB)?;
366 let txn = self.db.begin_ro_txn()?;
367
368 let mut elements = Vec::new();
369 let range = pagination.to_range();
370 let mut idx = 0;
371
372 if !pagination.reverse {
373 let mut line = 0;
375 loop {
376 match self.check_line(&txn, &db_ptr, line as u64, cid)? {
378 Some(true) => { }
379 Some(false) => {
380 line += 1;
381 continue;
382 }
383 None => break,
384 }
385 if range.start <= idx && idx < range.end {
387 let (entry, cid, tx_ctx) = self
388 .get(&txn, &db_ptr, line as u64)?
389 .context("line must exist")?;
390 elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
391 }
392 idx += 1;
394 line += 1;
395 }
396 } else {
397 let meta_key = LedgerKey::meta(self.ledger_id);
399 let all_entries = match txn
400 .read(&db_ptr, &meta_key)?
401 .and_then(|b| postcard::from_bytes::<LedgerMeta>(b).ok())
402 {
403 Some(meta) => meta.len as usize,
404 None => return Ok(PaginatedElements::empty(pagination)),
405 };
406
407 let mut line = all_entries;
408 while line > 0 {
409 line -= 1;
412 if !self
414 .check_line(&txn, &db_ptr, line as u64, cid)?
415 .unwrap_or_default()
416 {
417 continue;
418 }
419 if range.start <= idx && idx < range.end {
420 let (entry, cid, tx_ctx) = self
421 .get(&txn, &db_ptr, line as u64)?
422 .context("line must exist")?;
423 elements.push(LedgerEntryDto::new(entry, cid, tx_ctx));
424 }
425 idx += 1;
426 }
427 }
428 Ok(PaginatedElements {
429 elements,
430 total_elements: idx,
431 pagination,
432 })
433 }
434
435 fn read_column<T>(
437 &self,
438 txn: &<S as Db>::RoTx<'_>,
439 db_ptr: &<S as Db>::Handle,
440 line: u64,
441 column: &'static str,
442 transformer: impl Fn(&[u8]) -> Option<T>,
443 ) -> Result<Option<T>> {
444 let key = LedgerKey::new(self.ledger_id, line, column);
445 let out = txn.read(db_ptr, &key)?.and_then(transformer);
446 Ok(out)
447 }
448
449 fn check_line(
454 &self,
455 txn: &<S as Db>::RoTx<'_>,
456 db_ptr: &<S as Db>::Handle,
457 line: u64,
458 target_cid: ContractId,
459 ) -> Result<Option<bool>> {
460 let contract_id = match self.read_column(txn, db_ptr, line, "contract_id", |b| {
461 ContractId::from_slice(b).ok()
462 })? {
463 Some(c) => c,
464 None => {
465 return Ok(None);
467 }
468 };
469 Ok(Some(target_cid == contract_id))
471 }
472
473 fn get(
475 &self,
476 txn: &<S as Db>::RoTx<'_>,
477 db_ptr: &<S as Db>::Handle,
478 line: u64,
479 ) -> Result<Option<(LedgerEntry, ContractId, TxCtx)>> {
480 let contract_id = match self.read_column(txn, db_ptr, line, "contract_id", |b| {
481 ContractId::from_slice(b).ok()
482 })? {
483 Some(c) => c,
484 None => {
485 return Ok(None);
487 }
488 };
489 let creditor = self
491 .read_column(txn, db_ptr, line, "creditor", |b| {
492 BorderlessId::from_slice(b).ok()
493 })?
494 .context("missing creditor")?;
495 let debitor = self
496 .read_column(txn, db_ptr, line, "debitor", |b| {
497 BorderlessId::from_slice(b).ok()
498 })?
499 .context("missing debitor")?;
500 let amount_milli = self
501 .read_column(txn, db_ptr, line, "amount", i64_from_slice)?
502 .context("missing field amount")?;
503 let tax_milli = self
504 .read_column(txn, db_ptr, line, "tax", i64_from_slice)?
505 .context("missing field tax")?;
506 let currency = self
507 .read_column(txn, db_ptr, line, "currency", Currency::from_be_bytes)?
508 .context("missing field currency")?;
509 let kind = self
510 .read_column(txn, db_ptr, line, "kind", EntryType::from_be_bytes)?
511 .context("missing field tag")?;
512 let tag = self
513 .read_column(txn, db_ptr, line, "tag", |b| {
514 Some(String::from_utf8_lossy(b).to_string())
515 })?
516 .context("missing field tag")?;
517 let tx_ctx = self
518 .read_column(txn, db_ptr, line, "tx_ctx", |b| {
519 postcard::from_bytes(b).ok()
520 })?
521 .context("missing field tx-ctx")?;
522 let entry = LedgerEntry {
523 creditor,
524 debitor,
525 amount_milli,
526 tax_milli,
527 currency,
528 kind,
529 tag: tag.to_string(),
530 };
531 Ok(Some((entry, contract_id, tx_ctx)))
532 }
533}
534
535fn i64_from_slice(slice: &[u8]) -> Option<i64> {
536 let b = slice.try_into().ok()?;
537 Some(i64::from_be_bytes(b))
538}
539
540#[derive(Serialize)]
542pub struct LedgerEntryDto {
543 pub creditor: BorderlessId,
544 pub debitor: BorderlessId,
545 pub amount: String,
546 pub tax: String,
547 pub kind: String,
548 pub tag: String,
549 pub contract_id: ContractId,
550 pub tx_ctx: TxCtx,
551}
552
553impl LedgerEntryDto {
554 pub fn new(entry: LedgerEntry, contract_id: ContractId, tx_ctx: TxCtx) -> LedgerEntryDto {
555 let amount = Money::from_milli(entry.currency, entry.amount_milli).to_string();
556 let tax = Money::from_milli(entry.currency, entry.tax_milli).to_string();
557 LedgerEntryDto {
558 creditor: entry.creditor,
559 debitor: entry.debitor,
560 amount,
561 tax,
562 kind: entry.kind.to_string(),
563 tag: entry.tag,
564 contract_id,
565 tx_ctx,
566 }
567 }
568}
569
570#[derive(Serialize)]
571pub struct LedgerIds {
572 pub creditor: BorderlessId,
573 pub debitor: BorderlessId,
574 pub ledger_id: u64,
575}
576
577#[derive(Debug, Serialize, Deserialize)]
579pub struct LedgerMeta {
580 pub creditor: BorderlessId,
582 pub debitor: BorderlessId,
584 pub len: u64,
586 pub balances: HashMap<Currency, i64>,
588}
589
590#[derive(Serialize)]
595pub struct LedgerMetaDto {
596 pub ledger_id: u64,
598 pub creditor: BorderlessId,
600 pub debitor: BorderlessId,
602 pub len: u64,
604 pub balances: HashMap<Currency, f64>,
606 #[serde(default)]
607 #[serde(skip_serializing_if = "Option::is_none")]
608 pub contract_id: Option<ContractId>,
610}
611
612impl LedgerMeta {
613 pub fn new(creditor: BorderlessId, debitor: BorderlessId) -> Self {
614 LedgerMeta {
615 creditor,
616 debitor,
617 len: 0,
618 balances: HashMap::new(),
619 }
620 }
621
622 pub fn into_dto(self) -> LedgerMetaDto {
623 let ledger_id = self.creditor.merge_compact(&self.debitor);
624 let balances = self
625 .balances
626 .into_iter()
627 .map(|(k, v)| (k, v as f64 / 1000.0))
628 .collect();
629 LedgerMetaDto {
630 ledger_id,
631 creditor: self.creditor,
632 debitor: self.debitor,
633 len: self.len,
634 balances,
635 contract_id: None,
636 }
637 }
638
639 pub fn reset_balance(&mut self) {
641 self.balances.clear();
642 self.len = 0;
643 }
644
645 pub fn update(&mut self, entry: &LedgerEntry) -> Result<()> {
649 let balance = self.balances.entry(entry.currency).or_default();
650
651 let mul = if (entry.creditor, entry.debitor) == (self.creditor, self.debitor) {
653 1
655 } else if (entry.creditor, entry.debitor) == (self.debitor, self.creditor) {
656 -1
658 } else {
659 return Err(Error::msg("ledger-entry does not match ledger owners"));
660 };
661 match entry.kind {
662 EntryType::CREATE => {
663 *balance += mul * entry.amount_milli;
664 }
665 EntryType::SETTLE | EntryType::CANCEL => {
666 *balance -= mul * entry.amount_milli;
667 }
668 }
669 self.len += 1;
670 Ok(())
671 }
672}
673
674struct LedgerKey([u8; 24]);
687
688impl LedgerKey {
689 pub fn new(ledger_id: u64, line_idx: u64, column: &'static str) -> Self {
693 let participant_key = ledger_id.to_be_bytes();
694 let line_key = line_idx.to_be_bytes();
695 let column_key = xxhash_rust::const_xxh3::xxh3_64(column.as_bytes()).to_be_bytes();
696 let mut key = [0; 24];
697 key[0..8].copy_from_slice(&participant_key);
698 key[8..16].copy_from_slice(&line_key);
699 key[16..24].copy_from_slice(&column_key);
700 LedgerKey(key)
701 }
702
703 pub fn meta(ledger_id: u64) -> Self {
705 let participant_key = ledger_id.to_be_bytes();
706 let mut key = [0xff; 24];
708 key[0..8].copy_from_slice(&participant_key);
709 LedgerKey(key)
710 }
711
712 pub fn from_slice(slice: &[u8]) -> Self {
714 let mut key = [0; 24];
715 key[..].copy_from_slice(slice);
716 LedgerKey(key)
717 }
718
719 pub fn line(&self) -> u64 {
721 let mut out = [0; 8];
722 out.copy_from_slice(&self.0[8..16]);
723 u64::from_be_bytes(out)
724 }
725
726 pub fn ledger_id(&self) -> u64 {
728 let mut out = [0; 8];
729 out.copy_from_slice(&self.0[0..8]);
730 u64::from_be_bytes(out)
731 }
732
733 pub fn is_meta(&self) -> bool {
734 self.line() == u64::MAX
735 }
736}
737
738impl TryFrom<&[u8]> for LedgerKey {
739 type Error = TryFromSliceError;
740
741 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
742 let buf = value.try_into()?;
743 Ok(LedgerKey(buf))
744 }
745}
746
747impl AsRef<[u8]> for LedgerKey {
748 fn as_ref(&self) -> &[u8] {
749 &self.0
750 }
751}
752
753