1package fee_split23import (4 "chain"5 "chain/banker"6 "chain/runtime/unsafe"7 "sort"8 "strconv"9 "strings"10)1112// Split holds a fee-splitting configuration with percentage-based shares13// denominated in basis points (1 bp = 0.01%, 10000 bp = 100%).14type Split struct {15 Owner address16 PendingOwner address // two-step handover, must AcceptOwnership17 Recipients []address18 Shares []int64 // basis points, must sum to 1000019 Balances map[address]int6420 TotalDeposited int6421 TotalClaimed int6422 Frozen bool23 Archived bool24}2526const (27 MaxRecipients = 2028 MaxSplitsPerOwner = 1029 // Quotas are PER-OWNER only (round-4 audit): a global cap is a shared30 // resource 50 sybil accounts could fill forever — and seeding grief31 // splits with balances to keyless recipients made the fill32 // unrecoverable even by the sybils. Per-owner quotas mean an attacker33 // consumes only their own budget; state growth is gas-priced.34 // Render is bounded separately (MaxRenderSplits).35 MaxRenderSplits = 10036 // MaxFeeBps is an IMMUTABLE ceiling on the protocol fee (1%). The37 // admin can set any fee from 0 up to this cap, never above it — the38 // cap, not the current setting, is what users must trust.39 MaxFeeBps = int64(100)4041 // Pre-parse input bounds (round-4 audit): caps were enforced only42 // AFTER full parsing, so a 1MB recipient list burned ~11B gas before43 // refusal. 20 bech32 addresses + separators fit well within these.44 MaxRecipientListLen = 102445 MaxShareListLen = 1284647 // Denomination handled by this realm. Deposits must be exactly one48 // coin of this denom; claims pay out in it.49 Denom = "ugnot"5051 // Largest single deposit for which share math (amount * share,52 // share <= 10000) cannot overflow int64.53 MaxDepositAmount = int64(9223372036854775807) / 1000054)5556var (57 splits map[string]*Split58 splitIDs []string // insertion-ordered for deterministic Render59 ownerSplits map[address]int60 nextID int6162 // Protocol fee: taken from each deposit BEFORE distribution, at the63 // rate in force at deposit time (never retroactive — credited64 // balances are never touched). Defaults to zero.65 feeBps int6466 feeAdmin address // deployer; can set the fee and claim accrued fees67 pendingFeeAdmin address // two-step handover, must AcceptFeeAdmin68 feesAccrued int6469 feesClaimed int6470)7172func init() {73 splits = make(map[string]*Split)74 splitIDs = []string{}75 ownerSplits = make(map[address]int)76 nextID = 177 // The package deployer becomes the fee admin: on-chain, AddPackage78 // runs init with the message creator as origin caller (verified79 // against the VM keeper — a MsgAddPackage's creator is never zero).80 // If OriginCaller() were ever empty (e.g. the gno test VM, which has81 // no MsgAddPackage), the fee feature degrades SAFELY to disabled:82 // SetFee requires the runtime-derived caller to equal feeAdmin and83 // that caller is never empty, so84 // feeBps can never leave 0 and no fee ever accrues — zero funds at85 // risk. Do not "harden" this into a panic; the soft-disable is the86 // safe behavior. Fee starts at ZERO regardless.87 feeAdmin = unsafe.OriginCaller()88 feeBps = 089}9091// ---------- helpers ----------9293func formatPct(bp int64) string {94 whole := strconv.FormatInt(bp/100, 10)95 frac := strconv.FormatInt(bp%100, 10)96 if len(frac) == 1 {97 frac = "0" + frac98 }99 return whole + "." + frac + "%"100}101102func mustGetSplit(id string) *Split {103 s, ok := splits[id]104 if !ok {105 panic("split not found: " + id)106 }107 return s108}109110func mustGetActive(id string) *Split {111 s := mustGetSplit(id)112 if s.Archived {113 panic("split is archived: " + id)114 }115 return s116}117118func boolStr(v bool) string {119 if v {120 return "yes"121 }122 return "no"123}124125func parseBasisPoints(raw string) []int64 {126 if len(raw) > MaxShareListLen {127 panic("share list too long")128 }129 parts := strings.Split(raw, ",")130 out := make([]int64, len(parts))131 var total int64132 for i, s := range parts {133 v, err := strconv.Atoi(strings.TrimSpace(s))134 if err != nil || v <= 0 || v > 10000 {135 // the upper bound is a security invariant, not hygiene: unbounded136 // shares let the int64 total wrap back to exactly 10000, minting137 // unbacked balances paid from the shared pool (re-audit P1)138 panic("invalid share value: " + strings.TrimSpace(s))139 }140 out[i] = int64(v)141 total += int64(v)142 }143 if total != 10000 {144 panic("shares must sum to 10000 basis points, got " + strconv.FormatInt(total, 10))145 }146 return out147}148149func parseAddresses(raw string) []address {150 if len(raw) > MaxRecipientListLen {151 panic("recipient list too long")152 }153 parts := strings.Split(raw, ",")154 out := make([]address, len(parts))155 for i, r := range parts {156 a := address(strings.TrimSpace(r))157 if a == "" {158 panic("empty recipient address at position " + strconv.Itoa(i))159 }160 if !a.IsValid() || string(a) != strings.ToLower(string(a)) {161 // lowercase is required, not cosmetic (round-3 audit): bech32162 // accepts ALL-UPPERCASE as valid, but the runtime always returns163 // the lowercase canonical form — an uppercase-keyed balance164 // could never be claimed and would block Archive forever, and165 // upper/lower duplicates would bypass the duplicate check166 panic("invalid recipient address: " + string(a))167 }168 // NOTE: IsValid is a format check only — a well-formed address with169 // no key holder (e.g. another package's derived address) will170 // accumulate a balance nobody can claim, which also blocks Archive171 // forever. Owners must list addresses they know can call Claim.172 out[i] = a173 }174 return out175}176177func validateRecipients(recipients []address, shares []int64) {178 if len(recipients) != len(shares) {179 panic("recipients and shares must have the same length")180 }181 if len(recipients) == 0 {182 panic("at least one recipient is required")183 }184 if len(recipients) > MaxRecipients {185 panic("too many recipients (max " + strconv.Itoa(MaxRecipients) + ")")186 }187 seen := make(map[address]bool)188 for _, r := range recipients {189 if seen[r] {190 panic("duplicate recipient: " + string(r))191 }192 seen[r] = true193 }194}195196// sortedBalanceAddrs returns the balance-map keys in deterministic order,197// so panics and renders never depend on map iteration order.198func sortedBalanceAddrs(s *Split) []string {199 addrs := make([]string, 0, len(s.Balances))200 for a := range s.Balances {201 addrs = append(addrs, string(a))202 }203 sort.Strings(addrs)204 return addrs205}206207// rejectStraySend aborts when coins are attached to a call that does not208// accept them — the abort reverts the transfer back to the sender instead209// of stranding the coins on the realm address (re-audit P3). Direct bank210// transfers to the realm address remain unrecoverable by design.211func rejectStraySend(cur realm) {212 if !cur.IsCurrent() {213 // every call site passes the entrypoint's own first cur; this214 // pins that discipline against future call-site drift (v2215 // audit G3 hardening)216 panic("realm capability is not current")217 }218 if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 {219 panic("this function does not accept coins; attach coins to Deposit only")220 }221}222223// ---------- write operations ----------224225// CreateSplit registers a new split. The caller becomes the owner.226// Recipients and shares are comma-separated; shares are in basis points227// summing to 10000.228func CreateSplit(cur realm, recipientList, shareList string) string {229 rejectStraySend(cur)230 owner := cur.Previous().Address()231 if ownerSplits[owner] >= MaxSplitsPerOwner {232 panic("per-owner split limit reached")233 }234235 recipients := parseAddresses(recipientList)236 shares := parseBasisPoints(shareList)237 validateRecipients(recipients, shares)238239 id := "split_" + strconv.Itoa(nextID)240 nextID++241242 balances := make(map[address]int64)243 for _, r := range recipients {244 balances[r] = 0245 }246247 splits[id] = &Split{248 Owner: owner,249 Recipients: recipients,250 Shares: shares,251 Balances: balances,252 }253 splitIDs = append(splitIDs, id)254 ownerSplits[owner]++255 return id256}257258// Deposit distributes the coins sent with the call across recipients259// proportionally.260//261// DEPLOYMENT PRECONDITION (round-4 audit): on a network with262// restricted/token-locked ugnot transfers, the bank gate is263// SENDER-whitelist-based — a whitelisted user's Deposit succeeds but264// Claim sends FROM this realm's (non-whitelisted) address and reverts.265// Funds would flow in and not out until the restriction lifts. Deploy266// only to networks with unrestricted ugnot, or have governance267// whitelist this realm's address first.268//269// LIMITATION (round-3 audit, documented): only direct user calls can270// deposit. A DAO/realm treasury has NO deposit path — a realm-routed271// call is refused, and a bare banker send to this realm's address is272// an unrecoverable donation. Realm treasuries must route deposits273// through a user account. The deposit is the ACTUAL attached send — exactly one274// coin of Denom — so balances are always backed by funds this realm275// holds. Direct user calls only: a deposit routed through an276// intermediary realm would deliver its coins to that realm, not here,277// and must be rejected. Rounding dust goes to the highest-share278// recipient (deterministic, not order-dependent).279func Deposit(cur realm, splitID string) {280 s := mustGetActive(splitID)281 if s.Frozen {282 panic("split is frozen")283 }284285 // IsUserCall, not IsUser: MsgRun passes IsUser but its attached send286 // goes caller->caller — the coins never reach this realm, and OriginSend287 // could be re-read across k calls in one run script (re-audit P1). A288 // direct MsgCall's send provably lands on the called package address.289 if !cur.Previous().IsUserCall() {290 panic("deposits must be sent by direct call, not through another realm or a run script")291 }292 sent := unsafe.OriginSend()293 if len(sent) != 1 || sent[0].Denom != Denom {294 panic("deposit must send exactly one coin of " + Denom)295 }296 amount := sent[0].Amount297 if amount <= 0 {298 panic("amount must be greater than zero")299 }300 if amount > MaxDepositAmount {301 panic("deposit exceeds maximum supported amount")302 }303 if s.TotalDeposited > int64(9223372036854775807)-amount {304 panic("deposit would overflow split accounting")305 }306307 // Protocol fee comes off the top; everything below distributes the308 // NET amount, so the per-split conservation invariant309 // (sum(balances)+TotalClaimed == TotalDeposited) is untouched.310 // amount <= MaxDepositAmount and feeBps <= 100, so the product is311 // far below overflow.312 fee := (amount * feeBps) / 10000313 if fee > 0 {314 if feesAccrued > int64(9223372036854775807)-fee {315 panic("fee accrual would overflow")316 }317 feesAccrued += fee318 amount -= fee319 }320 if amount == 0 {321 panic("deposit too small: fully consumed by the protocol fee")322 }323324 s.TotalDeposited += amount325326 // Find the highest-share recipient for dust assignment327 dustIdx := 0328 for i := 1; i < len(s.Shares); i++ {329 if s.Shares[i] > s.Shares[dustIdx] {330 dustIdx = i331 }332 }333334 var distributed int64335 for i, r := range s.Recipients {336 share := (amount * s.Shares[i]) / 10000337 s.Balances[r] += share338 distributed += share339 }340341 // Assign dust to highest-share recipient342 dust := amount - distributed343 if dust > 0 {344 s.Balances[s.Recipients[dustIdx]] += dust345 }346}347348// Claim withdraws the caller's accumulated balance and SENDS the coins349// to the caller's address. Balance is zeroed before the transfer.350// Claims remain possible on frozen splits, and by ex-recipients whose351// accrued balance predates a share update.352func Claim(cur realm, splitID string) int64 {353 rejectStraySend(cur)354 s := mustGetActive(splitID)355 addr := cur.Previous().Address()356357 bal, exists := s.Balances[addr]358 if !exists {359 panic("not a recipient of this split")360 }361 if bal == 0 {362 panic("nothing to claim")363 }364365 s.Balances[addr] = 0366 s.TotalClaimed += bal367368 b := banker.NewBanker(banker.BankerTypeRealmSend, cur)369 b.SendCoins(cur.Address(), addr,370 chain.Coins{{Denom: Denom, Amount: bal}})371 return bal372}373374// UpdateShares replaces recipients and shares. Owner only. Not if frozen.375// Removed recipients keep any accrued balance and can still Claim it.376func UpdateShares(cur realm, splitID, recipientList, shareList string) {377 rejectStraySend(cur)378 s := mustGetActive(splitID)379 if cur.Previous().Address() != s.Owner {380 panic("only the owner can update shares")381 }382 if s.Frozen {383 panic("split is frozen")384 }385386 recipients := parseAddresses(recipientList)387 shares := parseBasisPoints(shareList)388 validateRecipients(recipients, shares)389390 for _, r := range recipients {391 if _, ok := s.Balances[r]; !ok {392 s.Balances[r] = 0393 }394 }395396 s.Recipients = recipients397 s.Shares = shares398}399400// TransferOwnership stages a two-step ownership handover; nothing401// moves until the nominee calls AcceptOwnership. Two-step for the same402// reason as the fee admin, plus one more (v2 audit Y1): a one-step403// transfer consumed the RECIPIENT's per-owner quota without consent,404// so poisoned splits could burn a victim's slots unarchivably. Staging405// consumes nothing of the nominee's. Pass "" to clear a pending406// nomination.407func TransferOwnership(cur realm, splitID string, newOwner address) {408 rejectStraySend(cur)409 s := mustGetActive(splitID)410 if cur.Previous().Address() != s.Owner {411 panic("only the owner can transfer ownership")412 }413 if newOwner == "" {414 s.PendingOwner = ""415 return416 }417 if !newOwner.IsValid() || string(newOwner) != strings.ToLower(string(newOwner)) {418 // see parseAddresses: an uppercase owner could never match the419 // runtime-derived caller again — the split would be owner-less420 panic("invalid new owner address: " + string(newOwner))421 }422 if newOwner == s.Owner {423 panic("new owner is already the owner")424 }425 s.PendingOwner = newOwner426}427428// AcceptOwnership completes the handover; only the nominee can accept.429// The nominee's quota is checked HERE, at consent time — the split430// slot moves only with the acceptor's own signature (v2 audit Y1: a431// recipient's quota is never consumed without consent). A nomination432// on a split that is later archived is inert (mustGetActive).433func AcceptOwnership(cur realm, splitID string) {434 rejectStraySend(cur)435 s := mustGetActive(splitID)436 caller := cur.Previous().Address()437 if s.PendingOwner == "" || caller != s.PendingOwner {438 panic("caller is not the pending owner")439 }440 if ownerSplits[caller] >= MaxSplitsPerOwner {441 panic("new owner is at the per-owner split limit")442 }443444 ownerSplits[s.Owner]--445 if ownerSplits[s.Owner] <= 0 {446 delete(ownerSplits, s.Owner)447 }448 ownerSplits[caller]++449 s.Owner = caller450 s.PendingOwner = ""451}452453// Freeze permanently locks shares and stops further deposits. One-way,454// cannot be undone. Claims remain possible.455func Freeze(cur realm, splitID string) {456 rejectStraySend(cur)457 s := mustGetActive(splitID)458 if cur.Previous().Address() != s.Owner {459 panic("only the owner can freeze")460 }461 s.Frozen = true462}463464// Archive marks a fully-claimed split as archived. Only the owner can465// archive, and only if EVERY balance — including balances held by466// ex-recipients removed in a share update — is zero, since archiving467// blocks all further claims. Cannot be undone.468func Archive(cur realm, splitID string) {469 rejectStraySend(cur)470 s := mustGetActive(splitID)471 if cur.Previous().Address() != s.Owner {472 panic("only the owner can archive")473 }474475 for _, a := range sortedBalanceAddrs(s) {476 if s.Balances[address(a)] > 0 {477 panic("cannot archive: outstanding balance for " + a)478 }479 }480481 s.Archived = true482483 // Free the owner's slot so they can create new splits484 ownerSplits[s.Owner]--485 if ownerSplits[s.Owner] <= 0 {486 delete(ownerSplits, s.Owner)487 }488489 // Remove from active ID list (keeps map entry for audit)490 for i, id := range splitIDs {491 if id == splitID {492 splitIDs = append(splitIDs[:i], splitIDs[i+1:]...)493 break494 }495 }496}497498// ---------- protocol fee ----------499500// SetFee sets the protocol fee in basis points, admin only, hard-capped501// at MaxFeeBps. Applies to FUTURE deposits only.502func SetFee(cur realm, bps int64) {503 rejectStraySend(cur)504 if cur.Previous().Address() != feeAdmin {505 panic("only the fee admin can set the fee")506 }507 if bps < 0 || bps > MaxFeeBps {508 panic("fee must be between 0 and " + strconv.FormatInt(MaxFeeBps, 10) + " basis points")509 }510 feeBps = bps511}512513// ClaimFees sends all accrued protocol fees to the fee admin.514func ClaimFees(cur realm) int64 {515 rejectStraySend(cur)516 if cur.Previous().Address() != feeAdmin {517 panic("only the fee admin can claim fees")518 }519 if feesAccrued == 0 {520 panic("no fees accrued")521 }522 amount := feesAccrued523 feesAccrued = 0524 feesClaimed += amount525526 b := banker.NewBanker(banker.BankerTypeRealmSend, cur)527 b.SendCoins(cur.Address(), feeAdmin,528 chain.Coins{{Denom: Denom, Amount: amount}})529 return amount530}531532// NominateFeeAdmin begins a two-step admin handover; the nominee must533// AcceptFeeAdmin. Pass "" to clear a pending nomination. Two-step534// because the admin address is a funds destination: a typo'd one-step535// transfer would strand all future fees.536func NominateFeeAdmin(cur realm, nominee address) {537 rejectStraySend(cur)538 if cur.Previous().Address() != feeAdmin {539 panic("only the fee admin can nominate a successor")540 }541 if nominee == "" {542 pendingFeeAdmin = ""543 return544 }545 if !nominee.IsValid() || string(nominee) != strings.ToLower(string(nominee)) {546 panic("invalid nominee address: " + string(nominee))547 }548 pendingFeeAdmin = nominee549}550551// AcceptFeeAdmin completes the handover; only the nominee can accept.552func AcceptFeeAdmin(cur realm) {553 rejectStraySend(cur)554 if pendingFeeAdmin == "" || cur.Previous().Address() != pendingFeeAdmin {555 panic("caller is not the pending fee admin")556 }557 feeAdmin = pendingFeeAdmin558 pendingFeeAdmin = ""559}560561// GetFeeInfo returns the current fee configuration and accrued total.562func GetFeeInfo() string {563 return "fee: " + formatPct(feeBps) + " (cap " + formatPct(MaxFeeBps) +564 ") | admin: " + string(feeAdmin) +565 " | accrued: " + strconv.FormatInt(feesAccrued, 10) +566 " | claimed: " + strconv.FormatInt(feesClaimed, 10)567}568569// ---------- read-only queries ----------570571// GetSplitInfo returns a human-readable summary.572func GetSplitInfo(splitID string) string {573 s := mustGetSplit(splitID)574575 var b strings.Builder576 b.WriteString("Split: " + splitID + "\n")577 b.WriteString("Owner: " + string(s.Owner) + "\n")578 if s.PendingOwner != "" {579 // a nominee must be able to READ the offer before spending gas580 // probing it with AcceptOwnership (v2 audit Y1-obs)581 b.WriteString("Pending owner: " + string(s.PendingOwner) + "\n")582 }583 b.WriteString("Frozen: " + boolStr(s.Frozen) + "\n")584 b.WriteString("Archived: " + boolStr(s.Archived) + "\n")585 b.WriteString("Total deposited: " + strconv.FormatInt(s.TotalDeposited, 10) + "\n")586 b.WriteString("Total claimed: " + strconv.FormatInt(s.TotalClaimed, 10) + "\n")587 b.WriteString("Recipients:\n")588 for i, r := range s.Recipients {589 b.WriteString(" " + string(r) + " " + formatPct(s.Shares[i]) + " claimable: " + strconv.FormatInt(s.Balances[r], 10) + "\n")590 }591 return b.String()592}593594// GetPendingOwner returns the staged nominee for a split, or "" when no595// handover is pending. The consent half of a two-step transfer only means596// something if the nominee can inspect the offer without paying gas to597// probe it (v2 audit Y1-obs); this is that read path.598func GetPendingOwner(splitID string) string {599 return string(mustGetSplit(splitID).PendingOwner)600}601602// GetClaimable returns claimable balance for an address.603func GetClaimable(splitID string, addr address) int64 {604 s := mustGetSplit(splitID)605 return s.Balances[addr]606}607608// ---------- render ----------609610// Render returns a markdown overview. Never panics.611func Render(path string) string {612 if len(splitIDs) == 0 {613 return "# Fee Split\n\nNo active splits.\n"614 }615616 var b strings.Builder617 b.WriteString("# Fee Split\n\n")618 if feeBps > 0 {619 b.WriteString("**Protocol fee:** " + formatPct(feeBps) + " (hard cap " + formatPct(MaxFeeBps) + ")\n\n")620 }621622 show := splitIDs623 if len(show) > MaxRenderSplits {624 b.WriteString("_Showing the most recent " + strconv.Itoa(MaxRenderSplits) + " active splits._\n\n")625 show = show[len(show)-MaxRenderSplits:]626 }627 for _, id := range show {628 s := splits[id]629 b.WriteString("## " + id + "\n\n")630 if s == nil {631 b.WriteString("_(invalid)_\n\n")632 continue633 }634 b.WriteString("```\n")635 b.WriteString("Owner: " + string(s.Owner) + "\n")636 if s.PendingOwner != "" {637 b.WriteString("Pending owner: " + string(s.PendingOwner) + "\n")638 }639 b.WriteString("Frozen: " + boolStr(s.Frozen) + "\n")640 b.WriteString("Total deposited: " + strconv.FormatInt(s.TotalDeposited, 10) + "\n")641 b.WriteString("Total claimed: " + strconv.FormatInt(s.TotalClaimed, 10) + "\n")642 if len(s.Recipients) > 0 {643 b.WriteString("Recipients:\n")644 for i, r := range s.Recipients {645 share := "??%"646 if i < len(s.Shares) {647 share = formatPct(s.Shares[i])648 }649 bal := "0"650 if s.Balances != nil {651 bal = strconv.FormatInt(s.Balances[r], 10)652 }653 b.WriteString(" " + string(r) + " " + share + " claimable: " + bal + "\n")654 }655 }656 b.WriteString("```\n\n")657 }658 return b.String()659}660AcceptFeeAdmin(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})
AcceptOwnership(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}, splitID string)
Archive(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}, splitID string)
Claim(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}, splitID string) int64
ClaimFees(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}) int64
CreateSplit(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}, recipientList string, shareList string) string
Deposit(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}, splitID string)
Freeze(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}, splitID string)
GetClaimable(splitID string, addr string) int64
GetFeeInfo() string
GetPendingOwner(splitID string) string
GetSplitInfo(splitID string) string
NominateFeeAdmin(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}, nominee string)
Render(path string) string
SetFee(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}, bps int64)
TransferOwnership(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}, splitID string, newOwner string)
UpdateShares(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}, splitID string, recipientList string, shareList 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.