Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}