Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}