Transaction

6A37BC080D8B9A…695E3FCD8FC1

Block 652,415 · index 0 · indexed

Summary

Hash
6A37BC080D8B9A236CCE51E7C1674F15A1B256ED5A22F16E20E0695E3FCD8FC1
Block
652,415
Size
32008 bytes
Gas used
46,114,565 / 125,000,000
Fee
125000 ugnot
Status
success

Messages

Attached funds
15000000 ugnot

Arguments · 11

  1. #1curation
  2. #2curation.gno
  3. #3// Package curation records who curates the Launchpad marketplace and what // they decided about a collection: applications, feature slots, temporary // holds, verification and appeals. It holds no coins and no tokens, never // changes a collection's terms and never blocks a transfer, sale, refund or // claim: a client reads these records and decides what to show. package curation import ( "chain" "chain/runtime/unsafe" "strconv" "strings" "time" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/samcrew/launchpad/currency/v1" "gno.land/p/samcrew/launchpad/meta/v1" "gno.land/r/samcrew/launchpad/nft/v1" ) const ( maxSeats = 5 maxTerm = 90 * 24 * 60 * 60 // seconds StatusSubmitted = "submitted" StatusChangesRequested = "changes_requested" StatusRecommended = "recommended" StatusDeclined = "declined" ) // A seat is active strictly before until. Lead is a public label and grants // nothing. type seat struct { lead bool until int64 } // An Application is a founder's request for review and the latest decision on // it. Earlier revisions and decisions stay in the event log. type Application struct { Founder address // who filed the latest revision StatementHash string StatementCID string Revision int64 // 0: nobody applied Status string Reviewer address ReasonHash string ReasonCID string UpdatedAt int64 } var ( admin address pendingAdmin address seats avl.Tree // manager address -> seat; at most maxSeats entries conflicts avl.Tree // "<collection>/<address>" -> true; never removed applications avl.Tree // collection -> *Application lastFiling seqid.ID filings avl.Tree // seqid key -> collection, in order of first filing ) // The account that publishes the package is the first admin. func init() { admin = unsafe.OriginCaller() } func now() int64 { return time.Now().Unix() } // caller returns the immediate caller. Nothing here takes payment: coins a // wallet attaches to a direct call would stay in this realm, so they are // refused. A calling realm keeps whatever its own caller sent. func caller(cur realm) address { if !cur.IsCurrent() { panic("launchpad/curation: stale realm context") } if cur.Previous().IsUserCall() { currency.AssertNoCoins(0, cur) } return cur.Previous().Address() } // direct returns the wallet that called this realm itself. Founders and // managers act in person: no realm can apply, review or recuse for them. func direct(cur realm) address { who := caller(cur) if !cur.Previous().IsUserCall() { panic("launchpad/curation: direct user call required") } return who } func requireAdmin(cur realm) { if caller(cur) != admin { panic("launchpad/curation: admin only") } } var zeroHash = strings.Repeat("0", 64) // commitment checks a pointer to public text: the SHA-256 of its exact bytes // as 64 lowercase hex digits, not all zero, and a bounded IPFS CID it can be // fetched from (CIDv1 "bafy..." or "bafk..." in base32, or CIDv0 "Qm..." in // base58). Clients must hash what they fetch before showing it. Private prose // never goes on chain. Byte ranges, not alphabet lookups: this is most of the // cost of a call that takes a pointer. func commitment(hash, cid string) { ok := len(hash) == 64 && hash != zeroHash for i := 0; ok && i < len(hash); i++ { c := hash[i] ok = c >= '0' && c <= '9' || c >= 'a' && c <= 'f' } switch { case len(cid) >= 59 && len(cid) <= 90 && (cid[:4] == "bafy" || cid[:4] == "bafk"): for i := 4; ok && i < len(cid); i++ { c := cid[i] ok = c >= 'a' && c <= 'z' || c >= '2' && c <= '7' } case len(cid) == 46 && cid[:2] == "Qm": for i := 2; ok && i < len(cid); i++ { c := cid[i] ok = c >= '1' && c <= '9' || c >= 'A' && c <= 'H' || c >= 'J' && c <= 'N' || c >= 'P' && c <= 'Z' || c >= 'a' && c <= 'k' || c >= 'm' && c <= 'z' } default: ok = false } if !ok { panic("launchpad/curation: invalid hash or CID") } } // ProposeAdmin starts a handoff to any account or realm. Nothing changes // until that exact address accepts. func ProposeAdmin(cur realm, next address) { requireAdmin(cur) if !meta.ValidAddress(next) || next == admin { panic("launchpad/curation: invalid next admin") } pendingAdmin = next chain.Emit("AdminProposed", "admin", admin.String(), "pending", next.String()) } func CancelAdminProposal(cur realm) { requireAdmin(cur) if pendingAdmin == "" { panic("launchpad/curation: no pending admin") } pendingAdmin = "" chain.Emit("AdminProposalCancelled", "admin", admin.String()) } // AcceptAdmin completes the handoff. No caller has the empty address, so // nobody accepts while nothing is proposed. The admin holds no seat: a // manager who accepts gives its seat up. func AcceptAdmin(cur realm) { who := caller(cur) if who != pendingAdmin { panic("launchpad/curation: pending admin only") } previous := admin admin, pendingAdmin = who, "" if _, held := seats.Remove(who.String()); held { chain.Emit("ManagerRemoved", "manager", who.String()) } chain.Emit("AdminAccepted", "admin", who.String(), "previous", previous.String()) } func GetAdmin() address { return admin } func GetPendingAdmin() address { return pendingAdmin } // checkAccount refuses an address the chain never gives a caller: another // spelling of an account would read as a stranger. func checkAccount(who address) { if !meta.ValidAddress(who) { panic("launchpad/curation: invalid account") } } func active(who address) bool { s := seats.Get(who.String()) return s != nil && now() < s.(seat).until } func ActiveManager(who address) bool { checkAccount(who) return active(who) } func ActiveManagerCount() int { n, at := 0, now() seats.Iterate("", "", func(_ string, v any) bool { if at < v.(seat).until { n++ } return false }) return n } // AppointManager seats a manager until a time at most 90 days ahead, or // replaces the term of one already seated. Expired seats are dropped first, // so the tree never holds more than maxSeats entries. func AppointManager(cur realm, who address, lead bool, until int64) { requireAdmin(cur) at := now() if !meta.ValidAddress(who) || who == admin || until <= at || until > at+maxTerm { panic("launchpad/curation: invalid manager term") } var expired []string seats.Iterate("", "", func(key string, v any) bool { if v.(seat).until <= at { expired = append(expired, key) } return false }) for _, key := range expired { seats.Remove(key) } if !seats.Has(who.String()) && seats.Size() >= maxSeats { panic("launchpad/curation: every seat is taken") } seats.Set(who.String(), seat{lead, until}) chain.Emit("ManagerAppointed", "manager", who.String(), "lead", strconv.FormatBool(lead), "until", strconv.FormatInt(until, 10)) } func RemoveManager(cur realm, who address) { requireAdmin(cur) if !ActiveManager(who) { panic("launchpad/curation: no active manager") } seats.Remove(who.String()) chain.Emit("ManagerRemoved", "manager", who.String()) } func conflictKey(collection string, who address) string { return collection + "/" + who.String() } func recorded(collection string, who address) bool { return conflicts.Has(conflictKey(collection, who)) } // HasConflict reports a recorded conflict only: a filing, a recusal or an // admin's mark. Conflicted is the full rule. func HasConflict(collection string, who address) bool { checkAccount(who) return recorded(collection, who) } // Conflicted reports whether an account may not act as a manager on a // collection: it created the collection, holds or is proposed for its // creator role now, or has a recorded conflict. It aborts on an unknown // collection. func Conflicted(collection string, who address) bool { checkAccount(who) return conflicted(nft.GetInfo(collection), who) } func conflicted(info nft.Info, who address) bool { return info.Originator == who || info.Creator == who || info.PendingCreator == who || recorded(info.ID, who) } func seated(cur realm) address { who := direct(cur) if !active(who) { panic("launchpad/curation: active manager only") } return who } // manager returns the calling manager, who must hold an active seat and have // no conflict on the collection. func manager(cur realm, collection string) address { who := seated(cur) if conflicted(nft.GetInfo(collection), who) { panic("launchpad/curation: conflicted manager") } return who } // Resign gives up the calling manager's own seat at once. func Resign(cur realm) { who := seated(cur) seats.Remove(who.String()) chain.Emit("ManagerResigned", "manager", who.String()) } // reopen voids the decision of a manager found conflicted after making it: // the application awaits review again, so a conflict cannot be dodged by // deciding first and disclosing later. func reopen(collection string, who address) { a := applicationOf(collection) if a != nil && a.Reviewer == who { a.Status, a.Reviewer, a.ReasonHash, a.ReasonCID = StatusSubmitted, "", "", "" a.UpdatedAt = now() chain.Emit("ApplicationReopened", "collection", collection, "revision", strconv.FormatInt(a.Revision, 10), "reason", "conflict") } } // Recuse records the calling manager's own conflict on a collection and voids // its decision there, if it made the latest one. A recorded conflict is // permanent: nobody, the admin included, can clear it, and recusing again // records and emits nothing. func Recuse(cur realm, collection string) { who := seated(cur) nft.GetInfo(collection) if !recorded(collection, who) { conflicts.Set(conflictKey(collection, who), true) chain.Emit("ManagerRecused", "collection", collection, "manager", who.String()) reopen(collection, who) } } // MarkConflict records a conflict the admin knows of, on a manager or on an // account that may become one, and voids that account's latest decision on // the collection as Recuse does. Marking a recorded conflict again records // and emits nothing. func MarkConflict(cur realm, collection string, who address) { requireAdmin(cur) nft.GetInfo(collection) checkAccount(who) if !recorded(collection, who) { conflicts.Set(conflictKey(collection, who), true) chain.Emit("ConflictMarked", "collection", collection, "account", who.String()) reopen(collection, who) } } // founder returns the calling wallet, which must hold the collection's // creator role. func founder(cur realm, collection string) address { who := direct(cur) if nft.GetInfo(collection).Creator != who { panic("launchpad/curation: collection creator only") } return who } func applicationOf(collection string) *Application { if a := applications.Get(collection); a != nil { return a.(*Application) } return nil } // GetApplication returns a copy of a collection's application, the zero value // (revision 0) when nobody applied, as ApplicationJSON answers null. func GetApplication(collection string) Application { if a := applicationOf(collection); a != nil { return *a } return Application{} } // Apply files the founder's application for a collection. The founder may // file again at any time before a recommendation, to correct a pointer or // after a manager asked for changes or declined. Each filing is a new // revision awaiting review. Filing records the filer's conflict on the // collection for good. A realm that holds the creator role never applies: the // founder calls in person. func Apply(cur realm, collection, statementHash, statementCID string) { who := founder(cur, collection) commitment(statementHash, statementCID) a := applicationOf(collection) if a == nil { a = &Application{} applications.Set(collection, a) filings.Set(lastFiling.Next().String(), collection) } else if a.Status == StatusRecommended { panic("launchpad/curation: application recommended") } a.Founder = who // the creator role may have changed hands since the last filing if !recorded(collection, who) { conflicts.Set(conflictKey(collection, who), true) } a.Revision++ a.StatementHash, a.StatementCID = statementHash, statementCID a.Status, a.Reviewer, a.ReasonHash, a.ReasonCID = StatusSubmitted, "", "", "" a.UpdatedAt = now() chain.Emit("ApplicationSubmitted", "collection", collection, "founder", who.String(), "revision", strconv.FormatInt(a.Revision, 10), "statement_hash", statementHash, "statement_cid", statementCID) } // Review records one manager's decision on the revision it read, which must // still be the latest. Any unconflicted manager may decide again, whatever the // latest decision. A recommendation is advice to the admin and gives the // collection nothing by itself. func Review(cur realm, collection string, revision int64, status, reasonHash, reasonCID string) { who := manager(cur, collection) a := applicationOf(collection) if a == nil { panic("launchpad/curation: no application open for review") } if revision != a.Revision { panic("launchpad/curation: application changed") } if status != StatusChangesRequested && status != StatusRecommended && status != StatusDeclined { panic("launchpad/curation: invalid review status") } commitment(reasonHash, reasonCID) a.Status, a.Reviewer, a.ReasonHash, a.ReasonCID = status, who, reasonHash, reasonCID a.UpdatedAt = now() chain.Emit("ApplicationReviewed", "collection", collection, "manager", who.String(), "revision", strconv.FormatInt(a.Revision, 10), "status", status, "reason_hash", reasonHash, "reason_cid", reasonCID) } func Render(_ string) string { return "# Launchpad curation\n\n" + strconv.Itoa(ActiveManagerCount()) + " active managers, " + strconv.Itoa(applications.Size()) + " applications.\n" }
  4. #4editorial.gno
  5. #5package curation import ( "chain" "strconv" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/r/samcrew/launchpad/nft/v1" ) const ( maxFeature = 14 * 24 * 60 * 60 // seconds holdSpan = 24 * 60 * 60 maxHold = 30 * 24 * 60 * 60 ) // What a founder may appeal: the decision on its application, a hold, the // collection's verification, or a feature decision. var subjects = [...]string{"application", "hold", "verification", "feature"} // A feature slot exists only when one manager proposed it and another // approved it. The admin can clear a slot and can never create one. type feature struct { collection string proposer address approver address // empty until approved reasonHash string reasonCID string until int64 } // A hold hides a collection from curated views until a time. It changes // presentation only: nothing about the collection's tokens is frozen. type hold struct { actor address confirmer address // empty until a second manager confirms extended bool // the admin set the term and its reason reasonHash string reasonCID string until int64 } type verification struct { verified bool reasonHash string reasonCID string updatedAt int64 } type appeal struct { collection string subject string statementHash string statementCID string filedAt int64 openKey string // its key in openAppeals; empty once resolved upheld bool reasonHash string reasonCID string resolvedAt int64 } var ( features avl.Tree // collection -> *feature holds avl.Tree // collection -> *hold verifications avl.Tree // collection -> verification appeals avl.Tree // "<collection>/<subject>" -> *appeal, the latest lastAppeal seqid.ID openAppeals avl.Tree // seqid key -> appeals key, in filing order ) func featureOf(collection string) *feature { if f := features.Get(collection); f != nil { return f.(*feature) } return nil } func holdOf(collection string) *hold { if h := holds.Get(collection); h != nil { return h.(*hold) } return nil } func appealOf(key string) *appeal { if a := appeals.Get(key); a != nil { return a.(*appeal) } return nil } // standing reports whether who could act on the collection as a manager now. // An unapproved slot's empty approver holds no seat. func standing(collection string, who address) bool { return active(who) && !conflicted(nft.GetInfo(collection), who) } // pair enforces the two-manager rule when second completes what first began: // two different managers, the first still seated and unconflicted. The second // was checked as the caller. func pair(collection string, first, second address) { if first == second || !standing(collection, first) { panic("launchpad/curation: a second unconflicted manager is required") } } // running reports a slot that runs, approved by two managers that both keep // their standing: whether hidden or not, it is the collection's slot. func running(collection string, f *feature) bool { return now() < f.until && standing(collection, f.proposer) && standing(collection, f.approver) } // ProposeFeature opens a slot of at most 14 days. It features nothing until // a second manager approves. It replaces a slot that is not running: one // still unapproved, or one whose proposer or approver lost its standing. func ProposeFeature(cur realm, collection, reasonHash, reasonCID string, until int64) { who := manager(cur, collection) commitment(reasonHash, reasonCID) at := now() if until <= at || until > at+maxFeature { panic("launchpad/curation: invalid feature term") } if f := featureOf(collection); f != nil && running(collection, f) { panic("launchpad/curation: feature slot running") } features.Set(collection, &feature{collection: collection, proposer: who, reasonHash: reasonHash, reasonCID: reasonCID, until: until}) chain.Emit("FeatureProposed", "collection", collection, "manager", who.String(), "until", strconv.FormatInt(until, 10), "reason_hash", reasonHash, "reason_cid", reasonCID) } func ApproveFeature(cur realm, collection string) { who := manager(cur, collection) f := featureOf(collection) if f == nil || f.approver != "" || now() >= f.until { panic("launchpad/curation: no feature slot awaiting approval") } pair(collection, f.proposer, who) f.approver = who chain.Emit("FeatureApproved", "collection", collection, "proposer", f.proposer.String(), "manager", who.String(), "until", strconv.FormatInt(f.until, 10)) } // IsFeatured holds only while the slot runs, both of its managers keep their // standing and the collection is not hidden. func IsFeatured(collection string) bool { f := featureOf(collection) return f != nil && running(collection, f) && !IsHidden(collection) } func ClearFeature(cur realm, collection, reasonHash, reasonCID string) { requireAdmin(cur) commitment(reasonHash, reasonCID) if _, had := features.Remove(collection); !had { panic("launchpad/curation: no feature slot") } chain.Emit("FeatureCleared", "collection", collection, "reason_hash", reasonHash, "reason_cid", reasonCID) } // HideTemporarily hides a collection for 24 hours on one manager's word. The // record outlives the hold: no manager can start another until the admin // clears it. func HideTemporarily(cur realm, collection, reasonHash, reasonCID string) { who := manager(cur, collection) commitment(reasonHash, reasonCID) if holds.Has(collection) { panic("launchpad/curation: a hold is on record until the admin clears it") } until := now() + holdSpan holds.Set(collection, &hold{actor: who, reasonHash: reasonHash, reasonCID: reasonCID, until: until}) chain.Emit("HideStarted", "collection", collection, "manager", who.String(), "until", strconv.FormatInt(until, 10), "reason_hash", reasonHash, "reason_cid", reasonCID) } // ConfirmHide lets a second manager keep a running hold for 24 hours from // now, once. After that, or once the admin has set the term, only the admin // changes it. func ConfirmHide(cur realm, collection string) { who := manager(cur, collection) h := holdOf(collection) at := now() if h == nil || h.confirmer != "" || h.extended || at >= h.until { panic("launchpad/curation: no hold awaiting confirmation") } pair(collection, h.actor, who) h.confirmer, h.until = who, at+holdSpan chain.Emit("HideConfirmed", "collection", collection, "manager", who.String(), "until", strconv.FormatInt(h.until, 10)) } // ExtendHide sets the term of a recorded hold, running or over, to a time at // most 30 days ahead, and replaces its public reason. func ExtendHide(cur realm, collection string, until int64, reasonHash, reasonCID string) { requireAdmin(cur) commitment(reasonHash, reasonCID) h := holdOf(collection) at := now() if h == nil || until <= at || until > at+maxHold { panic("launchpad/curation: invalid hold extension") } h.extended, h.until, h.reasonHash, h.reasonCID = true, until, reasonHash, reasonCID chain.Emit("HideExtended", "collection", collection, "until", strconv.FormatInt(until, 10), "reason_hash", reasonHash, "reason_cid", reasonCID) } func ClearHide(cur realm, collection, reasonHash, reasonCID string) { requireAdmin(cur) commitment(reasonHash, reasonCID) if _, had := holds.Remove(collection); !had { panic("launchpad/curation: no hold on record") } chain.Emit("HideCleared", "collection", collection, "reason_hash", reasonHash, "reason_cid", reasonCID) } // IsHidden holds while the hold runs and the manager whose word set its term // keeps its standing: the starter, or the confirmer once it confirmed. A term // the admin set rests on the admin alone. func IsHidden(collection string) bool { h := holdOf(collection) switch { case h == nil || now() >= h.until: return false case h.extended: return true case h.confirmer != "": return standing(collection, h.confirmer) } return standing(collection, h.actor) } // SetVerified is the admin's statement that a collection is what it claims // to be. It says nothing about price or future value. func SetVerified(cur realm, collection string, value bool, reasonHash, reasonCID string) { requireAdmin(cur) nft.GetInfo(collection) commitment(reasonHash, reasonCID) if IsVerified(collection) == value { panic("launchpad/curation: verification unchanged") } verifications.Set(collection, verification{value, reasonHash, reasonCID, now()}) chain.Emit("VerificationChanged", "collection", collection, "verified", strconv.FormatBool(value), "reason_hash", reasonHash, "reason_cid", reasonCID) } func IsVerified(collection string) bool { v := verifications.Get(collection) return v != nil && v.(verification).verified } // Appeal files the founder's objection on one subject. A collection has at // most one open appeal per subject; a resolved one can be followed by another. func Appeal(cur realm, collection, subject, statementHash, statementCID string) { who := founder(cur, collection) known := false for _, s := range subjects { known = known || s == subject } if !known { panic("launchpad/curation: invalid appeal subject") } commitment(statementHash, statementCID) key := collection + "/" + subject if a := appealOf(key); a != nil && a.openKey != "" { panic("launchpad/curation: appeal already open") } openKey := lastAppeal.Next().String() appeals.Set(key, &appeal{collection: collection, subject: subject, statementHash: statementHash, statementCID: statementCID, filedAt: now(), openKey: openKey}) openAppeals.Set(openKey, key) chain.Emit("AppealFiled", "collection", collection, "subject", subject, "founder", who.String(), "statement_hash", statementHash, "statement_cid", statementCID) } // ResolveAppeal records the admin's answer and its public reason. It changes // nothing else: the admin acts on an upheld appeal with the ordinary calls. func ResolveAppeal(cur realm, collection, subject string, upheld bool, reasonHash, reasonCID string) { requireAdmin(cur) commitment(reasonHash, reasonCID) a := appealOf(collection + "/" + subject) if a == nil || a.openKey == "" { panic("launchpad/curation: no open appeal") } openAppeals.Remove(a.openKey) a.openKey, a.upheld, a.resolvedAt = "", upheld, now() a.reasonHash, a.reasonCID = reasonHash, reasonCID chain.Emit("AppealResolved", "collection", collection, "subject", subject, "upheld", strconv.FormatBool(upheld), "reason_hash", reasonHash, "reason_cid", reasonCID) }
  6. #6gnomod.toml
  7. #7module = "gno.land/r/samcrew/launchpad/curation/v1" gno = "0.9"
  8. #8views.gno
  9. #9package curation import ( "chain/runtime" "strconv" "strings" "gno.land/p/nt/avl/v0" "gno.land/p/samcrew/launchpad/meta/v1" "gno.land/r/samcrew/launchpad/nft/v1" ) // int64 values are decimal strings so a JSON client cannot round them. func jsonInt(n int64) string { return meta.JSONString(strconv.FormatInt(n, 10)) } func StateJSON() string { return "{" + `"admin":` + meta.JSONString(admin.String()) + `,"pendingAdmin":` + meta.JSONString(pendingAdmin.String()) + `,"activeManagers":` + strconv.Itoa(ActiveManagerCount()) + `,"maxSeats":` + strconv.Itoa(maxSeats) + "}" } // ManagersJSON lists the active seats, ordered by account. func ManagersJSON() string { var b strings.Builder b.WriteByte('[') at := now() seats.Iterate("", "", func(account string, v any) bool { s := v.(seat) if at < s.until { if b.Len() > 1 { b.WriteByte(',') } b.WriteString(`{"account":` + meta.JSONString(account) + `,"lead":` + strconv.FormatBool(s.lead) + `,"until":` + jsonInt(s.until) + "}") } return false }) b.WriteByte(']') return b.String() } // ApplicationJSON returns null for a collection nobody applied for. func ApplicationJSON(collection string) string { a := applicationOf(collection) if a == nil { return "null" } return "{" + `"collection":` + meta.JSONString(collection) + `,"founder":` + meta.JSONString(a.Founder.String()) + `,"statementHash":` + meta.JSONString(a.StatementHash) + `,"statementCID":` + meta.JSONString(a.StatementCID) + `,"revision":` + jsonInt(a.Revision) + `,"status":` + meta.JSONString(a.Status) + `,"reviewer":` + meta.JSONString(a.Reviewer.String()) + `,"reasonHash":` + meta.JSONString(a.ReasonHash) + `,"reasonCID":` + meta.JSONString(a.ReasonCID) + `,"updatedAt":` + jsonInt(a.UpdatedAt) + "}" } // pageJSON renders a zero-based page of a tree in key order; the page size is // 1..50. func pageJSON(tree *avl.Tree, page, size int64, item func(v any) string) string { if page < 0 || size < 1 || size > 50 { panic("launchpad/curation: invalid page") } var b strings.Builder b.WriteByte('[') // page < total comes first so that page*size cannot overflow. if total := int64(tree.Size()); page < total && page*size < total { tree.IterateByOffset(int(page*size), int(size), func(_ string, v any) bool { if b.Len() > 1 { b.WriteByte(',') } b.WriteString(item(v)) return false }) } b.WriteByte(']') return b.String() } // ApplicationsJSON pages through the applications in order of first filing. func ApplicationsJSON(page, size int64) string { return pageJSON(&filings, page, size, func(v any) string { return ApplicationJSON(v.(string)) }) } // AccessJSON tells a private inbox what an account is to a collection right // now: whether it holds the creator role ("founder"), an active manager, and // whether Conflicted bars it from acting as one. It is public data to be read // on every request, not a token. // // The answer also says where and when it was evaluated: the chain ID, the // block height and the block time. The answer is not signed: checking them // exposes an honest node on the wrong chain or behind it, and proves nothing // against a node that lies. Its eight fields are a contract with that reader. func AccessJSON(collection, who string) string { account := address(who) checkAccount(account) info := nft.GetInfo(collection) return "{" + `"chainId":` + meta.JSONString(runtime.ChainID()) + `,"height":` + jsonInt(runtime.ChainHeight()) + `,"time":` + jsonInt(now()) + `,"collection":` + meta.JSONString(collection) + `,"account":` + meta.JSONString(who) + `,"founder":` + strconv.FormatBool(info.Creator == account) + `,"manager":` + strconv.FormatBool(active(account)) + `,"conflicted":` + strconv.FormatBool(conflicted(info, account)) + "}" } func featureJSON(collection string) string { f := featureOf(collection) if f == nil { return "null" } return "{" + `"proposer":` + meta.JSONString(f.proposer.String()) + `,"approver":` + meta.JSONString(f.approver.String()) + `,"reasonHash":` + meta.JSONString(f.reasonHash) + `,"reasonCID":` + meta.JSONString(f.reasonCID) + `,"until":` + jsonInt(f.until) + "}" } func holdJSON(collection string) string { h := holdOf(collection) if h == nil { return "null" } return "{" + `"actor":` + meta.JSONString(h.actor.String()) + `,"confirmer":` + meta.JSONString(h.confirmer.String()) + `,"extended":` + strconv.FormatBool(h.extended) + `,"reasonHash":` + meta.JSONString(h.reasonHash) + `,"reasonCID":` + meta.JSONString(h.reasonCID) + `,"until":` + jsonInt(h.until) + "}" } func verificationJSON(collection string) string { v := verifications.Get(collection) if v == nil { return "null" } r := v.(verification) return "{" + `"verified":` + strconv.FormatBool(r.verified) + `,"reasonHash":` + meta.JSONString(r.reasonHash) + `,"reasonCID":` + meta.JSONString(r.reasonCID) + `,"updatedAt":` + jsonInt(r.updatedAt) + "}" } func appealJSON(key string) string { a := appealOf(key) if a == nil { return "null" } return "{" + `"collection":` + meta.JSONString(a.collection) + `,"subject":` + meta.JSONString(a.subject) + `,"statementHash":` + meta.JSONString(a.statementHash) + `,"statementCID":` + meta.JSONString(a.statementCID) + `,"filedAt":` + jsonInt(a.filedAt) + `,"open":` + strconv.FormatBool(a.openKey != "") + `,"upheld":` + strconv.FormatBool(a.upheld) + `,"reasonHash":` + meta.JSONString(a.reasonHash) + `,"reasonCID":` + meta.JSONString(a.reasonCID) + `,"resolvedAt":` + jsonInt(a.resolvedAt) + "}" } // CollectionJSON is a collection's editorial record: what a client may show // now, then the records behind it and the latest appeal on each subject. A // part with no record is null. func CollectionJSON(collection string) string { nft.GetInfo(collection) var b strings.Builder b.WriteString("{" + `"collection":` + meta.JSONString(collection) + `,"verified":` + strconv.FormatBool(IsVerified(collection)) + `,"featured":` + strconv.FormatBool(IsFeatured(collection)) + `,"hidden":` + strconv.FormatBool(IsHidden(collection)) + `,"verification":` + verificationJSON(collection) + `,"feature":` + featureJSON(collection) + `,"hold":` + holdJSON(collection) + `,"appeals":{`) for i, subject := range subjects { if i > 0 { b.WriteByte(',') } b.WriteString(meta.JSONString(subject) + ":" + appealJSON(collection+"/"+subject)) } b.WriteString("}}") return b.String() } // AppealsJSON pages through the open appeals in filing order. func AppealsJSON(page, size int64) string { return pageJSON(&openAppeals, page, size, func(v any) string { return appealJSON(v.(string)) }) } // FeaturesJSON pages through every collection that has a feature slot on // record, in ID text order, saying whether it is featured right now. func FeaturesJSON(page, size int64) string { return pageJSON(&features, page, size, func(v any) string { f := v.(*feature) return "{" + `"collection":` + meta.JSONString(f.collection) + `,"featured":` + strconv.FormatBool(IsFeatured(f.collection)) + `,"until":` + jsonInt(f.until) + "}" }) }
  10. #10/gno.MemPackageType
  11. #11 MPUserAll

Result log

msg:0,success:true,log:,events:[]