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

editorial.gno

10.21 Kb · 302 lines
  1package curation
  2
  3import (
  4	"chain"
  5	"strconv"
  6
  7	"gno.land/p/nt/avl/v0"
  8	"gno.land/p/nt/seqid/v0"
  9	"gno.land/r/samcrew/launchpad/nft/v1"
 10)
 11
 12const (
 13	maxFeature = 14 * 24 * 60 * 60 // seconds
 14	holdSpan   = 24 * 60 * 60
 15	maxHold    = 30 * 24 * 60 * 60
 16)
 17
 18// What a founder may appeal: the decision on its application, a hold, the
 19// collection's verification, or a feature decision.
 20var subjects = [...]string{"application", "hold", "verification", "feature"}
 21
 22// A feature slot exists only when one manager proposed it and another
 23// approved it. The admin can clear a slot and can never create one.
 24type feature struct {
 25	collection string
 26	proposer   address
 27	approver   address // empty until approved
 28	reasonHash string
 29	reasonCID  string
 30	until      int64
 31}
 32
 33// A hold hides a collection from curated views until a time. It changes
 34// presentation only: nothing about the collection's tokens is frozen.
 35type hold struct {
 36	actor      address
 37	confirmer  address // empty until a second manager confirms
 38	extended   bool    // the admin set the term and its reason
 39	reasonHash string
 40	reasonCID  string
 41	until      int64
 42}
 43
 44type verification struct {
 45	verified   bool
 46	reasonHash string
 47	reasonCID  string
 48	updatedAt  int64
 49}
 50
 51type appeal struct {
 52	collection    string
 53	subject       string
 54	statementHash string
 55	statementCID  string
 56	filedAt       int64
 57	openKey       string // its key in openAppeals; empty once resolved
 58	upheld        bool
 59	reasonHash    string
 60	reasonCID     string
 61	resolvedAt    int64
 62}
 63
 64var (
 65	features      avl.Tree // collection -> *feature
 66	holds         avl.Tree // collection -> *hold
 67	verifications avl.Tree // collection -> verification
 68	appeals       avl.Tree // "<collection>/<subject>" -> *appeal, the latest
 69	lastAppeal    seqid.ID
 70	openAppeals   avl.Tree // seqid key -> appeals key, in filing order
 71)
 72
 73func featureOf(collection string) *feature {
 74	if f := features.Get(collection); f != nil {
 75		return f.(*feature)
 76	}
 77	return nil
 78}
 79
 80func holdOf(collection string) *hold {
 81	if h := holds.Get(collection); h != nil {
 82		return h.(*hold)
 83	}
 84	return nil
 85}
 86
 87func appealOf(key string) *appeal {
 88	if a := appeals.Get(key); a != nil {
 89		return a.(*appeal)
 90	}
 91	return nil
 92}
 93
 94// standing reports whether who could act on the collection as a manager now.
 95// An unapproved slot's empty approver holds no seat.
 96func standing(collection string, who address) bool {
 97	return active(who) && !conflicted(nft.GetInfo(collection), who)
 98}
 99
100// pair enforces the two-manager rule when second completes what first began:
101// two different managers, the first still seated and unconflicted. The second
102// was checked as the caller.
103func pair(collection string, first, second address) {
104	if first == second || !standing(collection, first) {
105		panic("launchpad/curation: a second unconflicted manager is required")
106	}
107}
108
109// running reports a slot that runs, approved by two managers that both keep
110// their standing: whether hidden or not, it is the collection's slot.
111func running(collection string, f *feature) bool {
112	return now() < f.until &&
113		standing(collection, f.proposer) && standing(collection, f.approver)
114}
115
116// ProposeFeature opens a slot of at most 14 days. It features nothing until
117// a second manager approves. It replaces a slot that is not running: one
118// still unapproved, or one whose proposer or approver lost its standing.
119func ProposeFeature(cur realm, collection, reasonHash, reasonCID string, until int64) {
120	who := manager(cur, collection)
121	commitment(reasonHash, reasonCID)
122	at := now()
123	if until <= at || until > at+maxFeature {
124		panic("launchpad/curation: invalid feature term")
125	}
126	if f := featureOf(collection); f != nil && running(collection, f) {
127		panic("launchpad/curation: feature slot running")
128	}
129	features.Set(collection, &feature{collection: collection, proposer: who,
130		reasonHash: reasonHash, reasonCID: reasonCID, until: until})
131	chain.Emit("FeatureProposed", "collection", collection,
132		"manager", who.String(), "until", strconv.FormatInt(until, 10),
133		"reason_hash", reasonHash, "reason_cid", reasonCID)
134}
135
136func ApproveFeature(cur realm, collection string) {
137	who := manager(cur, collection)
138	f := featureOf(collection)
139	if f == nil || f.approver != "" || now() >= f.until {
140		panic("launchpad/curation: no feature slot awaiting approval")
141	}
142	pair(collection, f.proposer, who)
143	f.approver = who
144	chain.Emit("FeatureApproved", "collection", collection,
145		"proposer", f.proposer.String(), "manager", who.String(),
146		"until", strconv.FormatInt(f.until, 10))
147}
148
149// IsFeatured holds only while the slot runs, both of its managers keep their
150// standing and the collection is not hidden.
151func IsFeatured(collection string) bool {
152	f := featureOf(collection)
153	return f != nil && running(collection, f) && !IsHidden(collection)
154}
155
156func ClearFeature(cur realm, collection, reasonHash, reasonCID string) {
157	requireAdmin(cur)
158	commitment(reasonHash, reasonCID)
159	if _, had := features.Remove(collection); !had {
160		panic("launchpad/curation: no feature slot")
161	}
162	chain.Emit("FeatureCleared", "collection", collection,
163		"reason_hash", reasonHash, "reason_cid", reasonCID)
164}
165
166// HideTemporarily hides a collection for 24 hours on one manager's word. The
167// record outlives the hold: no manager can start another until the admin
168// clears it.
169func HideTemporarily(cur realm, collection, reasonHash, reasonCID string) {
170	who := manager(cur, collection)
171	commitment(reasonHash, reasonCID)
172	if holds.Has(collection) {
173		panic("launchpad/curation: a hold is on record until the admin clears it")
174	}
175	until := now() + holdSpan
176	holds.Set(collection, &hold{actor: who, reasonHash: reasonHash,
177		reasonCID: reasonCID, until: until})
178	chain.Emit("HideStarted", "collection", collection,
179		"manager", who.String(), "until", strconv.FormatInt(until, 10),
180		"reason_hash", reasonHash, "reason_cid", reasonCID)
181}
182
183// ConfirmHide lets a second manager keep a running hold for 24 hours from
184// now, once. After that, or once the admin has set the term, only the admin
185// changes it.
186func ConfirmHide(cur realm, collection string) {
187	who := manager(cur, collection)
188	h := holdOf(collection)
189	at := now()
190	if h == nil || h.confirmer != "" || h.extended || at >= h.until {
191		panic("launchpad/curation: no hold awaiting confirmation")
192	}
193	pair(collection, h.actor, who)
194	h.confirmer, h.until = who, at+holdSpan
195	chain.Emit("HideConfirmed", "collection", collection,
196		"manager", who.String(), "until", strconv.FormatInt(h.until, 10))
197}
198
199// ExtendHide sets the term of a recorded hold, running or over, to a time at
200// most 30 days ahead, and replaces its public reason.
201func ExtendHide(cur realm, collection string, until int64, reasonHash, reasonCID string) {
202	requireAdmin(cur)
203	commitment(reasonHash, reasonCID)
204	h := holdOf(collection)
205	at := now()
206	if h == nil || until <= at || until > at+maxHold {
207		panic("launchpad/curation: invalid hold extension")
208	}
209	h.extended, h.until, h.reasonHash, h.reasonCID = true, until, reasonHash, reasonCID
210	chain.Emit("HideExtended", "collection", collection,
211		"until", strconv.FormatInt(until, 10),
212		"reason_hash", reasonHash, "reason_cid", reasonCID)
213}
214
215func ClearHide(cur realm, collection, reasonHash, reasonCID string) {
216	requireAdmin(cur)
217	commitment(reasonHash, reasonCID)
218	if _, had := holds.Remove(collection); !had {
219		panic("launchpad/curation: no hold on record")
220	}
221	chain.Emit("HideCleared", "collection", collection,
222		"reason_hash", reasonHash, "reason_cid", reasonCID)
223}
224
225// IsHidden holds while the hold runs and the manager whose word set its term
226// keeps its standing: the starter, or the confirmer once it confirmed. A term
227// the admin set rests on the admin alone.
228func IsHidden(collection string) bool {
229	h := holdOf(collection)
230	switch {
231	case h == nil || now() >= h.until:
232		return false
233	case h.extended:
234		return true
235	case h.confirmer != "":
236		return standing(collection, h.confirmer)
237	}
238	return standing(collection, h.actor)
239}
240
241// SetVerified is the admin's statement that a collection is what it claims
242// to be. It says nothing about price or future value.
243func SetVerified(cur realm, collection string, value bool, reasonHash, reasonCID string) {
244	requireAdmin(cur)
245	nft.GetInfo(collection)
246	commitment(reasonHash, reasonCID)
247	if IsVerified(collection) == value {
248		panic("launchpad/curation: verification unchanged")
249	}
250	verifications.Set(collection, verification{value, reasonHash, reasonCID, now()})
251	chain.Emit("VerificationChanged", "collection", collection,
252		"verified", strconv.FormatBool(value),
253		"reason_hash", reasonHash, "reason_cid", reasonCID)
254}
255
256func IsVerified(collection string) bool {
257	v := verifications.Get(collection)
258	return v != nil && v.(verification).verified
259}
260
261// Appeal files the founder's objection on one subject. A collection has at
262// most one open appeal per subject; a resolved one can be followed by another.
263func Appeal(cur realm, collection, subject, statementHash, statementCID string) {
264	who := founder(cur, collection)
265	known := false
266	for _, s := range subjects {
267		known = known || s == subject
268	}
269	if !known {
270		panic("launchpad/curation: invalid appeal subject")
271	}
272	commitment(statementHash, statementCID)
273	key := collection + "/" + subject
274	if a := appealOf(key); a != nil && a.openKey != "" {
275		panic("launchpad/curation: appeal already open")
276	}
277	openKey := lastAppeal.Next().String()
278	appeals.Set(key, &appeal{collection: collection, subject: subject,
279		statementHash: statementHash, statementCID: statementCID,
280		filedAt: now(), openKey: openKey})
281	openAppeals.Set(openKey, key)
282	chain.Emit("AppealFiled", "collection", collection, "subject", subject,
283		"founder", who.String(),
284		"statement_hash", statementHash, "statement_cid", statementCID)
285}
286
287// ResolveAppeal records the admin's answer and its public reason. It changes
288// nothing else: the admin acts on an upheld appeal with the ordinary calls.
289func ResolveAppeal(cur realm, collection, subject string, upheld bool, reasonHash, reasonCID string) {
290	requireAdmin(cur)
291	commitment(reasonHash, reasonCID)
292	a := appealOf(collection + "/" + subject)
293	if a == nil || a.openKey == "" {
294		panic("launchpad/curation: no open appeal")
295	}
296	openAppeals.Remove(a.openKey)
297	a.openKey, a.upheld, a.resolvedAt = "", upheld, now()
298	a.reasonHash, a.reasonCID = reasonHash, reasonCID
299	chain.Emit("AppealResolved", "collection", collection, "subject", subject,
300		"upheld", strconv.FormatBool(upheld),
301		"reason_hash", reasonHash, "reason_cid", reasonCID)
302}