Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

engines.gno

7.51 Kb · 239 lines
  1package core
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"strings"
  7
  8	"gno.land/p/nt/avl/v0"
  9
 10	"gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/engine/v1"
 11	"gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/safe/v1"
 12)
 13
 14// Engine registry ("art as code", design §5).
 15//
 16// An engine registers itself from its own /r/ adapter realm; its id is that
 17// realm's package path, so nobody can register in someone else's name. The
 18// stored interface value's methods are declared in the adapter, so every
 19// call runs with the adapter's authority, not core's (interrealm v2 borrow
 20// rule #1): engine code cannot write core state. It also cannot call back
 21// into a core mutator: all of core's state-changing functions are crossing
 22// and need a live cur the engine never has.
 23//
 24// Honest limits, surfaced to users rather than hidden:
 25//   - an adapter realm with state could change its art later; the sample
 26//     fingerprint (VerifyEngine) detects a change of the sample, and review
 27//     means reading the adapter for statelessness;
 28//   - a private (re-deployable) adapter cannot register: the VM aborts the
 29//     registration when core's state would retain the private realm's type
 30//     (spike A9, observed in gno v1.7.0 test mode; confirm on Onyx).
 31type engineRecord struct {
 32	id               string
 33	impl             engine.Engine
 34	manifest         engine.Manifest // core-owned copy
 35	fingerprint      string          // sha256 of Render(SampleInput())
 36	claimed          bool
 37	deprecated       bool
 38	reviewed         bool
 39	registeredHeight int64
 40}
 41
 42var (
 43	engines     avl.Tree // engine id -> *engineRecord
 44	engineOrder avl.Tree // safe.PadInt(seq) -> engine id
 45	engineCount int64
 46)
 47
 48// RegisterEngine stores the calling realm's engine (I-12 exception: one
 49// sample render in the engine's own registration transaction). The record
 50// is immutable afterwards except for the claim/deprecate/review flags.
 51func RegisterEngine(cur realm, e engine.Engine) string {
 52	id := cur.Previous().PkgPath()
 53	if !strings.HasPrefix(id, runtime.ChainDomain()+"/r/") || strings.Contains(id, "#") {
 54		panic("an engine registers from its own realm (an /r/ package, not a sub-realm or wallet)")
 55	}
 56	if e == nil {
 57		panic("nil engine")
 58	}
 59	if engines.Has(id) {
 60		panic("engine already registered: " + id + " (publish a new version under a new path)")
 61	}
 62	m := copyManifest(e.Manifest())
 63	if err := engine.ValidateManifest(m); err != nil {
 64		panic(err)
 65	}
 66	if err := e.ValidateParams(""); err != nil {
 67		panic("engine must accept empty params (its defaults): " + err.Error())
 68	}
 69	sample := e.Render(engine.SampleInput())
 70	if err := engine.CheckSVG(m, sample); err != nil {
 71		panic("sample render: " + err.Error())
 72	}
 73	if err := engine.ValidateTraits(m, e.Traits(engine.SampleInput())); err != nil {
 74		panic("sample traits: " + err.Error())
 75	}
 76
 77	rec := &engineRecord{
 78		id:               id,
 79		impl:             e,
 80		manifest:         m,
 81		fingerprint:      safe.Sha256Hex(sample),
 82		registeredHeight: height(),
 83	}
 84	engines.Set(id, rec)
 85	engineCount++
 86	engineOrder.Set(safe.PadInt(engineCount, cidWidth), id)
 87	chain.Emit("EngineRegistered", "id", id, "author", m.Author.String(), "fingerprint", rec.fingerprint)
 88	return id
 89}
 90
 91// ClaimEngine proves the manifest author controls that address. Unclaimed
 92// engines cannot back a collection.
 93func ClaimEngine(cur realm, id string) {
 94	assertNoDeposit(cur)
 95	rec := mustEngine(id)
 96	if caller(cur) != rec.manifest.Author {
 97		panic("only the manifest author " + rec.manifest.Author.String() + " can claim this engine")
 98	}
 99	rec.claimed = true
100	chain.Emit("EngineClaimed", "id", id)
101}
102
103// DeprecateEngine stops new collections; existing ones keep rendering.
104func DeprecateEngine(cur realm, id string) {
105	assertNoDeposit(cur)
106	rec := mustEngine(id)
107	if !rec.claimed || caller(cur) != rec.manifest.Author {
108		panic("only the engine's author can deprecate it")
109	}
110	rec.deprecated = true
111	chain.Emit("EngineDeprecated", "id", id)
112}
113
114func mustEngine(id string) *engineRecord {
115	v := engines.Get(id)
116	if v == nil {
117		panic("unknown engine " + id)
118	}
119	return v.(*engineRecord)
120}
121
122// copyManifest gives core its own copy: the engine's slice was allocated in
123// the engine realm.
124func copyManifest(m engine.Manifest) engine.Manifest {
125	out := m
126	out.TraitTypes = append([]string{}, m.TraitTypes...)
127	return out
128}
129
130// --- engine reads (Studio, Verify) -------------------------------------------
131
132// RenderWith renders any registered engine for arbitrary params and seed as
133// a data URI — Studio previews and the Verify button.
134func RenderWith(id, params, seed string) string {
135	rec := mustEngine(id)
136	if err := rec.impl.ValidateParams(params); err != nil {
137		panic("params rejected by engine: " + err.Error())
138	}
139	svg := rec.impl.Render(studioInput(params, seed))
140	if err := engine.CheckSVG(rec.manifest, svg); err != nil {
141		return dataURI(placeholderSVG(rec.manifest.Name, "Engine output rejected"))
142	}
143	return dataURI(svg)
144}
145
146// EngineTraits returns a core-owned copy of the engine's traits for seed.
147func EngineTraits(id, params, seed string) []engine.Trait {
148	rec := mustEngine(id)
149	if err := rec.impl.ValidateParams(params); err != nil {
150		panic("params rejected by engine: " + err.Error())
151	}
152	ts := rec.impl.Traits(studioInput(params, seed))
153	if err := engine.ValidateTraits(rec.manifest, ts); err != nil {
154		panic("engine traits rejected: " + err.Error())
155	}
156	return append([]engine.Trait{}, ts...)
157}
158
159func TraitsWith(id, params, seed string) string {
160	w := &safe.JSONWriter{}
161	writeTraits(w, EngineTraits(id, params, seed))
162	return w.String()
163}
164
165// VerifyEngine re-renders the sample and compares it with the fingerprint
166// stored at registration.
167func VerifyEngine(id string) bool {
168	rec := mustEngine(id)
169	return safe.Sha256Hex(rec.impl.Render(engine.SampleInput())) == rec.fingerprint
170}
171
172// EngineManifest is the typed read for other realms (gallery).
173func EngineManifest(id string) engine.Manifest { return copyManifest(mustEngine(id).manifest) }
174
175// EngineStatus is "unclaimed", "unverified", "reviewed" or "deprecated".
176func EngineStatus(id string) string { return engineStatus(mustEngine(id)) }
177
178func engineStatus(rec *engineRecord) string {
179	switch {
180	case rec.deprecated:
181		return "deprecated"
182	case !rec.claimed:
183		return "unclaimed"
184	case rec.reviewed:
185		return "reviewed"
186	}
187	return "unverified"
188}
189
190// Engine is the record as JSON.
191func Engine(id string) string {
192	w := &safe.JSONWriter{}
193	writeEngine(w, mustEngine(id))
194	return w.String()
195}
196
197func Engines(offset, limit int64) string {
198	w := &safe.JSONWriter{}
199	w.BeginArray()
200	engineOrder.IterateByOffset(int(offset), int(clampLimit(limit)), func(_ string, v any) bool {
201		writeEngine(w, mustEngine(v.(string)))
202		return false
203	})
204	w.EndArray()
205	return w.String()
206}
207
208func writeEngine(w *safe.JSONWriter, rec *engineRecord) {
209	m := rec.manifest
210	w.BeginObject()
211	w.StrField("id", rec.id)
212	w.StrField("pkgPath", rec.id) // the adapter realm; read its source (and imports) with vm/qfile
213	w.StrField("name", m.Name)
214	w.StrField("version", m.Version)
215	w.StrField("description", m.Description)
216	w.StrField("license", m.License)
217	w.StrField("author", m.Author.String())
218	w.StrField("paramsSchema", m.ParamsSchema)
219	w.Key("traitTypes")
220	w.BeginArray()
221	for _, t := range m.TraitTypes {
222		w.Str(t)
223	}
224	w.EndArray()
225	w.BoolField("living", m.Living)
226	w.IntField("maxSvgBytes", m.MaxSVGBytes)
227	w.StrField("fingerprint", rec.fingerprint)
228	w.StrField("status", engineStatus(rec))
229	w.IntField("registeredHeight", rec.registeredHeight)
230	w.EndObject()
231}
232
233func studioInput(params, seed string) engine.Input {
234	in := engine.SampleInput()
235	in.Seed = seed
236	in.Params = params
237	in.Height = height()
238	return in
239}