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

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}