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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

splitter.gno

4.30 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/nym-moultest123/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