escrow.gno
4.75 Kb · 198 lines
1package escrow
2
3import (
4 "chain"
5 "errors"
6 "strconv"
7 "strings"
8
9 "gno.land/p/nym-moultest123/kit/ui/v0"
10 "gno.land/p/nym-moultest123/md/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 !cur.IsCurrent() {
52 panic("escrow: spoofed realm")
53 }
54 if !counterparty.IsValid() {
55 panic(errBadAddr)
56 }
57 creator := cur.Previous().Address()
58 if counterparty == creator {
59 panic(errSelfDeal)
60 }
61
62 id := nextID
63 nextID++
64
65 d := &Deal{
66 ID: id,
67 Creator: creator,
68 Counterparty: counterparty,
69 Terms: terms,
70 State: StateOpen,
71 }
72 deals.Set(strconv.Itoa(id), d)
73
74 chain.Emit("Created", "id", strconv.Itoa(id),
75 "creator", creator.String(), "counterparty", counterparty.String())
76 return id
77}
78
79// Confirm records confirmation by whichever party is calling. Once both
80// parties have confirmed, the deal transitions to Settled.
81func Confirm(cur realm, id int) {
82 if !cur.IsCurrent() {
83 panic("escrow: spoofed realm")
84 }
85 d := mustGet(id)
86 if d.State != StateOpen {
87 panic(errNotOpen)
88 }
89 caller := cur.Previous().Address()
90
91 switch caller {
92 case d.Creator:
93 d.CreatorOK = true
94 case d.Counterparty:
95 d.CounterOK = true
96 default:
97 panic(errNotParty)
98 }
99
100 chain.Emit("Confirmed", "id", strconv.Itoa(id), "party", caller.String())
101
102 if d.CreatorOK && d.CounterOK {
103 d.State = StateSettled
104 chain.Emit("Settled", "id", strconv.Itoa(id))
105 }
106}
107
108// Cancel voids an open deal. Only the creator may cancel, and only
109// before the deal has settled.
110func Cancel(cur realm, id int) {
111 if !cur.IsCurrent() {
112 panic("escrow: spoofed realm")
113 }
114 d := mustGet(id)
115 if d.State != StateOpen {
116 panic(errNotOpen)
117 }
118 caller := cur.Previous().Address()
119 if caller != d.Creator {
120 panic(errNotCreator)
121 }
122 d.State = StateCanceled
123 chain.Emit("Canceled", "id", strconv.Itoa(id))
124}
125
126func mustGet(id int) *Deal {
127 v := deals.Get(strconv.Itoa(id))
128 if v == nil {
129 panic(errNotFound)
130 }
131 return v.(*Deal)
132}
133
134// Render lists all escrow deals with their parties, terms, and state.
135// When path is a numeric id, renders just that deal.
136func Render(path string) string {
137 path = strings.TrimSpace(strings.Trim(path, "/"))
138 if path != "" {
139 if id, err := strconv.Atoi(path); err == nil {
140 v := deals.Get(strconv.Itoa(id))
141 if v == nil {
142 return "# Escrow\n\nDeal " + md.InlineCode(path) + " not found.\n"
143 }
144 return renderOne(v.(*Deal))
145 }
146 }
147
148 var b strings.Builder
149 b.WriteString("# Escrow\n\n")
150 b.WriteString("2-party escrow deals (accounting only — no funds move).\n\n")
151
152 if deals.Size() == 0 {
153 b.WriteString("_No deals yet._\n")
154 return b.String()
155 }
156
157 b.WriteString("| ID | Creator | Counterparty | State | Confirmations | Terms |\n")
158 b.WriteString("|----|---------|--------------|-------|---------------|-------|\n")
159 deals.Iterate("", "", func(_ string, v interface{}) bool {
160 d := v.(*Deal)
161 b.WriteString("| " + strconv.Itoa(d.ID) +
162 " | " + ui.Addr(d.Creator) +
163 " | " + ui.Addr(d.Counterparty) +
164 " | " + d.State +
165 " | " + confs(d) +
166 " | " + ui.Cell(d.Terms) + " |\n")
167 return false
168 })
169 return b.String()
170}
171
172func renderOne(d *Deal) string {
173 var b strings.Builder
174 b.WriteString("# Escrow Deal #" + strconv.Itoa(d.ID) + "\n\n")
175 b.WriteString("- **State:** " + d.State + "\n")
176 b.WriteString("- **Creator:** " + d.Creator.String() + " (" + yn(d.CreatorOK) + ")\n")
177 b.WriteString("- **Counterparty:** " + d.Counterparty.String() + " (" + yn(d.CounterOK) + ")\n")
178 b.WriteString("- **Terms:** " + ui.Inline(d.Terms) + "\n")
179 return b.String()
180}
181
182func confs(d *Deal) string {
183 n := 0
184 if d.CreatorOK {
185 n++
186 }
187 if d.CounterOK {
188 n++
189 }
190 return strconv.Itoa(n) + "/2"
191}
192
193func yn(b bool) string {
194 if b {
195 return "confirmed"
196 }
197 return "pending"
198}