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 a5// matching list of integer shares. Income recorded against a group is6// pooled, and each payee is owed a slice of that pool proportional to7// their shares: owed = pool * share / totalShares.8package splitter910import (11 "errors"12 "strconv"13 "strings"1415 "chain"16 "chain/runtime/unsafe"1718 "gno.land/p/nt/avl/v0"19)2021// payee is a single share holder inside a group.22type payee struct {23 addr address24 shares int25}2627// group is one payment-splitting arrangement.28type group struct {29 id int30 owner address31 payees []payee32 totalShares int33 pool int // total income recorded, in abstract units34}3536var (37 groups avl.Tree // id (zero-padded string) -> *group38 nextID int39 errEmpty = errors.New("splitter: no payees")40)4142// 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()4849 addrParts := splitTrim(payees)50 shareParts := splitTrim(shares)5152 if len(addrParts) == 0 {53 panic(errEmpty)54 }55 if len(addrParts) != len(shareParts) {56 panic("splitter: payees and shares count mismatch")57 }5859 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 += s79 }8081 groups.Set(key(g.id), g)82 nextID++8384 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.id91}9293// 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 += amount104 groups.Set(key(id), g)105106 chain.Emit(107 "IncomeRecorded",108 "id", strconv.Itoa(id),109 "amount", strconv.Itoa(amount),110 "pool", strconv.Itoa(g.pool),111 )112}113114// 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 0118 }119 return g.pool * p.shares / g.totalShares120}121122// 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 }127128 var b strings.Builder129 b.WriteString("# Payment Splitter\n\n")130 b.WriteString("Share-based accounting. `owed = pool * share / totalShares`.\n\n")131132 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")138139 b.WriteString("| Payee | Shares | Owed |\n")140 b.WriteString("|---|---:|---:|\n")141 distributed := 0142 for _, p := range g.payees {143 o := g.owed(p)144 distributed += o145 b.WriteString("| `" + p.addr.String() + "` | " +146 strconv.Itoa(p.shares) + " | " + strconv.Itoa(o) + " |\n")147 }148 remainder := g.pool - distributed149 if remainder > 0 {150 b.WriteString("| _remainder (rounding)_ | | " +151 strconv.Itoa(remainder) + " |\n")152 }153 b.WriteString("\n")154 return false155 })156157 return b.String()158}159160// splitTrim splits on commas and trims whitespace around each element.161func splitTrim(s string) []string {162 if strings.TrimSpace(s) == "" {163 return nil164 }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 out171}172173// 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" + s179 }180 return s181}182RecordIncome(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, amount int)
Register(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, payees string, shares string) int
Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.