donate.gno
4.46 Kb · 131 lines
1package bolo
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime"
7 "math"
8 "strconv"
9
10 "gno.land/p/nt/seqid/v0"
11)
12
13// Donate gives to campaign id, and returns your receipt's ID. Send the gift and any
14// tip together: tip is the part of what you send that goes to Bolo, in ugnot — 0 for
15// none. message is optional, and public. anonymous, if true, keeps your address off
16// Bolo's pages; the chain still shows it. In a direct campaign the gift goes straight
17// to the beneficiary; in escrow, to the campaign's escrow, until the beneficiary
18// withdraws it.
19//
20// Any user, by a direct transaction, the donor being the caller (API §3.2). The
21// envelope is the gift plus the tip. The gift goes to the beneficiary, or to the
22// campaign's escrow, and the tip to the tip recipient: the realm keeps nothing
23// (INV-3), and coins found there are never credited (C4).
24func Donate(cur realm, id string, tip int64, message string, anonymous bool) string {
25 assertCurrent(cur)
26 if !cur.Previous().IsUserCall() {
27 panic(ErrNotUserCall)
28 }
29 runtime.AssertOriginCall()
30 c := campaignOf(id)
31 recordDeadline(cur, c)
32 amount := checkGift(c, envelopeUgnot(), tip)
33 message = checkText(message, 0, messageMax, false, ErrMessage)
34 checkRoom(c, amount)
35
36 donor := cur.Previous().Address()
37 s := c.state
38 s.raised += amount
39 s.donationCount++
40 s.lastDonationAt = now()
41 key := donor.String()
42 var k *contribution
43 if v := c.contributions.Get(key); v != nil {
44 k = v.(*contribution)
45 } else {
46 k = &contribution{}
47 c.contributions.Set(key, k)
48 s.donorCount++
49 }
50 k.amount += amount
51 if !anonymous && !k.public {
52 k.public = true
53 byDonor.Set(key+"/"+c.id.Binary(), true)
54 }
55 seq := s.receiptSeq.Next()
56 c.receipts.Set(seq.Binary(), &receipt{
57 seq: uint64(seq), donor: donor, anonymous: anonymous, amount: amount, tip: tip,
58 message: message, at: now(), raisedAfter: s.raised, refundedBefore: k.refunded,
59 })
60 s.recent[s.recentNext] = uint64(seq)
61 s.recentNext = (s.recentNext + 1) % recentSize
62
63 if c.mode == modeDirect {
64 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(realmAddr(), c.beneficiary, chain.NewCoins(chain.NewCoin("ugnot", amount)))
65 } else {
66 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(realmAddr(), escrowAddr(c.id), chain.NewCoins(chain.NewCoin("ugnot", amount)))
67 }
68 if tip > 0 {
69 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(realmAddr(), tipRecipient, chain.NewCoins(chain.NewCoin("ugnot", tip)))
70 }
71 chain.Emit(evDonated,
72 "id", idString(c.id),
73 "seq", idString(seq),
74 "donor", key,
75 "amount", itoa(amount),
76 "tip", itoa(tip),
77 "anonymous", strconv.FormatBool(anonymous),
78 )
79 return receiptID(c.id, seq)
80}
81
82// checkGift returns the gift in an envelope of total ugnot carrying tip, or panics
83// (API §3.2), in this order: a negative tip, or nothing left for the gift; a
84// campaign that isn't open; a paused Bolo; a gift under the minimum.
85func checkGift(c *campaign, total, tip int64) int64 {
86 if tip < 0 || total-tip <= 0 {
87 panic(ErrBadAmount)
88 }
89 amount := total - tip
90 assertOpen(c)
91 if paused {
92 panic(ErrPaused)
93 }
94 if amount < minDonation {
95 panic(ErrBadAmount)
96 }
97 return amount
98}
99
100// checkRoom refuses a gift of amount that c can't take (API §3.2): one that would
101// push Raised past int64, or, in escrow mode with a cap set, the tracked balance —
102// never the bank's — past MaxEscrowBalance.
103func checkRoom(c *campaign, amount int64) {
104 if amount > math.MaxInt64-c.state.raised {
105 panic(ErrBadAmount)
106 }
107 if c.mode == modeEscrow && escrowCap > 0 && trackedBalance(c)+amount > escrowCap {
108 panic(ErrCapReached)
109 }
110}
111
112// receiptID is a receipt's ID: its campaign's, a slash, and its sequence number
113// (API C9).
114func receiptID(campaign, seq seqid.ID) string { return idString(campaign) + "/" + idString(seq) }
115
116// SweepStrays sends the coins someone sent to Bolo's own address by mistake to the
117// tip recipient, and returns how much: anyone can.
118//
119// It sends the ugnot at the realm's address to the tip recipient (API §3.4). The
120// realm holds nothing between transactions (INV-3), so all of it is strays; anyone
121// may call, and with nothing there it sends nothing and emits nothing (Q3).
122func SweepStrays(cur realm) int64 {
123 assertCurrent(cur)
124 amount := banker.NewReadonlyBanker().GetCoin(realmAddr(), "ugnot")
125 if amount == 0 {
126 return 0
127 }
128 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(realmAddr(), tipRecipient, chain.NewCoins(chain.NewCoin("ugnot", amount)))
129 chain.Emit(evStraysSwept, "amount", itoa(amount), "to", tipRecipient.String())
130 return amount
131}