Transaction
60424EF78457AA…907539F4D517
Block 652,444 · index 0 · indexed
Summary
- Hash
- 60424EF78457AA953CA47F77557E1CC49A4A0B078B399AAC1831907539F4D517
- Block
- 652,444
- Size
- 29155 bytes
- Gas used
- 42,458,784 / 120,000,000
- Fee
- 120000 ugnot
- Status
- success
Messages
- Attached funds
- 13000000 ugnot
Arguments · 13
- #1market
- #2gnomod.toml
- #3module = "gno.land/r/samcrew/launchpad/market/v1" gno = "0.9"
- #4market.gno
- #5// Package market trades Launchpad NFTs: fixed-price listings and funded // offers. A sale pays the seller, the treasury and the collection's royalty // receivers inside the same transaction. The realm holds only the escrow of // open offers, which their buyers can always take back, and has no admin. It // keeps open orders only: a closed one is deleted, so its storage deposit is // released, and its history is the event log. package market import ( "chain" "strconv" "time" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/samcrew/launchpad/currency/v1" "gno.land/r/samcrew/launchpad/nft/v1" ) const ( marketPath = "gno.land/r/samcrew/launchpad/market/v1" // Why an order closed, as its closing event says. StatusFilled = "filled" StatusCancelled = "cancelled" StatusReplaced = "replaced" StatusExpired = "expired" // Hard limits, fixed for the life of this realm. maxSecondaryFeeBPS = 200 // 2% of a sale price maxLifetime = 90 * 24 * 60 * 60 // How long after its expiry an order is still its owner's alone to close. // The chain returns the storage deposit of the bytes a call frees to that // call's sender (while ugnot is not a restricted denomination): the grace // keeps a stranger from taking a closed order's deposit at the very second // it expires, and leaves abandoned orders to whoever cleans them up. graceToClose = 7 * 24 * 60 * 60 maxOrders = 9_999_999_999 ) // Listing offers one token at a fixed price. The seller keeps the token and // approves this realm; nothing is held here. A listing exists while it is // open and no longer, and can be bought only within the token's custody epoch // it was made in. type Listing struct { ID string // "L<n>" Collection string Number int64 Seller address Price int64 Currency string CreatedAt int64 ExpiresAt int64 FeeBPS int64 // protocol fee, pinned when the seller signed key string // its key in listings epoch int64 // the token's custody epoch when it was listed } var ( lastListing seqid.ID listings avl.Tree // listing key -> *Listing, in creation order listed avl.Tree // "<collection>/<token number key>" -> *Listing bySeller avl.Tree // "<seller>/<listing key>" -> *Listing ) func directCaller(cur realm) address { if !cur.IsCurrent() || !cur.Previous().IsUserCall() { panic("launchpad/market: direct user call required") } return cur.Previous().Address() } // orderKey reads an order ID, its prefix ('L' listing, 'O' offer) then its // number, as its tree key. func orderKey(prefix byte, id string) (string, bool) { if len(id) < 2 || len(id) > 11 || id[0] != prefix || id[1] == '0' { return "", false } n, err := strconv.ParseUint(id[1:], 10, 64) if err != nil { return "", false } return seqid.ID(n).String(), true } func nextOrder(last *seqid.ID, prefix string) (string, string) { if uint64(*last) >= maxOrders { panic("launchpad/market: order ID exhausted") } n := last.Next() return prefix + strconv.FormatUint(uint64(n), 10), n.String() } // findListing returns an open listing, or nil: a closed one is gone. func findListing(id string) *Listing { if key, ok := orderKey('L', id); ok { if l := listings.Get(key); l != nil { return l.(*Listing) } } return nil } func mustListing(id string) *Listing { if l := findListing(id); l != nil { return l } panic("launchpad/market: unknown listing") } // tokenKey sorts a collection's tokens in number order. func tokenKey(collection string, number int64) string { return collection + "/" + seqid.ID(number).String() } // listingOf returns the open listing of a token, or nil. func listingOf(collection string, number int64) *Listing { if l := listed.Get(tokenKey(collection, number)); l != nil { return l.(*Listing) } return nil } // sellable reports whether seller owns the active token and has approved this // realm for that token. Only a single-token approval counts, as an approval // for all would outlive the token's moves. func sellable(collection string, number int64, seller address) bool { return nft.StatusOf(collection, number) == nft.StatusActive && nft.OwnerOf(collection, number) == seller && nft.GetApproved(collection, number) == chain.PackageAddress(marketPath) } // pinned checks what every new order needs (a collection that can be sold, a // price, a lifetime of at most 90 days) and returns the protocol fee to pin // on it: the current one, which must not exceed what its owner accepts nor // this realm's cap. func pinned(collection string, price, expiresAt int64, currencyKey string, maxFeeBPS int64) int64 { fee := policy(currencyKey).feeBPS if fee > maxFeeBPS || fee > maxSecondaryFeeBPS { panic("launchpad/market: protocol fee above the accepted maximum") } now := time.Now().Unix() if nft.GetInfo(collection).Mode == nft.ModeSoulbound || price <= 0 || expiresAt <= now || expiresAt > now+maxLifetime { panic("launchpad/market: invalid order") } return fee } // List offers the caller's token for sale and returns the listing ID. The // caller must first approve this realm in the NFT ledger, and names the // highest protocol fee it accepts; the fee in force is pinned on the listing. // A new listing replaces the token's previous one. func List(cur realm, collection string, number, price, expiresAt int64, currencyKey string, maxFeeBPS int64) string { seller := directCaller(cur) currency.AssertNoCoins(0, cur) fee := pinned(collection, price, expiresAt, currencyKey, maxFeeBPS) if !sellable(collection, number, seller) { panic("launchpad/market: not the owner, or the market is not approved") } if old := listingOf(collection, number); old != nil { closeListing(old, StatusReplaced) } id, treeKey := nextOrder(&lastListing, "L") l := &Listing{ID: id, Collection: collection, Number: number, Seller: seller, Price: price, Currency: currencyKey, CreatedAt: time.Now().Unix(), ExpiresAt: expiresAt, FeeBPS: fee, key: treeKey, epoch: nft.Epoch(collection, number)} listings.Set(treeKey, l) listed.Set(tokenKey(collection, number), l) bySeller.Set(seller.String()+"/"+treeKey, l) chain.Emit("Listed", "listing", id, "collection", collection, "number", strconv.FormatInt(number, 10), "seller", seller.String(), "currency", currencyKey, "price", strconv.FormatInt(price, 10), "fee_bps", strconv.FormatInt(fee, 10), "expires_at", strconv.FormatInt(expiresAt, 10)) return id } // closeListing deletes a listing and says why in its closing event. func closeListing(l *Listing, status string) { listings.Remove(l.key) listed.Remove(tokenKey(l.Collection, l.Number)) bySeller.Remove(l.Seller.String() + "/" + l.key) chain.Emit("ListingClosed", "listing", l.ID, "status", status) } // live reports whether a listing can be bought right now. A move or an // approval change starts a new custody epoch, so a listing never comes back to // life when a token that left the seller returns or the market is approved // again: the seller lists anew. func live(l *Listing) bool { return time.Now().Unix() < l.ExpiresAt && sellable(l.Collection, l.Number, l.Seller) && nft.Epoch(l.Collection, l.Number) == l.epoch } // Cancel withdraws a listing. The seller can always do it, paused market or // not. Anyone can clear a listing whose seller no longer owns the token, or // one that expired more than graceToClose ago; the chain returns the storage // deposit of what a call frees to its caller, so clearing pays whoever does // it. func Cancel(cur realm, id string) { who := directCaller(cur) currency.AssertNoCoins(0, cur) l := mustListing(id) stale := time.Now().Unix() >= l.ExpiresAt+graceToClose || nft.StatusOf(l.Collection, l.Number) != nft.StatusActive || nft.OwnerOf(l.Collection, l.Number) != l.Seller switch { case who == l.Seller: closeListing(l, StatusCancelled) case stale: closeListing(l, StatusExpired) default: panic("launchpad/market: seller only") } } // Buy pays a listing's exact price and moves the token to the caller. // currencyKey and price are the terms the buyer read: a listing on other // terms, such as an ID the buyer never saw, takes nothing. func Buy(cur realm, id, currencyKey string, price int64) { buyer := directCaller(cur) l := mustListing(id) if l.Currency != currencyKey || l.Price != price { panic("launchpad/market: order terms differ from those signed") } p := policy(l.Currency) if buyer == l.Seller || !live(l) { panic("launchpad/market: listing unavailable") } before := take(cur, l.Currency, l.Price) closeListing(l, StatusFilled) nft.TransferFrom(cross(cur), l.Collection, l.Seller, buyer, l.Number) settle(cur, l.Currency, l.Collection, l.Price, l.FeeBPS, l.Seller, p.treasury) settled(cur, l.Currency, before) chain.Emit("Sold", "listing", id, "collection", l.Collection, "number", strconv.FormatInt(l.Number, 10), "buyer", buyer.String(), "seller", l.Seller.String(), "currency", l.Currency, "price", strconv.FormatInt(l.Price, 10)) } // GetListing returns an open listing; a closed or unknown ID aborts. func GetListing(id string) Listing { return *mustListing(id) } func Render(path string) string { return "# Launchpad NFT market\n\n" + strconv.Itoa(listings.Size()) + " open listings, " + strconv.Itoa(offers.Size()) + " open offers.\n" }
- #6offers.gno
- #7package market import ( "chain" "strconv" "strings" "time" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/samcrew/launchpad/merkle/v1" sm "gno.land/p/samcrew/launchpad/safemath/v1" "gno.land/r/samcrew/launchpad/nft/v1" ) const ( OfferToken = "token" // one named token OfferCollection = "collection" // any token of the collection OfferTrait = "trait" // any token proven to carry a trait StatusAccepted = "accepted" // The token's own listing, closed because the token was sold to an offer. StatusSuperseded = "superseded" maxTraitBytes = 100 ) // Offer is a bid whose full price sits in this realm until it is accepted, // cancelled by its buyer or returned after expiry. An offer exists while it // is open and no longer. type Offer struct { ID string // "O<n>" Kind string Collection string Number int64 // token offers only Trait string // trait offers only: "<type>=<value>" Buyer address Price int64 Currency string CreatedAt int64 ExpiresAt int64 FeeBPS int64 // protocol fee, pinned when the buyer funded the offer } var ( lastOffer seqid.ID offers avl.Tree // offer key -> *Offer, in creation order open avl.Tree // "<collection>/<offer key>" -> *Offer byBuyer avl.Tree // "<buyer>/<offer key>" -> *Offer ) // findOffer returns an open offer and its key, or nil: a closed one is gone. func findOffer(id string) (*Offer, string) { if key, ok := orderKey('O', id); ok { if o := offers.Get(key); o != nil { return o.(*Offer), key } } return nil, "" } func mustOffer(id string) (*Offer, string) { o, key := findOffer(id) if o == nil { panic("launchpad/market: unknown offer") } return o, key } // TraitLeaf is the Merkle leaf that proves a token carries a trait, as // committed by the collection's creator in the NFT ledger. func TraitLeaf(collection string, number int64, trait string) merkle.Hash { return merkle.Leaf([]byte("launchpad-trait-v1|" + collection + "|" + strconv.FormatInt(number, 10) + "|" + trait)) } // MakeOffer escrows price and returns the offer ID. kind selects what the // offer is for: number is set for a token offer, trait for a trait offer, and // both are zero and empty otherwise. A trait offer needs a collection whose // creator committed its traits. The protocol fee in force is pinned on the // offer; it comes out of what the seller receives, never on top of the price, // and maxFeeBPS is the highest the buyer lets a seller be charged. func MakeOffer(cur realm, kind, collection string, number int64, trait string, price, expiresAt int64, currencyKey string, maxFeeBPS int64) string { buyer := directCaller(cur) fee := pinned(collection, price, expiresAt, currencyKey, maxFeeBPS) ok := false switch kind { case OfferToken: ok = trait == "" && nft.StatusOf(collection, number) == nft.StatusActive && nft.OwnerOf(collection, number) != buyer case OfferCollection: ok = trait == "" && number == 0 case OfferTrait: ok = number == 0 && len(trait) <= maxTraitBytes && strings.Count(trait, "=") == 1 && !strings.HasPrefix(trait, "=") && !strings.HasSuffix(trait, "=") && !strings.Contains(trait, "|") && nft.TraitsRoot(collection) != "" } if !ok { panic("launchpad/market: invalid offer") } before := take(cur, currencyKey, price) escrow[currencyKey] = sm.Add(escrow[currencyKey], price) id, key := nextOrder(&lastOffer, "O") o := &Offer{ID: id, Kind: kind, Collection: collection, Number: number, Trait: trait, Buyer: buyer, Price: price, Currency: currencyKey, CreatedAt: time.Now().Unix(), ExpiresAt: expiresAt, FeeBPS: fee} offers.Set(key, o) open.Set(collection+"/"+key, o) byBuyer.Set(buyer.String()+"/"+key, o) settled(cur, currencyKey, before) chain.Emit("OfferMade", "offer", id, "kind", kind, "collection", collection, "number", strconv.FormatInt(number, 10), "trait", trait, "buyer", buyer.String(), "currency", currencyKey, "price", strconv.FormatInt(price, 10), "fee_bps", strconv.FormatInt(fee, 10), "expires_at", strconv.FormatInt(expiresAt, 10)) return id } // release deletes an offer, takes its price out of escrow and says why in its // closing event; the caller then pays the price out. Every exit goes through // here, and an offer leaves escrow once, by its own key: with `settled`, an // exit pays out exactly the price of the one offer it closed, so a fault in // one offer's exit can never spend another offer's escrow. func release(o *Offer, key, status string) { if v := offers.Get(key); v == nil || v.(*Offer) != o { panic("launchpad/market: offer already closed") } offers.Remove(key) open.Remove(o.Collection + "/" + key) byBuyer.Remove(o.Buyer.String() + "/" + key) escrow[o.Currency] = sm.Sub(escrow[o.Currency], o.Price) chain.Emit("OfferClosed", "offer", o.ID, "status", status) } // refund returns an open offer's escrow to its buyer. Refunds never consult // the config realm: no pause or policy change can hold a buyer's funds. func refund(cur realm, o *Offer, key, status string) { before := untouched(cur, o.Currency) release(o, key, status) pay(cur, o.Currency, o.Buyer, o.Price) settled(cur, o.Currency, before) } // CancelOffer takes back the caller's own open offer, expired or not. func CancelOffer(cur realm, id string) { who := directCaller(cur) o, key := mustOffer(id) if who != o.Buyer { panic("launchpad/market: buyer only") } refund(cur, o, key, StatusCancelled) } // ExpireOffer returns to its buyer the escrow of an offer that expired more // than graceToClose ago. Anyone may call it: the price goes to the buyer, and // the chain pays the caller the storage deposit of the bytes the offer // occupied, so for a week after expiry only the buyer, with CancelOffer, // takes its own offer back. func ExpireOffer(cur realm, id string) { directCaller(cur) o, key := mustOffer(id) if time.Now().Unix() < o.ExpiresAt+graceToClose { panic("launchpad/market: only its buyer closes this offer yet") } refund(cur, o, key, StatusExpired) } // AcceptOffer sells the caller's token number to an open offer. proof is the // comma-separated Merkle path of the token's trait for a trait offer, and // empty otherwise. currencyKey and price are the terms the seller read: an // offer on other terms, such as an ID the seller never saw, buys nothing. The // caller must first approve this realm in the NFT ledger. func AcceptOffer(cur realm, id string, number int64, proof, currencyKey string, price int64) { seller := directCaller(cur) o, key := mustOffer(id) if o.Currency != currencyKey || o.Price != price { panic("launchpad/market: order terms differ from those signed") } before := untouched(cur, o.Currency) p := policy(o.Currency) ok := time.Now().Unix() < o.ExpiresAt && seller != o.Buyer && sellable(o.Collection, number, seller) switch { case !ok: case o.Kind == OfferToken: ok = number == o.Number && proof == "" case o.Kind == OfferCollection: ok = proof == "" default: root, _ := merkle.ParseHash(nft.TraitsRoot(o.Collection)) path, err := merkle.ParseProof(proof) ok = err == nil && merkle.Verify(root, TraitLeaf(o.Collection, number, o.Trait), path) } if !ok { panic("launchpad/market: offer unavailable for this token") } release(o, key, StatusAccepted) if l := listingOf(o.Collection, number); l != nil { closeListing(l, StatusSuperseded) } nft.TransferFrom(cross(cur), o.Collection, seller, o.Buyer, number) settle(cur, o.Currency, o.Collection, o.Price, o.FeeBPS, seller, p.treasury) settled(cur, o.Currency, before) chain.Emit("OfferAccepted", "offer", id, "collection", o.Collection, "number", strconv.FormatInt(number, 10), "buyer", o.Buyer.String(), "seller", seller.String(), "currency", o.Currency, "price", strconv.FormatInt(o.Price, 10)) } // GetOffer returns an open offer; a closed or unknown ID aborts. func GetOffer(id string) Offer { o, _ := mustOffer(id) return *o } // EscrowOf is the total of open offers in a currency. func EscrowOf(currencyKey string) int64 { return escrow[currencyKey] }
- #8payment.gno
- #9package market import ( "chain" "chain/banker" "strconv" "gno.land/p/samcrew/launchpad/currency/v1" sm "gno.land/p/samcrew/launchpad/safemath/v1" "gno.land/r/nt/grc20reg/v0" "gno.land/r/samcrew/launchpad/config/v1" "gno.land/r/samcrew/launchpad/nft/v1" ) // escrow is, per currency, the total of open offers: everything this realm // owes. Currencies are few (config allows sixteen), so a map is enough. var escrow = map[string]int64{} // terms is what this realm needs from config. type terms struct { treasury address feeBPS int64 // protocol share of each sale price } // policy returns the current terms after checking that the market lane is // open for the currency. It is a variable only so that unit tests can stand in // for the config realm, whose admin cannot be set from another package's tests. var policy = func(currencyKey string) terms { config.AssertLaneOpen(config.LaneNFTMarket, currencyKey) p := config.GetVersion(config.GetCurrentVersion()) return terms{treasury: p.Treasury, feeBPS: p.NFTSecondaryFeeBPS} } func balance(cur realm, key string) int64 { if key == config.NativeCurrency { return banker.NewReadonlyBanker().GetCoin(cur.Address(), key) } return grc20reg.MustGet(key).BalanceOf(cur.Address()) } // surplus is what the realm holds beyond its open offers. Every entry point // must leave it exactly as it found it. func surplus(cur realm, key string) int64 { return sm.Sub(balance(cur, key), escrow[key]) } // take collects exactly amount from the direct caller, attached for native // coins and pulled for a token, and returns the surplus before the payment. func take(cur realm, key string, amount int64) int64 { if key == config.NativeCurrency { currency.CollectNative(0, cur, amount) return sm.Sub(surplus(cur, key), amount) } before := surplus(cur, key) currency.CollectToken(0, cur, grc20reg.MustGet(key), amount) return before } // untouched is take for an entry point that receives nothing. func untouched(cur realm, key string) int64 { currency.AssertNoCoins(0, cur) return surplus(cur, key) } func pay(cur realm, key string, to address, amount int64) { if amount == 0 { return } if key == config.NativeCurrency { currency.PayNative(0, cur, to, amount) } else { currency.PayToken(0, cur, grc20reg.MustGet(key), to, amount) } } // settle splits a sale price: the protocol fee to the treasury, each royalty // to its receiver, the rest, rounding included, to the seller. func settle(cur realm, key, collection string, price, feeBPS int64, seller, treasury address) { fee := sm.BPS(price, feeBPS) rest := sm.Sub(price, fee) pay(cur, key, treasury, fee) var royalties int64 for _, r := range nft.RoyaltyQuote(collection, price) { pay(cur, key, r.Account, r.Amount) rest = sm.Sub(rest, r.Amount) royalties += r.Amount } pay(cur, key, seller, rest) chain.Emit("Settled", "collection", collection, "currency", key, "price", strconv.FormatInt(price, 10), "seller", strconv.FormatInt(rest, 10), "fee", strconv.FormatInt(fee, 10), "royalties", strconv.FormatInt(royalties, 10)) } // settled aborts the transaction unless the realm holds exactly its open // offers plus the surplus it had before the call: nothing taken is left // behind, and nothing owed has been spent. func settled(cur realm, key string, before int64) { if surplus(cur, key) != before { panic("launchpad/market: payment not fully settled") } }
- #10views.gno
- #11package market import ( "strconv" "strings" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/samcrew/launchpad/meta/v1" sm "gno.land/p/samcrew/launchpad/safemath/v1" "gno.land/r/samcrew/launchpad/config/v1" "gno.land/r/samcrew/launchpad/nft/v1" ) func num(n int64) string { return meta.JSONString(strconv.FormatInt(n, 10)) } // splitJSON is how a price would be divided at a protocol fee: what the // seller keeps, the treasury's fee and each royalty. func splitJSON(collection string, price, feeBPS int64) string { fee := sm.BPS(price, feeBPS) seller := sm.Sub(price, fee) var b strings.Builder for i, r := range nft.RoyaltyQuote(collection, price) { if i > 0 { b.WriteByte(',') } b.WriteString(`{"account":` + meta.JSONString(r.Account.String()) + `,"amount":` + num(r.Amount) + "}") seller = sm.Sub(seller, r.Amount) } return `{"seller":` + num(seller) + `,"fee":` + num(fee) + `,"royalties":[` + b.String() + "]}" } func listingJSON(l *Listing) string { return "{" + `"id":` + meta.JSONString(l.ID) + `,"collection":` + meta.JSONString(l.Collection) + `,"number":` + num(l.Number) + `,"seller":` + meta.JSONString(l.Seller.String()) + `,"price":` + num(l.Price) + `,"currency":` + meta.JSONString(l.Currency) + `,"createdAt":` + num(l.CreatedAt) + `,"expiresAt":` + num(l.ExpiresAt) + `,"feeBPS":` + num(l.FeeBPS) + `,"buyable":` + strconv.FormatBool(live(l)) + `,"split":` + splitJSON(l.Collection, l.Price, l.FeeBPS) + "}" } func offerJSON(o *Offer) string { return "{" + `"id":` + meta.JSONString(o.ID) + `,"kind":` + meta.JSONString(o.Kind) + `,"collection":` + meta.JSONString(o.Collection) + `,"number":` + num(o.Number) + `,"trait":` + meta.JSONString(o.Trait) + `,"buyer":` + meta.JSONString(o.Buyer.String()) + `,"price":` + num(o.Price) + `,"currency":` + meta.JSONString(o.Currency) + `,"createdAt":` + num(o.CreatedAt) + `,"expiresAt":` + num(o.ExpiresAt) + `,"feeBPS":` + num(o.FeeBPS) + `,"split":` + splitJSON(o.Collection, o.Price, o.FeeBPS) + "}" } // ListingJSON is an open listing, or "null" once it is closed. func ListingJSON(id string) string { if l := findListing(id); l != nil { return listingJSON(l) } return "null" } // OfferJSON is an open offer, or "null" once it is closed. An open offer can // be past its expiry: it then only waits for its refund. func OfferJSON(id string) string { if o, _ := findOffer(id); o != nil { return offerJSON(o) } return "null" } // Lists are read in slices of 1 to maxSlice entries that start after a // cursor: what identifies the last entry already read, or nothing to start // from the beginning. Orders come and go between two reads, and a cursor // never skips or repeats one that stayed. A slice costs the same wherever it // starts. const maxSlice = 50 // sliceJSON renders up to size entries of tree whose keys lie in [from, to), // in key order, or from the last key down when reverse. func sliceJSON(tree *avl.Tree, from, to string, reverse bool, size int64, render func(v any) string) string { if size < 1 || size > maxSlice { panic("launchpad/market: invalid slice size") } var b strings.Builder b.WriteByte('[') visit := func(_ string, v any) bool { if b.Len() > 1 { b.WriteByte(',') } b.WriteString(render(v)) size-- return size == 0 } if reverse { tree.ReverseIterate(from, to, visit) } else { tree.Iterate(from, to, visit) } b.WriteByte(']') return b.String() } // cursorKey reads an order cursor: the ID of the last order already read. func cursorKey(prefix byte, cursor string) seqid.ID { key, ok := orderKey(prefix, cursor) if !ok { panic("launchpad/market: invalid cursor") } n, _ := seqid.FromString(key) return n } // after is the first key an oldest-first slice may hold: the one following // the cursor, or "" with no cursor. func after(prefix byte, cursor string) string { if cursor == "" { return "" } return (cursorKey(prefix, cursor) + 1).String() } // below is the last key a newest-first slice may hold: the one preceding the // cursor, or "" with no cursor. No order has key zero, so the first order's // ID leaves nothing below it. func below(prefix byte, cursor string) string { if cursor == "" { return "" } return (cursorKey(prefix, cursor) - 1).String() } func renderListing(v any) string { return listingJSON(v.(*Listing)) } func renderOffer(v any) string { return offerJSON(v.(*Offer)) } // ListingsJSON reads every open listing, newest first, starting below the // listing ID before ("" starts from the newest). func ListingsJSON(before string, size int64) string { // A descending range includes its upper bound. return sliceJSON(&listings, "", below('L', before), true, size, renderListing) } // CollectionListingsJSON reads a collection's open listings in token number // order, starting after the token number afterNumber (0 starts from the // first). func CollectionListingsJSON(collection string, afterNumber, size int64) string { if afterNumber < 0 { panic("launchpad/market: invalid cursor") } // "0" is the byte after "/": the range ends with this collection's keys. return sliceJSON(&listed, tokenKey(collection, sm.Add(afterNumber, 1)), collection+"0", false, size, renderListing) } // account checks an address argument of a view. func account(who string) string { if !meta.ValidAddress(address(who)) { panic("launchpad/market: invalid account") } return who } // SellerListingsJSON reads an account's open listings, oldest first, starting // after the listing ID afterID ("" starts from the oldest). func SellerListingsJSON(seller, afterID string, size int64) string { return sliceJSON(&bySeller, account(seller)+"/"+after('L', afterID), seller+"0", false, size, renderListing) } // OffersJSON reads every open offer, newest first, starting below the offer // ID before ("" starts from the newest). func OffersJSON(before string, size int64) string { return sliceJSON(&offers, "", below('O', before), true, size, renderOffer) } // CollectionOffersJSON reads a collection's open offers of every kind, oldest // first, starting after the offer ID afterID ("" starts from the oldest). func CollectionOffersJSON(collection, afterID string, size int64) string { return sliceJSON(&open, collection+"/"+after('O', afterID), collection+"0", false, size, renderOffer) } // BuyerOffersJSON reads an account's open offers, oldest first, starting // after the offer ID afterID ("" starts from the oldest). func BuyerOffersJSON(buyer, afterID string, size int64) string { return sliceJSON(&byBuyer, account(buyer)+"/"+after('O', afterID), buyer+"0", false, size, renderOffer) } // TokenListingJSON is the open listing of a token, or "null". func TokenListingJSON(collection string, number int64) string { if l := listingOf(collection, number); l != nil { return listingJSON(l) } return "null" } // TermsJSON is what a sale costs now: the protocol fee a new listing would // pin, and how a price would split for the collection. A fee of // -1 means the lane is closed; config's ActionStatusJSON tells whether the // market lane accepts new orders right now. func TermsJSON(collection string, price int64) string { p := config.GetVersion(config.GetCurrentVersion()) split := "null" if p.NFTSecondaryFeeBPS >= 0 && p.NFTSecondaryFeeBPS <= maxSecondaryFeeBPS { split = splitJSON(collection, price, p.NFTSecondaryFeeBPS) } return "{" + `"feeBPS":` + num(p.NFTSecondaryFeeBPS) + `,"maxFeeBPS":` + num(maxSecondaryFeeBPS) + `,"treasury":` + meta.JSONString(p.Treasury.String()) + `,"split":` + split + "}" }
- #12/gno.MemPackageType
- #13 MPUserAll
Result log
msg:0,success:true,log:,events:[]