1use bitcoin::secp256k1::SecretKey;
32use serde::{Deserialize, Serialize};
33use serde_json::{Map, Value};
34use sha2::{Digest, Sha256};
35use sidestr_nostr::event::{Event, UnsignedEvent};
36
37use crate::account::{account_of, check_sats, sats, x_of};
38use crate::error::{Error, Result};
39use crate::{CONTEXT, DEFAULT_CURRENCY, LEDGER_KIND};
40
41pub const MAX_NAME: usize = 80;
44
45pub const LEDGER_TOPIC: &str = "webledgers";
48
49pub fn jcs(v: &Value) -> String {
65 let mut out = String::new();
66 write_jcs(v, &mut out);
67 out
68}
69
70fn write_jcs(v: &Value, out: &mut String) {
71 match v {
72 Value::Object(m) => {
73 let mut keys: Vec<&String> = m.keys().collect();
74 keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
75 out.push('{');
76 for (i, k) in keys.into_iter().enumerate() {
77 if i > 0 {
78 out.push(',');
79 }
80 out.push_str(&serde_json::to_string(k).expect("a string serialises"));
81 out.push(':');
82 write_jcs(&m[k], out);
83 }
84 out.push('}');
85 }
86 Value::Array(a) => {
87 out.push('[');
88 for (i, x) in a.iter().enumerate() {
89 if i > 0 {
90 out.push(',');
91 }
92 write_jcs(x, out);
93 }
94 out.push(']');
95 }
96 Value::Number(n) if n.as_u64().is_none() && n.as_i64().is_none() => {
97 let f = n.as_f64().expect("a JSON number is finite");
98 if f.fract() == 0.0 && f.abs() < 1e21 {
99 out.push_str(&(f as i128).to_string());
100 } else {
101 out.push_str(&n.to_string());
102 }
103 }
104 other => out.push_str(&serde_json::to_string(other).expect("a scalar serialises")),
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct Genesis {
113 pub operator: String,
116 pub name: String,
118 pub currency: String,
120 pub created: u64,
122 pub confirmations: u64,
124 #[serde(flatten)]
126 pub extra: Map<String, Value>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct Entry {
132 pub r#type: String,
134 pub url: String,
136 pub amount: String,
138 #[serde(flatten)]
140 pub extra: Map<String, Value>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub struct Deposit {
146 pub outpoint: String,
148 pub account: String,
150 pub value: u64,
152 #[serde(default)]
154 pub height: Option<u64>,
155 pub at: u64,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct Payout {
162 pub id: String,
164 pub account: String,
166 pub value: u64,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub to: Option<String>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub txid: Option<String>,
174 pub at: u64,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct Ledger {
185 #[serde(rename = "@context")]
187 pub context: String,
188 pub r#type: String,
190 pub id: String,
192 pub hash: String,
194 pub name: String,
196 #[serde(rename = "defaultCurrency")]
198 pub default_currency: String,
199 pub genesis: Genesis,
201 pub updated: u64,
203 pub entries: Vec<Entry>,
205 pub deposits: Vec<Deposit>,
207 pub applied: Vec<String>,
209 pub payouts: Vec<Payout>,
211 #[serde(flatten)]
213 pub extra: Map<String, Value>,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct LedgerParams<'a> {
220 pub operator: &'a str,
222 pub name: &'a str,
224 pub currency: &'a str,
226 pub created: u64,
229 pub confirmations: u64,
231}
232
233impl<'a> LedgerParams<'a> {
234 pub fn new(operator: &'a str, name: &'a str, created: u64) -> Self {
236 Self {
237 operator,
238 name,
239 currency: DEFAULT_CURRENCY,
240 created,
241 confirmations: 1,
242 }
243 }
244}
245
246pub fn genesis_hash(genesis: &Genesis) -> String {
248 let v = serde_json::to_value(genesis).expect("a genesis serialises");
249 hex::encode(Sha256::digest(jcs(&v).as_bytes()))
250}
251
252pub fn new_ledger(p: &LedgerParams<'_>) -> Result<Ledger> {
257 let genesis = Genesis {
258 operator: account_of(p.operator)?,
259 name: p.name.to_owned(),
260 currency: p.currency.to_owned(),
261 created: p.created,
262 confirmations: p.confirmations,
263 extra: Map::new(),
264 };
265 if genesis.name.is_empty() || genesis.name.encode_utf16().count() > MAX_NAME {
266 return Err(Error::Name);
267 }
268 let h = genesis_hash(&genesis);
269 Ok(Ledger {
270 context: CONTEXT.to_owned(),
271 r#type: "WebLedger".to_owned(),
272 id: format!("urn:webledgers:{h}"),
273 hash: h,
274 name: genesis.name.clone(),
275 default_currency: genesis.currency.clone(),
276 updated: genesis.created,
277 genesis,
278 entries: Vec::new(),
279 deposits: Vec::new(),
280 applied: Vec::new(),
281 payouts: Vec::new(),
282 extra: Map::new(),
283 })
284}
285
286pub fn ledger_hash(ledger: &Ledger) -> String {
288 genesis_hash(&ledger.genesis)
289}
290
291pub fn check_ledger(ledger: &Ledger) -> Result<()> {
294 if ledger.r#type != "WebLedger" {
295 return Err(Error::NotALedger);
296 }
297 if ledger_hash(ledger) != ledger.hash {
298 return Err(Error::HashMismatch);
299 }
300 Ok(())
301}
302
303pub fn parse_ledger(json: &str) -> Result<Ledger> {
308 let v: Value = serde_json::from_str(json).map_err(|_| Error::NotALedger)?;
309 if v.get("type").and_then(Value::as_str) != Some("WebLedger")
310 || v.get("genesis").is_none_or(Value::is_null)
311 {
312 return Err(Error::NotALedger);
313 }
314 let ledger: Ledger =
315 serde_json::from_value(v).map_err(|e| Error::LedgerContent(e.to_string()))?;
316 check_ledger(&ledger)?;
317 Ok(ledger)
318}
319
320fn entry_index(ledger: &Ledger, account: &str) -> Option<usize> {
321 ledger.entries.iter().position(|e| e.url == account)
322}
323
324pub fn balance(ledger: &Ledger, account: &str) -> Result<u64> {
326 let a = account_of(account)?;
327 match entry_index(ledger, &a) {
328 Some(i) => sats(&ledger.entries[i].amount),
329 None => Ok(0),
330 }
331}
332
333fn set_balance(ledger: &mut Ledger, account: &str, n: u64) {
334 match entry_index(ledger, account) {
335 Some(i) => ledger.entries[i].amount = n.to_string(),
336 None => ledger.entries.push(Entry {
337 r#type: "Entry".to_owned(),
338 url: account.to_owned(),
339 amount: n.to_string(),
340 extra: Map::new(),
341 }),
342 }
343}
344
345fn touch(ledger: &mut Ledger, now: u64) {
346 ledger.updated = ledger.updated.max(now);
347}
348
349pub fn total(ledger: &Ledger) -> Result<u64> {
351 ledger.entries.iter().try_fold(0u64, |s, e| {
352 s.checked_add(sats(&e.amount)?).ok_or(Error::Amount)
353 })
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum Outcome {
359 Applied,
361 Skipped(&'static str),
364}
365
366impl Outcome {
367 pub fn applied(&self) -> bool {
369 matches!(self, Outcome::Applied)
370 }
371
372 pub fn why(&self) -> Option<&'static str> {
374 match self {
375 Outcome::Applied => None,
376 Outcome::Skipped(w) => Some(w),
377 }
378 }
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct Credit {
384 pub account: String,
386 pub txid: String,
388 pub vout: u32,
390 pub value: u64,
392 pub height: Option<u64>,
394}
395
396pub fn credit(ledger: &mut Ledger, c: &Credit, now: u64) -> Result<Outcome> {
400 let a = account_of(&c.account)?;
401 let key = format!("{}:{}", c.txid, c.vout);
402 let lower_hex = c.txid.len() == 64
403 && c.txid
404 .bytes()
405 .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'));
406 if !lower_hex {
407 return Err(Error::Outpoint);
408 }
409 if ledger.deposits.iter().any(|d| d.outpoint == key) {
410 return Ok(Outcome::Skipped("already credited"));
411 }
412 let v = check_sats(c.value)?;
413 let b = balance(ledger, &a)?;
414 set_balance(ledger, &a, b + v);
415 ledger.deposits.push(Deposit {
416 outpoint: key,
417 account: a,
418 value: v,
419 height: c.height,
420 at: now,
421 });
422 touch(ledger, now);
423 Ok(Outcome::Applied)
424}
425
426#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct Transfer {
430 pub id: String,
432 pub from: String,
434 pub to: String,
436 pub amount: u64,
438}
439
440fn short(account: &str) -> String {
441 account.chars().take(20).collect()
442}
443
444pub fn transfer(ledger: &mut Ledger, t: &Transfer, now: u64) -> Result<Outcome> {
447 let f = account_of(&t.from)?;
448 let to = account_of(&t.to)?;
449 let v = check_sats(t.amount)?;
450 if v == 0 {
451 return Err(Error::ZeroTransfer);
452 }
453 if ledger.applied.contains(&t.id) {
454 return Ok(Outcome::Skipped("already applied"));
455 }
456 let has = balance(ledger, &f)?;
457 if has < v {
458 return Err(Error::Insufficient {
459 account: short(&f),
460 has,
461 wanted: v,
462 });
463 }
464 set_balance(ledger, &f, has - v);
465 let b = balance(ledger, &to)?;
466 set_balance(ledger, &to, b + v);
467 ledger.applied.push(t.id.clone());
468 touch(ledger, now);
469 Ok(Outcome::Applied)
470}
471
472#[derive(Debug, Clone, PartialEq, Eq)]
474pub struct Debit {
475 pub id: String,
477 pub account: String,
479 pub amount: u64,
481 pub to: String,
483 pub txid: String,
485}
486
487pub fn debit(ledger: &mut Ledger, d: &Debit, now: u64) -> Result<Outcome> {
491 let a = account_of(&d.account)?;
492 let v = check_sats(d.amount)?;
493 if v < crate::MIN_PAY {
494 return Err(Error::WithdrawalTooSmall);
495 }
496 if ledger.applied.contains(&d.id) {
497 return Ok(Outcome::Skipped("already applied"));
498 }
499 let has = balance(ledger, &a)?;
500 if has < v {
501 return Err(Error::Insufficient {
502 account: short(&a),
503 has,
504 wanted: v,
505 });
506 }
507 set_balance(ledger, &a, has - v);
508 ledger.applied.push(d.id.clone());
509 ledger.payouts.push(Payout {
510 id: d.id.clone(),
511 account: a,
512 value: v,
513 to: Some(d.to.clone()),
514 txid: Some(d.txid.clone()),
515 at: now,
516 });
517 touch(ledger, now);
518 Ok(Outcome::Applied)
519}
520
521pub fn ledger_template(ledger: &Ledger, created_at: u64) -> UnsignedEvent {
528 UnsignedEvent {
529 pubkey: String::new(),
530 created_at,
531 kind: LEDGER_KIND,
532 tags: vec![
533 vec!["d".into(), ledger.hash.clone()],
534 vec!["t".into(), LEDGER_TOPIC.into()],
535 vec!["name".into(), ledger.name.clone()],
536 vec![
537 "alt".into(),
538 format!("Web Ledger {} ({})", ledger.name, ledger.default_currency),
539 ],
540 ],
541 content: serde_json::to_string(ledger).expect("a ledger serialises"),
542 }
543}
544
545pub fn ledger_event(
547 operator_secret: &SecretKey,
548 ledger: &Ledger,
549 created_at: u64,
550) -> Result<Event> {
551 crate::sign::sign_event(operator_secret, ledger_template(ledger, created_at))
552}
553
554pub fn read_ledger_event(ev: &Event, hash: &str) -> Result<Ledger> {
559 if ev.kind != LEDGER_KIND {
560 return Err(Error::NotALedgerEvent);
561 }
562 ev.verify().map_err(|e| Error::Event(e.to_string()))?;
563 let doc = parse_ledger(&ev.content).map_err(|e| match e {
564 Error::HashMismatch => e,
565 other => Error::LedgerContent(other.to_string()),
566 })?;
567 if doc.hash != hash || x_of(&doc.genesis.operator)? != ev.pubkey {
568 return Err(Error::NotTheOperators);
569 }
570 Ok(doc)
571}
572
573pub fn latest_ledger<'e>(
577 events: impl IntoIterator<Item = &'e Event>,
578 hash: &str,
579) -> Option<(Ledger, &'e Event)> {
580 let mut best: Option<(Ledger, &'e Event)> = None;
581 for ev in events {
582 let Ok(doc) = read_ledger_event(ev, hash) else {
583 continue;
584 };
585 if best
586 .as_ref()
587 .is_none_or(|(_, b)| ev.created_at > b.created_at)
588 {
589 best = Some((doc, ev));
590 }
591 }
592 best
593}