1package bazaarv523import (4 "chain"5 "chain/banker"6 "chain/runtime/unsafe"7 "strconv"8 "strings"910 "gno.land/p/nt/avl/v0"11)1213const (14 pkgPath = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv5"15 nativeDenom = "ugnot"16 defaultLaunchFee = int64(1_000_000_000) // 1000 GNOT17 maxNameLen = 6418 maxImageLen = 20019 maxSupplyBound = 1_000_00020 maxRoyaltyBps = 100021 maxSymbolLen = 1122 maxSlugLen = 1123 minSlugLen = 224 defaultProtocolBps = int64(200) // 2% Magic Eden25 maxProtocolBps = int64(500)26)2728type collection struct {29 slug string30 name string31 symbol string32 cover string33 pkg string34 creator address35 maxSupply int36 mintPrice int6437 minted int38 royaltyBps int39}4041type reservation struct {42 who address43 paid int6444}4546var (47 bySlug avl.Tree48 byPkg avl.Tree49 reserved avl.Tree50 inited bool51 admin address52 self address53 protocolSink address54 launchFee int6455 protocolBps int6456 genesisPkg string57 genesisSink address58 genesisShareBps int6459 discountPusher string60 holderDisc avl.Tree61)6263func Init(cur realm) {64 if inited {65 panic("factory: already initialized")66 }67 if !cur.Previous().IsUserCall() {68 panic("factory: EOA only")69 }70 admin = cur.Previous().Address()71 protocolSink = admin72 self = cur.Address()73 bySlug = avl.Tree{}74 byPkg = avl.Tree{}75 reserved = avl.Tree{}76 holderDisc = avl.Tree{}77 genesisPkg = ""78 genesisShareBps = 079 discountPusher = ""80 var z address81 genesisSink = z82 launchFee = defaultLaunchFee83 protocolBps = defaultProtocolBps84 inited = true85 chain.Emit("Init", "admin", admin.String())86}8788func Address() address {89 return chain.PackageAddress(pkgPath)90}9192func LaunchFee() int64 {93 requireInit()94 return launchFee95}9697func Admin() string {98 requireInit()99 return admin.String()100}101102func SetLaunchFee(cur realm, ugnot int64) {103 requireAdmin(cur)104 if ugnot < 0 {105 panic("factory: launch fee")106 }107 launchFee = ugnot108 chain.Emit("SetLaunchFee", "ugnot", strconv.FormatInt(ugnot, 10))109}110111func ProtocolBps() int64 {112 requireInit()113 if protocolBps <= 0 {114 return defaultProtocolBps115 }116 return protocolBps117}118119func SetProtocolBps(cur realm, bps int64) {120 requireAdmin(cur)121 if bps < 0 || bps > maxProtocolBps {122 panic("factory: protocol bps")123 }124 protocolBps = bps125 chain.Emit("SetProtocolBps", "bps", strconv.FormatInt(bps, 10))126}127128func ProtocolSink() address {129 requireInit()130 return protocolSink131}132133func SetProtocolSink(cur realm, next address) {134 requireAdmin(cur)135 if !next.IsValid() {136 panic("factory: invalid address")137 }138 if next == protocolSink {139 panic("factory: already sink")140 }141 protocolSink = next142 chain.Emit("SetProtocolSink", "addr", next.String())143}144145func GenesisPkg() string {146 requireInit()147 return genesisPkg148}149150func GenesisSink() address {151 requireInit()152 return genesisSink153}154155func GenesisShareBps() int64 {156 requireInit()157 if genesisPkg == "" {158 return 0159 }160 if genesisShareBps <= 0 {161 return 1100162 }163 return genesisShareBps164}165166func HolderDiscount(owner string) int64 {167 requireInit()168 if genesisPkg == "" {169 return 0170 }171 v := holderDisc.Get(strings.ToLower(strings.TrimSpace(owner)))172 if v == nil {173 return 0174 }175 return v.(int64)176}177178func AttachGenesis(cur realm) {179 requireInit()180 prev := cur.Previous()181 if !prev.IsCode() {182 panic("factory: genesis realm")183 }184 eoa := prev.Previous()185 if !eoa.IsUserCall() || eoa.Address() != admin {186 panic("factory: not admin")187 }188 pkg := prev.PkgPath()189 if pkg == "" {190 panic("factory: genesis pkg")191 }192 if genesisPkg != "" && genesisPkg != pkg {193 panic("factory: genesis set")194 }195 genesisPkg = pkg196 genesisSink = prev.Address()197 if genesisShareBps <= 0 {198 genesisShareBps = 1100199 }200 chain.Emit("AttachGenesis", "pkg", pkg)201}202203func SetGenesis(cur realm, pkg string) {204 requireAdmin(cur)205 pkg = strings.TrimSpace(pkg)206 if pkg == "" || !strings.HasPrefix(pkg, "gno.land/r/") {207 panic("factory: genesis pkg")208 }209 genesisPkg = pkg210 genesisSink = chain.PackageAddress(pkg)211 if genesisShareBps <= 0 {212 genesisShareBps = 1100213 }214 chain.Emit("SetGenesis", "pkg", pkg)215}216217func SetDiscountPusher(cur realm, pkg string) {218 requireAdmin(cur)219 pkg = strings.TrimSpace(pkg)220 if pkg == "" || !strings.HasPrefix(pkg, "gno.land/r/") {221 panic("factory: pusher pkg")222 }223 discountPusher = pkg224 chain.Emit("SetDiscountPusher", "pkg", pkg)225}226227func DiscountPusher() string {228 requireInit()229 return discountPusher230}231232func NoteHolderDiscount(cur realm, owner string, bps int64) {233 requireInit()234 prev := cur.Previous()235 if !prev.IsCode() {236 panic("factory: not genesis")237 }238 from := prev.PkgPath()239 ok := (genesisPkg != "" && from == genesisPkg) || (discountPusher != "" && from == discountPusher)240 if !ok {241 panic("factory: not genesis")242 }243 if bps < 0 {244 panic("factory: discount")245 }246 who := strings.ToLower(strings.TrimSpace(owner))247 if who == "" {248 panic("factory: owner")249 }250 if bps == 0 {251 holderDisc.Remove(who)252 return253 }254 holderDisc.Set(who, bps)255}256257func Register(cur realm, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {258 requireInit()259 prev := cur.Previous()260 if !prev.IsCode() {261 panic("factory: collection realm")262 }263 creatorRlm := prev.Previous()264 if !creatorRlm.IsUserCall() {265 panic("factory: EOA creator")266 }267 pkg := prev.PkgPath()268 slug := lastElem(pkg)269 res := mustReserve(slug)270 if creatorRlm.Address() != res.who {271 panic("factory: reserved")272 }273 reserved.Remove(slug)274 register(pkg, creatorRlm.Address(), name, symbol, cover, maxSupply, mintPrice, royaltyBps)275}276277func Reserve(cur realm, slug string) {278 requireInit()279 if !cur.Previous().IsUserCall() {280 panic("factory: EOA only")281 }282 slug = strings.ToLower(strings.TrimSpace(slug))283 if !validSlug(slug) {284 panic("factory: slug")285 }286 if bySlug.Has(slug) || reserved.Has(slug) {287 panic("factory: slug taken")288 }289 caller := cur.Previous().Address()290 paid := reserveDue(caller)291 takeFactorySend(paid)292 reserved.Set(slug, &reservation{who: caller, paid: paid})293 chain.Emit("Reserve", "slug", slug, "paid", strconv.FormatInt(paid, 10))294}295296// ReserveDue is the ugnot Reserve will require from addr. The factory admin297// (the wallet that called Init) pays 0. Everyone else pays LaunchFee.298func ReserveDue(addr string) int64 {299 requireInit()300 who, ok := parseAddr(addr)301 if !ok {302 return launchFee303 }304 return reserveDue(who)305}306307func reserveDue(caller address) int64 {308 if caller == admin {309 return 0310 }311 return launchFee312}313314func parseAddr(raw string) (address, bool) {315 s := strings.TrimSpace(raw)316 if s == "" {317 var zero address318 return zero, false319 }320 who := address(s)321 if !who.IsValid() {322 var zero address323 return zero, false324 }325 return who, true326}327328func CancelReserve(cur realm, slug string) {329 requireInit()330 if !cur.Previous().IsUserCall() {331 panic("factory: EOA only")332 }333 slug = strings.ToLower(strings.TrimSpace(slug))334 res := mustReserve(slug)335 if res.who != cur.Previous().Address() {336 panic("factory: not reserver")337 }338 if bySlug.Has(slug) {339 panic("factory: already registered")340 }341 reserved.Remove(slug)342 if res.paid > 0 {343 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)344 bk.SendCoins(self, res.who, chain.Coins{{Denom: nativeDenom, Amount: res.paid}})345 }346 chain.Emit("CancelReserve", "slug", slug, "paid", strconv.FormatInt(res.paid, 10))347}348349func ReservedCreator(slug string) string {350 requireInit()351 slug = strings.ToLower(strings.TrimSpace(slug))352 if !reserved.Has(slug) {353 return ""354 }355 return reserved.Get(slug).(*reservation).who.String()356}357358func takeFactorySend(want int64) {359 sent := unsafe.OriginSend()360 if want == 0 {361 if len(sent) != 0 {362 panic("factory: extra denoms")363 }364 return365 }366 if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {367 panic("factory: wrong payment")368 }369}370371func NoteMinted(cur realm, n int) {372 requireInit()373 if n < 0 {374 panic("factory: minted")375 }376 prev := cur.Previous()377 if !prev.IsCode() {378 panic("factory: collection realm")379 }380 c := mustByPkg(prev.PkgPath())381 c.minted = n382 byPkg.Set(c.pkg, c)383 bySlug.Set(c.slug, c)384}385386func NoteRoyalty(cur realm, bps int) {387 requireInit()388 if bps < 0 || bps > maxRoyaltyBps {389 panic("factory: royalty")390 }391 prev := cur.Previous()392 if !prev.IsCode() {393 panic("factory: collection realm")394 }395 c := mustByPkg(prev.PkgPath())396 c.royaltyBps = bps397 byPkg.Set(c.pkg, c)398 bySlug.Set(c.slug, c)399}400401func CollectionOf(slug string) string {402 requireInit()403 c := mustBySlug(slug)404 return c.pkg405}406407func CollectionAddr(slug string) string {408 requireInit()409 c := mustBySlug(slug)410 return chain.PackageAddress(c.pkg).String()411}412413func RoyaltyOf(slug string) int {414 requireInit()415 c := mustBySlug(slug)416 return c.royaltyBps417}418419func ListCollections() string {420 requireInit()421 out := ""422 bySlug.Iterate("", "", func(_ string, value any) bool {423 c := value.(*collection)424 if out != "" {425 out += "\n"426 }427 out += c.slug + "|" + c.name + "|" + c.cover + "|" + c.pkg + "|" + strconv.FormatInt(c.mintPrice, 10) + "|" + strconv.Itoa(c.maxSupply) + "|" + strconv.Itoa(c.minted) + "|" + c.creator.String() + "|" + chain.PackageAddress(c.pkg).String() + "|" + strconv.Itoa(c.royaltyBps)428 return false429 })430 return out431}432433func Withdraw(cur realm) {434 requireAdmin(cur)435 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)436 ugnot := bk.GetCoins(self).AmountOf(nativeDenom)437 locked := reservedPaid()438 avail := ugnot - locked439 if avail <= 0 {440 panic("factory: no funds")441 }442 bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: avail}})443 chain.Emit("Withdraw", "amount", strconv.FormatInt(avail, 10))444}445446func TransferAdmin(cur realm, next address) {447 requireAdmin(cur)448 if !next.IsValid() {449 panic("factory: invalid address")450 }451 if next == admin {452 panic("factory: already admin")453 }454 admin = next455 chain.Emit("TransferAdmin", "addr", next.String())456}457458func Render(_ string) string {459 if !inited {460 return "# Bazaar factory\n\nCall Init first.\n"461 }462 out := "# Bazaar factory\n\n"463 out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n"464 out += "- admin reserve: 0 ugnot\n"465 out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n"466 lines := ListCollections()467 if lines == "" {468 out += "_No collections._\n"469 } else {470 out += "```\n" + lines + "\n```\n"471 }472 return out473}474475func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {476 if !strings.HasPrefix(pkg, "gno.land/r/") {477 panic("factory: pkg")478 }479 slug := lastElem(pkg)480 if !validSlug(slug) {481 panic("factory: slug")482 }483 if !validName(name) {484 panic("factory: name")485 }486 if !validSymbol(symbol) {487 panic("factory: symbol")488 }489 if !validImage(cover) {490 panic("factory: cover")491 }492 if maxSupply < 0 || maxSupply > maxSupplyBound {493 panic("factory: maxSupply")494 }495 if mintPrice < 0 {496 panic("factory: mintPrice")497 }498 if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {499 panic("factory: royalty")500 }501 if bySlug.Has(slug) {502 panic("factory: slug taken")503 }504 if byPkg.Has(pkg) {505 panic("factory: pkg already registered")506 }507 c := &collection{508 slug: slug,509 name: name,510 symbol: symbol,511 cover: cover,512 pkg: pkg,513 creator: creator,514 maxSupply: maxSupply,515 mintPrice: mintPrice,516 royaltyBps: int(royaltyBps),517 }518 bySlug.Set(slug, c)519 byPkg.Set(pkg, c)520 chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String())521}522523func requireInit() {524 if !inited {525 panic("factory: call Init first")526 }527}528529func requireAdmin(cur realm) {530 requireInit()531 if !cur.Previous().IsUserCall() {532 panic("factory: EOA only")533 }534 if cur.Previous().Address() != admin {535 panic("factory: not admin")536 }537}538539func mustReserve(slug string) *reservation {540 v := reserved.Get(slug)541 if v == nil {542 panic("factory: not reserved")543 }544 return v.(*reservation)545}546547func reservedPaid() int64 {548 var n int64549 reserved.Iterate("", "", func(_ string, value any) bool {550 n += value.(*reservation).paid551 return false552 })553 return n554}555556func mustBySlug(slug string) *collection {557 v := bySlug.Get(slug)558 if v == nil {559 panic("factory: unknown collection")560 }561 return v.(*collection)562}563564func mustByPkg(pkg string) *collection {565 v := byPkg.Get(pkg)566 if v == nil {567 panic("factory: unknown collection")568 }569 return v.(*collection)570}571572func lastElem(pkg string) string {573 i := strings.LastIndex(pkg, "/")574 if i < 0 || i+1 >= len(pkg) {575 panic("factory: pkg")576 }577 return pkg[i+1:]578}579580func validSlug(s string) bool {581 if len(s) < minSlugLen || len(s) > maxSlugLen {582 return false583 }584 if s[0] < 'a' || s[0] > 'z' {585 return false586 }587 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")588}589590func validSymbol(s string) bool {591 if len(s) < 1 || len(s) > maxSymbolLen {592 return false593 }594 return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")595}596597func validName(name string) bool {598 if len(name) < 1 || len(name) > maxNameLen {599 return false600 }601 return !strings.ContainsAny(name, "\n\r|")602}603604func validImage(u string) bool {605 if u == "" {606 return true607 }608 if len(u) > maxImageLen {609 return false610 }611 if strings.ContainsAny(u, " \t\n\r|<>\"'`") {612 return false613 }614 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {615 return true616 }617 const prefix = "/samples/"618 if !strings.HasPrefix(u, prefix) {619 return false620 }621 rest := u[len(prefix):]622 if rest == "" {623 return false624 }625 return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")626}627628func asciiSet(s, allowed string) bool {629 for i := 0; i < len(s); i++ {630 if !strings.ContainsRune(allowed, rune(s[i])) {631 return false632 }633 }634 return true635}636Address() string
Admin() string
AttachGenesis(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
CancelReserve(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string)
CollectionAddr(slug string) string
CollectionOf(slug string) string
DiscountPusher() string
GenesisPkg() string
GenesisShareBps() int64
GenesisSink() string
HolderDiscount(owner string) int64
Init(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
LaunchFee() int64
ListCollections() string
NoteHolderDiscount(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, owner string, bps int64)
NoteMinted(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, n int)
NoteRoyalty(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, bps int)
ProtocolBps() int64
ProtocolSink() string
Register(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, symbol string, cover string, maxSupply int, mintPrice int64, royaltyBps int64)
Render(string) string
Reserve(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string)
ReservedCreator(slug string) string
ReserveDue(addr string) int64
RoyaltyOf(slug string) int
SetDiscountPusher(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, pkg string)
SetGenesis(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, pkg string)
SetLaunchFee(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, ugnot int64)
SetProtocolBps(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, bps int64)
SetProtocolSink(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, next string)
TransferAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, next string)
Withdraw(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.