Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

splitter.gno

4.29 Kb · 164 lines
  1// Package splitter is a share-based payment splitter for gno.land.
  2//
  3// It is an accounting-only port of the Solidity PaymentSplitter pattern:
  4// no real coins move. A group is registered with a list of payees and a
  5// matching list of integer shares. Income recorded against a group is
  6// pooled, and each payee is owed a slice of that pool proportional to
  7// their shares: owed = pool * share / totalShares.
  8package splitter
  9
 10import (
 11	"errors"
 12	"strconv"
 13	"strings"
 14
 15	"chain"
 16	"chain/runtime/unsafe"
 17
 18	"gno.land/p/moul/kit/store/v0"
 19)
 20
 21// payee is a single share holder inside a group.
 22type payee struct {
 23	addr   address
 24	shares int
 25}
 26
 27// group is one payment-splitting arrangement. It carries no id field: the id
 28// belongs to the store, which hands it back on lookup and iteration.
 29type group struct {
 30	owner       address
 31	payees      []payee
 32	totalShares int
 33	pool        int // total income recorded, in abstract units
 34}
 35
 36var (
 37	// groups assigns the group ids. v0 kept its own nextID plus a key() that
 38	// zero-padded to width 12, which stopped ordering Render past 10^12.
 39	groups = store.Named("splitter: group")
 40
 41	errEmpty = errors.New("splitter: no payees")
 42)
 43
 44// Register creates a new group from comma-separated payees and shares.
 45// payees:  "g1abc...,g1def..."  (addresses)
 46// shares:  "3,1"                (positive integers, same count as payees)
 47// Returns the new group id. Panics on malformed input.
 48func Register(cur realm, payees string, shares string) int {
 49	caller := unsafe.PreviousRealm().Address()
 50
 51	addrParts := splitTrim(payees)
 52	shareParts := splitTrim(shares)
 53
 54	if len(addrParts) == 0 {
 55		panic(errEmpty)
 56	}
 57	if len(addrParts) != len(shareParts) {
 58		panic("splitter: payees and shares count mismatch")
 59	}
 60
 61	g := &group{owner: caller}
 62	for i := range addrParts {
 63		if addrParts[i] == "" {
 64			panic("splitter: empty payee address")
 65		}
 66		s, err := strconv.Atoi(shareParts[i])
 67		if err != nil {
 68			panic("splitter: bad share value: " + shareParts[i])
 69		}
 70		if s <= 0 {
 71			panic("splitter: share must be positive")
 72		}
 73		g.payees = append(g.payees, payee{
 74			addr:   address(addrParts[i]),
 75			shares: s,
 76		})
 77		g.totalShares += s
 78	}
 79
 80	id := groups.Add(g)
 81
 82	chain.Emit(
 83		"GroupRegistered",
 84		"id", id.String(),
 85		"payees", strconv.Itoa(len(g.payees)),
 86		"totalShares", strconv.Itoa(g.totalShares),
 87	)
 88	return int(id)
 89}
 90
 91// RecordIncome adds amount to the pool of group id. amount must be positive.
 92func RecordIncome(cur realm, id int, amount int) {
 93	if amount <= 0 {
 94		panic("splitter: amount must be positive")
 95	}
 96	g := groups.MustGet(store.ID(id)).(*group)
 97	g.pool += amount
 98
 99	chain.Emit(
100		"IncomeRecorded",
101		"id", strconv.Itoa(id),
102		"amount", strconv.Itoa(amount),
103		"pool", strconv.Itoa(g.pool),
104	)
105}
106
107// owed returns the amount owed to a payee: pool * shares / totalShares.
108func (g *group) owed(p payee) int {
109	if g.totalShares == 0 {
110		return 0
111	}
112	return g.pool * p.shares / g.totalShares
113}
114
115// Render shows all groups, their payees, shares, and computed owed amounts.
116func Render(path string) string {
117	if groups.Len() == 0 {
118		return "# Payment Splitter\n\n_No groups registered yet._\n"
119	}
120
121	var b strings.Builder
122	b.WriteString("# Payment Splitter\n\n")
123	b.WriteString("Share-based accounting. `owed = pool * share / totalShares`.\n\n")
124
125	groups.Each(func(id store.ID, v any) {
126		g := v.(*group)
127		b.WriteString("## Group #" + id.String() + "\n\n")
128		b.WriteString("- Owner: `" + g.owner.String() + "`\n")
129		b.WriteString("- Pool: " + strconv.Itoa(g.pool) + "\n")
130		b.WriteString("- Total shares: " + strconv.Itoa(g.totalShares) + "\n\n")
131
132		b.WriteString("| Payee | Shares | Owed |\n")
133		b.WriteString("|---|---:|---:|\n")
134		distributed := 0
135		for _, p := range g.payees {
136			o := g.owed(p)
137			distributed += o
138			b.WriteString("| `" + p.addr.String() + "` | " +
139				strconv.Itoa(p.shares) + " | " + strconv.Itoa(o) + " |\n")
140		}
141		remainder := g.pool - distributed
142		if remainder > 0 {
143			b.WriteString("| _remainder (rounding)_ | | " +
144				strconv.Itoa(remainder) + " |\n")
145		}
146		b.WriteString("\n")
147	})
148
149	return b.String()
150}
151
152// splitTrim splits on commas and trims whitespace around each element.
153func splitTrim(s string) []string {
154	if strings.TrimSpace(s) == "" {
155		return nil
156	}
157	parts := strings.Split(s, ",")
158	out := make([]string, 0, len(parts))
159	for _, p := range parts {
160		out = append(out, strings.TrimSpace(p))
161	}
162	return out
163}
164