ratelimitdemo.gno
3.23 Kb · 89 lines
1// Package ratelimitdemo is a small on-chain demo of the token-bucket rate
2// limiter provided by the [p/moul/x/daily/ratelimit](/p/moul/x/daily/ratelimit/v0)
3// library.
4//
5// It holds a single package-level [ratelimit.Limiter] and wires it to the
6// chain: the tick is runtime.ChainHeight() and each caller is keyed by its
7// address (unsafe.PreviousRealm().Address()). It contains no limiter logic of
8// its own — refill/consume math all lives in the library.
9package ratelimitdemo
10
11import (
12 "strconv"
13
14 "chain/runtime"
15 "chain/runtime/unsafe"
16
17 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/md/v0"
18 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/ratelimit/v0"
19)
20
21// limiter starts at 0.5 tokens/block (one every two blocks), capacity 5.
22var limiter = ratelimit.New(0.5, 5.0)
23
24// Allow consumes one token for the calling realm/user at the current block
25// height and reports whether the request is permitted. Returns false (and
26// consumes nothing) when the caller's bucket is empty.
27func Allow(cur realm) bool {
28 addr := unsafe.PreviousRealm().Address()
29 return limiter.Allow(string(addr), runtime.ChainHeight())
30}
31
32// SetConfig updates the shared rate (tokens per block) and burst (capacity).
33// rate is clamped to >= 0, burst to >= 1.
34func SetConfig(cur realm, rate, burst float64) {
35 limiter.SetConfig(rate, burst)
36}
37
38// Tokens reports how many whole tokens `addr` has available at the current
39// block height, without mutating any state.
40func Tokens(addr address) int {
41 return limiter.Tokens(string(addr), runtime.ChainHeight())
42}
43
44// --- rendering --------------------------------------------------------------
45
46func ftoa(f float64) string { return strconv.FormatFloat(f, 'g', -1, 64) }
47
48// Render shows the limiter config and a table of known callers with their
49// current tokens and last-seen height. Path "/addr/<address>" focuses one
50// caller.
51func Render(path string) string {
52 now := runtime.ChainHeight()
53 rate, burst := limiter.Config()
54
55 if len(path) > 6 && path[:6] == "/addr/" {
56 key := path[6:]
57 out := "# Rate Limiter — caller\n\n"
58 out += "Address: " + md.InlineCode(key) + "\n\n"
59 out += "- Current tokens: **" + strconv.Itoa(limiter.Tokens(key, now)) + "** / " + ftoa(burst) + "\n"
60 out += "- Now (height): " + strconv.FormatInt(now, 10) + "\n"
61 return out
62 }
63
64 out := "# Rate Limiter\n\n"
65 out += "Demo of the [`p/moul/x/daily/ratelimit`](/p/moul/x/daily/ratelimit/v0) library — "
66 out += "an on-chain token-bucket limiter (port of `golang.org/x/time/rate`).\n\n"
67 out += "## Config\n\n"
68 out += "| Parameter | Value |\n|---|---|\n"
69 out += "| Rate (tokens/block) | " + ftoa(rate) + " |\n"
70 out += "| Burst (capacity) | " + ftoa(burst) + " |\n"
71 out += "| Clock height | " + strconv.FormatInt(now, 10) + " |\n\n"
72
73 out += "## Callers\n\n"
74 if limiter.Len() == 0 {
75 out += "_No caller has hit the limiter yet._\n\n"
76 } else {
77 out += "| Address | Tokens | Last height |\n|---|---|---|\n"
78 limiter.Iterate(now, func(key string, tokens int, last int64) bool {
79 out += "| " + md.InlineCode(key) + " | " + strconv.Itoa(tokens) +
80 " | " + strconv.FormatInt(last, 10) + " |\n"
81 return false
82 })
83 out += "\n"
84 }
85
86 out += "---\n"
87 out += "Call `Allow(cur)` to consume a token; read `Tokens(addr)` for a caller's balance.\n"
88 return out
89}