Transaction
D8CB2CBCF03C6F…64B2FE9C7EE3
Block 578,030 · index 0 · indexed
Summary
- Hash
- D8CB2CBCF03C6F4D249707561E0F6D3E3FFC00E5EB0E741CBEF964B2FE9C7EE3
- Block
- 578,030
- Size
- 20302 bytes
- Gas used
- 27,685,054 / 200,000,000
- Fee
- 200000 ugnot
- Status
- success
Messages
#1AddPackagegno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/market/bazaar/v1/gnofly17 arguments
- Attached funds
- 25000000 ugnot
Arguments · 17
- #1gnofly
- #2bazaar.gno
- #3package gnofly import ( "strconv" "chain" bazaar "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv5" "gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes" ) // Bazaar (bazaar.gnomi.fun) lists the collections registered on its factory, // bazaarv5. This realm registers itself there with Init: the factory takes // its path as the collection (slug: the last element, "gnofly") and sends the // site's List, Buy and Cancel here. Its fee on a sale is read from the factory // at each Buy. const ( // What Bazaar shows for the collection: the planes realm's. collectionName = "gnofly planes" collectionSymbol = "PLANE" collectionCover = "https://gnofly.xyz/nft/collections/planes/777.png" collectionSupply = 777 // The planes realm's mint price, 77 GNOT: shown by Bazaar, never charged // here (planes are minted on the planes realm). collectionMintPrice = int64(77_000_000) ) // registered is set once Init has registered this realm on Bazaar's factory: // only then does it report its minted count there. var registered bool // Init registers this realm on Bazaar's factory. The factory requires the // slug "gnofly" reserved first, with Reserve, by the wallet calling Init; it // must be an admin of the game realm here too: the gnofly wallet. Once only. func Init(cur realm) { userOf(0, cur) assertAdmin(0, cur) if registered { panic(errRegistered) } registered = true bazaar.Register(cross(cur), collectionName, collectionSymbol, collectionCover, collectionSupply, collectionMintPrice, planes.RoyaltyBps()) noteMinted(0, cur) chain.Emit("Registered", "slug", slug) } // noteMinted reports the planes minted so far to Bazaar's factory, which // shows the count. Nothing before Init: the factory does not know this realm. func noteMinted(_ int, rlm realm) { if registered { bazaar.NoteMinted(cross(rlm), Minted()) } } // bazaarFee is what Bazaar takes on a sale at price between seller and // buyer: its ProtocolBps less the better holder discount of the two, never // below zero. Of it, GenesisShareBps goes to its genesis sink, the rest to its // protocol sink. All read from the factory now: its admin sets them. func bazaarFee(price int64, seller, buyer address) (genesis, protocol int64) { discount := bazaar.HolderDiscount(seller.String()) if d := bazaar.HolderDiscount(buyer.String()); d > discount { discount = d } bps := bazaar.ProtocolBps() - discount if bps <= 0 { return 0, 0 } fee := price * bps / 10_000 genesis = fee * bazaar.GenesisShareBps() / 10_000 return genesis, fee - genesis } // Registered reports whether Init has registered this realm on Bazaar. func Registered() bool { return registered } // itoa64 is strconv.FormatInt in base 10. func itoa64(n int64) string { return strconv.FormatInt(n, 10) }
- #4doc.gno
- #5// Package gnofly is the wrapper that puts gnofly's planes on Bazaar // (bazaar.gnomi.fun), the NFT marketplace of gno.land. // // Bazaar lists the collections registered on its factory, bazaarv5, and sends // its site's List, Buy and Cancel to the collection's own package. This realm // is that package for the planes: Init registers it under the slug "gnofly" // (its last path element), and it holds the book. It mints nothing: the // planes are the tokens of // gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes, // moved with that realm's TransferFrom. A seller first approves this realm // there (Approve, or SetApprovalForAll once); List then moves the plane here, // in escrow, Buy hands it to the buyer and Cancel back to the seller. // // A sale pays, out of its price: Bazaar's fee (read from the factory: its // ProtocolBps less the holder discount, split between its sinks), the planes // realm's royalty (5% to the gnofly treasury), and the rest to the seller. // // It is version v1 of the market; v0 is the same book without Bazaar, which // cannot register. SetPaused stops new listings for a move to a newer version; // Cancel and Buy keep working, so no plane is ever stuck here. // // Every write is a direct call from a wallet: no realm can list, buy or // cancel in a user's name. package gnofly
- #6errors.gno
- #7package gnofly // The refusals of this realm, as strings: a string constant costs its length // in storage where an errors.New value costs about a kilobyte (see the planes // realm's errors.gno). A panic with a string aborts the call with it. const ( errSpoofedRealm = "gnofly market: spoofed realm" errNotAdmin = "gnofly market: caller is not an admin" errNotUserCall = "gnofly market: call it directly from a wallet (maketx call or Adena)" errUnknownPlane = "gnofly market: no such plane" errNotOwner = "gnofly market: caller does not own this plane" errNotSeller = "gnofly market: caller is not the seller of this plane" errListed = "gnofly market: this plane is listed" errNotListed = "gnofly market: this plane is not listed" errOwnListing = "gnofly market: a seller cannot buy their own listing" errPaused = "gnofly market: new listings are paused" errBadPrice = "gnofly market: price must be between 1000000 and 1000000000000 ugnot" errNotApproved = "gnofly market: approve the market on the planes realm first (Approve or SetApprovalForAll)" errBadRecipient = "gnofly market: invalid recipient address" errNotStray = "gnofly market: this plane is not held by the market outside a listing" errRegistered = "gnofly market: already registered on Bazaar" )
- #8gnomod.toml
- #9module = "gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/market/bazaar/v1/gnofly" gno = "0.9"
- #10market.gno
- #11package gnofly import ( "chain" "chain/banker" "strconv" // Only for OriginSend, in Buy. Caller identity always comes from cur, // never from this package. "chain/runtime/unsafe" "gno.land/p/nt/bptree/v0" "gno.land/p/nt/grc721/v0" bazaar "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv5" "gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes" ) const ( payDenom = "ugnot" // minPrice keeps the royalty above zero after rounding: 5% of 1 GNOT. minPrice = int64(1_000_000) // maxPrice keeps price * royalty bps far below an int64. maxPrice = int64(1_000_000_000_000) supply = 777 // plane ids run from 1 to 777 ) // listing is one plane for sale. The plane itself is held by this realm // (escrow) until it is bought or the listing is cancelled. type listing struct { seller address price int64 // ugnot } var ( // listings by plane id, as a three-digit key ("042") so that iteration // runs in id order. listings bptree.BPTree // paused stops new listings and price changes, for a move to a newer // version of the market. Cancel and Buy keep working: nothing listed here // can be stuck. paused bool // self is this realm's address, recorded at init: what sellers approve // on the planes realm. self address ) func init(cur realm) { self = cur.Address() } // List puts plane id up for sale at priceUgnot. The caller must own it and // have approved this realm on the planes realm (Approve for this plane, or // SetApprovalForAll): the plane moves here, in escrow, until it is bought or // the listing is cancelled. func List(cur realm, id int, priceUgnot int64) { seller := userOf(0, cur) if paused { panic(errPaused) } checkPrice(priceUgnot) if listings.Has(key(id)) { panic(errListed) } if owner := ownerOf(id); owner != seller { panic(errNotOwner) } if !planes.IsApprovedForAll(seller, self) && planes.GetApproved(int64(id)) != self { panic(errNotApproved) } listings.Set(key(id), &listing{seller: seller, price: priceUgnot}) planes.TransferFrom(cross(cur), seller, self, int64(id)) noteMinted(0, cur) chain.Emit("List", "id", strconv.Itoa(id), "seller", seller.String(), "price", strconv.FormatInt(priceUgnot, 10)) } // UpdatePrice changes the price of a plane the caller listed. func UpdatePrice(cur realm, id int, priceUgnot int64) { seller := userOf(0, cur) if paused { panic(errPaused) } checkPrice(priceUgnot) l := mustListing(id) if l.seller != seller { panic(errNotSeller) } l.price = priceUgnot chain.Emit("UpdatePrice", "id", strconv.Itoa(id), "seller", seller.String(), "price", strconv.FormatInt(priceUgnot, 10)) } // Cancel takes a listing down and gives the plane back to its seller. It // works while the market is paused. func Cancel(cur realm, id int) { seller := userOf(0, cur) l := mustListing(id) if l.seller != seller { panic(errNotSeller) } listings.Remove(key(id)) planes.TransferFrom(cross(cur), cur.Address(), seller, int64(id)) chain.Emit("Cancel", "id", strconv.Itoa(id), "seller", seller.String()) } // Buy buys a listed plane: send exactly its price in ugnot. Out of the price: // Bazaar's fee (bazaarFee, to its sinks), the planes realm's royalty // (RoyaltyInfo: 5% to the gnofly treasury), and the rest to the seller; the // plane goes to the buyer. It works while the market is paused. func Buy(cur realm, id int) { // A direct user call: only then has the -send envelope landed at this // realm's address, which the OriginSend banker below pays out of. buyer := userOf(0, cur) l := mustListing(id) if l.seller == buyer { panic(errOwnListing) } sent := unsafe.OriginSend() if len(sent) != 1 || sent[0].Denom != payDenom || sent[0].Amount != l.price { panic("gnofly market: wrong payment, send exactly " + strconv.FormatInt(l.price, 10) + payDenom) } // The listing goes before any coin or plane moves. listings.Remove(key(id)) bank := banker.NewBanker(banker.BankerTypeOriginSend, cur) pay := func(to address, amount int64) int64 { if amount <= 0 || !to.IsValid() { return 0 } bank.SendCoins(cur.Address(), to, chain.NewCoins(chain.NewCoin(payDenom, amount))) return amount } genesis, protocol := bazaarFee(l.price, l.seller, buyer) fee := pay(bazaar.GenesisSink(), genesis) + pay(bazaar.ProtocolSink(), protocol) receiver, royalty := planes.RoyaltyInfo(int64(id), l.price) if royalty < 0 || royalty > l.price-fee { royalty = 0 } royalty = pay(receiver, royalty) pay(l.seller, l.price-fee-royalty) planes.TransferFrom(cross(cur), cur.Address(), buyer, int64(id)) noteMinted(0, cur) chain.Emit("Buy", "id", strconv.Itoa(id), "seller", l.seller.String(), "buyer", buyer.String(), "price", itoa64(l.price), "royalty", itoa64(royalty), "fee", itoa64(fee)) } // Transfer gives a plane the caller owns, not listed, to another address, // through this realm: the call Bazaar's site makes. It needs the same // approval as List. A plane can always be moved on the planes realm itself. func Transfer(cur realm, to address, id int) { from := userOf(0, cur) if !to.IsValid() { panic(errBadRecipient) } if listings.Has(key(id)) { panic(errListed) } if owner := ownerOf(id); owner != from { panic(errNotOwner) } planes.TransferFrom(cross(cur), from, to, int64(id)) chain.Emit("Transfer", "id", strconv.Itoa(id), "from", from.String(), "to", to.String()) } // SetPaused stops (or resumes) new listings and price changes. Admin of the // game realm only. Cancel and Buy keep working, so a move to a newer market // never strands a plane: sellers cancel here and list there. func SetPaused(cur realm, p bool) { assertAdmin(0, cur) paused = p chain.Emit("Paused", "paused", strconv.FormatBool(p)) } // ReturnStray gives back a plane this realm holds outside any listing: one // someone sent here by mistake. Admin of the game realm only; to is who it // came from, as the chain's transfer events show. func ReturnStray(cur realm, id int, to address) { assertAdmin(0, cur) if !to.IsValid() { panic(errBadRecipient) } if listings.Has(key(id)) || ownerOf(id) != cur.Address() { panic(errNotStray) } planes.TransferFrom(cross(cur), cur.Address(), to, int64(id)) chain.Emit("ReturnStray", "id", strconv.Itoa(id), "to", to.String()) } // Paused reports whether new listings are stopped. func Paused() bool { return paused } // checkPrice panics unless p is a price this market takes. func checkPrice(p int64) { if p < minPrice || p > maxPrice { panic(errBadPrice) } } // mustListing returns the listing of plane id, or panics. func mustListing(id int) *listing { v := listings.Get(key(id)) if v == nil { panic(errNotListed) } return v.(*listing) } // getListing returns the listing of plane id, or nil. func getListing(id int) *listing { if v := listings.Get(key(id)); v != nil { return v.(*listing) } return nil } // ownerOf returns the owner of plane id on the planes realm, or panics for // an id that is not a minted plane. func ownerOf(id int) address { if id < 1 || id > supply { panic(errUnknownPlane) } owner, err := planes.OwnerOf(tid(id)) if err != nil { panic(errUnknownPlane) } return owner } // key is the listings key of plane id: three digits, "042". func key(id int) string { s := strconv.Itoa(id) for len(s) < 3 { s = "0" + s } return s } func tid(id int) grc721.TokenID { return grc721.TokenID(strconv.Itoa(id)) }
- #12read.gno
- #13package gnofly import ( "strconv" "strings" "gno.land/p/nt/grc721/v0" "gno.land/p/onbloc/json/v0" "gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes" ) // The reads are in the shapes Bazaar's site (bazaar.gnomi.fun) parses on a // collection package, so that this market can be registered there later // without a new version. Lines are fields joined by "|": // // id|name|owner|seller|price|listed|image|collection|rarity|traits|revealed|slot // // owner is the planes realm's owner, this realm for a listed plane (escrow); // seller and price are empty and 0 when the plane is not listed. collection // is the slug Bazaar would know this realm by: the last path element. // slug is the last element of this realm's path. const slug = "gnofly" // maxOpen is the most listings ListOpen returns, as Bazaar's own realms do. const maxOpen = 50 // Name and Symbol are the planes collection's: the market sells nothing else. func Name() string { return "gnofly planes" } func Symbol() string { return "PLANE" } // Minted returns how many planes were minted. func Minted() int { return int(planes.Minted()) } // OwnerOf returns the owner of a plane on the planes realm: this realm while // it is listed. func OwnerOf(tid string) (address, error) { return planes.OwnerOf(grc721.TokenID(tid)) } // TokenURI returns the image of a plane, an https URL. func TokenURI(tid string) string { uri, err := planes.TokenURI(grc721.TokenID(tid)) if err != nil { return "" } return uri } // TokensOf returns the ids of the planes owner holds, comma-separated, in // id order: "3,42,700". A listed plane is not among them: it is held here. func TokensOf(owner string) string { ids := strings.TrimSuffix(strings.TrimPrefix(planes.PlanesOf(address(owner)), "["), "]") return ids } // ListOpen returns the listings, one line each, in id order, at most 50. func ListOpen() string { var lines []string listings.Iterate("", "", func(k string, v any) bool { id, _ := strconv.Atoi(k) lines = append(lines, line(id, v.(*listing))) return len(lines) >= maxOpen }) return strings.Join(lines, "\n") } // ItemLine returns the line of plane id, listed or not. It panics for an id // that is not a minted plane: Bazaar's site skips the ids it cannot read. func ItemLine(id int) string { ownerOf(id) // panics for an id that is not minted return line(id, getListing(id)) } // line builds the line of plane id; l is its listing, nil when it has none. func line(id int, l *listing) string { p := planeInfo(id) owner, _ := planes.OwnerOf(tid(id)) seller, price, listed := "", "0", "false" if l != nil { seller, price, listed = l.seller.String(), strconv.FormatInt(l.price, 10), "true" } traits := "model:" + p.model + ",pilot:" + p.pilot + ",laser:" + p.laser return strings.Join([]string{ strconv.Itoa(id), clean(p.name), owner.String(), seller, price, listed, clean(p.image), slug, p.tier, clean(traits), "true", "", }, "|") } type plane struct { name, image, tier, model, pilot, laser string } // planeInfo reads plane id from the planes realm's PlaneJSON. func planeInfo(id int) plane { node, err := json.Unmarshal([]byte(planes.PlaneJSON(int64(id)))) if err != nil { panic(errUnknownPlane) } str := func(k string) string { v, err := node.GetKey(k) if err != nil { return "" } s, err := v.GetString() if err != nil { return "" } return s } return plane{ name: str("name"), image: str("image"), tier: str("tier"), model: str("model"), pilot: str("pilot"), laser: str("laser"), } } // clean keeps a field from breaking a line: no "|" and no line break. The // planes realm's names and URLs have none; this is a guard, not a filter. func clean(s string) string { return strings.NewReplacer("|", " ", "\n", " ", "\r", " ").Replace(s) } // Render shows the listings on gnoweb. func Render(_ string) string { var b strings.Builder b.WriteString("# gnofly planes on Bazaar\n\n") b.WriteString("The [gnofly](https://gnofly.xyz) planes for sale on [Bazaar](https://bazaar.gnomi.fun), the collection `gnofly`. ") b.WriteString("A listed plane is held by this realm until it is bought or the listing is cancelled. ") b.WriteString("Each sale pays Bazaar's fee and the planes' royalty to the gnofly treasury, the rest to the seller.\n\n") if !registered { b.WriteString("Not registered on Bazaar yet.\n\n") } if paused { b.WriteString("New listings are paused: listed planes can still be bought or cancelled.\n\n") } if listings.Size() == 0 { b.WriteString("No plane is for sale.\n") return b.String() } b.WriteString("| Plane | Tier | Price | Seller |\n|---|---|---|---|\n") listings.Iterate("", "", func(k string, v any) bool { id, _ := strconv.Atoi(k) l := v.(*listing) p := planeInfo(id) b.WriteString("| [#" + k + " " + p.name + "](https://gnofly.xyz/nft/collections/planes/" + strconv.Itoa(id) + ") | " + p.tier + " | " + gnot(l.price) + " | " + l.seller.String() + " |\n") return false }) return b.String() } // gnot writes ugnot as GNOT: 1000000 is "1 GNOT", 1500000 "1.5 GNOT". func gnot(ugnot int64) string { whole, frac := ugnot/1_000_000, ugnot%1_000_000 if frac == 0 { return strconv.FormatInt(whole, 10) + " GNOT" } f := strings.TrimRight(strconv.FormatInt(frac+1_000_000, 10)[1:], "0") return strconv.FormatInt(whole, 10) + "." + f + " GNOT" }
- #14roles.gno
- #15package gnofly import "gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/game/v0" // The market keeps no roles of its own: its admins are the game realm's, // asked again at every call, as the planes and skins realms do. // authority is the one question this realm asks about roles. type authority interface { IsAdmin(addr address) bool } // gameRoles answers with the roles of the game realm. type gameRoles struct{} func (gameRoles) IsAdmin(addr address) bool { return game.IsAdmin(addr) } // roles is the authority of this realm. It is a variable only so that the // tests can answer for the game realm, which has no admin under `gno test`. var roles authority = gameRoles{} // callerOf returns the verified caller of a crossing function; rlm must be // its live cur. func callerOf(_ int, rlm realm) address { if !rlm.IsCurrent() { panic(errSpoofedRealm) } return rlm.Previous().Address() } // userOf returns the caller, who must be a wallet calling directly: never a // realm, so that no contract can act in a user's name or wrap a purchase. func userOf(_ int, rlm realm) address { caller := callerOf(0, rlm) if !rlm.Previous().IsUserCall() { panic(errNotUserCall) } return caller } // assertAdmin panics unless the caller is an admin of the game realm. func assertAdmin(_ int, rlm realm) address { caller := callerOf(0, rlm) if !roles.IsAdmin(caller) { panic(errNotAdmin) } return caller }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]