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