batch.gno
4.42 Kb · 144 lines
1package governor
2
3import (
4 "strings"
5
6 ufmt "gno.land/p/nt/ufmt/v0"
7)
8
9// 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 must
14// already be adopted: a batch decides several things at once and smuggles in
15// nothing.
16//
17// All or nothing, because there is no halfway. A member's Do returning an error
18// makes this panic, aborting the transaction and unwinding what earlier members
19// did — the only atomicity gno offers.
20//
21// So a failed batch leaves the proposal Succeeded rather than Failed: the
22// transaction wrote nothing, including the failure. It can be run again and
23// 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 finish
26// — since there is nothing to be atomic with respect to.
27type batchKind struct{}
28
29// maxBatch bounds a batch, because Describe and Check both walk it and a
30// proposal nobody can render is a proposal nobody can vote on.
31const maxBatch = 16
32
33func (k batchKind) Name() string { return reserved + "batch" }
34
35func (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 — the
43 // same strings their Do will be handed. A batch that summarised its
44 // members in its own words would be the description gap this design
45 // 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 out
49}
50
51func (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}
58
59func (k batchKind) check(g *Governor, payload string) error {
60 members, err := parseBatch(payload)
61 if err != nil {
62 return err
63 }
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 a
75 // tree of unbounded depth, and the point of the description is
76 // 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 nil
84}
85
86func (k batchKind) run(g *Governor, dispatch Dispatch, payload string) error {
87 members, err := parseBatch(payload)
88 if err != nil {
89 return err
90 }
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 error
97 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 several
102 // things at once, not a way to launder one kind's authority into
103 // 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 keep
108 // whatever the earlier members already did, which is the one
109 // 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 nil
115}
116
117type member struct {
118 kind string
119 payload string
120}
121
122func parseBatch(payload string) ([]member, error) {
123 var out []member
124 for _, line := range strings.Split(payload, "\n") {
125 line = strings.TrimSpace(line)
126 if line == "" {
127 continue
128 }
129 sp := strings.Index(line, " ")
130 if sp <= 0 {
131 // A member with no payload is almost always a typo, and a batch is
132 // 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, nil
144}