package planes
import (
"chain"
"strconv"
"strings"
"gno.land/p/nt/bptree/v0"
"gno.land/p/nt/grc721/royalty/v0"
"gno.land/p/nt/grc721/v0"
)
// The read side of the collection, exported for other realms and for
// vm/qeval, as GRC721 tokens are on gno.land. Neither type has a mutator: a
// Token hands out tellers that act as the calling realm and go through the
// ledger's approval checks, Royalty only reads. There is no metadata
// extension: the traits are in the table, read with TokenMetadata(tid) and
// PlaneJSON(id), and the token URI, the image, is computed by TokenURI(tid).
//
// planes.TokenMetadata("42")
// planes.Token.RealmTeller(0, cur).TransferFrom(0, cur, from, to, "42")
var (
Token *grc721.Token // name, symbol, owners, balances, approvals
Royalties *royalty.Royalty // EIP-2981 royalty
)
// The rest of the state. Everything is unexported and nothing below is ever
// returned by reference: the ledgers and the trees are /p/ types with mutator
// methods, so handing one out would be handing out write access to the
// collection.
var (
ledger *grc721.PrivateLedger // the collection, write side: mint, burn
teller grc721.Teller // acts as the caller of this realm: transfers and approvals
royaltyLedger *royalty.Ledger // royalty, write side
mintCount int64 // planes minted so far, burned ones included; the ids left are in pool (draw.gno)
price int64 // ugnot, what a mint costs
mintOpen bool // whether Mint is accepting calls
mintCap int64 // how many planes one wallet may mint, over its lifetime
imageBase string // "https://gnofly.xyz/nft/collections/planes/", the image of plane 42 is at 042.png
mints bptree.BPTree // minter address -> int64, planes minted so far, for the cap
tierMinted [4]int64 // planes minted so far per tier, in the order of tiers
recent [5]int64 // the last ids minted, newest first, 0 for none, for Render
webPath string // "/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes", used to build links in Render
)
// The collection, and the settings at deploy.
const (
supply = int64(777)
tokenName = "gnofly planes"
tokenSymbol = "PLANE"
payDenom = "ugnot" // the only coin planes are sold for
defaultPrice = int64(77000000) // 77 GNOT
minPrice = int64(1000000)
maxPrice = int64(10000000000)
defaultMintCap = int64(7)
defaultImageBase = "https://gnofly.xyz/nft/collections/planes/"
defaultRoyaltyBps = int64(500) // 5% of a resale, to the treasury
maxRoyaltyBps = int64(1000) // the most SetRoyaltyBps accepts
// Models are what the game client renders, tiers are what the table
// says. Both are validated against these lists.
models = "prop,biplane,stunt,jet,seaplane,twin,delta,glider,supergnome"
pilots = "none,gnome"
tiers = "common,rare,epic,legendary"
)
// tierSupply is how many planes of each tier the table holds, in the order
// of tiers. The tests check it against the table.
var tierSupply = [4]int64{500, 200, 70, 7}
// tierIndex returns the position of tier in tiers, or -1.
func tierIndex(tier string) int {
for i, t := range strings.Split(tiers, ",") {
if t == tier {
return i
}
}
return -1
}
// init runs once, when the package is added to the chain. The realm has
// nothing to remember about its deployer: it has no role of its own.
func init(cur realm) {
bootstrap(0, cur)
}
// bootstrap (re)creates the initial state: the token, its extensions and the
// settings. The tests call it to start from a clean slate. rlm is the live
// realm of the caller: grc721 records its package path as the home of the
// Token.
func bootstrap(_ int, rlm realm) {
// A table of the wrong shape is refused before anything is written, so
// that a bad hand edit fails at publish rather than at the first mint.
if first, last := rowID(0), rowID(len(table)-1); first != 1 || last != supply {
panic(errBadTable)
}
Token, ledger = grc721.NewToken(tokenName, tokenSymbol, 0, rlm)
// Minted from the ledger: the frame-relative teller is confined to this
// realm, which is where it is used.
teller = ledger.CallerTeller()
Royalties, royaltyLedger = royalty.NewRoyalty(ledger, maxRoyaltyBps)
mintCount = 0
resetPool(rlm.PkgPath())
recent = [5]int64{}
price = defaultPrice
mintOpen = false
mintCap = defaultMintCap
imageBase = defaultImageBase
mints = bptree.BPTree{}
tierMinted = [4]int64{}
webPath = webPathOf(rlm.PkgPath())
// Under `gno test` the game realm has no treasury yet (see roles.gno):
// the tests set one and call bootstrap again.
if roles.Treasury().IsValid() {
setRoyalty(defaultRoyaltyBps)
}
}
// setRoyalty points the royalty of every plane at the treasury of the game
// realm, as it is now, at bps basis points.
func setRoyalty(bps int64) {
if err := royaltyLedger.SetDefaultRoyalty(roles.Treasury(), bps); err != nil {
panic(err)
}
}
// SetMintOpen opens or closes the mint. Admin only. The mint starts closed.
func SetMintOpen(cur realm, open bool) {
assertAdmin(0, cur)
mintOpen = open
chain.Emit("MintOpen", "open", boolString(open))
}
// SetPrice sets what a mint costs, from 1 to 10000 GNOT. Admin only. It
// applies to the next mint.
func SetPrice(cur realm, ugnot int64) {
assertAdmin(0, cur)
if ugnot < minPrice || ugnot > maxPrice {
panic(errInvalidPrice)
}
price = ugnot
chain.Emit("PriceSet", "price", itoa(ugnot))
}
// SetMintCap sets how many planes one wallet may mint over its lifetime, from
// 1 to 777. Admin only. A wallet already over a lowered cap keeps its planes
// and mints no more.
func SetMintCap(cur realm, n int64) {
assertAdmin(0, cur)
if n < 1 || n > supply {
panic(errInvalidCap)
}
mintCap = n
chain.Emit("MintCapSet", "cap", itoa(n))
}
// SetImageBase sets where the images and the metadata of the planes are
// served from: the image of plane 42 is 042.png and its token URI is
// 42.json. Admin only.
//
// It applies at once to every plane: the URLs are computed on read, nothing
// is stored per token.
func SetImageBase(cur realm, base string) {
assertAdmin(0, cur)
if !validBase(base) {
panic(errInvalidBase)
}
imageBase = base
chain.Emit("ImageBaseSet", "base", base)
}
// SetRoyaltyBps sets the royalty of a resale, from 0 to 1000 basis points,
// and points it at the treasury of the game realm as it is now. Admin only.
//
// The royalty is advisory, as EIP-2981 is: a marketplace reads RoyaltyInfo
// and pays it, the realm cannot enforce it on a transfer.
func SetRoyaltyBps(cur realm, bps int64) {
assertAdmin(0, cur)
if bps < 0 || bps > maxRoyaltyBps {
panic(errInvalidRoyalty)
}
setRoyalty(bps)
}
// Minted returns how many planes were minted so far, burned ones included.
// Planes are drawn at random, so it says nothing of which ids. The
// settings (price, open, cap, image base, royalty) are read with
// CollectionJSON: every exported function costs storage at deploy, measured
// at about 650 bytes each, so there is no getter per setting.
func Minted() int64 {
return mintCount
}
// webPathOf turns "gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes" into "/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes".
func webPathOf(pkgPath string) string {
for i := 0; i < len(pkgPath); i++ {
if pkgPath[i] == '/' {
return pkgPath[i:]
}
}
return ""
}
// imageURL returns where the image of plane id is: .png,
// the id on three digits, "042.png", as the image files are named. The token
// URI keeps the plain id, "42.json".
func imageURL(id int64) string {
return imageBase + pad3(id) + ".png"
}
// pad3 writes an id of 1 to 777 on three digits: 7 -> "007", 42 -> "042".
func pad3(id int64) string {
s := itoa(id)
for len(s) < 3 {
s = "0" + s
}
return s
}
func itoa(n int64) string {
return strconv.FormatInt(n, 10)
}
func boolString(b bool) string {
if b {
return "true"
}
return "false"
}