hlbubble.gno
25.97 Kb · 617 lines
1// Package hlbubble is the BUBBLE lock: the bridge's BUBBLE route on gno.land (bridge/PLAN.md §5.2–§5.5), the same
2// shape as r/hlgnot over the BUBBLE token (gno.land/r/…/bubble) instead of coins.
3//
4// Out. TransferRemote takes amount + fee of the caller's BUBBLE through the token's allowance (the caller approves
5// this realm first), keeps the amount in the lock, pays the fee to r/hlfees, and sends a token message through
6// r/hlmailbox to the synthetic BUBBLE program on Solana, which mints as much there.
7//
8// In. A burn on Solana comes back as a token message: Process takes it from the mailbox, checks it came from the
9// enrolled router, and writes a claim in the recipient's queue; Claim pays claims in turns under the meters — a
10// tenth of the lock a day (never below the floor), a quarter of that for any one recipient, the fee first. The old
11// 1,000,000 BUBBLE day cap and the admin's pause are gone: the meter and the guardians' flags replace them.
12//
13// Who may change what. Nobody but the governor through the builders until it yields on r/hlmailbox, and GovDAO
14// after (M1k); the guardians and watch keys can only pause, through r/hlmailbox. Every launch value is compiled in
15// below; nothing moves until the Launch decision executes. The state machine is gno.land/p/…/hlroute/v0; this
16// realm holds the BUBBLE and makes the calls.
17package hlbubble
18
19import (
20 "chain"
21 "chain/runtime"
22 "encoding/hex"
23 "strconv"
24 "strings"
25 "time"
26
27 "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0"
28 "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlroute/v0"
29 hl "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hyperlane/v0"
30 "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble"
31 "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlmailbox"
32 "gno.land/r/gov/dao"
33)
34
35const (
36 namespace = "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/"
37 pkgPath = namespace + "hlbubble" // must match gnomod.toml
38
39 mailboxPath = namespace + "hlmailbox"
40 feesPath = namespace + "hlfees"
41 bubblePath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble" // the live token realm, never redeployed
42 hlgnotPath = namespace + "hlgnot"
43 hlgovPath = namespace + "hlgov"
44
45 one = int64(1_000_000) // one BUBBLE in base units
46
47 // The launch values (§5.3, §5.5). The router — the synthetic BUBBLE program on Solana — is a PLACEHOLDER (the
48 // test route of the vectors) until the setup step (§5.8); bridge/local/e2e.sh swaps it for its run's.
49 launchFee = 100 * one
50 launchMinimum = 200 * one
51 launchFeeFloor = 40 * one
52 launchCapFloor = 10_000 * one
53 launchMaximum = 25_000 * one
54 launchRouter = "c807d773013b14cee2855ca9efafeac0b3bbc3a42fd397c2fbffda6417d290f1" // the BUBBLE token program ETqUXu1uJPj8qgmw8AJMqhgWuL5QW5ymgUMiCARbXYPA
55
56 maxI64 = uint64(1<<63 - 1)
57)
58
59// launchGovernor is the governor (M1k): the mailbox's, compiled here too so a member reads it in this realm's
60// served source; init refuses a deploy with another. bridge/local/e2e.sh swaps it for gnodev's test1.
61const launchGovernor = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6")
62
63var (
64 st = hlroute.New(hlroute.Launch{Symbol: "BUBBLE", Fee: launchFee, Minimum: launchMinimum, FeeFloor: launchFeeFloor,
65 CapFloor: launchCapFloor, Maximum: launchMaximum, Domain: hlmailbox.LaunchOrigin(), Router: mustKey(launchRouter),
66 Now: time.Now().Unix()})
67
68 // realms are the addresses a transfer may not name as its recipient on either side (§5.2).
69 realms = []address{chain.PackageAddress(pkgPath), chain.PackageAddress(mailboxPath), chain.PackageAddress(feesPath),
70 chain.PackageAddress(bubblePath), chain.PackageAddress(hlgnotPath), chain.PackageAddress(hlgovPath)}
71)
72
73// init refuses a deploy whose compiled governor is not the mailbox's (M1k): the bridge's realms are governed by
74// one key until it yields, or by GovDAO from the first block, never a mix.
75func init() {
76 if launchGovernor != hlmailbox.Governor() {
77 panic("hlbubble: the compiled governor " + string(launchGovernor) + " is not the mailbox's " + string(hlmailbox.Governor()))
78 }
79}
80
81func mustKey(s string) [32]byte {
82 k, err := hl.ParseKey32(s)
83 if err != nil {
84 panic(err.Error())
85 }
86 return k
87}
88
89func now() int64 { return time.Now().Unix() }
90
91func u32(n uint32) string { return strconv.FormatUint(uint64(n), 10) }
92func itoa(n int64) string { return strconv.FormatInt(n, 10) }
93
94func caller(cur realm) address {
95 if !cur.IsCurrent() {
96 panic("hlbubble: that realm value is not live; pass your own cur")
97 }
98 return cur.Previous().Address()
99}
100
101func mustHex(s, what string) []byte {
102 b, err := hex.DecodeString(strings.TrimPrefix(strings.TrimSpace(s), "0x"))
103 if err != nil {
104 panic("hlbubble: the " + what + " is not hex")
105 }
106 return b
107}
108
109// isRealm is whether an address is a realm of the bridge: the six compiled here, every route realm the mailbox
110// registers (R39: a successor's address is refused as a recipient at send and swept at a credit, to be credited to
111// its owner by id, never paid into a realm's account outside every counter) and every one it dropped (AT6: a
112// re-pointed-away successor, a route no more that nothing of the line can move BUBBLE out of). A handful of reads.
113func isRealm(a address) bool {
114 for _, r := range realms {
115 if r == a {
116 return true
117 }
118 }
119 for _, p := range hlmailbox.Routes() {
120 if chain.PackageAddress(p) == a {
121 return true
122 }
123 }
124 for _, p := range hlmailbox.DroppedRoutes() {
125 if chain.PackageAddress(p) == a {
126 return true
127 }
128 }
129 return false
130}
131
132func mustBeLive() {
133 if !hlmailbox.Live() {
134 panic("hlbubble: not launched: nothing is sent, credited or paid before the Launch decision")
135 }
136}
137
138func mustNotBePaused(fn string) {
139 if hlmailbox.Paused(pkgPath, fn) {
140 panic("hlbubble: " + fn + " are paused by a guardian or a watch key")
141 }
142}
143
144// mustNotBeDropped refuses a realm the mailbox dropped from its line (R35): its predecessor re-pointed the migration
145// to another successor while this realm had pulled nothing, so it is a route no more — Solana's token may still
146// name it the router until the next kind 6, and a send or a credit here would be against a lock no realm of the
147// line holds. The mailbox refuses its Dispatch and its Process too; the line's last realm takes every message
148// addressed to it once its import has ended.
149func mustNotBeDropped() {
150 if by := hlmailbox.Dropped(pkgPath); by != "" {
151 panic("hlbubble: a route no more: " + by + " re-pointed its migration to another successor; the line's last realm takes every message and send")
152 }
153}
154
155func syncLaunch() {
156 if la := hlmailbox.LaunchedAt(); la != st.LaunchSeen {
157 st.ResetMeters(la)
158 st.LaunchSeen = la
159 }
160}
161
162// ------------------------------------------------------------- reading --
163
164// Address is this realm's address: the spender to approve on the bubble realm before TransferRemote, the holder
165// of the lock, and (as 32 bytes) the router the Solana program enrolls.
166func Address() address { return chain.PackageAddress(pkgPath) }
167
168// Locked is the lock: what backs synthetic BUBBLE on Solana, by the counters (sent − paid).
169func Locked() int64 { return st.Lock }
170
171// Balance is the BUBBLE this realm holds; it is the lock unless someone sent BUBBLE here outside a transfer.
172func Balance() int64 { return bubble.BalanceOf(Address()) }
173
174// Router is the enrolled router on Solana, in hex.
175func Router() string { return hex.EncodeToString(st.Router[:]) }
176
177// Domain is the Solana domain this route sends to and takes from.
178func Domain() uint32 { return st.Domain }
179
180func Fee() int64 { return st.Fee }
181func Minimum() int64 { return st.Minimum }
182func FeeFloor() int64 { return st.FeeFloor }
183func CapFloor() int64 { return st.CapFloor }
184func Maximum() int64 { return st.Maximum }
185func Cap() int64 { return st.Cap() }
186
187// Counters is the identity's five counters (§5.9): sentTotal, paidTotal, owed, voided, lost.
188func Counters() (sentTotal, paidTotal, owed, voided, lost int64) {
189 return st.SentTotal, st.PaidTotal, st.Owed, st.Voided, st.Lost
190}
191
192// Migrated is the successor's pkgpath once a migration applied, "" before.
193func Migrated() string { return st.MigratedTo }
194
195// ---------------------------------------------------------------- out --
196
197// TransferRemote sends amount of the caller's BUBBLE to recipient on destination (a Solana address in base58, or
198// 32 bytes in hex) and returns the message id in hex. First, on the bubble realm, the caller approves THIS realm
199// (bubble.Approve, spender Address()) for at least amount + fee: the amount is locked here, the fee goes to
200// r/hlfees. The amount is between the minimum and the per-message maximum.
201func TransferRemote(cur realm, destination uint32, recipient string, amount int64) string {
202 who := caller(cur)
203 mustBeLive()
204 mustNotBePaused(hlmailbox.FnSends)
205 mustNotBeDropped()
206 st.MustSend(hlmailbox.Predecessor(pkgPath), env(cur)) // a successor takes no send before its last page is in (R35, F28) and before kind 6 names it (AT7)
207 if destination != st.Domain {
208 panic("hlbubble: no route to domain " + u32(destination))
209 }
210 st.CheckSend(amount)
211 to, err := hl.ParseKey32(recipient)
212 if err != nil {
213 panic(err.Error())
214 }
215 if to == ([32]byte{}) {
216 panic("hlbubble: the recipient is all zeros")
217 }
218 if a, err := hl.Bytes32ToAddress(to); err == nil && isRealm(a) {
219 panic("hlbubble: the recipient is a realm of the bridge")
220 }
221 self := Address()
222 before := bubble.BalanceOf(self)
223 bubble.TransferFrom(cross(cur), who, self, amount+st.Fee)
224 if bubble.BalanceOf(self) != before+amount+st.Fee {
225 panic("hlbubble: the lock moved by the wrong amount")
226 }
227 bubble.Transfer(cross(cur), chain.PackageAddress(feesPath), st.Fee)
228 id := hlmailbox.Dispatch(cross(cur), destination, st.Router, hl.TokenMessage(to, uint64(amount), nil))
229 idHex := hex.EncodeToString(id[:])
230 st.Send(now(), idHex, who, amount)
231 chain.Emit("SentTransferRemote",
232 "destination", u32(destination),
233 "recipient", hex.EncodeToString(to[:]),
234 "amount", itoa(amount),
235 "fee", itoa(st.Fee),
236 "from", string(who),
237 "id", idHex,
238 )
239 return idHex
240}
241
242// ----------------------------------------------------------------- in --
243
244// Process takes a token message from Solana and credits a claim to its recipient. Anyone may call it with the
245// verifier's metadata and the message, both in hex. A refused message stays undelivered. A realm registered as a
246// successor credits nothing — its own messages included (R32) — before the last page of its predecessor's
247// migration is in (AQ1); a migrated realm refuses before the mailbox is asked (see hlgnot's).
248func Process(cur realm, metadata, message string) {
249 caller(cur)
250 mustBeLive()
251 mustNotBePaused(hlmailbox.FnCredits)
252 mustNotBeDropped()
253 msg := mustHex(message, "message")
254 m, err := hl.ParseMessage(msg)
255 if err != nil {
256 panic(err.Error())
257 }
258 if st.Migrated() {
259 panic("hlbubble: migrated to " + st.MigratedTo + ": the successor takes this message")
260 }
261 if self, err := hl.AddressToBytes32(Address()); err != nil {
262 panic(err.Error())
263 } else {
264 st.MustTake(m.Recipient == self, hlmailbox.Predecessor(pkgPath))
265 }
266 t := now()
267 if m.Origin != st.Domain || !st.RouterAccepts(t, m.Sender) {
268 panic("hlbubble: the message is not from the router enrolled for domain " + u32(st.Domain))
269 }
270 rcpt, amount, _, err := hl.ParseTokenMessage(m.Body)
271 if err != nil {
272 panic(err.Error())
273 }
274 if amount > maxI64 {
275 panic("hlbubble: the amount is past what BUBBLE can hold")
276 }
277 origin, sender, body := hlmailbox.Process(cross(cur), mustHex(metadata, "metadata"), msg)
278 if origin != m.Origin || sender != m.Sender || string(body) != string(m.Body) {
279 panic("hlbubble: the mailbox read the message differently") // cannot happen: the same bytes, the same parser
280 }
281 // a recipient that is no gno address (its top twelve bytes not zero, §13) or a realm of the bridge is swept:
282 // owed, paying nobody until GovDAO credits it by id to its owner
283 to, err := hl.Bytes32ToAddress(rcpt)
284 recipient := string(to)
285 if err != nil {
286 recipient = hex.EncodeToString(rcpt[:])
287 }
288 idb := hl.ID(msg)
289 id := hex.EncodeToString(idb[:])
290 epoch, _ := hlmailbox.PayoutsEpoch(pkgPath)
291 swept := st.Credit(t, runtime.ChainHeight(), epoch, id, to, int64(amount), hlroute.BySigners, err != nil || isRealm(to))
292 chain.Emit("Credited",
293 "origin", u32(origin),
294 "recipient", recipient,
295 "amount", itoa(int64(amount)),
296 "id", id,
297 "swept", strconv.FormatBool(swept),
298 )
299}
300
301// Claim pays up to n claims (n ≤ 20) taking recipients in turns under the meters, the fee as each claim's first
302// installment. Anyone may call it; the relayers do. It returns how many claims it paid something on.
303func Claim(cur realm, n int) int {
304 caller(cur)
305 mustBeLive()
306 mustNotBePaused(hlmailbox.FnPayouts)
307 mustNotBeDropped()
308 syncLaunch()
309 epoch, at := hlmailbox.PayoutsEpoch(pkgPath)
310 paid := st.Claim(now(), n, epoch, at, env(cur))
311 chain.Emit("Claimed", "count", strconv.Itoa(paid))
312 return paid
313}
314
315func pay(cur realm, to address, amount int64) { bubble.Transfer(cross(cur), to, amount) }
316
317func env(cur realm) hlroute.Env {
318 return hlroute.Env{
319 Flagged: func(id string, height int64, verifier bool) bool { return hlmailbox.Flagged(pkgPath, id, height, verifier) },
320 ClearFlags: func(fn string) bool { return hlmailbox.ClearFlags(cross(cur), fn) },
321 RemoveSeat: func(seat int, holder address) bool { return hlmailbox.VacateSeat(cross(cur), seat, holder) },
322 Delivered: func(id string) bool { return hlmailbox.Delivered(id) != 0 },
323 MarkDone: func(id string) { hlmailbox.MarkDone(cross(cur), id) },
324 Epoch: func() int64 { e, _ := hlmailbox.PayoutsEpoch(pkgPath); return e },
325 Height: func() int64 { return runtime.ChainHeight() },
326 Pay: func(to address, amount int64) { pay(cur, to, amount) },
327 PayFee: func(amount int64) { pay(cur, chain.PackageAddress(feesPath), amount) },
328 AddRoute: func(path string) { hlmailbox.AddRoute(cross(cur), path) },
329 AnnouncedFor: func(path string) string { return hlmailbox.SuccessorFor(path) },
330 Predecessor: func() string { return hlmailbox.Predecessor(pkgPath) },
331 IsRoute: func(path string) bool { return hlmailbox.IsRoute(path) },
332 Enrolled: func() (int64, int64) { return hlmailbox.Enrolled(pkgPath) },
333 }
334}
335
336// ------------------------------------------------------------ builders --
337
338// The builders (§5.3): the same as r/hlgnot's, amounts in BUBBLE base units. A decision made with one of them —
339// the governor's through Govern until it yields on r/hlmailbox, a GovDAO proposal's after (M1k) — is the only way
340// a setting changes; see hlgnot's for the clocks and the Request.
341
342// Request is what a builder returns (M1k): the proposal GovDAO's proxy executes after the yield (Proposal), and
343// the same decision the governor runs directly before it (Govern); see hlgnot's.
344type Request struct {
345 proposal dao.ProposalRequest
346 text string
347 decide func(cur realm) error
348}
349
350// Proposal is the request as a GovDAO proposal: `dao.MustCreateProposal(cross(cur), req.Proposal())`.
351func (r Request) Proposal() dao.ProposalRequest { return r.proposal }
352
353// Description is the proposal's: the parameters as the builder wrote them.
354func (r Request) Description() string { return r.text }
355
356// who names a previous realm in a refusal: its pkgpath, or a user's address.
357func who(path string, a address) string {
358 if path != "" {
359 return path
360 }
361 return string(a)
362}
363
364// mustBeGovernor is a decision's identity check (M1k), read from the closure's own live cur: the governor's own
365// call until it yields on r/hlmailbox, gno.land/r/gov/dao's proxy exactly after (or from the first block, when no
366// governor was compiled); see hlgnot's.
367func mustBeGovernor(cur realm) {
368 prev := cur.Previous()
369 if decisions.DAOGoverns(launchGovernor, hlmailbox.Yielded()) {
370 if p := prev.PkgPath(); p != decisions.DAOPath {
371 panic("hlbubble: only " + decisions.DAOPath + " executes a decision, not " + who(prev.PkgPath(), prev.Address()))
372 }
373 return
374 }
375 if !prev.IsUser() || prev.Address() != launchGovernor {
376 panic("hlbubble: until the governor yields, " + string(launchGovernor) + " alone decides, by Govern from its own key: not " + who(prev.PkgPath(), prev.Address()) + " (a GovDAO proposal is refused until the yield)")
377 }
378}
379
380func propose(cur realm, d hlroute.Draft) Request {
381 if !cur.IsCurrent() {
382 panic("hlbubble: that realm value is not live; pass your own cur")
383 }
384 made := now()
385 cb := func(cur realm) error {
386 mustBeGovernor(cur)
387 t := now()
388 if err := decisions.Expired(made, t); err != nil {
389 return err
390 }
391 if d.Kind != hlroute.KindCancel {
392 mustBeLive()
393 }
394 if d.Kind == hlroute.KindApplyRefund {
395 mustNotBePaused(hlmailbox.FnPayouts) // a refund moves BUBBLE out of the lock: under a standing payouts flag it waits like a claim (§5.9; the proposal survives for a retry)
396 }
397 dec := st.Decide(made, t, d, env(cur))
398 chain.Emit("Decision", "id", itoa(dec.ID), "kind", dec.Kind, "state", dec.State())
399 return nil
400 }
401 return Request{proposal: dao.NewProposalRequest("hlbubble: "+d.Kind, d.Text, dao.NewSimpleExecutor(0, cur, cb, d.Text)), text: d.Text, decide: cb}
402}
403
404// Govern is the governor's way in before the yield (M1k): the same closure GovDAO's proxy runs after it, called
405// non-crossing in this frame so it reads the governor from cur.Previous(); refused for anyone else and for
406// everyone once the governor yielded on r/hlmailbox (or when none was compiled); see hlgnot's.
407func Govern(cur realm, r Request) {
408 if !cur.IsCurrent() {
409 panic("hlbubble: that realm value is not live; pass your own cur")
410 }
411 if r.decide == nil {
412 panic("hlbubble: not a builder's request")
413 }
414 if yielded := hlmailbox.Yielded(); decisions.DAOGoverns(launchGovernor, yielded) {
415 if launchGovernor == "" {
416 panic("hlbubble: no governor was compiled, " + decisions.DAOPath + " governs from the first block: a decision is a GovDAO proposal, never Govern")
417 }
418 panic("hlbubble: the governor yielded at height " + itoa(yielded) + ", " + decisions.DAOPath + " governs since: a decision is a GovDAO proposal, never Govern")
419 }
420 mustBeGovernor(cur)
421 if err := r.decide(cur); err != nil {
422 panic("hlbubble: " + err.Error())
423 }
424}
425
426func NewSetCapFloorRequest(cur realm, n int64) Request {
427 mustBeLive()
428 return propose(cur, st.DraftCapFloor(n))
429}
430
431func NewSetMaximumRequest(cur realm, n int64) Request {
432 mustBeLive()
433 return propose(cur, st.DraftMaximum(n))
434}
435
436func NewSetFeeRequest(cur realm, fee, minimum int64) Request {
437 mustBeLive()
438 return propose(cur, st.DraftFee(fee, minimum))
439}
440
441func NewSetFeeFloorRequest(cur realm, n int64) Request {
442 mustBeLive()
443 return propose(cur, st.DraftFeeFloor(n))
444}
445
446func NewSetRouterRequest(cur realm, router string) Request {
447 mustBeLive()
448 return propose(cur, st.DraftRouter(mustKey(router), router))
449}
450
451// NewVoidRequest voids credits by message id (a claim, or a sweep entry as forged) or by a range of credits
452// ("heights:<from>-<to>" or "since:<from>", closed at the height the proposal is made, at most 1,000 credits), as
453// withheld or forged, with the unpause option (the seats' holders recorded; a seat whose occupant changed by
454// execution is skipped): see hlgnot's.
455func NewVoidRequest(cur realm, ids string, forged bool, unpauseFns, removeSeats string) Request {
456 mustBeLive()
457 return propose(cur, st.DraftVoid(ids, forged, unpauseFns, removeSeats, runtime.ChainHeight(), hlmailbox.Seat))
458}
459
460func NewRestoreRequest(cur realm, ids string) Request {
461 mustBeLive()
462 return propose(cur, st.DraftRestore(ids))
463}
464
465func NewCreditRequest(cur realm, id string, to address, amount int64) Request {
466 mustBeLive()
467 if !to.IsValid() || isRealm(to) {
468 panic("hlbubble: the recipient must be an address outside the bridge's realms")
469 }
470 return propose(cur, st.DraftCredit(id, to, amount, hlmailbox.Delivered(id) != 0))
471}
472
473// NewSweepRequest pays sweep entries, named by message id, to an address after 7 days — their sum, at most the
474// lock — and drops them (R19): see hlgnot's.
475func NewSweepRequest(cur realm, ids string, to address) Request {
476 mustBeLive()
477 if !to.IsValid() || isRealm(to) {
478 panic("hlbubble: the recipient must be an address outside the bridge's realms")
479 }
480 return propose(cur, st.DraftSweep(ids, to))
481}
482
483// NewRefundRequest queues a refund of imported sends by id, 7 days, once kind 6 has enrolled this realm on Solana
484// for 14 days (AR1, R37); applied then by NewApplyRefundRequest, never by Execute (AT2); see hlgnot's.
485func NewRefundRequest(cur realm, ids string) Request {
486 mustBeLive()
487 return propose(cur, st.DraftRefund(ids, now(), env(cur)))
488}
489
490// NewApplyRefundRequest applies a queued refund decision whose 7 days have passed, at once (AT2): GovDAO's own act
491// after hlwatch's check that the token's router on Solana is this realm; under a standing payouts flag it waits
492// like a claim; see hlgnot's.
493func NewApplyRefundRequest(cur realm, id int64) Request {
494 mustBeLive()
495 return propose(cur, st.DraftApplyRefund(id, now(), env(cur)))
496}
497
498// NewWriteOffRequest books refunded sends delivered on Solana after their refund as lost, after 7 days (AR1); see
499// hlgnot's.
500func NewWriteOffRequest(cur realm, ids string) Request {
501 mustBeLive()
502 return propose(cur, st.DraftWriteOff(ids))
503}
504
505// NewMigrateRequest: a successor that announced itself to the mailbox for the BUBBLE route (R25, AQ5) and is no
506// route already (R31), after 7 days; a realm that succeeds another migrates on only once its own import has ended
507// (R31); see hlgnot's.
508func NewMigrateRequest(cur realm, to string) Request {
509 mustBeLive()
510 return propose(cur, st.DraftMigrate(to, pkgPath, env(cur)))
511}
512
513func NewCancelRequest(cur realm, id int64) Request {
514 return propose(cur, st.DraftCancel(id))
515}
516
517// Execute applies a queued decision whose 7 days have passed. Anyone may call it. A sweep payment moves BUBBLE
518// out of the lock, so under a standing payouts flag it waits for the unpause like a claim (§5.9). A refund is never
519// Execute's (AT2): the state refuses the kind, and GovDAO applies it by NewApplyRefundRequest.
520func Execute(cur realm, id int64) {
521 caller(cur)
522 mustBeLive()
523 if d := st.Ledger.Get(id); d != nil && d.Kind == hlroute.KindSweep {
524 mustNotBePaused(hlmailbox.FnPayouts)
525 }
526 dec := st.Execute(now(), id, env(cur))
527 chain.Emit("Decision", "id", itoa(dec.ID), "kind", dec.Kind, "state", dec.State())
528}
529
530// Pull is the successor's: once a migrate decision names it, the realm at that pkgpath calls Pull from its own
531// code, page by page from 0, and receives the lock (transferred to its address, with page 0) and the records to
532// import, up to 500 a page (the snapshot's More says whether a page follows); a page pulled already hands over
533// nothing. Page 0 moves the lock, so under a standing payouts flag it waits for the unpause like a sweep payment
534// (R27, R16); the flag follows the claims to the successor (hlmailbox.AddRoute).
535func Pull(cur realm, page int) *hlroute.Snapshot {
536 if !cur.IsCurrent() {
537 panic("hlbubble: that realm value is not live; pass your own cur")
538 }
539 path := cur.Previous().PkgPath()
540 if st.MigratedTo == "" || path != st.MigratedTo {
541 panic("hlbubble: only the successor named by a migrate decision pulls")
542 }
543 if page == 0 {
544 mustNotBePaused(hlmailbox.FnPayouts)
545 }
546 snap := st.Export(page, hlroute.PullPage)
547 if page == 0 {
548 hlmailbox.NotePulled(cross(cur), path) // the lock moved: the mailbox answers Pulled (R35) — kind 6 may enrol the successor, and the migration is never re-pointed
549 }
550 if snap.Lock > 0 {
551 pay(cur, chain.PackageAddress(path), snap.Lock)
552 }
553 chain.Emit("Migrated", "to", path, "page", strconv.Itoa(page), "lock", itoa(snap.Lock), "claims", strconv.Itoa(len(snap.Claims)), "sweeps", strconv.Itoa(len(snap.Sweeps)), "sends", strconv.Itoa(len(snap.Sends)), "more", strconv.FormatBool(snap.More))
554 return snap
555}
556
557// --------------------------------------------------------------- pages --
558
559func pausedText() string {
560 var fns []string
561 for _, fn := range []string{hlmailbox.FnSends, hlmailbox.FnCredits, hlmailbox.FnPayouts} {
562 if hlmailbox.Paused(pkgPath, fn) {
563 fns = append(fns, fn)
564 }
565 }
566 return strings.Join(fns, ", ")
567}
568
569// Render: "" the route's page; "decisions"; "json"; "decisions.json"; "claim/<id>"; "send/<id>"; "claims/<from>",
570// "sweeps/<from>" and "refunds/<from>", up to 100 entries a page (hlgnot's Render says more).
571func Render(path string) string {
572 epoch, at := hlmailbox.PayoutsEpoch(pkgPath)
573 switch {
574 case strings.HasPrefix(path, "claims/"):
575 return st.ClaimsPage(parseFrom(path[len("claims/"):]))
576 case strings.HasPrefix(path, "sweeps/"):
577 return st.SweepsPage(parseFrom(path[len("sweeps/"):]))
578 case strings.HasPrefix(path, "send/"):
579 return st.SendJSON(path[len("send/"):])
580 case strings.HasPrefix(path, "refunds/"):
581 return st.RefundsPage(parseFrom(path[len("refunds/"):]))
582 case path == "":
583 var b strings.Builder
584 b.WriteString("# BUBBLE across chains\n\n")
585 b.WriteString("The bridge's BUBBLE route on gno.land. BUBBLE sent to Solana is locked here, and the synthetic BUBBLE there is backed by it one to one; a burn there is credited here as a claim, paid under the meters. Every setting moves only by a decision made with this realm's builders — the governor's own until it yields, a GovDAO proposal after.\n\n")
586 if hlmailbox.Live() {
587 b.WriteString("- **live** since " + itoa(hlmailbox.LaunchedAt()) + "\n")
588 } else {
589 b.WriteString("- **not launched**: the values below are the compiled launch values, waiting for the Launch decision\n")
590 }
591 b.WriteString("- " + decisions.GovernorLine(launchGovernor, hlmailbox.Yielded()) + "\n")
592 b.WriteString("- balance: " + st.Amount(Balance()) + "\n")
593 b.WriteString(st.Figures(now(), epoch, at, hlmailbox.LaunchedAt(), pausedText()))
594 b.WriteString("\n## Decisions\n\n" + st.Ledger.Page())
595 return b.String()
596 case path == "decisions":
597 return "# Decisions\n\n" + st.Ledger.Page()
598 case path == "json":
599 return "{\"address\":" + strconv.Quote(string(Address())) + ",\"live\":" + strconv.FormatBool(hlmailbox.Live()) +
600 "," + decisions.GovernorJSON(launchGovernor, hlmailbox.Yielded()) +
601 ",\"balance\":" + itoa(Balance()) + ",\"paused\":" + strconv.Quote(pausedText()) + "," + st.JSON(now(), epoch, at, hlmailbox.LaunchedAt()) + "}"
602 case path == "decisions.json":
603 return st.Ledger.JSON()
604 case strings.HasPrefix(path, "claim/"):
605 return st.ClaimJSON(path[len("claim/"):])
606 }
607 return "unknown path; see the route's page"
608}
609
610// parseFrom reads a page's offset; -1 (an empty page) for anything but a number.
611func parseFrom(s string) int {
612 n, err := strconv.Atoi(s)
613 if err != nil || n < 0 {
614 return -1
615 }
616 return n
617}