1// Package multisig implements an M-of-N approval multisig realm.2//3// It is a port of the classic Solidity multisig idea: a fixed set of owners4// is configured once, any owner may create a proposal, owners approve it, and5// once the number of distinct approvals reaches the threshold the proposal is6// marked "Executed". This realm tracks approvals only — it does not move funds.7package multisig89import (10 "errors"11 "strconv"12 "strings"1314 "chain"15 "chain/runtime/unsafe"1617 "gno.land/p/nt/avl/v0"18)1920// State of a single proposal.21type stateKind int2223const (24 statePending stateKind = iota25 stateExecuted26)2728func (s stateKind) String() string {29 if s == stateExecuted {30 return "Executed"31 }32 return "Pending"33}3435// proposal is a single approval request.36type proposal struct {37 id int38 description string39 proposer address40 approvals *avl.Tree // owner address string -> struct{}{}, distinct approvers41 state stateKind42}4344// Package-level persistent state.45var (46 owners *avl.Tree // owner address string -> struct{}{}47 ownerCount int48 threshold int49 initialized bool5051 proposals *avl.Tree // decimal id string -> *proposal52 nextID int53)5455var (56 errAlreadyInit = errors.New("multisig: already initialized")57 errNotInit = errors.New("multisig: not initialized")58 errBadThreshold = errors.New("multisig: threshold must be between 1 and number of owners")59 errNoOwners = errors.New("multisig: at least one owner required")60 errNotOwner = errors.New("multisig: caller is not an owner")61 errNoProposal = errors.New("multisig: proposal not found")62 errDupApproval = errors.New("multisig: caller already approved this proposal")63 errExecuted = errors.New("multisig: proposal already executed")64)6566func init() {67 owners = avl.NewTree()68 proposals = avl.NewTree()69}7071// caller returns the address of the realm/user that called into this realm.72func caller() address {73 return unsafe.PreviousRealm().Address()74}7576func isOwner(a address) bool {77 return owners.Has(a.String())78}7980// Setup configures the multisig exactly once. owners is a comma-separated list81// of bech32 addresses; threshold is the number of approvals required to execute82// a proposal. It panics (cross-realm abort) on invalid input or if already set.83func Setup(cur realm, ownersCSV string, thresholdN int) {84 if initialized {85 panic(errAlreadyInit)86 }8788 parts := strings.Split(ownersCSV, ",")89 count := 090 for _, p := range parts {91 a := strings.TrimSpace(p)92 if a == "" {93 continue94 }95 addr := address(a)96 if owners.Has(addr.String()) {97 continue // dedupe98 }99 owners.Set(addr.String(), struct{}{})100 count++101 }102 if count == 0 {103 panic(errNoOwners)104 }105 if thresholdN < 1 || thresholdN > count {106 panic(errBadThreshold)107 }108109 ownerCount = count110 threshold = thresholdN111 initialized = true112113 chain.Emit("Setup", "owners", strconv.Itoa(count), "threshold", strconv.Itoa(thresholdN))114}115116// Propose creates a new proposal authored by an owner and returns its id.117func Propose(cur realm, description string) int {118 if !initialized {119 panic(errNotInit)120 }121 c := caller()122 if !isOwner(c) {123 panic(errNotOwner)124 }125126 id := nextID127 nextID++128129 p := &proposal{130 id: id,131 description: description,132 proposer: c,133 approvals: avl.NewTree(),134 state: statePending,135 }136 proposals.Set(strconv.Itoa(id), p)137138 chain.Emit("Propose", "id", strconv.Itoa(id), "proposer", c.String())139 return id140}141142// Approve records an owner's approval for a proposal. When the count of distinct143// approvals reaches the threshold, the proposal becomes Executed.144func Approve(cur realm, id int) {145 if !initialized {146 panic(errNotInit)147 }148 c := caller()149 if !isOwner(c) {150 panic(errNotOwner)151 }152153 v := proposals.Get(strconv.Itoa(id))154 if v == nil {155 panic(errNoProposal)156 }157 p := v.(*proposal)158 if p.state == stateExecuted {159 panic(errExecuted)160 }161 if p.approvals.Has(c.String()) {162 panic(errDupApproval)163 }164165 p.approvals.Set(c.String(), struct{}{})166 chain.Emit("Approve", "id", strconv.Itoa(id), "owner", c.String(), "approvals", strconv.Itoa(p.approvals.Size()))167168 if p.approvals.Size() >= threshold {169 p.state = stateExecuted170 chain.Emit("Executed", "id", strconv.Itoa(id))171 }172}173174// Render returns a Markdown view of the multisig configuration and proposals.175func Render(path string) string {176 var b strings.Builder177 b.WriteString("# M-of-N Multisig\n\n")178179 if !initialized {180 b.WriteString("_Not initialized. Call `Setup(owners, threshold)` once._\n")181 return b.String()182 }183184 b.WriteString("**Threshold:** ")185 b.WriteString(strconv.Itoa(threshold))186 b.WriteString(" of ")187 b.WriteString(strconv.Itoa(ownerCount))188 b.WriteString("\n\n## Owners\n\n")189 owners.Iterate("", "", func(key string, _ interface{}) bool {190 b.WriteString("- `")191 b.WriteString(key)192 b.WriteString("`\n")193 return false194 })195196 b.WriteString("\n## Proposals\n\n")197 if proposals.Size() == 0 {198 b.WriteString("_No proposals yet._\n")199 return b.String()200 }201202 b.WriteString("| ID | Description | Proposer | Approvals | State |\n")203 b.WriteString("|----|-------------|----------|-----------|-------|\n")204 proposals.Iterate("", "", func(_ string, v interface{}) bool {205 p := v.(*proposal)206 b.WriteString("| ")207 b.WriteString(strconv.Itoa(p.id))208 b.WriteString(" | ")209 b.WriteString(p.description)210 b.WriteString(" | `")211 b.WriteString(p.proposer.String())212 b.WriteString("` | ")213 b.WriteString(strconv.Itoa(p.approvals.Size()))214 b.WriteString("/")215 b.WriteString(strconv.Itoa(threshold))216 b.WriteString(" | ")217 b.WriteString(p.state.String())218 b.WriteString(" |\n")219 return false220 })221222 return b.String()223}224Approve(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 int)
Propose(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}, description string) int
Render(path string) string
Setup(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}, ownersCSV string, thresholdN int)
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.