faucet.gno
5.53 Kb · 175 lines
1// Package grc20faucet issues two free GRC20 tokens, RED and BLUE, and gives
2// them away on request.
3//
4// It exists so that anything needing a token to experiment on has one that is
5// worthless, unlimited and shared. Both tokens are registered in
6// gno.land/r/nt/grc20reg/v0, which is what lets another realm find and move
7// them by key without importing this package: that indirection is the whole
8// point, and it is what gno.land/r/moul/x/grc20wrapdemo/v0 wraps.
9//
10// Two tokens rather than one, because the interesting patterns start at two: a
11// meta-token over a pair needs a pair.
12package grc20faucet
13
14import (
15 "chain/runtime"
16
17 "gno.land/p/nt/avl/v0"
18 "gno.land/p/nt/grc20/v0"
19 "gno.land/p/nt/ufmt/v0"
20 "gno.land/r/nt/grc20reg/v0"
21)
22
23// Tokens are exported so an importing realm can read them directly; the
24// ledgers stay private, since they carry unrestricted mint and burn.
25var (
26 Red *grc20.Token
27 Blue *grc20.Token
28
29 redLedger *grc20.PrivateLedger
30 blueLedger *grc20.PrivateLedger
31
32 // RedKey and BlueKey are the grc20reg lookup keys, the way a realm that
33 // does NOT import this package refers to these tokens.
34 RedKey string
35 BlueKey string
36
37 lastClaim = avl.NewTree() // address -> block height of its last claim
38)
39
40const (
41 decimals = 4
42
43 // ClaimAmount is minted of EACH token per claim: 100.0000 units.
44 ClaimAmount = int64(1_000_000)
45
46 // ClaimEvery is the cooldown, in blocks, between two claims by the same
47 // account. Free money with no cooldown is a spam vector, not a faucet.
48 ClaimEvery = int64(100)
49)
50
51func init(cur realm) {
52 Red, redLedger = grc20.NewToken("Red Token", "RED", decimals, 0, cur)
53 Blue, blueLedger = grc20.NewToken("Blue Token", "BLUE", decimals, 1, cur)
54 RedKey = grc20reg.Register(cross(cur), Red, "red")
55 BlueKey = grc20reg.Register(cross(cur), Blue, "blue")
56}
57
58// Claim mints ClaimAmount of both RED and BLUE to the caller.
59func Claim(cur realm) {
60 who := caller(cur)
61 now := runtime.ChainHeight()
62 if next, ok := NextClaim(who); ok && now < next {
63 panic(ufmt.Sprintf("too soon: next claim at block %d, current %d", next, now))
64 }
65 lastClaim.Set(who.String(), now)
66 checkErr(redLedger.Mint(who, ClaimAmount))
67 checkErr(blueLedger.Mint(who, ClaimAmount))
68}
69
70// NextClaim returns the block height at which `who` may claim again, and
71// whether they have ever claimed at all.
72func NextClaim(who address) (int64, bool) {
73 v := lastClaim.Get(who.String())
74 if v == nil {
75 return 0, false
76 }
77 return v.(int64) + ClaimEvery, true
78}
79
80// Transfer moves the caller's own units of `symbol`.
81func Transfer(cur realm, symbol string, to address, amount int64) {
82 checkErr(ledgerOf(symbol).CallerTeller().Transfer(0, cur, to, amount))
83}
84
85// Approve lets `spender` draw `amount` of `symbol` from the caller's balance.
86//
87// This is the call that makes every wrapper in grc20wrapdemo work: the wrapper
88// realm has no authority over anybody's tokens until its address is named here.
89func Approve(cur realm, symbol string, spender address, amount int64) {
90 checkErr(ledgerOf(symbol).CallerTeller().Approve(0, cur, spender, amount))
91}
92
93// TransferFrom spends an allowance the caller was granted.
94func TransferFrom(cur realm, symbol string, from, to address, amount int64) {
95 checkErr(ledgerOf(symbol).CallerTeller().TransferFrom(0, cur, from, to, amount))
96}
97
98// BalanceOf returns `owner`'s balance of `symbol`.
99func BalanceOf(symbol string, owner address) int64 {
100 return tokenOf(symbol).BalanceOf(owner)
101}
102
103// Allowance returns what `owner` let `spender` draw of `symbol`.
104func Allowance(symbol string, owner, spender address) int64 {
105 return tokenOf(symbol).Allowance(owner, spender)
106}
107
108// TotalSupply returns how much of `symbol` has been claimed so far.
109func TotalSupply(symbol string) int64 {
110 return tokenOf(symbol).TotalSupply()
111}
112
113func tokenOf(symbol string) *grc20.Token {
114 switch symbol {
115 case "RED":
116 return Red
117 case "BLUE":
118 return Blue
119 }
120 panic("unknown symbol " + symbol + " (RED or BLUE)")
121}
122
123func ledgerOf(symbol string) *grc20.PrivateLedger {
124 switch symbol {
125 case "RED":
126 return redLedger
127 case "BLUE":
128 return blueLedger
129 }
130 panic("unknown symbol " + symbol + " (RED or BLUE)")
131}
132
133// caller is the account or realm that crossed into this one.
134func caller(cur realm) address {
135 if !cur.IsCurrent() {
136 panic("grc20faucet: stale realm token")
137 }
138 return cur.Previous().Address()
139}
140
141func checkErr(err error) {
142 if err != nil {
143 panic(err)
144 }
145}
146
147func Render(path string) string {
148 s := "# GRC20 faucet: RED and BLUE\n\n"
149 s += "Two worthless tokens to experiment on. `Claim` gives you "
150 s += ufmt.Sprintf("%d of each, once every %d blocks.\n\n", ClaimAmount, ClaimEvery)
151
152 s += "| token | symbol | decimals | supply | registry key |\n"
153 s += "|---|---|---|---|---|\n"
154 s += ufmt.Sprintf("| %s | RED | %d | %d | `%s` |\n",
155 Red.GetName(), Red.GetDecimals(), Red.TotalSupply(), RedKey)
156 s += ufmt.Sprintf("| %s | BLUE | %d | %d | `%s` |\n",
157 Blue.GetName(), Blue.GetDecimals(), Blue.TotalSupply(), BlueKey)
158
159 s += "\n## Use it\n\n"
160 s += "```\n"
161 s += "gnokey maketx call -pkgpath gno.land/r/moul/x/grc20faucet/v0 -func Claim ...\n"
162 s += "gnokey maketx call -pkgpath gno.land/r/moul/x/grc20faucet/v0 -func Approve \\\n"
163 s += " -args RED -args <spender> -args 1000000 ...\n"
164 s += "```\n\n"
165 s += "Both tokens are registered in [grc20reg](/r/nt/grc20reg/v0), so a realm can\n"
166 s += "move them by key without importing this one. That is what\n"
167 s += "[grc20wrapdemo](/r/moul/x/grc20wrapdemo/v0) does with them.\n"
168
169 if path != "" {
170 s += "\n---\n\n"
171 s += ufmt.Sprintf("Balance of `%s`: RED %d, BLUE %d\n",
172 path, Red.BalanceOf(address(path)), Blue.BalanceOf(address(path)))
173 }
174 return s
175}