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

views.gno

8.18 Kb · 232 lines
  1package nft
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/p/nt/avl/v0"
  8	"gno.land/p/nt/seqid/v0"
  9	"gno.land/p/samcrew/launchpad/meta/v1"
 10)
 11
 12// Lists are paged: a zero-based page of 1 to maxPage entries.
 13const maxPage = 50
 14
 15// int64 values are decimal strings so a JSON client cannot round them.
 16func num(n int64) string { return meta.JSONString(strconv.FormatInt(n, 10)) }
 17
 18// listJSON renders one page of a tree in key order. The tree seeks the first
 19// entry by subtree sizes, so a deep page costs what the first one does.
 20func listJSON(tree *avl.Tree, page, size int64, render func(key string, value any) string) string {
 21	if page < 0 || size < 1 || size > maxPage {
 22		panic("launchpad/nft: invalid page")
 23	}
 24	var b strings.Builder
 25	b.WriteByte('[')
 26	// A page past the end cannot overflow: nothing is multiplied before this.
 27	if tree != nil && page <= int64(tree.Size())/size {
 28		tree.IterateByOffset(int(page*size), int(size), func(key string, value any) bool {
 29			if b.Len() > 1 {
 30				b.WriteByte(',')
 31			}
 32			b.WriteString(render(key, value))
 33			return false
 34		})
 35	}
 36	b.WriteByte(']')
 37	return b.String()
 38}
 39
 40func CollectionJSON(id string) string {
 41	c := mustCollection(id)
 42	i := c.info
 43	return "{" +
 44		`"id":` + meta.JSONString(i.ID) +
 45		`,"grc721Id":` + meta.JSONString(i.GRC721ID) +
 46		`,"issuer":` + meta.JSONString(i.Issuer.String()) +
 47		`,"creator":` + meta.JSONString(i.Creator.String()) +
 48		`,"originator":` + meta.JSONString(i.Originator.String()) +
 49		`,"pendingCreator":` + meta.JSONString(i.PendingCreator.String()) +
 50		`,"name":` + meta.JSONString(i.Name) +
 51		`,"symbol":` + meta.JSONString(i.Symbol) +
 52		`,"description":` + meta.JSONString(i.Description) +
 53		`,"image":` + meta.JSONString(i.Image) +
 54		`,"banner":` + meta.JSONString(i.Banner) +
 55		`,"website":` + meta.JSONString(i.Website) +
 56		`,"mode":` + meta.JSONString(i.Mode) +
 57		`,"revocable":` + strconv.FormatBool(i.Revocable) +
 58		`,"maxSupply":` + num(i.MaxSupply) +
 59		`,"sealed":` + strconv.FormatBool(i.Sealed) +
 60		`,"minted":` + num(i.Minted) +
 61		`,"totalSupply":` + num(c.token.TotalSupply()) +
 62		`,"profileFrozen":` + strconv.FormatBool(i.ProfileFrozen) +
 63		`,"metadataMode":` + meta.JSONString(c.metadata.mode) +
 64		`,"metadataFrozen":` + strconv.FormatBool(c.metadata.frozen) +
 65		`,"metadataRevision":` + num(c.metadata.revision) +
 66		`,"baseURI":` + meta.JSONString(c.metadata.baseURI) +
 67		`,"placeholderURI":` + meta.JSONString(c.metadata.placeholderURI) +
 68		`,"baseURICommitment":` + meta.JSONString(c.metadata.commitment) +
 69		`,"committer":` + meta.JSONString(c.metadata.committer.String()) +
 70		`,"provenanceHash":` + meta.JSONString(c.metadata.provenance) +
 71		`,"traitsRoot":` + meta.JSONString(c.traitsRoot) +
 72		`,"royaltyBPS":` + num(c.royaltyBPS) +
 73		`,"royalties":` + royaltiesJSON(c.royalties) +
 74		`,"markets":` + addressesJSON(c.markets) + "}"
 75}
 76
 77func royaltiesJSON(royalties []royalty) string {
 78	var b strings.Builder
 79	b.WriteByte('[')
 80	for i, r := range royalties {
 81		if i > 0 {
 82			b.WriteByte(',')
 83		}
 84		b.WriteString(`{"account":` + meta.JSONString(r.Account.String()) +
 85			`,"bps":` + num(r.BPS) + "}")
 86	}
 87	b.WriteByte(']')
 88	return b.String()
 89}
 90
 91func addressesJSON(list []address) string {
 92	var b strings.Builder
 93	b.WriteByte('[')
 94	for i, a := range list {
 95		if i > 0 {
 96			b.WriteByte(',')
 97		}
 98		b.WriteString(meta.JSONString(a.String()))
 99	}
100	b.WriteByte(']')
101	return b.String()
102}
103
104// CapabilitiesJSON tells a wallet or a market what a holder of this
105// collection can do, without it knowing the modes. "markets" is empty when
106// any operator may be approved. Royalties are paid by markets at sale.
107// "royaltyEnforcement" is "none" without royalties; "market_sales" for an open
108// collection, where the ledger enforces nothing: a market that honours
109// royalties pays them at a sale, and a direct transfer or any other realm
110// pays none; "listed_markets" when the ledger lets a token move only through
111// the realms in "markets". Whether a listed realm pays is the word of the
112// creator who listed it. "creatorRevoke" is the creator's right to revoke
113// a token of a revocable soulbound collection.
114func CapabilitiesJSON(id string) string {
115	c := mustCollection(id)
116	i := c.info
117	enforcement := "none"
118	if c.royaltyBPS > 0 {
119		enforcement = "market_sales"
120		if i.Mode == ModeRoyaltyProtected {
121			enforcement = "listed_markets"
122		}
123	}
124	return "{" +
125		`"schema":"launchpad-nft-capabilities/v1"` +
126		`,"collection":` + meta.JSONString(id) +
127		`,"standard":"grc721"` +
128		`,"mode":` + meta.JSONString(i.Mode) +
129		`,"holderTransfer":` + strconv.FormatBool(i.Mode == ModeOpen) +
130		`,"marketSale":` + strconv.FormatBool(i.Mode != ModeSoulbound) +
131		`,"markets":` + addressesJSON(c.markets) +
132		`,"holderBurn":true` +
133		`,"creatorRevoke":` + strconv.FormatBool(i.Revocable) +
134		`,"maxSupply":` + num(i.MaxSupply) +
135		`,"metadataMode":` + meta.JSONString(c.metadata.mode) +
136		`,"metadataFrozen":` + strconv.FormatBool(c.metadata.frozen) +
137		`,"traitsCommitted":` + strconv.FormatBool(c.traitsRoot != "") +
138		`,"royaltyBPS":` + num(c.royaltyBPS) +
139		`,"royaltyEnforcement":` + meta.JSONString(enforcement) + "}"
140}
141
142// ListCollectionsJSON pages through a summary of every collection, in
143// creation order. CollectionJSON has the full record.
144func ListCollectionsJSON(page, size int64) string {
145	return listJSON(&collections, page, size, func(_ string, value any) string {
146		i := value.(*collection).info
147		return "{" +
148			`"id":` + meta.JSONString(i.ID) +
149			`,"creator":` + meta.JSONString(i.Creator.String()) +
150			`,"name":` + meta.JSONString(i.Name) +
151			`,"symbol":` + meta.JSONString(i.Symbol) +
152			`,"image":` + meta.JSONString(i.Image) +
153			`,"mode":` + meta.JSONString(i.Mode) +
154			`,"maxSupply":` + num(i.MaxSupply) +
155			`,"sealed":` + strconv.FormatBool(i.Sealed) +
156			`,"minted":` + num(i.Minted) + "}"
157	})
158}
159
160func tokenJSON(c *collection, number int64) string {
161	status := statusOf(c, number)
162	var owner address
163	if status == StatusActive {
164		owner = ownerOf(c, number)
165	}
166	return "{" +
167		`"collection":` + meta.JSONString(c.info.ID) +
168		`,"number":` + num(number) +
169		`,"owner":` + meta.JSONString(owner.String()) +
170		`,"status":` + meta.JSONString(status) +
171		`,"uri":` + meta.JSONString(c.metadata.tokenURI(number)) + "}"
172}
173
174// TokenJSON describes one minted number. A retired token has no owner.
175func TokenJSON(id string, number int64) string { return tokenJSON(mustCollection(id), number) }
176
177// ApprovalJSON answers, in one read, whether operator may move an active
178// token: by a single-token approval or by the owner's collection-wide one.
179func ApprovalJSON(id string, number int64, operator string) string {
180	c := mustCollection(id)
181	owner := ownerOf(c, number)
182	who := address(operator)
183	if !meta.ValidAddress(who) {
184		panic("launchpad/nft: invalid operator")
185	}
186	return "{" +
187		`"collection":` + meta.JSONString(id) +
188		`,"number":` + num(number) +
189		`,"owner":` + meta.JSONString(owner.String()) +
190		`,"operator":` + meta.JSONString(operator) +
191		`,"tokenApproved":` + strconv.FormatBool(GetApproved(id, number) == who) +
192		`,"collectionApproved":` + strconv.FormatBool(c.token.IsApprovedForAll(owner, who)) + "}"
193}
194
195// TokensJSON pages through a collection's minted numbers in order, retired
196// ones included.
197func TokensJSON(id string, page, size int64) string {
198	if page < 0 || size < 1 || size > maxPage {
199		panic("launchpad/nft: invalid page")
200	}
201	c := mustCollection(id)
202	var b strings.Builder
203	b.WriteByte('[')
204	if page <= c.info.Minted/size {
205		for number := page*size + 1; number <= c.info.Minted && size > 0; number, size = number+1, size-1 {
206			if b.Len() > 1 {
207				b.WriteByte(',')
208			}
209			b.WriteString(tokenJSON(c, number))
210		}
211	}
212	b.WriteByte(']')
213	return b.String()
214}
215
216// HoldingsJSON pages through the tokens an account holds, across every
217// collection, in collection then number order.
218func HoldingsJSON(owner string, page, size int64) string {
219	if !meta.ValidAddress(address(owner)) {
220		panic("launchpad/nft: invalid owner")
221	}
222	mine, _ := holdings.Get(owner).(*avl.Tree)
223	return listJSON(mine, page, size, func(key string, _ any) string {
224		parts := strings.Split(key, "/")
225		c := collections.Get(parts[0]).(*collection)
226		number, _ := seqid.FromString(parts[1])
227		return "{" +
228			`"collection":` + meta.JSONString(c.info.ID) +
229			`,"number":` + num(int64(number)) +
230			`,"uri":` + meta.JSONString(c.metadata.tokenURI(int64(number))) + "}"
231	})
232}