pullpaymentdemo.gno
3.62 Kb · 87 lines
1// Package pullpaymentdemo is a small gnoweb demo of the escrow ledger provided
2// by the [p/moul/x/daily/pullpayment](/p/moul/x/daily/pullpayment/v0) library:
3// crediting a split, claiming it, and why a reentrant claim gets nothing.
4//
5// It contains no ledger logic of its own and holds no coins. Stateless, so
6// Render is deterministic — which is precisely what the library is for.
7package pullpaymentdemo
8
9import (
10 "strconv"
11 "strings"
12
13 "gno.land/p/moul/x/daily/pullpayment/v0"
14)
15
16// Render renders the demo for gnoweb.
17func Render(path string) string {
18 var b strings.Builder
19 b.WriteString("# Pull Payment\n\n")
20 b.WriteString("Credit and let recipients withdraw, demoing the ")
21 b.WriteString("[`p/moul/x/daily/pullpayment`](/p/moul/x/daily/pullpayment/v0) library.\n\n")
22
23 b.WriteString("## Why not just send?\n\n")
24 b.WriteString("Pushing value hands control to the recipient in the middle of your ")
25 b.WriteString("state transition, and a hostile recipient re-enters before you have ")
26 b.WriteString("finished updating. Pulling inverts it: you record a debt, and the ")
27 b.WriteString("recipient's own withdrawal is the only state being touched.\n\n")
28
29 l := pullpayment.New()
30 l.CreditMany([]string{"alice", "bob", "carol"}, []int64{500, 300, 200})
31
32 b.WriteString("## A split, credited\n\n")
33 b.WriteString(table(l))
34 b.WriteString("\nThe holding realm must keep `")
35 b.WriteString(strconv.FormatInt(l.TotalOwed(), 10))
36 b.WriteString("` in reserve — that is what `TotalOwed` is for.\n\n")
37
38 b.WriteString("## Bob claims\n\n")
39 amt, _ := l.Withdraw("bob")
40 b.WriteString("`Withdraw(\"bob\")` → `" + strconv.FormatInt(amt, 10) + "`, and the ledger ")
41 b.WriteString("is updated **before** the caller transfers anything:\n\n")
42 b.WriteString(table(l))
43 b.WriteString("\nReserve is now `" + strconv.FormatInt(l.TotalOwed(), 10) + "`.\n\n")
44
45 b.WriteString("## The reentrancy attempt\n\n")
46 b.WriteString("Bob's handler calls straight back in, before the transfer completes:\n\n")
47 again, err := l.Withdraw("bob")
48 b.WriteString("- second `Withdraw(\"bob\")` → `" + strconv.FormatInt(again, 10) + "`, error: `")
49 if err != nil {
50 b.WriteString(err.Error())
51 } else {
52 b.WriteString("<nil>")
53 }
54 b.WriteString("`\n\n")
55 b.WriteString("Nothing left to take. The balance was deleted before control left the ")
56 b.WriteString("function — checks, effects, *then* interactions. Lifetime withdrawn is ")
57 b.WriteString("still `" + strconv.FormatInt(l.TotalWithdrawn(), 10) + "`, not double.\n\n")
58
59 b.WriteString("## Credits accumulate\n\n")
60 l.Credit("alice", 100)
61 b.WriteString("`Credit(\"alice\", 100)` on top of her existing balance:\n\n")
62 b.WriteString(table(l))
63 b.WriteString("\nOne withdrawal collects the lot — no dust left behind.\n\n")
64
65 b.WriteString("## Batch credits are all-or-nothing\n\n")
66 b.WriteString("A split that is partly invalid applies **none** of itself; a ledger ")
67 b.WriteString("half-agreeing with the funds it guards is worse than a rejected call:\n\n")
68 before := l.TotalOwed()
69 batchErr := l.CreditMany([]string{"dave", "eve"}, []int64{10, -1})
70 b.WriteString("- `CreditMany([dave, eve], [10, -1])` → `")
71 if batchErr != nil {
72 b.WriteString(batchErr.Error())
73 }
74 b.WriteString("`\n- total owed before: `" + strconv.FormatInt(before, 10) + "`, after: `")
75 b.WriteString(strconv.FormatInt(l.TotalOwed(), 10) + "` — unchanged, and `dave` was not credited\n")
76 return b.String()
77}
78
79func table(l *pullpayment.Ledger) string {
80 var b strings.Builder
81 b.WriteString("| payee | owed |\n|---|---|\n")
82 l.Iterate(func(p string, amt int64) bool {
83 b.WriteString("| `" + p + "` | " + strconv.FormatInt(amt, 10) + " |\n")
84 return false
85 })
86 return b.String()
87}