Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}