treasury.gno
5.08 Kb · 163 lines
1package treasury
2
3import (
4 "errors"
5
6 "gno.land/p/nt/bptree/v0"
7 "gno.land/p/nt/bptree/v0/pager"
8 "gno.land/p/nt/ufmt/v0"
9)
10
11var (
12 ErrNoBankerProvided = errors.New("no banker provided")
13 ErrDuplicateBanker = errors.New("duplicate banker")
14 ErrBankerNotFound = errors.New("banker not found")
15 ErrSendPaymentFailed = errors.New("failed to send payment")
16 ErrNonCanonicalBankerImpl = errors.New("non-canonical Banker impl: only *CoinsBanker / *GRC20Banker accepted")
17)
18
19// New creates a new Treasury instance with the given bankers.
20//
21// pkgPath should be the package path of the owning realm. It is
22// captured on the Treasury and used to build the "See full history"
23// link in RenderBanker. Pass `cur.PkgPath()` from the owning realm's
24// init (or another live-cur context). Passing "" disables the link.
25//
26// The path is stored as plain data — no IsCurrent guard inside this
27// /p/ constructor. The caller is the trust boundary; it must derive
28// the value from a real `cur realm` rather than accept it from
29// untrusted input. The captured path is consumed only for render
30// output (Class-2 designation-forgery shape, accepted as
31// display-only — see docs/resources/gno-security.md).
32//
33// Each banker must be one of treasury's canonical concrete impls
34// (*CoinsBanker, *GRC20Banker) per IsCanonicalBanker. Foreign-realm impls
35// (including embedded wrappers around canonical types) are rejected: a
36// malicious Send impl would receive a capability token via its rlm
37// parameter when treasury.Send dispatches into it.
38//
39// The allowlist validates type only, not captured state. Treasury operators
40// must construct their own bankers; never accept a pre-built *Banker value
41// from an external realm.
42func New(bankers []Banker, pkgPath string) (*Treasury, error) {
43 if len(bankers) == 0 {
44 return nil, ErrNoBankerProvided
45 }
46
47 // Canonical-impl allowlist: reject foreign types (embedding-based
48 // bypasses fail here because type assertions are nominal).
49 for _, b := range bankers {
50 if !IsCanonicalBanker(b) {
51 return nil, ErrNonCanonicalBankerImpl
52 }
53 }
54
55 // Create a new Treasury instance.
56 treasury := &Treasury{bankers: bptree.NewBPTree32(), realmPath: pkgPath}
57
58 // Register the bankers.
59 for _, banker := range bankers {
60 if treasury.bankers.Has(banker.ID()) {
61 return nil, ufmt.Errorf("%v: %s", ErrDuplicateBanker, banker.ID())
62 }
63
64 treasury.bankers.Set(
65 banker.ID(),
66 &bankerRecord{banker: banker},
67 )
68 }
69
70 // Register the Render routes.
71 treasury.initRenderRouter()
72
73 return treasury, nil
74}
75
76// Send sends a payment using the corresponding banker. rlm is threaded
77// to the banker's Send for IsCurrent + owner validation.
78func (t *Treasury) Send(_ int, rlm realm, p Payment) error {
79 // Get the banker record corresponding to this Payment.
80 br, ok := t.bankers.Get(p.BankerID())
81 if !ok {
82 return ufmt.Errorf("%v: %s", ErrBankerNotFound, p.BankerID())
83 }
84 record := br.(*bankerRecord)
85
86 // Send the payment using the corresponding banker.
87 if err := record.banker.Send(0, rlm, p); err != nil {
88 return ufmt.Errorf("%v: %s", ErrSendPaymentFailed, err)
89 }
90
91 // Add the payment to the history of the banker.
92 record.history.Append(p)
93
94 return nil
95}
96
97// History returns the payment history sent by the banker with the given ID.
98// Payments are paginated, with the most recent payments first.
99func (t *Treasury) History(
100 bankerID string,
101 pageNumber int,
102 pageSize int,
103) ([]Payment, error) {
104 // Get the banker record corresponding to this ID.
105 br, ok := t.bankers.Get(bankerID)
106 if !ok {
107 return nil, ufmt.Errorf("%v: %s", ErrBankerNotFound, bankerID)
108 }
109 history := br.(*bankerRecord).history
110
111 // Get the page of payments from the history.
112 p := pager.NewPager(history.Tree(), pageSize, true)
113 page := p.GetPage(pageNumber)
114
115 // Convert the items in the page to a slice of Payments.
116 payments := make([]Payment, len(page.Items))
117 for i := range page.Items {
118 payments[i] = page.Items[i].Value.(Payment)
119 }
120
121 return payments, nil
122}
123
124// Balances returns the balances of the banker with the given ID.
125func (t *Treasury) Balances(bankerID string) ([]Balance, error) {
126 // Get the banker record corresponding to this ID.
127 br, ok := t.bankers.Get(bankerID)
128 if !ok {
129 return nil, ufmt.Errorf("%v: %s", ErrBankerNotFound, bankerID)
130 }
131
132 // Get the balances from the banker.
133 return br.(*bankerRecord).banker.Balances(), nil
134}
135
136// Address returns the address of the banker with the given ID.
137func (t *Treasury) Address(bankerID string) (string, error) {
138 // Get the banker record corresponding to this ID.
139 br, ok := t.bankers.Get(bankerID)
140 if !ok {
141 return "", ufmt.Errorf("%v: %s", ErrBankerNotFound, bankerID)
142 }
143
144 // Get the address from the banker.
145 return br.(*bankerRecord).banker.Address(), nil
146}
147
148// HasBanker checks if a banker with the given ID is registered.
149func (t *Treasury) HasBanker(bankerID string) bool {
150 return t.bankers.Has(bankerID)
151}
152
153// ListBankerIDs returns a list of all registered banker IDs.
154func (t *Treasury) ListBankerIDs() []string {
155 var bankerIDs []string
156
157 t.bankers.Iterate("", "", func(bankerID string, _ any) bool {
158 bankerIDs = append(bankerIDs, bankerID)
159 return false
160 })
161
162 return bankerIDs
163}