Kind Realm (renderable)
Name v0
Namespace moul / vesting
Exported functions 7
Module gno.land/r/moul/vesting/v0
gno 0.9 ⧉
1 // Realm vesting answers one question for a gno.land account: of the coins it 2 // holds, how many can actually move right now. 3 // 4 // # Read this before you trust a number here 5 // 6 // A realm CANNOT read an account's vesting schedule. The VM's whole view of an 7 // account is banker.GetCoins, which returns the TOTAL balance with the locked Count () int
Declare (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, original int64, start int64, end int64, delayed bool)
Forget (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
LockedOf (addr string) (locked int64, ok bool)
Render (path string) string
ScheduleOf (addr string) (original int64, start int64, end int64, delayed bool, ok bool)
SpendableOf (addr string) (spendable int64, ok bool)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
8 // part included, and no native exposes std.VestingSchedule. So this realm
9 // reads two things from the chain and is told the third:
10 //
11 // balance read from the chain, banker.GetCoins always true
12 // now read from the chain, block time always true
13 // schedule supplied, see below only as good as its source
14 //
15 // Every rendered figure says which of the two it rests on. Nothing here is
16 // presented as verified when it is not.
17 //
18 // # Where a schedule comes from
19 //
20 // Either the query string, for a one-off calculation that stores nothing, or
21 // the registry. [Declare] writes a schedule for the CALLER'S OWN address and
22 // no other, which is the whole trust model: an address can only misdescribe
23 // itself, and a wrong entry misleads nobody but its author.
24 //
25 // Anyone can read their real schedule in one command and declare it:
26 //
27 // gnokey query auth/accounts/g1youraddress -remote https://rpc.gno.land:443
28 //
29 // # Why the schedule can be cached at all
30 //
31 // Because it can never change. std.SetVesting has exactly one caller in the
32 // monorepo, gno.land/pkg/gnoland/app.go, during genesis balance loading, and
33 // no message type creates or modifies a schedule. A schedule declared once is
34 // correct forever, which is what makes a registry honest rather than stale.
35 package vesting
36
37 import (
38 "strconv"
39 "strings"
40 "time"
41
42 "chain"
43 "chain/banker"
44 "chain/runtime"
45 "chain/runtime/unsafe"
46
47 "gno.land/p/nt/avl/v0"
48 "gno.land/p/nt/ufmt/v0"
49
50 "gno.land/p/moul/vesting/v0"
51 )
52
53 const (
54 // Path is this realm's package path.
55 Path = "gno.land/r/moul/vesting/v0"
56 // Link is Path as a gnoweb route.
57 Link = "/r/moul/vesting/v0"
58 // Denom is the only coin this realm reports on.
59 Denom = "ugnot"
60 )
61
62 // seeded are the schedules this realm ships with, restored by init() after
63 // every redeploy.
64 //
65 // They are source constants and not registry rows on purpose: the realm is
66 // private = true, so a redeploy wipes realm state, and the one schedule the
67 // page exists to show should not need a transaction to come back. Each is
68 // copied from the genesis allocation sheet that built mainnet, which is the
69 // public, pinned input named in misc/deployments/mainnet.gno.land/gen-genesis.sh.
70 var seeded = [] struct {
71 addr address
72 label string
73 original int64
74 start int64
75 end int64
76 delayed bool
77 }{
78 {
79 addr: "g1manfred47kzduec920z88wfr64ylksmdcedlf5" ,
80 label: "moul" ,
81 original: 106560000000 ,
82 start: 1789225200 ,
83 end: 1852383600 ,
84 },
85 }
86
87 // declared maps an address to the schedule that address declared for itself.
88 var declared avl.Tree
89
90 // entry is one row of the registry.
91 type entry struct {
92 schedule vesting.Schedule
93 label string // non-empty only for a seeded row
94 seeded bool
95 at int64 // block height the row was written at; 0 for a seeded row
96 }
97
98 func init() { reset() }
99
100 // reset restores the registry to exactly what the source declares. init() is
101 // its only caller on chain; a test calls it to render a page that does not
102 // depend on what the test before it wrote, since an Example runs after every
103 // Test in the package and sees the state they left.
104 func reset() {
105 declared = avl.Tree{}
106 for _, s := range seeded {
107 typ := vesting.Continuous
108 if s.delayed {
109 typ = vesting.Delayed
110 }
111 sch, err := vesting.New(s.original, s.start, s.end, typ)
112 if err != nil {
113 panic( "vesting: seeded schedule for " + s.label + ": " + err.Error())
114 }
115 declared.Set(s.addr.String(), &entry{schedule: sch, label: s.label, seeded: true })
116 }
117 }
118
119 // Declare records the caller's own vesting schedule, replacing any previous
120 // one. It cannot write a row for anybody else, which is what keeps the
121 // registry honest without the realm being able to verify a thing.
122 //
123 // Pass original = 0 to declare that the address has no schedule at all.
124 func Declare(cur realm, original, start, end int64 , delayed bool ) {
125 typ := vesting.Continuous
126 if delayed {
127 typ = vesting.Delayed
128 }
129 sch, err := vesting.New(original, start, end, typ)
130 if err != nil {
131 panic(err.Error())
132 }
133 who := unsafe.PreviousRealm().Address()
134 if e, ok := lookup(who); ok && e.seeded {
135 panic( "vesting: " + who.String() + " is seeded in the source; edit the realm instead" )
136 }
137 declared.Set(who.String(), &entry{schedule: sch, at: chainHeight()})
138 chain.Emit( "declared" , "addr" , who.String(),
139 "original" , strconv.FormatInt(original, 10 ),
140 "start" , strconv.FormatInt(start, 10 ),
141 "end" , strconv.FormatInt(end, 10 ),
142 "type" , typ.String())
143 }
144
145 // Forget removes the caller's own declaration.
146 func Forget(cur realm) {
147 who := unsafe.PreviousRealm().Address()
148 e, ok := lookup(who)
149 if !ok {
150 panic( "vesting: nothing declared for " + who.String())
151 }
152 if e.seeded {
153 panic( "vesting: " + who.String() + " is seeded in the source; edit the realm instead" )
154 }
155 declared.Remove(who.String())
156 chain.Emit( "forgot" , "addr" , who.String())
157 }
158
159 // Count returns how many schedules the registry holds.
160 func Count() int { return declared.Size() }
161
162 // ScheduleOf returns the declared schedule for addr, and whether there is one.
163 func ScheduleOf(addr address) (original, start, end int64 , delayed, ok bool ) {
164 e, found := lookup(addr)
165 if !found {
166 return 0 , 0 , 0 , false , false
167 }
168 s := e.schedule
169 return s.Original, s.Start, s.End, s.Type == vesting.Delayed, true
170 }
171
172 // SpendableOf returns what addr can move right now, and whether the answer
173 // rests on a declared schedule. With ok false the figure is the whole balance,
174 // which is correct only if the address really has no schedule.
175 func SpendableOf(addr address) (spendable int64 , ok bool ) {
176 bal := balanceOf(addr)
177 e, found := lookup(addr)
178 if !found {
179 return bal, false
180 }
181 return e.schedule.Spendable(bal, now()), true
182 }
183
184 // LockedOf returns what addr cannot move right now, and whether the answer
185 // rests on a declared schedule.
186 func LockedOf(addr address) (locked int64 , ok bool ) {
187 e, found := lookup(addr)
188 if !found {
189 return 0 , false
190 }
191 bal := balanceOf(addr)
192 l := e.schedule.Locked(now())
193 if l > bal {
194 l = bal // cannot lock more than is there
195 }
196 return l, true
197 }
198
199 // lookup reads a row. avl's Get returns ONE value and nil for a miss, unlike
200 // Remove which returns two; that asymmetry is a documented gno-vs-Go trap.
201 func lookup(addr address) (*entry, bool ) {
202 v := declared.Get(addr.String())
203 if v == nil {
204 return nil , false
205 }
206 return v.(*entry), true
207 }
208
209 // balanceOf reads the address's TOTAL ugnot, locked included. That is what
210 // banker.GetCoins reports and the only balance figure a realm can obtain.
211 // A readonly banker takes no realm, so this works from inside Render.
212 func balanceOf(addr address) int64 {
213 return banker.NewReadonlyBanker().GetCoins(addr).AmountOf(Denom)
214 }
215
216 // gnot renders a ugnot amount as GNOT with six decimals and no float.
217 func gnot(u int64 ) string {
218 neg := u < 0
219 if neg {
220 u = -u
221 }
222 whole, frac := u/ 1_000_000 , u% 1_000_000
223 s := ufmt.Sprintf( "%d.%s" , whole, pad6(frac))
224 if neg {
225 s = "-" + s
226 }
227 return s + " GNOT"
228 }
229
230 // pad6 left-pads to six digits. ufmt has no width flags, so this is by hand.
231 func pad6(n int64 ) string {
232 s := strconv.FormatInt(n, 10 )
233 for len(s) < 6 {
234 s = "0" + s
235 }
236 return s
237 }
238
239 // permille renders tenths of a percent as a percentage with one decimal.
240 func permille(p int64 ) string {
241 return ufmt.Sprintf( "%d.%d%%" , p/ 10 , p% 10 )
242 }
243
244 // duration renders a span of seconds as days and hours, which is the only
245 // resolution worth reading on a two-year schedule.
246 func duration(sec int64 ) string {
247 if sec <= 0 {
248 return "complete"
249 }
250 d, h := sec/ 86400 , (sec% 86400 )/ 3600
251 var b strings.Builder
252 if d > 0 {
253 b.WriteString(strconv.FormatInt(d, 10 ))
254 b.WriteString( "d " )
255 }
256 b.WriteString(strconv.FormatInt(h, 10 ))
257 b.WriteString( "h" )
258 return b.String()
259 }
260
261 // isAddr is a cheap shape check, so a typo renders as "not an address" rather
262 // than as an account with a zero balance.
263 func isAddr(s string ) bool {
264 if len(s) != 40 || !strings.HasPrefix(s, "g1" ) {
265 return false
266 }
267 for _, r := range s {
268 if !(r >= 'a' && r <= 'z' ) && !(r >= '0' && r <= '9' ) {
269 return false
270 }
271 }
272 return true
273 }
274
275 // now is the chain's own clock, in unix seconds. time.Now() in a realm returns
276 // the BLOCK time, not a wall clock, which is exactly the value the ante
277 // handler compares a vesting schedule against.
278 func now() int64 { return time.Now().Unix() }
279
280 func chainHeight() int64 { return runtime.ChainHeight() }
281
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.