money.gno
2.72 Kb · 95 lines
1package safe
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/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}