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

7.43 Kb · 220 lines
  1package market
  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	sm "gno.land/p/samcrew/launchpad/safemath/v1"
 11	"gno.land/r/samcrew/launchpad/config/v1"
 12	"gno.land/r/samcrew/launchpad/nft/v1"
 13)
 14
 15func num(n int64) string { return meta.JSONString(strconv.FormatInt(n, 10)) }
 16
 17// splitJSON is how a price would be divided at a protocol fee: what the
 18// seller keeps, the treasury's fee and each royalty.
 19func splitJSON(collection string, price, feeBPS int64) string {
 20	fee := sm.BPS(price, feeBPS)
 21	seller := sm.Sub(price, fee)
 22	var b strings.Builder
 23	for i, r := range nft.RoyaltyQuote(collection, price) {
 24		if i > 0 {
 25			b.WriteByte(',')
 26		}
 27		b.WriteString(`{"account":` + meta.JSONString(r.Account.String()) +
 28			`,"amount":` + num(r.Amount) + "}")
 29		seller = sm.Sub(seller, r.Amount)
 30	}
 31	return `{"seller":` + num(seller) + `,"fee":` + num(fee) +
 32		`,"royalties":[` + b.String() + "]}"
 33}
 34
 35func listingJSON(l *Listing) string {
 36	return "{" +
 37		`"id":` + meta.JSONString(l.ID) +
 38		`,"collection":` + meta.JSONString(l.Collection) +
 39		`,"number":` + num(l.Number) +
 40		`,"seller":` + meta.JSONString(l.Seller.String()) +
 41		`,"price":` + num(l.Price) +
 42		`,"currency":` + meta.JSONString(l.Currency) +
 43		`,"createdAt":` + num(l.CreatedAt) +
 44		`,"expiresAt":` + num(l.ExpiresAt) +
 45		`,"feeBPS":` + num(l.FeeBPS) +
 46		`,"buyable":` + strconv.FormatBool(live(l)) +
 47		`,"split":` + splitJSON(l.Collection, l.Price, l.FeeBPS) + "}"
 48}
 49
 50func offerJSON(o *Offer) string {
 51	return "{" +
 52		`"id":` + meta.JSONString(o.ID) +
 53		`,"kind":` + meta.JSONString(o.Kind) +
 54		`,"collection":` + meta.JSONString(o.Collection) +
 55		`,"number":` + num(o.Number) +
 56		`,"trait":` + meta.JSONString(o.Trait) +
 57		`,"buyer":` + meta.JSONString(o.Buyer.String()) +
 58		`,"price":` + num(o.Price) +
 59		`,"currency":` + meta.JSONString(o.Currency) +
 60		`,"createdAt":` + num(o.CreatedAt) +
 61		`,"expiresAt":` + num(o.ExpiresAt) +
 62		`,"feeBPS":` + num(o.FeeBPS) +
 63		`,"split":` + splitJSON(o.Collection, o.Price, o.FeeBPS) + "}"
 64}
 65
 66// ListingJSON is an open listing, or "null" once it is closed.
 67func ListingJSON(id string) string {
 68	if l := findListing(id); l != nil {
 69		return listingJSON(l)
 70	}
 71	return "null"
 72}
 73
 74// OfferJSON is an open offer, or "null" once it is closed. An open offer can
 75// be past its expiry: it then only waits for its refund.
 76func OfferJSON(id string) string {
 77	if o, _ := findOffer(id); o != nil {
 78		return offerJSON(o)
 79	}
 80	return "null"
 81}
 82
 83// Lists are read in slices of 1 to maxSlice entries that start after a
 84// cursor: what identifies the last entry already read, or nothing to start
 85// from the beginning. Orders come and go between two reads, and a cursor
 86// never skips or repeats one that stayed. A slice costs the same wherever it
 87// starts.
 88const maxSlice = 50
 89
 90// sliceJSON renders up to size entries of tree whose keys lie in [from, to),
 91// in key order, or from the last key down when reverse.
 92func sliceJSON(tree *avl.Tree, from, to string, reverse bool, size int64, render func(v any) string) string {
 93	if size < 1 || size > maxSlice {
 94		panic("launchpad/market: invalid slice size")
 95	}
 96	var b strings.Builder
 97	b.WriteByte('[')
 98	visit := func(_ string, v any) bool {
 99		if b.Len() > 1 {
100			b.WriteByte(',')
101		}
102		b.WriteString(render(v))
103		size--
104		return size == 0
105	}
106	if reverse {
107		tree.ReverseIterate(from, to, visit)
108	} else {
109		tree.Iterate(from, to, visit)
110	}
111	b.WriteByte(']')
112	return b.String()
113}
114
115// cursorKey reads an order cursor: the ID of the last order already read.
116func cursorKey(prefix byte, cursor string) seqid.ID {
117	key, ok := orderKey(prefix, cursor)
118	if !ok {
119		panic("launchpad/market: invalid cursor")
120	}
121	n, _ := seqid.FromString(key)
122	return n
123}
124
125// after is the first key an oldest-first slice may hold: the one following
126// the cursor, or "" with no cursor.
127func after(prefix byte, cursor string) string {
128	if cursor == "" {
129		return ""
130	}
131	return (cursorKey(prefix, cursor) + 1).String()
132}
133
134// below is the last key a newest-first slice may hold: the one preceding the
135// cursor, or "" with no cursor. No order has key zero, so the first order's
136// ID leaves nothing below it.
137func below(prefix byte, cursor string) string {
138	if cursor == "" {
139		return ""
140	}
141	return (cursorKey(prefix, cursor) - 1).String()
142}
143
144func renderListing(v any) string { return listingJSON(v.(*Listing)) }
145func renderOffer(v any) string   { return offerJSON(v.(*Offer)) }
146
147// ListingsJSON reads every open listing, newest first, starting below the
148// listing ID before ("" starts from the newest).
149func ListingsJSON(before string, size int64) string {
150	// A descending range includes its upper bound.
151	return sliceJSON(&listings, "", below('L', before), true, size, renderListing)
152}
153
154// CollectionListingsJSON reads a collection's open listings in token number
155// order, starting after the token number afterNumber (0 starts from the
156// first).
157func CollectionListingsJSON(collection string, afterNumber, size int64) string {
158	if afterNumber < 0 {
159		panic("launchpad/market: invalid cursor")
160	}
161	// "0" is the byte after "/": the range ends with this collection's keys.
162	return sliceJSON(&listed, tokenKey(collection, sm.Add(afterNumber, 1)), collection+"0", false, size, renderListing)
163}
164
165// account checks an address argument of a view.
166func account(who string) string {
167	if !meta.ValidAddress(address(who)) {
168		panic("launchpad/market: invalid account")
169	}
170	return who
171}
172
173// SellerListingsJSON reads an account's open listings, oldest first, starting
174// after the listing ID afterID ("" starts from the oldest).
175func SellerListingsJSON(seller, afterID string, size int64) string {
176	return sliceJSON(&bySeller, account(seller)+"/"+after('L', afterID), seller+"0", false, size, renderListing)
177}
178
179// OffersJSON reads every open offer, newest first, starting below the offer
180// ID before ("" starts from the newest).
181func OffersJSON(before string, size int64) string {
182	return sliceJSON(&offers, "", below('O', before), true, size, renderOffer)
183}
184
185// CollectionOffersJSON reads a collection's open offers of every kind, oldest
186// first, starting after the offer ID afterID ("" starts from the oldest).
187func CollectionOffersJSON(collection, afterID string, size int64) string {
188	return sliceJSON(&open, collection+"/"+after('O', afterID), collection+"0", false, size, renderOffer)
189}
190
191// BuyerOffersJSON reads an account's open offers, oldest first, starting
192// after the offer ID afterID ("" starts from the oldest).
193func BuyerOffersJSON(buyer, afterID string, size int64) string {
194	return sliceJSON(&byBuyer, account(buyer)+"/"+after('O', afterID), buyer+"0", false, size, renderOffer)
195}
196
197// TokenListingJSON is the open listing of a token, or "null".
198func TokenListingJSON(collection string, number int64) string {
199	if l := listingOf(collection, number); l != nil {
200		return listingJSON(l)
201	}
202	return "null"
203}
204
205// TermsJSON is what a sale costs now: the protocol fee a new listing would
206// pin, and how a price would split for the collection. A fee of
207// -1 means the lane is closed; config's ActionStatusJSON tells whether the
208// market lane accepts new orders right now.
209func TermsJSON(collection string, price int64) string {
210	p := config.GetVersion(config.GetCurrentVersion())
211	split := "null"
212	if p.NFTSecondaryFeeBPS >= 0 && p.NFTSecondaryFeeBPS <= maxSecondaryFeeBPS {
213		split = splitJSON(collection, price, p.NFTSecondaryFeeBPS)
214	}
215	return "{" +
216		`"feeBPS":` + num(p.NFTSecondaryFeeBPS) +
217		`,"maxFeeBPS":` + num(maxSecondaryFeeBPS) +
218		`,"treasury":` + meta.JSONString(p.Treasury.String()) +
219		`,"split":` + split + "}"
220}