README.md
v0 - Unaudited This is an initial version of this package that has not yet been formally audited. A fully audited version will be published as a subsequent release. Use in production at your own risk.
treasury - Coin and GRC20 treasury management
Treasury management for coin and GRC20 token transfers in Gno realms. A Treasury holds a set of Bankers, each responsible for sending a specific asset type, and records the payment history per banker.
1. Concepts
- Treasury: container that registers one or more
Bankers and exposes a unifiedSend/History/BalancesAPI. Also provides aRenderrouter for gnoweb pages. - Banker: handler for a single asset type. Built-ins are
CoinsBanker(native chain coins) andGRC20Banker(any number of GRC20 tokens, resolved through a user-suppliedTokenListerFunc). - Payment: opaque value produced by a banker-specific helper (
NewCoinsPayment,NewGRC20Payment). EachPaymentis bound to aBankerID(), which is how the treasury routes it.
2. Usage
1import (
2 "chain"
3 "chain/banker"
4 "chain/runtime"
5
6 "gno.land/p/demo/tokens/grc20"
7 "gno.land/p/nt/treasury/v0"
8)
9
10var (
11 tokens = map[string]*grc20.Token{}
12 tr *treasury.Treasury
13)
14
15func init() {
16 owner := runtime.CurrentRealm().Address() // this realm holds and sends the funds
17
18 // Coins banker owned by this realm.
19 coinsBanker, err := treasury.NewCoinsBankerWithOwner(
20 owner,
21 banker.NewBanker(banker.BankerTypeRealmSend),
22 )
23 if err != nil {
24 panic(err)
25 }
26
27 // GRC20 banker that resolves tokens through a lister.
28 grc20Banker, err := treasury.NewGRC20BankerWithOwner(owner, func() map[string]*grc20.Token {
29 return tokens
30 })
31 if err != nil {
32 panic(err)
33 }
34
35 tr, err = treasury.New(
36 []treasury.Banker{coinsBanker, grc20Banker},
37 runtime.CurrentRealm().PkgPath(),
38 )
39 if err != nil {
40 panic(err)
41 }
42}
43
44// SendUgnot transfers ugnot from the realm to `to`.
45func SendUgnot(cur realm, to address, amount int64) {
46 p := treasury.NewCoinsPayment(chain.Coins{{Denom: "ugnot", Amount: amount}}, to)
47 if err := tr.Send(0, cur, p); err != nil {
48 panic(err)
49 }
50}
51
52// Render exposes the treasury under the realm's render path.
53func Render(path string) string {
54 return tr.Render(path)
55}
3. API
3.1 Treasury
1// Builds a treasury with the provided bankers (at least one required, IDs must be unique).
2// pkgPath is the realm's package path, used as the base for the Render router.
3func New(bankers []Banker, pkgPath string) (*Treasury, error)
4
5func (t *Treasury) Send(_ int, rlm realm, p Payment) error
6func (t *Treasury) History(bankerID string, pageNumber, pageSize int) ([]Payment, error)
7func (t *Treasury) Balances(bankerID string) ([]Balance, error)
8func (t *Treasury) Address(bankerID string) (string, error)
9func (t *Treasury) HasBanker(bankerID string) bool
10func (t *Treasury) ListBankerIDs() []string
11
12// Render entry points (a mux router is initialized by `New`).
13func (t *Treasury) Render(path string) string
14func (t *Treasury) RenderLanding(path string) string
15func (t *Treasury) RenderBanker(bankerID, path string) string
16func (t *Treasury) RenderBankerHistory(bankerID, path string) string
Render routes:
""— landing page, lists each banker.{banker}— banker details (address, balances, last N payments).{banker}/history— paginated payment history.
The history_size query parameter on {banker} controls the preview size (default 5, 0 hides the preview).
3.2 Banker and Payment interfaces
1type Banker interface {
2 ID() string // unique banker ID used for routing
3 Send(int, realm, Payment) error // thread the caller's cur; pass 0 as the first arg
4 Balances() []Balance
5 Address() string // address used to receive payments
6}
7
8type Payment interface {
9 BankerID() string // routes the payment to a banker
10 String() string
11}
12
13type Balance struct {
14 Denom string
15 Amount int64
16}
17
18// Capability guard: any entry point that accepts a Banker from an external
19// caller MUST verify it before invoking its methods. Validates dynamic type
20// only (embedding-based wrappers are rejected), not captured state.
21func IsCanonicalBanker(b Banker) bool
3.3 CoinsBanker
Banker for native chain coins. Owns an address and an inner chain/banker.Banker (must be the canonical one returned by banker.NewBanker — fake implementations are rejected).
1func NewCoinsBankerWithOwner(owner address, banker_ banker.Banker) (*CoinsBanker, error)
2
3func NewCoinsPayment(coins chain.Coins, toAddress address) Payment
CoinsBanker.ID() returns "Coins".
3.4 GRC20Banker
Banker for GRC20 tokens. Tokens are resolved at send time through a TokenListerFunc, so the set of supported tokens can change without rebuilding the banker.
1type TokenListerFunc func() map[string]*grc20.Token
2
3func NewGRC20BankerWithOwner(owner address, lister TokenListerFunc) (*GRC20Banker, error)
4
5func NewGRC20Payment(tokenKey string, amount int64, toAddress address) Payment
GRC20Banker.ID() returns "GRC20". tokenKey must be a key in the map returned by the lister.
3.5 Errors
1ErrNoBankerProvided // New called with empty bankers slice
2ErrDuplicateBanker // two bankers share the same ID
3ErrBankerNotFound // Send/History/... called with an unknown banker ID
4ErrSendPaymentFailed // wraps the underlying banker error
5ErrCurrentRealmIsNotOwner // banker called from a realm other than its owner
6ErrNoOwnerProvided
7ErrInvalidPaymentType // payment routed to the wrong banker type
8ErrNonCanonicalBanker // CoinsBanker built from a non-canonical std banker
9ErrNonCanonicalBankerImpl // New given a Banker of a non-canonical type
10ErrSpoofedRealm // Send called with a non-current rlm
11ErrNoListerProvided
12ErrGRC20TokenNotFound
4. Security
The Banker capability model rests on three rules:
- Construct your own bankers. Never accept a pre-built
Banker(including a*WithOwnervalue) from an external realm. A hostileBalances/Addresscan report data tied to an attacker address.NewcallsIsCanonicalBankeron each banker and rejects foreign types withErrNonCanonicalBankerImpl. IsCanonicalBankerchecks dynamic TYPE only, not captured state. Embedding-based wrappers (type Evil struct { *CoinsBanker }) are rejected because type assertions are nominal. Any public entry point that takes aBankerfrom a caller must call it before invoking the banker's methods.- Owner must match the acting realm.
Sendassertsrlm.IsCurrent()(elseErrSpoofedRealm) and the banker rejects a caller that is not its owner (ErrCurrentRealmIsNotOwner). Set the owner to the realm that will actually send.