1package governor23import (4 "strings"56 ufmt "gno.land/p/nt/ufmt/v0"7)89// batchKind runs several adopted kinds as one decision.10//11// govern:batch "treasury/spend 100\nfee 250"12//13// One member per line, the kind first and the rest its payload. Members must14// already be adopted: a batch decides several things at once and smuggles in15// nothing.16//17// All or nothing, because there is no halfway. A member's Do returning an error18// makes this panic, aborting the transaction and unwinding what earlier members19// did — the only atomicity gno offers.20//21// So a failed batch leaves the proposal Succeeded rather than Failed: the22// transaction wrote nothing, including the failure. It can be run again and23// will fail identically until the world changes or the grace period ends it.24//25// Single-member kinds keep the other behaviour — record the failure and finish26// — since there is nothing to be atomic with respect to.27type batchKind struct{}2829// maxBatch bounds a batch, because Describe and Check both walk it and a30// proposal nobody can render is a proposal nobody can vote on.31const maxBatch = 163233func (k batchKind) Name() string { return reserved + "batch" }3435func (k batchKind) describe(g *Governor, payload string) string {36 members, err := parseBatch(payload)37 if err != nil {38 return "malformed batch: " + err.Error()39 }40 out := ufmt.Sprintf("do all %d of these, or none of them:\n", len(members))41 for i, m := range members {42 // Each member described by its own kind, from its own payload — the43 // same strings their Do will be handed. A batch that summarised its44 // members in its own words would be the description gap this design45 // exists to avoid, reintroduced one level up.46 out += ufmt.Sprintf("\n%d. **%s** — %s", i+1, m.kind, g.describeMember(m))47 }48 return out49}5051func (g *Governor) describeMember(m member) string {52 k := g.anyKind(m.kind)53 if k == nil {54 return "_no kind by that name_"55 }56 return g.describeKind(k, m.payload)57}5859func (k batchKind) check(g *Governor, payload string) error {60 members, err := parseBatch(payload)61 if err != nil {62 return err63 }64 for _, m := range members {65 e := g.entryOf(m.kind)66 if e == nil {67 return govErr("no kind called " + m.kind)68 }69 if !e.live {70 // A batch cannot reach a power the holders have not granted.71 return govErr(m.kind + " is not adopted")72 }73 if m.kind == (batchKind{}).Name() {74 // No batches of batches. Nesting makes the rendered description a75 // tree of unbounded depth, and the point of the description is76 // that somebody reads it.77 return govErr("a batch cannot contain a batch")78 }79 if err := g.checkKind(e.kind, m.payload); err != nil {80 return govErr(m.kind + ": " + err.Error())81 }82 }83 return nil84}8586func (k batchKind) run(g *Governor, dispatch Dispatch, payload string) error {87 members, err := parseBatch(payload)88 if err != nil {89 return err90 }91 for i, m := range members {92 e := g.entryOf(m.kind)93 if e == nil || !e.live {94 panic("govern: batch member " + m.kind + " is no longer adopted")95 }96 var err error97 if b, ok := asBuiltin(e.kind); ok {98 err = b.run(g, dispatch, m.payload)99 } else {100 // Each member gets its OWN sub-realm, named for the power it is,101 // rather than the batch's. A batch is a way to decide several102 // things at once, not a way to launder one kind's authority into103 // another's.104 err = dispatch(e.kind, subPathOf(m.kind), m.payload)105 }106 if err != nil {107 // Panic, not return. Returning would record a failure and keep108 // whatever the earlier members already did, which is the one109 // outcome a batch promises cannot happen.110 panic(ufmt.Sprintf("govern: batch member %d (%s) failed: %s",111 i+1, m.kind, err.Error()))112 }113 }114 return nil115}116117type member struct {118 kind string119 payload string120}121122func parseBatch(payload string) ([]member, error) {123 var out []member124 for _, line := range strings.Split(payload, "\n") {125 line = strings.TrimSpace(line)126 if line == "" {127 continue128 }129 sp := strings.Index(line, " ")130 if sp <= 0 {131 // A member with no payload is almost always a typo, and a batch is132 // the wrong place to find out.133 return nil, govErr("expected `<kind> <payload>`, got: " + line)134 }135 out = append(out, member{kind: line[:sp], payload: strings.TrimSpace(line[sp+1:])})136 }137 if len(out) < 2 {138 return nil, govErr("a batch needs at least two members; propose the kind directly")139 }140 if len(out) > maxBatch {141 return nil, govErr("too many members in one batch")142 }143 return out, nil144}145Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.