1// Package hlfees is the bridge's fee realm (bridge/PLAN.md §5.5): the routes pay every fee here — GNOT as coins,2// BUBBLE through the token realm — and only the treasurer a decision names may withdraw, to itself. The treasurer3// at launch is compiled in below; a decision made with the builder changes it, 7 days after it is made — the4// governor's own through Govern until it yields on r/hlmailbox, a GovDAO proposal's after (M1k).5package hlfees67import (8 "chain"9 "chain/banker"10 "strconv"11 "strings"12 "time"1314 "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0"15 "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubble"16 "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlmailbox"17 "gno.land/r/gov/dao"18)1920const (21 namespace = "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/"22 pkgPath = namespace + "hlfees" // must match gnomod.toml2324 // launchTreasurer is the operator's named key (§5.5, §11.12): a PLACEHOLDER — the deployer's hotkey — until25 // the launch notice names it; GovDAO can name the chain's own address instead at any vote.26 launchTreasurer = address("g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp")27)2829// launchGovernor is the governor (M1k): the mailbox's, compiled here too so a member reads it in this realm's30// served source; init refuses a deploy with another. bridge/local/e2e.sh swaps it for gnodev's test1.31const launchGovernor = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6")3233var (34 treasurer = launchTreasurer35 ledger decisions.Ledger36)3738// init refuses a deploy whose compiled governor is not the mailbox's (M1k): the bridge's realms are governed by39// one key until it yields, or by GovDAO from the first block, never a mix.40func init() {41 if launchGovernor != hlmailbox.Governor() {42 panic("hlfees: the compiled governor " + string(launchGovernor) + " is not the mailbox's " + string(hlmailbox.Governor()))43 }44}4546type (47 treasurerParams struct{ to address }48 cancelParams struct{ id int64 }49)5051func now() int64 { return time.Now().Unix() }5253func itoa(n int64) string { return strconv.FormatInt(n, 10) }5455// Address is this realm's address: where the routes pay the fees.56func Address() address { return chain.PackageAddress(pkgPath) }5758// Treasurer is the one address that may withdraw.59func Treasurer() address { return treasurer }6061// Balances are the fees held: ugnot and BUBBLE base units.62func Balances() (ugnot, bubbles int64) {63 return banker.NewReadonlyBanker().GetCoin(Address(), "ugnot"), bubble.BalanceOf(Address())64}6566// Withdraw pays everything held to the treasurer, who alone may call it.67func Withdraw(cur realm) {68 if !cur.IsCurrent() {69 panic("hlfees: that realm value is not live; pass your own cur")70 }71 if cur.Previous().Address() != treasurer {72 panic("hlfees: only the treasurer withdraws")73 }74 ugnot, bubbles := Balances()75 if ugnot > 0 {76 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(Address(), treasurer, chain.Coins{{"ugnot", ugnot}})77 }78 if bubbles > 0 {79 bubble.Transfer(cross(cur), treasurer, bubbles)80 }81 chain.Emit("Withdrawn", "to", string(treasurer), "ugnot", itoa(ugnot), "bubble", itoa(bubbles))82}8384// ------------------------------------------------------------ builders --8586// Request is what a builder returns (M1k): the proposal GovDAO's proxy executes after the yield (Proposal), and87// the same decision the governor runs directly before it (Govern); see hlgnot's.88type Request struct {89 proposal dao.ProposalRequest90 text string91 decide func(cur realm) error92}9394// Proposal is the request as a GovDAO proposal: `dao.MustCreateProposal(cross(cur), req.Proposal())`.95func (r Request) Proposal() dao.ProposalRequest { return r.proposal }9697// Description is the proposal's: the parameters as the builder wrote them.98func (r Request) Description() string { return r.text }99100// who names a previous realm in a refusal: its pkgpath, or a user's address.101func who(path string, a address) string {102 if path != "" {103 return path104 }105 return string(a)106}107108// mustBeGovernor is a decision's identity check (M1k), read from the closure's own live cur: the governor's own109// call until it yields on r/hlmailbox, gno.land/r/gov/dao's proxy exactly after (or from the first block, when no110// governor was compiled); see hlgnot's.111func mustBeGovernor(cur realm) {112 prev := cur.Previous()113 if decisions.DAOGoverns(launchGovernor, hlmailbox.Yielded()) {114 if p := prev.PkgPath(); p != decisions.DAOPath {115 panic("hlfees: only " + decisions.DAOPath + " executes a decision, not " + who(prev.PkgPath(), prev.Address()))116 }117 return118 }119 if !prev.IsUser() || prev.Address() != launchGovernor {120 panic("hlfees: 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)")121 }122}123124func mustBeLive() {125 if !hlmailbox.Live() {126 panic("hlfees: not launched: before the Launch decision only cancel is proposed")127 }128}129130func propose(cur realm, kind, text string, apply func(made, t int64)) Request {131 if !cur.IsCurrent() {132 panic("hlfees: that realm value is not live; pass your own cur")133 }134 made := now()135 cb := func(cur realm) error {136 mustBeGovernor(cur)137 t := now()138 if err := decisions.Expired(made, t); err != nil {139 return err140 }141 apply(made, t)142 return nil143 }144 return Request{proposal: dao.NewProposalRequest("hlfees: "+kind, text, dao.NewSimpleExecutor(0, cur, cb, text)), text: text, decide: cb}145}146147// Govern is the governor's way in before the yield (M1k): the same closure GovDAO's proxy runs after it, called148// non-crossing in this frame so it reads the governor from cur.Previous(); refused for anyone else and for149// everyone once the governor yielded on r/hlmailbox (or when none was compiled); see hlgnot's.150func Govern(cur realm, r Request) {151 if !cur.IsCurrent() {152 panic("hlfees: that realm value is not live; pass your own cur")153 }154 if r.decide == nil {155 panic("hlfees: not a builder's request")156 }157 if yielded := hlmailbox.Yielded(); decisions.DAOGoverns(launchGovernor, yielded) {158 if launchGovernor == "" {159 panic("hlfees: no governor was compiled, " + decisions.DAOPath + " governs from the first block: a decision is a GovDAO proposal, never Govern")160 }161 panic("hlfees: the governor yielded at height " + itoa(yielded) + ", " + decisions.DAOPath + " governs since: a decision is a GovDAO proposal, never Govern")162 }163 mustBeGovernor(cur)164 if err := r.decide(cur); err != nil {165 panic("hlfees: " + err.Error())166 }167}168169// NewSetTreasurerRequest names the address that may withdraw, 7 days after the decision is made.170func NewSetTreasurerRequest(cur realm, to address) Request {171 mustBeLive()172 if !to.IsValid() {173 panic("hlfees: not an address: " + string(to))174 }175 if to == treasurer {176 panic("hlfees: " + string(to) + " is the treasurer already")177 }178 text := "set treasurer to " + string(to) + " (now " + string(treasurer) + ")"179 return propose(cur, "set treasurer", text, func(made, t int64) {180 mustBeLive()181 d := ledger.Record("set treasurer", text, treasurerParams{to}, made, t, t+decisions.Delay)182 d.Note = "queued 7 days"183 chain.Emit("Decision", "id", itoa(d.ID), "kind", "set treasurer", "state", "queued")184 })185}186187// NewCancelRequest cancels a pending decision of this realm, at once.188func NewCancelRequest(cur realm, id int64) Request {189 d := ledger.Get(id)190 if d == nil || !d.Pending() {191 panic("hlfees: decision " + itoa(id) + " is not pending")192 }193 text := "cancel decision " + itoa(id) + " (" + d.Text + ")"194 return propose(cur, "cancel", text, func(made, t int64) {195 if _, err := ledger.Cancel(id, t); err != nil {196 panic("hlfees: " + err.Error())197 }198 c := ledger.Record("cancel", text, cancelParams{id}, made, t, t)199 ledger.Apply(c, t, "at once: cancelled")200 })201}202203// Execute applies a queued decision whose 7 days have passed; anyone may call it.204func Execute(cur realm, id int64) {205 if !cur.IsCurrent() {206 panic("hlfees: that realm value is not live; pass your own cur")207 }208 t := now()209 d, err := ledger.Due(id, t)210 if err != nil {211 panic("hlfees: " + err.Error())212 }213 p, ok := d.Params.(treasurerParams)214 if !ok {215 panic("hlfees: decision " + itoa(id) + " has nothing to apply")216 }217 treasurer = p.to218 ledger.Apply(d, t, "applied after the delay")219 chain.Emit("TreasurerSet", "address", string(treasurer))220}221222// --------------------------------------------------------------- pages --223224// Render: "" the page; "json" the figures; "decisions" and "decisions.json" the decisions.225func Render(path string) string {226 ugnot, bubbles := Balances()227 switch path {228 case "":229 var b strings.Builder230 b.WriteString("# The bridge's fees\n\n")231 b.WriteString("Every fee the bridge takes lands here: GNOT as coins, BUBBLE as the token. Only the treasurer a decision names withdraws, to itself.\n\n")232 b.WriteString("- held: " + itoa(ugnot) + " ugnot, " + itoa(bubbles) + " BUBBLE base units\n")233 b.WriteString("- treasurer: " + string(treasurer) + "\n")234 b.WriteString("- " + decisions.GovernorLine(launchGovernor, hlmailbox.Yielded()) + "\n")235 b.WriteString("- " + decisions.Clocks() + "\n")236 b.WriteString("\n## Decisions\n\n" + ledger.Page())237 return b.String()238 case "json":239 return "{\"address\":" + strconv.Quote(string(Address())) + ",\"treasurer\":" + strconv.Quote(string(treasurer)) +240 "," + decisions.GovernorJSON(launchGovernor, hlmailbox.Yielded()) +241 ",\"ugnot\":" + itoa(ugnot) + ",\"bubble\":" + itoa(bubbles) + ",\"clocks\":" + decisions.ClocksJSON() + ",\"decisions\":" + itoa(ledger.Last()) + "}"242 case "decisions":243 return "# Decisions\n\n" + ledger.Page()244 case "decisions.json":245 return ledger.JSON()246 }247 return "unknown path; see the page"248}249Address() string
Balances() (ugnot int64, bubbles int64)
Execute(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}, id int64)
Govern(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}, r struct{proposal gno.land/r/gov/dao.ProposalRequest; text string; decide func(.uverse.realm) .uverse.error})
NewCancelRequest(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}, id int64) struct{proposal gno.land/r/gov/dao.ProposalRequest; text string; decide func(.uverse.realm) .uverse.error}
NewSetTreasurerRequest(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}, to string) struct{proposal gno.land/r/gov/dao.ProposalRequest; text string; decide func(.uverse.realm) .uverse.error}
Render(path string) string
Treasurer() string
Withdraw(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})
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
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.