1package bazaarv223import (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/bazaarv2"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}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 launchFee int6454 protocolBps int6455)5657func Init(cur realm) {58 if inited {59 panic("factory: already initialized")60 }61 if !cur.Previous().IsUserCall() {62 panic("factory: EOA only")63 }64 admin = cur.Previous().Address()65 self = cur.Address()66 bySlug = avl.Tree{}67 byPkg = avl.Tree{}68 reserved = avl.Tree{}69 launchFee = defaultLaunchFee70 protocolBps = defaultProtocolBps71 inited = true72 chain.Emit("Init", "admin", admin.String())73}7475func Address() address {76 return chain.PackageAddress(pkgPath)77}7879func LaunchFee() int64 {80 requireInit()81 return launchFee82}8384func Admin() string {85 requireInit()86 return admin.String()87}8889func SetLaunchFee(cur realm, ugnot int64) {90 requireAdmin(cur)91 if ugnot < 0 {92 panic("factory: launch fee")93 }94 launchFee = ugnot95 chain.Emit("SetLaunchFee", "ugnot", strconv.FormatInt(ugnot, 10))96}9798func ProtocolBps() int64 {99 requireInit()100 if protocolBps <= 0 {101 return defaultProtocolBps102 }103 return protocolBps104}105106func SetProtocolBps(cur realm, bps int64) {107 requireAdmin(cur)108 if bps < 0 || bps > maxProtocolBps {109 panic("factory: protocol bps")110 }111 protocolBps = bps112 chain.Emit("SetProtocolBps", "bps", strconv.FormatInt(bps, 10))113}114115func Register(cur realm, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {116 requireInit()117 prev := cur.Previous()118 if !prev.IsCode() {119 panic("factory: collection realm")120 }121 creatorRlm := prev.Previous()122 if !creatorRlm.IsUserCall() {123 panic("factory: EOA creator")124 }125 pkg := prev.PkgPath()126 slug := lastElem(pkg)127 res := mustReserve(slug)128 if creatorRlm.Address() != res.who {129 panic("factory: reserved")130 }131 reserved.Remove(slug)132 register(pkg, creatorRlm.Address(), name, symbol, cover, maxSupply, mintPrice, royaltyBps)133}134135func Reserve(cur realm, slug string) {136 requireInit()137 if !cur.Previous().IsUserCall() {138 panic("factory: EOA only")139 }140 slug = strings.ToLower(strings.TrimSpace(slug))141 if !validSlug(slug) {142 panic("factory: slug")143 }144 if bySlug.Has(slug) || reserved.Has(slug) {145 panic("factory: slug taken")146 }147 paid := launchFee148 takeFactorySend(paid)149 reserved.Set(slug, &reservation{who: cur.Previous().Address(), paid: paid})150 chain.Emit("Reserve", "slug", slug, "paid", strconv.FormatInt(paid, 10))151}152153func CancelReserve(cur realm, slug string) {154 requireInit()155 if !cur.Previous().IsUserCall() {156 panic("factory: EOA only")157 }158 slug = strings.ToLower(strings.TrimSpace(slug))159 res := mustReserve(slug)160 if res.who != cur.Previous().Address() {161 panic("factory: not reserver")162 }163 if bySlug.Has(slug) {164 panic("factory: already registered")165 }166 reserved.Remove(slug)167 if res.paid > 0 {168 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)169 bk.SendCoins(self, res.who, chain.Coins{{Denom: nativeDenom, Amount: res.paid}})170 }171 chain.Emit("CancelReserve", "slug", slug, "paid", strconv.FormatInt(res.paid, 10))172}173174func ReservedCreator(slug string) string {175 requireInit()176 slug = strings.ToLower(strings.TrimSpace(slug))177 if !reserved.Has(slug) {178 return ""179 }180 return reserved.Get(slug).(*reservation).who.String()181}182183func takeFactorySend(want int64) {184 sent := unsafe.OriginSend()185 if want == 0 {186 if len(sent) != 0 {187 panic("factory: extra denoms")188 }189 return190 }191 if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {192 panic("factory: wrong payment")193 }194}195196func NoteMinted(cur realm, n int) {197 requireInit()198 if n < 0 {199 panic("factory: minted")200 }201 prev := cur.Previous()202 if !prev.IsCode() {203 panic("factory: collection realm")204 }205 c := mustByPkg(prev.PkgPath())206 c.minted = n207 byPkg.Set(c.pkg, c)208 bySlug.Set(c.slug, c)209}210211func NoteRoyalty(cur realm, bps int) {212 requireInit()213 if bps < 0 || bps > maxRoyaltyBps {214 panic("factory: royalty")215 }216 prev := cur.Previous()217 if !prev.IsCode() {218 panic("factory: collection realm")219 }220 c := mustByPkg(prev.PkgPath())221 c.royaltyBps = bps222 byPkg.Set(c.pkg, c)223 bySlug.Set(c.slug, c)224}225226func CollectionOf(slug string) string {227 requireInit()228 c := mustBySlug(slug)229 return c.pkg230}231232func CollectionAddr(slug string) string {233 requireInit()234 c := mustBySlug(slug)235 return chain.PackageAddress(c.pkg).String()236}237238func RoyaltyOf(slug string) int {239 requireInit()240 c := mustBySlug(slug)241 return c.royaltyBps242}243244func ListCollections() string {245 requireInit()246 out := ""247 bySlug.Iterate("", "", func(_ string, value any) bool {248 c := value.(*collection)249 if out != "" {250 out += "\n"251 }252 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)253 return false254 })255 return out256}257258func Withdraw(cur realm) {259 requireAdmin(cur)260 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)261 ugnot := bk.GetCoins(self).AmountOf(nativeDenom)262 locked := reservedPaid()263 avail := ugnot - locked264 if avail <= 0 {265 panic("factory: no funds")266 }267 bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: avail}})268 chain.Emit("Withdraw", "amount", strconv.FormatInt(avail, 10))269}270271func TransferAdmin(cur realm, next address) {272 requireAdmin(cur)273 if !next.IsValid() {274 panic("factory: invalid address")275 }276 if next == admin {277 panic("factory: already admin")278 }279 admin = next280 chain.Emit("TransferAdmin", "addr", next.String())281}282283func Render(_ string) string {284 if !inited {285 return "# Bazaar factory\n\nCall Init first.\n"286 }287 out := "# Bazaar factory\n\n"288 out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n"289 out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n"290 lines := ListCollections()291 if lines == "" {292 out += "_No collections._\n"293 } else {294 out += "```\n" + lines + "\n```\n"295 }296 return out297}298299func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {300 if !strings.HasPrefix(pkg, "gno.land/r/") {301 panic("factory: pkg")302 }303 slug := lastElem(pkg)304 if !validSlug(slug) {305 panic("factory: slug")306 }307 if !validName(name) {308 panic("factory: name")309 }310 if !validSymbol(symbol) {311 panic("factory: symbol")312 }313 if !validImage(cover) {314 panic("factory: cover")315 }316 if maxSupply < 0 || maxSupply > maxSupplyBound {317 panic("factory: maxSupply")318 }319 if mintPrice < 0 {320 panic("factory: mintPrice")321 }322 if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {323 panic("factory: royalty")324 }325 if bySlug.Has(slug) {326 panic("factory: slug taken")327 }328 if byPkg.Has(pkg) {329 panic("factory: pkg already registered")330 }331 c := &collection{332 slug: slug,333 name: name,334 symbol: symbol,335 cover: cover,336 pkg: pkg,337 creator: creator,338 maxSupply: maxSupply,339 mintPrice: mintPrice,340 royaltyBps: int(royaltyBps),341 }342 bySlug.Set(slug, c)343 byPkg.Set(pkg, c)344 chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String())345}346347func requireInit() {348 if !inited {349 panic("factory: call Init first")350 }351}352353func requireAdmin(cur realm) {354 requireInit()355 if !cur.Previous().IsUserCall() {356 panic("factory: EOA only")357 }358 if cur.Previous().Address() != admin {359 panic("factory: not admin")360 }361}362363func mustReserve(slug string) *reservation {364 v := reserved.Get(slug)365 if v == nil {366 panic("factory: not reserved")367 }368 return v.(*reservation)369}370371func reservedPaid() int64 {372 var n int64373 reserved.Iterate("", "", func(_ string, value any) bool {374 n += value.(*reservation).paid375 return false376 })377 return n378}379380func mustBySlug(slug string) *collection {381 v := bySlug.Get(slug)382 if v == nil {383 panic("factory: unknown collection")384 }385 return v.(*collection)386}387388func mustByPkg(pkg string) *collection {389 v := byPkg.Get(pkg)390 if v == nil {391 panic("factory: unknown collection")392 }393 return v.(*collection)394}395396func lastElem(pkg string) string {397 i := strings.LastIndex(pkg, "/")398 if i < 0 || i+1 >= len(pkg) {399 panic("factory: pkg")400 }401 return pkg[i+1:]402}403404func validSlug(s string) bool {405 if len(s) < minSlugLen || len(s) > maxSlugLen {406 return false407 }408 if s[0] < 'a' || s[0] > 'z' {409 return false410 }411 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")412}413414func validSymbol(s string) bool {415 if len(s) < 1 || len(s) > maxSymbolLen {416 return false417 }418 return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")419}420421func validName(name string) bool {422 if len(name) < 1 || len(name) > maxNameLen {423 return false424 }425 return !strings.ContainsAny(name, "\n\r|")426}427428func validImage(u string) bool {429 if u == "" {430 return true431 }432 if len(u) > maxImageLen {433 return false434 }435 if strings.ContainsAny(u, " \t\n\r|<>\"'`") {436 return false437 }438 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {439 return true440 }441 const prefix = "/samples/"442 if !strings.HasPrefix(u, prefix) {443 return false444 }445 rest := u[len(prefix):]446 if rest == "" {447 return false448 }449 return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")450}451452func asciiSet(s, allowed string) bool {453 for i := 0; i < len(s); i++ {454 if !strings.ContainsRune(allowed, rune(s[i])) {455 return false456 }457 }458 return true459}460Address() 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
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.