Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}