pay.gno
4.57 Kb · 144 lines
1package sugarproxy
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime/unsafe"
7 "math/overflow"
8 "strconv"
9 "strings"
10 "time"
11
12 "gno.land/p/nt/avl/v0"
13)
14
15// Paying sharers. The hub tallies the bytes each sharer's app carried and pays them through Pay, so every payout
16// is on chain: who was paid, how much, for how many bytes, in which period. The hub's account is an approved payer;
17// it signs with a gno.land session key whose spending the chain caps (only calls to this realm, so much a period),
18// so a stolen hub key can pay out at most one period's worth, and only through here, where it shows.
19
20const denom = "ugnot"
21
22// Totals is what a sharer has been paid, all told.
23type Totals struct {
24 Paid int64 // ugnot
25 Bytes int64
26 Payouts int
27 Last int64 // unix seconds
28}
29
30// Payout is one call to Pay.
31type Payout struct {
32 Time int64
33 Payer address
34 Period string
35 Receipts string // the hex sha256 of the payer's receipts for the period: the confirmed downloads it paid for
36 Count int
37 Ugnot int64
38 Bytes int64
39}
40
41var (
42 totals = avl.NewTree() // address -> *Totals
43 payouts []*Payout
44)
45
46// Pay pays sharers with the GNOT sent along. lines has one sharer a line: "address ugnot bytes"; the amounts must
47// add up to what was sent, and a payout is at most 500 lines. period names what it pays for ("2026-11-01", say);
48// receipts is the hex sha256 of the payer's receipts for it (one line per confirmed download: its tag, the file's
49// sha256 and size, who carried how much), recorded here so the receipts can be checked against it later. Only an
50// approved payer may call it; pausing doesn't stop it (sharers are owed for what they carried).
51func Pay(cur realm, period, receipts, lines string) {
52 payer := mustUser(cur)
53 if !payerActive(payer) {
54 panic("only an approved payer may pay")
55 }
56 if len(period) == 0 || len(period) > 40 || strings.ContainsAny(period, "\n\r\t") {
57 panic("the period is 1 to 40 characters on one line")
58 }
59 if !hexOf(receipts, 64) {
60 panic("receipts is the receipts' sha256: 64 lowercase hex")
61 }
62 sent := coinsSent(cur)
63 type line struct {
64 to address
65 ugnot, bytes int64
66 }
67 var all []line
68 var sum, bytes int64
69 for _, l := range strings.Split(strings.TrimSpace(lines), "\n") {
70 f := strings.Fields(l)
71 if len(f) != 3 {
72 panic("each line is: address ugnot bytes")
73 }
74 to := address(f[0])
75 u, err1 := strconv.ParseInt(f[1], 10, 64)
76 b, err2 := strconv.ParseInt(f[2], 10, 64)
77 if !to.IsValid() || err1 != nil || err2 != nil || u <= 0 || b < 0 {
78 panic("a bad line: " + l)
79 }
80 sum = overflow.Add64p(sum, u)
81 bytes = overflow.Add64p(bytes, b)
82 all = append(all, line{to, u, b})
83 }
84 if len(all) == 0 || len(all) > 500 {
85 panic("a payout is 1 to 500 lines")
86 }
87 if sum != sent {
88 panic("the amounts add up to " + strconv.FormatInt(sum, 10) + " ugnot, but " + strconv.FormatInt(sent, 10) + " were sent")
89 }
90 now := time.Now().Unix()
91 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
92 for _, l := range all {
93 bk.SendCoins(cur.Address(), l.to, chain.Coins{chain.NewCoin(denom, l.ugnot)})
94 t, _ := totals.Get(l.to.String()).(*Totals)
95 if t == nil {
96 t = &Totals{}
97 totals.Set(l.to.String(), t)
98 }
99 t.Paid = overflow.Add64p(t.Paid, l.ugnot)
100 t.Bytes = overflow.Add64p(t.Bytes, l.bytes)
101 t.Payouts++
102 t.Last = now
103 chain.Emit("Paid", "to", l.to.String(), "ugnot", strconv.FormatInt(l.ugnot, 10), "bytes", strconv.FormatInt(l.bytes, 10), "period", period)
104 }
105 payouts = append(payouts, &Payout{Time: now, Payer: payer, Period: period, Receipts: receipts, Count: len(all), Ugnot: sum, Bytes: bytes})
106 chain.Emit("Payout", "period", period, "receipts", receipts, "sharers", strconv.Itoa(len(all)), "ugnot", strconv.FormatInt(sum, 10))
107}
108
109// PaidTo is what a sharer has been paid, all told.
110func PaidTo(a address) (ugnot, bytes int64, count int) {
111 if t, ok := totals.Get(a.String()).(*Totals); ok && t != nil {
112 return t.Paid, t.Bytes, t.Payouts
113 }
114 return 0, 0, 0
115}
116
117func payerActive(a address) bool {
118 now := time.Now().Unix()
119 for _, it := range items {
120 if it.Kind == KindPayer && it.Fields[0] == a.String() && it.State == StateApproved && now >= it.After {
121 return true
122 }
123 }
124 return false
125}
126
127// coinsSent returns the ugnot sent with a direct call, refusing other denoms. Coins sent with a run go to the run's
128// own address, not here, so a run sends none.
129func coinsSent(cur realm) int64 {
130 if !cur.Previous().IsUserCall() {
131 return 0
132 }
133 var n int64
134 for _, c := range unsafe.OriginSend() {
135 if c.Amount == 0 {
136 continue
137 }
138 if c.Denom != denom || c.Amount < 0 {
139 panic("only " + denom + " is taken")
140 }
141 n = overflow.Add64p(n, c.Amount)
142 }
143 return n
144}