1package bazaarv323import (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/bazaarv3"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 holderDisc avl.Tree60)6162func Init(cur realm) {63 if inited {64 panic("factory: already initialized")65 }66 if !cur.Previous().IsUserCall() {67 panic("factory: EOA only")68 }69 admin = cur.Previous().Address()70 protocolSink = admin71 self = cur.Address()72 bySlug = avl.Tree{}73 byPkg = avl.Tree{}74 reserved = avl.Tree{}75 holderDisc = avl.Tree{}76 genesisPkg = ""77 genesisShareBps = 078 var z address79 genesisSink = z80 launchFee = defaultLaunchFee81 protocolBps = defaultProtocolBps82 inited = true83 chain.Emit("Init", "admin", admin.String())84}8586func Address() address {87 return chain.PackageAddress(pkgPath)88}8990func LaunchFee() int64 {91 requireInit()92 return launchFee93}9495func Admin() string {96 requireInit()97 return admin.String()98}99100func SetLaunchFee(cur realm, ugnot int64) {101 requireAdmin(cur)102 if ugnot < 0 {103 panic("factory: launch fee")104 }105 launchFee = ugnot106 chain.Emit("SetLaunchFee", "ugnot", strconv.FormatInt(ugnot, 10))107}108109func ProtocolBps() int64 {110 requireInit()111 if protocolBps <= 0 {112 return defaultProtocolBps113 }114 return protocolBps115}116117func SetProtocolBps(cur realm, bps int64) {118 requireAdmin(cur)119 if bps < 0 || bps > maxProtocolBps {120 panic("factory: protocol bps")121 }122 protocolBps = bps123 chain.Emit("SetProtocolBps", "bps", strconv.FormatInt(bps, 10))124}125126func ProtocolSink() address {127 requireInit()128 return protocolSink129}130131func SetProtocolSink(cur realm, next address) {132 requireAdmin(cur)133 if !next.IsValid() {134 panic("factory: invalid address")135 }136 if next == protocolSink {137 panic("factory: already sink")138 }139 protocolSink = next140 chain.Emit("SetProtocolSink", "addr", next.String())141}142143func GenesisPkg() string {144 requireInit()145 return genesisPkg146}147148func GenesisSink() address {149 requireInit()150 return genesisSink151}152153func GenesisShareBps() int64 {154 requireInit()155 if genesisPkg == "" {156 return 0157 }158 if genesisShareBps <= 0 {159 return 1100160 }161 return genesisShareBps162}163164func HolderDiscount(owner string) int64 {165 requireInit()166 if genesisPkg == "" {167 return 0168 }169 v := holderDisc.Get(strings.ToLower(strings.TrimSpace(owner)))170 if v == nil {171 return 0172 }173 return v.(int64)174}175176func AttachGenesis(cur realm) {177 requireInit()178 prev := cur.Previous()179 if !prev.IsCode() {180 panic("factory: genesis realm")181 }182 eoa := prev.Previous()183 if !eoa.IsUserCall() || eoa.Address() != admin {184 panic("factory: not admin")185 }186 pkg := prev.PkgPath()187 if pkg == "" {188 panic("factory: genesis pkg")189 }190 if genesisPkg != "" && genesisPkg != pkg {191 panic("factory: genesis set")192 }193 genesisPkg = pkg194 genesisSink = prev.Address()195 if genesisShareBps <= 0 {196 genesisShareBps = 1100197 }198 chain.Emit("AttachGenesis", "pkg", pkg)199}200201func SetGenesis(cur realm, pkg string) {202 requireAdmin(cur)203 pkg = strings.TrimSpace(pkg)204 if pkg == "" || !strings.HasPrefix(pkg, "gno.land/r/") {205 panic("factory: genesis pkg")206 }207 genesisPkg = pkg208 genesisSink = chain.PackageAddress(pkg)209 if genesisShareBps <= 0 {210 genesisShareBps = 1100211 }212 chain.Emit("SetGenesis", "pkg", pkg)213}214215func NoteHolderDiscount(cur realm, owner string, bps int64) {216 requireInit()217 prev := cur.Previous()218 if genesisPkg == "" || !prev.IsCode() || prev.PkgPath() != genesisPkg {219 panic("factory: not genesis")220 }221 if bps < 0 {222 panic("factory: discount")223 }224 who := strings.ToLower(strings.TrimSpace(owner))225 if who == "" {226 panic("factory: owner")227 }228 if bps == 0 {229 holderDisc.Remove(who)230 return231 }232 holderDisc.Set(who, bps)233}234235func Register(cur realm, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {236 requireInit()237 prev := cur.Previous()238 if !prev.IsCode() {239 panic("factory: collection realm")240 }241 creatorRlm := prev.Previous()242 if !creatorRlm.IsUserCall() {243 panic("factory: EOA creator")244 }245 pkg := prev.PkgPath()246 slug := lastElem(pkg)247 res := mustReserve(slug)248 if creatorRlm.Address() != res.who {249 panic("factory: reserved")250 }251 reserved.Remove(slug)252 register(pkg, creatorRlm.Address(), name, symbol, cover, maxSupply, mintPrice, royaltyBps)253}254255func Reserve(cur realm, slug string) {256 requireInit()257 if !cur.Previous().IsUserCall() {258 panic("factory: EOA only")259 }260 slug = strings.ToLower(strings.TrimSpace(slug))261 if !validSlug(slug) {262 panic("factory: slug")263 }264 if bySlug.Has(slug) || reserved.Has(slug) {265 panic("factory: slug taken")266 }267 paid := launchFee268 takeFactorySend(paid)269 reserved.Set(slug, &reservation{who: cur.Previous().Address(), paid: paid})270 chain.Emit("Reserve", "slug", slug, "paid", strconv.FormatInt(paid, 10))271}272273func CancelReserve(cur realm, slug string) {274 requireInit()275 if !cur.Previous().IsUserCall() {276 panic("factory: EOA only")277 }278 slug = strings.ToLower(strings.TrimSpace(slug))279 res := mustReserve(slug)280 if res.who != cur.Previous().Address() {281 panic("factory: not reserver")282 }283 if bySlug.Has(slug) {284 panic("factory: already registered")285 }286 reserved.Remove(slug)287 if res.paid > 0 {288 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)289 bk.SendCoins(self, res.who, chain.Coins{{Denom: nativeDenom, Amount: res.paid}})290 }291 chain.Emit("CancelReserve", "slug", slug, "paid", strconv.FormatInt(res.paid, 10))292}293294func ReservedCreator(slug string) string {295 requireInit()296 slug = strings.ToLower(strings.TrimSpace(slug))297 if !reserved.Has(slug) {298 return ""299 }300 return reserved.Get(slug).(*reservation).who.String()301}302303func takeFactorySend(want int64) {304 sent := unsafe.OriginSend()305 if want == 0 {306 if len(sent) != 0 {307 panic("factory: extra denoms")308 }309 return310 }311 if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {312 panic("factory: wrong payment")313 }314}315316func NoteMinted(cur realm, n int) {317 requireInit()318 if n < 0 {319 panic("factory: minted")320 }321 prev := cur.Previous()322 if !prev.IsCode() {323 panic("factory: collection realm")324 }325 c := mustByPkg(prev.PkgPath())326 c.minted = n327 byPkg.Set(c.pkg, c)328 bySlug.Set(c.slug, c)329}330331func NoteRoyalty(cur realm, bps int) {332 requireInit()333 if bps < 0 || bps > maxRoyaltyBps {334 panic("factory: royalty")335 }336 prev := cur.Previous()337 if !prev.IsCode() {338 panic("factory: collection realm")339 }340 c := mustByPkg(prev.PkgPath())341 c.royaltyBps = bps342 byPkg.Set(c.pkg, c)343 bySlug.Set(c.slug, c)344}345346func CollectionOf(slug string) string {347 requireInit()348 c := mustBySlug(slug)349 return c.pkg350}351352func CollectionAddr(slug string) string {353 requireInit()354 c := mustBySlug(slug)355 return chain.PackageAddress(c.pkg).String()356}357358func RoyaltyOf(slug string) int {359 requireInit()360 c := mustBySlug(slug)361 return c.royaltyBps362}363364func ListCollections() string {365 requireInit()366 out := ""367 bySlug.Iterate("", "", func(_ string, value any) bool {368 c := value.(*collection)369 if out != "" {370 out += "\n"371 }372 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)373 return false374 })375 return out376}377378func Withdraw(cur realm) {379 requireAdmin(cur)380 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)381 ugnot := bk.GetCoins(self).AmountOf(nativeDenom)382 locked := reservedPaid()383 avail := ugnot - locked384 if avail <= 0 {385 panic("factory: no funds")386 }387 bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: avail}})388 chain.Emit("Withdraw", "amount", strconv.FormatInt(avail, 10))389}390391func TransferAdmin(cur realm, next address) {392 requireAdmin(cur)393 if !next.IsValid() {394 panic("factory: invalid address")395 }396 if next == admin {397 panic("factory: already admin")398 }399 admin = next400 chain.Emit("TransferAdmin", "addr", next.String())401}402403func Render(_ string) string {404 if !inited {405 return "# Bazaar factory\n\nCall Init first.\n"406 }407 out := "# Bazaar factory\n\n"408 out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n"409 out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n"410 lines := ListCollections()411 if lines == "" {412 out += "_No collections._\n"413 } else {414 out += "```\n" + lines + "\n```\n"415 }416 return out417}418419func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {420 if !strings.HasPrefix(pkg, "gno.land/r/") {421 panic("factory: pkg")422 }423 slug := lastElem(pkg)424 if !validSlug(slug) {425 panic("factory: slug")426 }427 if !validName(name) {428 panic("factory: name")429 }430 if !validSymbol(symbol) {431 panic("factory: symbol")432 }433 if !validImage(cover) {434 panic("factory: cover")435 }436 if maxSupply < 0 || maxSupply > maxSupplyBound {437 panic("factory: maxSupply")438 }439 if mintPrice < 0 {440 panic("factory: mintPrice")441 }442 if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {443 panic("factory: royalty")444 }445 if bySlug.Has(slug) {446 panic("factory: slug taken")447 }448 if byPkg.Has(pkg) {449 panic("factory: pkg already registered")450 }451 c := &collection{452 slug: slug,453 name: name,454 symbol: symbol,455 cover: cover,456 pkg: pkg,457 creator: creator,458 maxSupply: maxSupply,459 mintPrice: mintPrice,460 royaltyBps: int(royaltyBps),461 }462 bySlug.Set(slug, c)463 byPkg.Set(pkg, c)464 chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String())465}466467func requireInit() {468 if !inited {469 panic("factory: call Init first")470 }471}472473func requireAdmin(cur realm) {474 requireInit()475 if !cur.Previous().IsUserCall() {476 panic("factory: EOA only")477 }478 if cur.Previous().Address() != admin {479 panic("factory: not admin")480 }481}482483func mustReserve(slug string) *reservation {484 v := reserved.Get(slug)485 if v == nil {486 panic("factory: not reserved")487 }488 return v.(*reservation)489}490491func reservedPaid() int64 {492 var n int64493 reserved.Iterate("", "", func(_ string, value any) bool {494 n += value.(*reservation).paid495 return false496 })497 return n498}499500func mustBySlug(slug string) *collection {501 v := bySlug.Get(slug)502 if v == nil {503 panic("factory: unknown collection")504 }505 return v.(*collection)506}507508func mustByPkg(pkg string) *collection {509 v := byPkg.Get(pkg)510 if v == nil {511 panic("factory: unknown collection")512 }513 return v.(*collection)514}515516func lastElem(pkg string) string {517 i := strings.LastIndex(pkg, "/")518 if i < 0 || i+1 >= len(pkg) {519 panic("factory: pkg")520 }521 return pkg[i+1:]522}523524func validSlug(s string) bool {525 if len(s) < minSlugLen || len(s) > maxSlugLen {526 return false527 }528 if s[0] < 'a' || s[0] > 'z' {529 return false530 }531 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")532}533534func validSymbol(s string) bool {535 if len(s) < 1 || len(s) > maxSymbolLen {536 return false537 }538 return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")539}540541func validName(name string) bool {542 if len(name) < 1 || len(name) > maxNameLen {543 return false544 }545 return !strings.ContainsAny(name, "\n\r|")546}547548func validImage(u string) bool {549 if u == "" {550 return true551 }552 if len(u) > maxImageLen {553 return false554 }555 if strings.ContainsAny(u, " \t\n\r|<>\"'`") {556 return false557 }558 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {559 return true560 }561 const prefix = "/samples/"562 if !strings.HasPrefix(u, prefix) {563 return false564 }565 rest := u[len(prefix):]566 if rest == "" {567 return false568 }569 return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")570}571572func asciiSet(s, allowed string) bool {573 for i := 0; i < len(s); i++ {574 if !strings.ContainsRune(allowed, rune(s[i])) {575 return false576 }577 }578 return true579}580Address() 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
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
RoyaltyOf(slug string) int
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.