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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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 := deals.Get(strconv.Itoa(id))
118	if v == nil {
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 := deals.Get(strconv.Itoa(id))
131			if v == nil {
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}