Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}