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

splitter.gno

4.49 Kb · 181 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/nt/avl/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.
 28type group struct {
 29	id          int
 30	owner       address
 31	payees      []payee
 32	totalShares int
 33	pool        int // total income recorded, in abstract units
 34}
 35
 36var (
 37	groups   avl.Tree // id (zero-padded string) -> *group
 38	nextID   int
 39	errEmpty = errors.New("splitter: no payees")
 40)
 41
 42// Register creates a new group from comma-separated payees and shares.
 43// payees:  "g1abc...,g1def..."  (addresses)
 44// shares:  "3,1"                (positive integers, same count as payees)
 45// Returns the new group id. Panics on malformed input.
 46func Register(cur realm, payees string, shares string) int {
 47	caller := unsafe.PreviousRealm().Address()
 48
 49	addrParts := splitTrim(payees)
 50	shareParts := splitTrim(shares)
 51
 52	if len(addrParts) == 0 {
 53		panic(errEmpty)
 54	}
 55	if len(addrParts) != len(shareParts) {
 56		panic("splitter: payees and shares count mismatch")
 57	}
 58
 59	g := &group{
 60		id:    nextID,
 61		owner: caller,
 62	}
 63	for i := range addrParts {
 64		if addrParts[i] == "" {
 65			panic("splitter: empty payee address")
 66		}
 67		s, err := strconv.Atoi(shareParts[i])
 68		if err != nil {
 69			panic("splitter: bad share value: " + shareParts[i])
 70		}
 71		if s <= 0 {
 72			panic("splitter: share must be positive")
 73		}
 74		g.payees = append(g.payees, payee{
 75			addr:   address(addrParts[i]),
 76			shares: s,
 77		})
 78		g.totalShares += s
 79	}
 80
 81	groups.Set(key(g.id), g)
 82	nextID++
 83
 84	chain.Emit(
 85		"GroupRegistered",
 86		"id", strconv.Itoa(g.id),
 87		"payees", strconv.Itoa(len(g.payees)),
 88		"totalShares", strconv.Itoa(g.totalShares),
 89	)
 90	return g.id
 91}
 92
 93// RecordIncome adds amount to the pool of group id. amount must be positive.
 94func RecordIncome(cur realm, id int, amount int) {
 95	if amount <= 0 {
 96		panic("splitter: amount must be positive")
 97	}
 98	v := groups.Get(key(id))
 99	if v == nil {
100		panic("splitter: unknown group id: " + strconv.Itoa(id))
101	}
102	g := v.(*group)
103	g.pool += amount
104	groups.Set(key(id), g)
105
106	chain.Emit(
107		"IncomeRecorded",
108		"id", strconv.Itoa(id),
109		"amount", strconv.Itoa(amount),
110		"pool", strconv.Itoa(g.pool),
111	)
112}
113
114// owed returns the amount owed to a payee: pool * shares / totalShares.
115func (g *group) owed(p payee) int {
116	if g.totalShares == 0 {
117		return 0
118	}
119	return g.pool * p.shares / g.totalShares
120}
121
122// Render shows all groups, their payees, shares, and computed owed amounts.
123func Render(path string) string {
124	if groups.Size() == 0 {
125		return "# Payment Splitter\n\n_No groups registered yet._\n"
126	}
127
128	var b strings.Builder
129	b.WriteString("# Payment Splitter\n\n")
130	b.WriteString("Share-based accounting. `owed = pool * share / totalShares`.\n\n")
131
132	groups.Iterate("", "", func(k string, v interface{}) bool {
133		g := v.(*group)
134		b.WriteString("## Group #" + strconv.Itoa(g.id) + "\n\n")
135		b.WriteString("- Owner: `" + g.owner.String() + "`\n")
136		b.WriteString("- Pool: " + strconv.Itoa(g.pool) + "\n")
137		b.WriteString("- Total shares: " + strconv.Itoa(g.totalShares) + "\n\n")
138
139		b.WriteString("| Payee | Shares | Owed |\n")
140		b.WriteString("|---|---:|---:|\n")
141		distributed := 0
142		for _, p := range g.payees {
143			o := g.owed(p)
144			distributed += o
145			b.WriteString("| `" + p.addr.String() + "` | " +
146				strconv.Itoa(p.shares) + " | " + strconv.Itoa(o) + " |\n")
147		}
148		remainder := g.pool - distributed
149		if remainder > 0 {
150			b.WriteString("| _remainder (rounding)_ | | " +
151				strconv.Itoa(remainder) + " |\n")
152		}
153		b.WriteString("\n")
154		return false
155	})
156
157	return b.String()
158}
159
160// splitTrim splits on commas and trims whitespace around each element.
161func splitTrim(s string) []string {
162	if strings.TrimSpace(s) == "" {
163		return nil
164	}
165	parts := strings.Split(s, ",")
166	out := make([]string, 0, len(parts))
167	for _, p := range parts {
168		out = append(out, strings.TrimSpace(p))
169	}
170	return out
171}
172
173// key produces a zero-padded, lexicographically-sortable avl key for an id,
174// so Render iterates groups in ascending numeric order.
175func key(id int) string {
176	s := strconv.Itoa(id)
177	for len(s) < 12 {
178		s = "0" + s
179	}
180	return s
181}