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.

money.gno

2.73 Kb · 95 lines
 1package safe
 2
 3import (
 4	"strconv"
 5	"strings"
 6
 7	"gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/types/v1"
 8)
 9
10// BpsDenominator is 100% in basis points (EIP-2981, royalty/v0).
11const BpsDenominator = 10000
12
13// BpsOf returns floor(amount * bps / 10000) without ever forming the full
14// product, so it cannot overflow for any amount >= 0 and 0 <= bps <= 10000.
15func BpsOf(amount, bps int64) int64 {
16	if amount < 0 {
17		panic("BpsOf: negative amount")
18	}
19	if bps < 0 || bps > BpsDenominator {
20		panic("BpsOf: bps out of range")
21	}
22	return (amount/BpsDenominator)*bps + (amount%BpsDenominator)*bps/BpsDenominator
23}
24
25// AssertBps panics unless 0 <= bps <= cap.
26func AssertBps(field string, bps, cap int64) {
27	if bps < 0 || bps > cap {
28		panic(field + ": must be between 0 and " + strconv.FormatInt(cap, 10) + " bps")
29	}
30}
31
32// ParsePayees parses "g1…:7000,g1…:3000": 1..max distinct valid addresses,
33// each with a positive share, shares summing to exactly 10000.
34func ParsePayees(field, s string, max int) []types.Payee {
35	if s == "" {
36		panic(field + ": must not be empty")
37	}
38	parts := strings.Split(s, ",")
39	if len(parts) > max {
40		panic(field + ": at most " + strconv.Itoa(max) + " payees")
41	}
42	out := make([]types.Payee, 0, len(parts))
43	var total int64
44	for _, part := range parts {
45		addrStr, bpsStr, ok := strings.Cut(part, ":")
46		if !ok {
47			panic(field + ": expected address:bps, got " + strconv.Quote(part))
48		}
49		addr := address(addrStr)
50		if !addr.IsValid() {
51			panic(field + ": invalid address " + strconv.Quote(addrStr))
52		}
53		bps, err := strconv.ParseInt(bpsStr, 10, 64)
54		if err != nil || bps <= 0 || bps > BpsDenominator {
55			panic(field + ": invalid share " + strconv.Quote(bpsStr))
56		}
57		for _, p := range out {
58			if p.Addr == addr {
59				panic(field + ": duplicate payee " + addrStr)
60			}
61		}
62		total += bps
63		out = append(out, types.Payee{Addr: addr, Bps: bps})
64	}
65	if total != BpsDenominator {
66		panic(field + ": shares must sum to 10000 bps, got " + strconv.FormatInt(total, 10))
67	}
68	return out
69}
70
71// FormatPayees is the inverse of ParsePayees.
72func FormatPayees(ps []types.Payee) string {
73	parts := make([]string, len(ps))
74	for i, p := range ps {
75		parts[i] = p.Addr.String() + ":" + strconv.FormatInt(p.Bps, 10)
76	}
77	return strings.Join(parts, ",")
78}
79
80// SplitByBps divides amount by the payees' shares. The rounding remainder
81// goes to the first payee, so the parts always sum to exactly amount — the
82// property every "no balance left behind" invariant (I-3, I-4) relies on.
83func SplitByBps(amount int64, ps []types.Payee) []int64 {
84	if len(ps) == 0 {
85		panic("SplitByBps: no payees")
86	}
87	out := make([]int64, len(ps))
88	var given int64
89	for i, p := range ps {
90		out[i] = BpsOf(amount, p.Bps)
91		given += out[i]
92	}
93	out[0] += amount - given
94	return out
95}