1package bazaarv123import (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/bazaarv1"15 nativeDenom = "ugnot"16 defaultLaunchFee = int64(10_000_000) // 10 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}4041var (42 bySlug avl.Tree43 byPkg avl.Tree44 pending avl.Tree45 reserved avl.Tree46 inited bool47 admin address48 self address49 launchFee int6450 protocolBps int6451 seenBank int6452)5354func Init(cur realm) {55 if inited {56 panic("factory: already initialized")57 }58 if !cur.Previous().IsUserCall() {59 panic("factory: EOA only")60 }61 admin = cur.Previous().Address()62 self = cur.Address()63 bySlug = avl.Tree{}64 byPkg = avl.Tree{}65 pending = avl.Tree{}66 reserved = avl.Tree{}67 launchFee = defaultLaunchFee68 protocolBps = defaultProtocolBps69 seenBank = bankUgnot()70 inited = true71 chain.Emit("Init", "admin", admin.String())72}7374func Address() address {75 return chain.PackageAddress(pkgPath)76}7778func LaunchFee() int64 {79 requireInit()80 return launchFee81}8283func Admin() string {84 requireInit()85 return admin.String()86}8788func SetLaunchFee(cur realm, ugnot int64) {89 requireAdmin(cur)90 if ugnot < 0 {91 panic("factory: launch fee")92 }93 launchFee = ugnot94 chain.Emit("SetLaunchFee", "ugnot", strconv.FormatInt(ugnot, 10))95}9697func ProtocolBps() int64 {98 requireInit()99 if protocolBps <= 0 {100 return defaultProtocolBps101 }102 return protocolBps103}104105func SetProtocolBps(cur realm, bps int64) {106 requireAdmin(cur)107 if bps < 0 || bps > maxProtocolBps {108 panic("factory: protocol bps")109 }110 protocolBps = bps111 chain.Emit("SetProtocolBps", "bps", strconv.FormatInt(bps, 10))112}113114func CreditLaunchFee(cur realm) {115 requireInit()116 prev := cur.Previous()117 if !prev.IsCode() {118 panic("factory: collection realm")119 }120 pkg := prev.PkgPath()121 if pending.Has(pkg) {122 panic("factory: already credited")123 }124 if byPkg.Has(pkg) {125 panic("factory: already registered")126 }127 now := bankUgnot()128 if now < seenBank {129 panic("factory: bank")130 }131 if now-seenBank != launchFee {132 panic("factory: launch fee")133 }134 seenBank = now135 pending.Set(pkg, true)136 chain.Emit("CreditLaunchFee", "pkg", pkg)137}138139func Register(cur realm, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {140 requireInit()141 prev := cur.Previous()142 if !prev.IsCode() {143 panic("factory: collection realm")144 }145 creatorRlm := prev.Previous()146 if !creatorRlm.IsUserCall() {147 panic("factory: EOA creator")148 }149 pkg := prev.PkgPath()150 slug := lastElem(pkg)151 if reserved.Has(slug) {152 who := reserved.Get(slug).(address)153 if creatorRlm.Address() != who {154 panic("factory: reserved")155 }156 reserved.Remove(slug)157 if pending.Has(pkg) {158 pending.Remove(pkg)159 }160 } else {161 if !pending.Has(pkg) {162 panic("factory: launch fee")163 }164 pending.Remove(pkg)165 }166 register(pkg, creatorRlm.Address(), name, symbol, cover, maxSupply, mintPrice, royaltyBps)167}168169func Reserve(cur realm, slug string) {170 requireInit()171 if !cur.Previous().IsUserCall() {172 panic("factory: EOA only")173 }174 slug = strings.ToLower(strings.TrimSpace(slug))175 if !validSlug(slug) {176 panic("factory: slug")177 }178 if bySlug.Has(slug) || reserved.Has(slug) {179 panic("factory: slug taken")180 }181 takeFactorySend(launchFee)182 seenBank = bankUgnot()183 reserved.Set(slug, cur.Previous().Address())184 chain.Emit("Reserve", "slug", slug)185}186187func CancelReserve(cur realm, slug string) {188 requireInit()189 if !cur.Previous().IsUserCall() {190 panic("factory: EOA only")191 }192 slug = strings.ToLower(strings.TrimSpace(slug))193 if !reserved.Has(slug) {194 panic("factory: not reserved")195 }196 who := reserved.Get(slug).(address)197 if who != cur.Previous().Address() {198 panic("factory: not reserver")199 }200 if bySlug.Has(slug) {201 panic("factory: already registered")202 }203 reserved.Remove(slug)204 if launchFee > 0 {205 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)206 bk.SendCoins(self, who, chain.Coins{{Denom: nativeDenom, Amount: launchFee}})207 }208 seenBank = bankUgnot()209 chain.Emit("CancelReserve", "slug", slug)210}211212func ReservedCreator(slug string) string {213 requireInit()214 slug = strings.ToLower(strings.TrimSpace(slug))215 if !reserved.Has(slug) {216 return ""217 }218 return reserved.Get(slug).(address).String()219}220221func takeFactorySend(want int64) {222 sent := unsafe.OriginSend()223 if want == 0 {224 if len(sent) != 0 {225 panic("factory: extra denoms")226 }227 return228 }229 if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {230 panic("factory: wrong payment")231 }232}233234func NoteMinted(cur realm, n int) {235 requireInit()236 if n < 0 {237 panic("factory: minted")238 }239 prev := cur.Previous()240 if !prev.IsCode() {241 panic("factory: collection realm")242 }243 c := mustByPkg(prev.PkgPath())244 c.minted = n245 byPkg.Set(c.pkg, c)246 bySlug.Set(c.slug, c)247}248249func NoteRoyalty(cur realm, bps int) {250 requireInit()251 if bps < 0 || bps > maxRoyaltyBps {252 panic("factory: royalty")253 }254 prev := cur.Previous()255 if !prev.IsCode() {256 panic("factory: collection realm")257 }258 c := mustByPkg(prev.PkgPath())259 c.royaltyBps = bps260 byPkg.Set(c.pkg, c)261 bySlug.Set(c.slug, c)262}263264func CollectionOf(slug string) string {265 requireInit()266 c := mustBySlug(slug)267 return c.pkg268}269270func CollectionAddr(slug string) string {271 requireInit()272 c := mustBySlug(slug)273 return chain.PackageAddress(c.pkg).String()274}275276func RoyaltyOf(slug string) int {277 requireInit()278 c := mustBySlug(slug)279 return c.royaltyBps280}281282func ListCollections() string {283 requireInit()284 out := ""285 bySlug.Iterate("", "", func(_ string, value any) bool {286 c := value.(*collection)287 if out != "" {288 out += "\n"289 }290 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)291 return false292 })293 return out294}295296func Withdraw(cur realm) {297 requireAdmin(cur)298 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)299 ugnot := bk.GetCoins(self).AmountOf(nativeDenom)300 if ugnot <= 0 {301 panic("factory: no funds")302 }303 seenBank = 0304 bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: ugnot}})305 chain.Emit("Withdraw", "amount", strconv.FormatInt(ugnot, 10))306}307308func TransferAdmin(cur realm, next address) {309 requireAdmin(cur)310 if !next.IsValid() {311 panic("factory: invalid address")312 }313 if next == admin {314 panic("factory: already admin")315 }316 admin = next317 chain.Emit("TransferAdmin", "addr", next.String())318}319320func Render(_ string) string {321 if !inited {322 return "# Bazaar factory\n\nCall Init first.\n"323 }324 out := "# Bazaar factory\n\n"325 out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n"326 out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n"327 lines := ListCollections()328 if lines == "" {329 out += "_No collections._\n"330 } else {331 out += "```\n" + lines + "\n```\n"332 }333 return out334}335336func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {337 if !strings.HasPrefix(pkg, "gno.land/r/") {338 panic("factory: pkg")339 }340 slug := lastElem(pkg)341 if !validSlug(slug) {342 panic("factory: slug")343 }344 if !validName(name) {345 panic("factory: name")346 }347 if !validSymbol(symbol) {348 panic("factory: symbol")349 }350 if !validImage(cover) {351 panic("factory: cover")352 }353 if maxSupply < 0 || maxSupply > maxSupplyBound {354 panic("factory: maxSupply")355 }356 if mintPrice < 0 {357 panic("factory: mintPrice")358 }359 if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {360 panic("factory: royalty")361 }362 if bySlug.Has(slug) {363 panic("factory: slug taken")364 }365 if byPkg.Has(pkg) {366 panic("factory: pkg already registered")367 }368 c := &collection{369 slug: slug,370 name: name,371 symbol: symbol,372 cover: cover,373 pkg: pkg,374 creator: creator,375 maxSupply: maxSupply,376 mintPrice: mintPrice,377 royaltyBps: int(royaltyBps),378 }379 bySlug.Set(slug, c)380 byPkg.Set(pkg, c)381 chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String())382}383384func requireInit() {385 if !inited {386 panic("factory: call Init first")387 }388}389390func requireAdmin(cur realm) {391 requireInit()392 if !cur.Previous().IsUserCall() {393 panic("factory: EOA only")394 }395 if cur.Previous().Address() != admin {396 panic("factory: not admin")397 }398}399400func bankUgnot() int64 {401 return banker.NewReadonlyBanker().GetCoins(Address()).AmountOf(nativeDenom)402}403404func mustBySlug(slug string) *collection {405 v := bySlug.Get(slug)406 if v == nil {407 panic("factory: unknown collection")408 }409 return v.(*collection)410}411412func mustByPkg(pkg string) *collection {413 v := byPkg.Get(pkg)414 if v == nil {415 panic("factory: unknown collection")416 }417 return v.(*collection)418}419420func lastElem(pkg string) string {421 i := strings.LastIndex(pkg, "/")422 if i < 0 || i+1 >= len(pkg) {423 panic("factory: pkg")424 }425 return pkg[i+1:]426}427428func validSlug(s string) bool {429 if len(s) < minSlugLen || len(s) > maxSlugLen {430 return false431 }432 if s[0] < 'a' || s[0] > 'z' {433 return false434 }435 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")436}437438func validSymbol(s string) bool {439 if len(s) < 1 || len(s) > maxSymbolLen {440 return false441 }442 return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")443}444445func validName(name string) bool {446 if len(name) < 1 || len(name) > maxNameLen {447 return false448 }449 return !strings.ContainsAny(name, "\n\r|")450}451452func validImage(u string) bool {453 if u == "" {454 return true455 }456 if len(u) > maxImageLen {457 return false458 }459 if strings.ContainsAny(u, " \t\n\r|<>\"'`") {460 return false461 }462 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {463 return true464 }465 const prefix = "/samples/"466 if !strings.HasPrefix(u, prefix) {467 return false468 }469 rest := u[len(prefix):]470 if rest == "" {471 return false472 }473 return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")474}475476func asciiSet(s, allowed string) bool {477 for i := 0; i < len(s); i++ {478 if !strings.ContainsRune(allowed, rune(s[i])) {479 return false480 }481 }482 return true483}484Address() string
Admin() 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
CreditLaunchFee(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})
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
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
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
RoyaltyOf(slug string) int
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)
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.