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

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}