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}