← Back to Realms

Realm

gno.land/r/moul/x/daily/escrow/v1

Overview

Realm Path
gno.land/r/moul/x/daily/escrow/v1
Exported Functions
4
State Entries
12
Source Files
3
Total Package Entries
20

Exported Functions

4 exported functions

State

12 state entries

Source Code

FILES
escrow.gno
go
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}
190

Raw Package Data

Raw JSON data