escrow.gno
4.56 Kb · 189 lines
1package escrow
2
3import (
4 "chain"
5 "chain/runtime/unsafe"
6 "errors"
7 "strconv"
8 "strings"
9
10 "gno.land/p/moul/kit/ui/v0"
11 "gno.land/p/nt/avl/v0"
12)
13
14// State constants for a deal's lifecycle.
15const (
16 StateOpen = "Open"
17 StateSettled = "Settled"
18 StateCanceled = "Canceled"
19)
20
21// Deal is a 2-party escrow record. Accounting only: no funds move,
22// this tracks agreement and mutual confirmation between two parties.
23type Deal struct {
24 ID int
25 Creator address
26 Counterparty address
27 Terms string
28 State string
29 CreatorOK bool
30 CounterOK bool
31}
32
33var (
34 deals = avl.NewTree() // id (string) -> *Deal
35 nextID int
36)
37
38var (
39 errNotFound = errors.New("escrow: deal not found")
40 errNotParty = errors.New("escrow: caller is not a party to this deal")
41 errNotOpen = errors.New("escrow: deal is not open")
42 errNotCreator = errors.New("escrow: only the creator can cancel")
43 errBadAddr = errors.New("escrow: invalid counterparty address")
44 errSelfDeal = errors.New("escrow: counterparty must differ from creator")
45)
46
47// Create opens a new escrow deal between the caller (creator) and the
48// given counterparty, governed by a free-form terms string. Returns the
49// new deal id. Panics on invalid input (cross-realm abort).
50func Create(cur realm, counterparty address, terms string) int {
51 if !counterparty.IsValid() {
52 panic(errBadAddr)
53 }
54 creator := unsafe.PreviousRealm().Address()
55 if counterparty == creator {
56 panic(errSelfDeal)
57 }
58
59 id := nextID
60 nextID++
61
62 d := &Deal{
63 ID: id,
64 Creator: creator,
65 Counterparty: counterparty,
66 Terms: terms,
67 State: StateOpen,
68 }
69 deals.Set(strconv.Itoa(id), d)
70
71 chain.Emit("Created", "id", strconv.Itoa(id),
72 "creator", creator.String(), "counterparty", counterparty.String())
73 return id
74}
75
76// Confirm records confirmation by whichever party is calling. Once both
77// parties have confirmed, the deal transitions to Settled.
78func Confirm(cur realm, id int) {
79 d := mustGet(id)
80 if d.State != StateOpen {
81 panic(errNotOpen)
82 }
83 caller := unsafe.PreviousRealm().Address()
84
85 switch caller {
86 case d.Creator:
87 d.CreatorOK = true
88 case d.Counterparty:
89 d.CounterOK = true
90 default:
91 panic(errNotParty)
92 }
93
94 chain.Emit("Confirmed", "id", strconv.Itoa(id), "party", caller.String())
95
96 if d.CreatorOK && d.CounterOK {
97 d.State = StateSettled
98 chain.Emit("Settled", "id", strconv.Itoa(id))
99 }
100}
101
102// Cancel voids an open deal. Only the creator may cancel, and only
103// before the deal has settled.
104func Cancel(cur realm, id int) {
105 d := mustGet(id)
106 if d.State != StateOpen {
107 panic(errNotOpen)
108 }
109 caller := unsafe.PreviousRealm().Address()
110 if caller != d.Creator {
111 panic(errNotCreator)
112 }
113 d.State = StateCanceled
114 chain.Emit("Canceled", "id", strconv.Itoa(id))
115}
116
117func mustGet(id int) *Deal {
118 v := deals.Get(strconv.Itoa(id))
119 if v == nil {
120 panic(errNotFound)
121 }
122 return v.(*Deal)
123}
124
125// Render lists all escrow deals with their parties, terms, and state.
126// When path is a numeric id, renders just that deal.
127func Render(path string) string {
128 path = strings.TrimSpace(strings.Trim(path, "/"))
129 if path != "" {
130 if id, err := strconv.Atoi(path); err == nil {
131 v := deals.Get(strconv.Itoa(id))
132 if v == nil {
133 return "# Escrow\n\nDeal `" + path + "` not found.\n"
134 }
135 return renderOne(v.(*Deal))
136 }
137 }
138
139 var b strings.Builder
140 b.WriteString("# Escrow\n\n")
141 b.WriteString("2-party escrow deals (accounting only — no funds move).\n\n")
142
143 if deals.Size() == 0 {
144 b.WriteString("_No deals yet._\n")
145 return b.String()
146 }
147
148 b.WriteString("| ID | Creator | Counterparty | State | Confirmations | Terms |\n")
149 b.WriteString("|----|---------|--------------|-------|---------------|-------|\n")
150 deals.Iterate("", "", func(_ string, v interface{}) bool {
151 d := v.(*Deal)
152 b.WriteString("| " + strconv.Itoa(d.ID) +
153 " | " + ui.Addr(d.Creator) +
154 " | " + ui.Addr(d.Counterparty) +
155 " | " + d.State +
156 " | " + confs(d) +
157 " | " + ui.Cell(d.Terms) + " |\n")
158 return false
159 })
160 return b.String()
161}
162
163func renderOne(d *Deal) string {
164 var b strings.Builder
165 b.WriteString("# Escrow Deal #" + strconv.Itoa(d.ID) + "\n\n")
166 b.WriteString("- **State:** " + d.State + "\n")
167 b.WriteString("- **Creator:** " + d.Creator.String() + " (" + yn(d.CreatorOK) + ")\n")
168 b.WriteString("- **Counterparty:** " + d.Counterparty.String() + " (" + yn(d.CounterOK) + ")\n")
169 b.WriteString("- **Terms:** " + d.Terms + "\n")
170 return b.String()
171}
172
173func confs(d *Deal) string {
174 n := 0
175 if d.CreatorOK {
176 n++
177 }
178 if d.CounterOK {
179 n++
180 }
181 return strconv.Itoa(n) + "/2"
182}
183
184func yn(b bool) string {
185 if b {
186 return "confirmed"
187 }
188 return "pending"
189}