Transaction

E0067DEEDD8B7A…5E1B45D36A49

Block 891,853 · index 0 · RPC

Summary

Hash
E0067DEEDD8B7A6C55289B879C9725F1087FE043C1A86F4D11FE5E1B45D36A49
Block
891,853
Size
19333 bytes
Gas used
26,565,141 / 100,000,000
Fee
1000000 ugnot
Status
success

Messages

Attached funds
20000000 ugnot

Arguments · 9

  1. #1nomaq
  2. #2gnomod.toml
  3. #3module = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/nomaq" gno = "0.9"
  4. #4nomaq.gno
  5. #5package nomaq import ( "chain" "chain/banker" "chain/runtime/unsafe" "strconv" "strings" "gno.land/p/nt/avl/v0" ) const ( pkgPath = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/nomaq" nativeDenom = "ugnot" defaultLaunchFee = int64(1_000_000_000) // 1000 GNOT maxNameLen = 64 maxImageLen = 200 maxSupplyBound = 1_000_000 maxRoyaltyBps = 1000 maxSymbolLen = 11 maxSlugLen = 11 minSlugLen = 2 defaultProtocolBps = int64(200) // 2% of the sale price maxProtocolBps = int64(500) // defaultGenesisShareBps is 25% of the protocol fee, not of the sale price. // 10000 would send the whole protocol fee to GenesisSink. defaultGenesisShareBps = int64(2500) maxGenesisShareBps = int64(10000) ) type collection struct { slug string name string symbol string cover string pkg string creator address maxSupply int mintPrice int64 minted int royaltyBps int } type reservation struct { who address paid int64 } var ( bySlug avl.Tree byPkg avl.Tree reserved avl.Tree inited bool admin address self address protocolSink address launchFee int64 protocolBps int64 genesisPkg string genesisSink address genesisShareBps int64 discountPusher string holderDisc avl.Tree ) func Init(cur realm) { if inited { panic("factory: already initialized") } if !cur.Previous().IsUserCall() { panic("factory: EOA only") } admin = cur.Previous().Address() protocolSink = admin self = cur.Address() bySlug = avl.Tree{} byPkg = avl.Tree{} reserved = avl.Tree{} holderDisc = avl.Tree{} genesisPkg = "" genesisShareBps = defaultGenesisShareBps discountPusher = "" var z address genesisSink = z launchFee = defaultLaunchFee protocolBps = defaultProtocolBps inited = true chain.Emit("Init", "admin", admin.String()) } func Address() address { return chain.PackageAddress(pkgPath) } func LaunchFee() int64 { requireInit() return launchFee } func Admin() string { requireInit() return admin.String() } func SetLaunchFee(cur realm, ugnot int64) { requireAdmin(cur) if ugnot < 0 { panic("factory: launch fee") } launchFee = ugnot chain.Emit("SetLaunchFee", "ugnot", strconv.FormatInt(ugnot, 10)) } func ProtocolBps() int64 { requireInit() if protocolBps <= 0 { return defaultProtocolBps } return protocolBps } func SetProtocolBps(cur realm, bps int64) { requireAdmin(cur) if bps < 0 || bps > maxProtocolBps { panic("factory: protocol bps") } protocolBps = bps chain.Emit("SetProtocolBps", "bps", strconv.FormatInt(bps, 10)) } func ProtocolSink() address { requireInit() return protocolSink } func SetProtocolSink(cur realm, next address) { requireAdmin(cur) if !next.IsValid() { panic("factory: invalid address") } if next == protocolSink { panic("factory: already sink") } protocolSink = next chain.Emit("SetProtocolSink", "addr", next.String()) } func GenesisPkg() string { requireInit() return genesisPkg } func GenesisSink() address { requireInit() return genesisSink } // GenesisShareBps is the holder share of the protocol fee, in bps of 10000. // It is 0 until a genesis package is attached. After that it returns the // stored value, including 0. The Init default is 2500 (25%). func GenesisShareBps() int64 { requireInit() if genesisPkg == "" { return 0 } return genesisShareBps } // SetGenesisShareBps changes the holder share for later sales. // bps is 0 through 10000. Only the admin EOA may call it. // Collection books must read GenesisShareBps at Buy time. func SetGenesisShareBps(cur realm, bps int64) { requireAdmin(cur) if bps < 0 || bps > maxGenesisShareBps { panic("factory: genesis share") } genesisShareBps = bps chain.Emit("SetGenesisShareBps", "bps", strconv.FormatInt(bps, 10)) } func HolderDiscount(owner string) int64 { requireInit() if genesisPkg == "" { return 0 } v := holderDisc.Get(strings.ToLower(strings.TrimSpace(owner))) if v == nil { return 0 } return v.(int64) } func AttachGenesis(cur realm) { requireInit() prev := cur.Previous() if !prev.IsCode() { panic("factory: genesis realm") } eoa := prev.Previous() if !eoa.IsUserCall() || eoa.Address() != admin { panic("factory: not admin") } pkg := prev.PkgPath() if pkg == "" { panic("factory: genesis pkg") } if genesisPkg != "" && genesisPkg != pkg { panic("factory: genesis set") } genesisPkg = pkg genesisSink = prev.Address() chain.Emit("AttachGenesis", "pkg", pkg) } func SetGenesis(cur realm, pkg string) { requireAdmin(cur) pkg = strings.TrimSpace(pkg) if pkg == "" || !strings.HasPrefix(pkg, "gno.land/r/") { panic("factory: genesis pkg") } genesisPkg = pkg genesisSink = chain.PackageAddress(pkg) chain.Emit("SetGenesis", "pkg", pkg) } func SetDiscountPusher(cur realm, pkg string) { requireAdmin(cur) pkg = strings.TrimSpace(pkg) if pkg == "" || !strings.HasPrefix(pkg, "gno.land/r/") { panic("factory: pusher pkg") } discountPusher = pkg chain.Emit("SetDiscountPusher", "pkg", pkg) } func DiscountPusher() string { requireInit() return discountPusher } func NoteHolderDiscount(cur realm, owner string, bps int64) { requireInit() prev := cur.Previous() if !prev.IsCode() { panic("factory: not genesis") } from := prev.PkgPath() ok := (genesisPkg != "" && from == genesisPkg) || (discountPusher != "" && from == discountPusher) if !ok { panic("factory: not genesis") } if bps < 0 { panic("factory: discount") } who := strings.ToLower(strings.TrimSpace(owner)) if who == "" { panic("factory: owner") } if bps == 0 { holderDisc.Remove(who) return } holderDisc.Set(who, bps) } func Register(cur realm, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) { requireInit() prev := cur.Previous() if !prev.IsCode() { panic("factory: collection realm") } creatorRlm := prev.Previous() if !creatorRlm.IsUserCall() { panic("factory: EOA creator") } pkg := prev.PkgPath() slug := lastElem(pkg) res := mustReserve(slug) if creatorRlm.Address() != res.who { panic("factory: reserved") } reserved.Remove(slug) register(pkg, creatorRlm.Address(), name, symbol, cover, maxSupply, mintPrice, royaltyBps) } func Reserve(cur realm, slug string) { requireInit() if !cur.Previous().IsUserCall() { panic("factory: EOA only") } slug = strings.ToLower(strings.TrimSpace(slug)) if !validSlug(slug) { panic("factory: slug") } if bySlug.Has(slug) || reserved.Has(slug) { panic("factory: slug taken") } caller := cur.Previous().Address() paid := reserveDue(caller) takeFactorySend(paid) reserved.Set(slug, &reservation{who: caller, paid: paid}) chain.Emit("Reserve", "slug", slug, "paid", strconv.FormatInt(paid, 10)) } // ReserveDue is the ugnot Reserve will require from addr. The factory admin // (the wallet that called Init) pays 0. Everyone else pays LaunchFee. func ReserveDue(addr string) int64 { requireInit() who, ok := parseAddr(addr) if !ok { return launchFee } return reserveDue(who) } func reserveDue(caller address) int64 { if caller == admin { return 0 } return launchFee } func parseAddr(raw string) (address, bool) { s := strings.TrimSpace(raw) if s == "" { var zero address return zero, false } who := address(s) if !who.IsValid() { var zero address return zero, false } return who, true } func CancelReserve(cur realm, slug string) { requireInit() if !cur.Previous().IsUserCall() { panic("factory: EOA only") } slug = strings.ToLower(strings.TrimSpace(slug)) res := mustReserve(slug) if res.who != cur.Previous().Address() { panic("factory: not reserver") } if bySlug.Has(slug) { panic("factory: already registered") } reserved.Remove(slug) if res.paid > 0 { bk := banker.NewBanker(banker.BankerTypeRealmSend, cur) bk.SendCoins(self, res.who, chain.Coins{{Denom: nativeDenom, Amount: res.paid}}) } chain.Emit("CancelReserve", "slug", slug, "paid", strconv.FormatInt(res.paid, 10)) } func ReservedCreator(slug string) string { requireInit() slug = strings.ToLower(strings.TrimSpace(slug)) if !reserved.Has(slug) { return "" } return reserved.Get(slug).(*reservation).who.String() } func takeFactorySend(want int64) { sent := unsafe.OriginSend() if want == 0 { if len(sent) != 0 { panic("factory: extra denoms") } return } if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want { panic("factory: wrong payment") } } func NoteMinted(cur realm, n int) { requireInit() if n < 0 { panic("factory: minted") } prev := cur.Previous() if !prev.IsCode() { panic("factory: collection realm") } c := mustByPkg(prev.PkgPath()) c.minted = n byPkg.Set(c.pkg, c) bySlug.Set(c.slug, c) } func NoteRoyalty(cur realm, bps int) { requireInit() if bps < 0 || bps > maxRoyaltyBps { panic("factory: royalty") } prev := cur.Previous() if !prev.IsCode() { panic("factory: collection realm") } c := mustByPkg(prev.PkgPath()) c.royaltyBps = bps byPkg.Set(c.pkg, c) bySlug.Set(c.slug, c) } func CollectionOf(slug string) string { requireInit() c := mustBySlug(slug) return c.pkg } func CollectionAddr(slug string) string { requireInit() c := mustBySlug(slug) return chain.PackageAddress(c.pkg).String() } func RoyaltyOf(slug string) int { requireInit() c := mustBySlug(slug) return c.royaltyBps } func ListCollections() string { requireInit() out := "" bySlug.Iterate("", "", func(_ string, value any) bool { c := value.(*collection) if out != "" { out += "\n" } 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) return false }) return out } func Withdraw(cur realm) { requireAdmin(cur) bk := banker.NewBanker(banker.BankerTypeRealmSend, cur) ugnot := bk.GetCoins(self).AmountOf(nativeDenom) locked := reservedPaid() avail := ugnot - locked if avail <= 0 { panic("factory: no funds") } bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: avail}}) chain.Emit("Withdraw", "amount", strconv.FormatInt(avail, 10)) } func TransferAdmin(cur realm, next address) { requireAdmin(cur) if !next.IsValid() { panic("factory: invalid address") } if next == admin { panic("factory: already admin") } admin = next chain.Emit("TransferAdmin", "addr", next.String()) } func Render(_ string) string { if !inited { return "# Nomaq\n\nCall Init first.\n" } out := "# Nomaq\n\n" out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n" out += "- admin reserve: 0 ugnot\n" out += "- holder share: " + strconv.FormatInt(GenesisShareBps(), 10) + " bps of the protocol fee\n" out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n" lines := ListCollections() if lines == "" { out += "_No collections._\n" } else { out += "```\n" + lines + "\n```\n" } return out } func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) { if !strings.HasPrefix(pkg, "gno.land/r/") { panic("factory: pkg") } slug := lastElem(pkg) if !validSlug(slug) { panic("factory: slug") } if !validName(name) { panic("factory: name") } if !validSymbol(symbol) { panic("factory: symbol") } if !validImage(cover) { panic("factory: cover") } if maxSupply < 0 || maxSupply > maxSupplyBound { panic("factory: maxSupply") } if mintPrice < 0 { panic("factory: mintPrice") } if royaltyBps < 0 || royaltyBps > maxRoyaltyBps { panic("factory: royalty") } if bySlug.Has(slug) { panic("factory: slug taken") } if byPkg.Has(pkg) { panic("factory: pkg already registered") } c := &collection{ slug: slug, name: name, symbol: symbol, cover: cover, pkg: pkg, creator: creator, maxSupply: maxSupply, mintPrice: mintPrice, royaltyBps: int(royaltyBps), } bySlug.Set(slug, c) byPkg.Set(pkg, c) chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String()) } func requireInit() { if !inited { panic("factory: call Init first") } } func requireAdmin(cur realm) { requireInit() if !cur.Previous().IsUserCall() { panic("factory: EOA only") } if cur.Previous().Address() != admin { panic("factory: not admin") } } func mustReserve(slug string) *reservation { v := reserved.Get(slug) if v == nil { panic("factory: not reserved") } return v.(*reservation) } func reservedPaid() int64 { var n int64 reserved.Iterate("", "", func(_ string, value any) bool { n += value.(*reservation).paid return false }) return n } func mustBySlug(slug string) *collection { v := bySlug.Get(slug) if v == nil { panic("factory: unknown collection") } return v.(*collection) } func mustByPkg(pkg string) *collection { v := byPkg.Get(pkg) if v == nil { panic("factory: unknown collection") } return v.(*collection) } func lastElem(pkg string) string { i := strings.LastIndex(pkg, "/") if i < 0 || i+1 >= len(pkg) { panic("factory: pkg") } return pkg[i+1:] } func validSlug(s string) bool { if len(s) < minSlugLen || len(s) > maxSlugLen { return false } if s[0] < 'a' || s[0] > 'z' { return false } return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789") } func validSymbol(s string) bool { if len(s) < 1 || len(s) > maxSymbolLen { return false } return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") } func validName(name string) bool { if len(name) < 1 || len(name) > maxNameLen { return false } return !strings.ContainsAny(name, "\n\r|") } func validImage(u string) bool { if u == "" { return true } if len(u) > maxImageLen { return false } if strings.ContainsAny(u, " \t\n\r|<>\"'`") { return false } if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") { return true } const prefix = "/samples/" if !strings.HasPrefix(u, prefix) { return false } rest := u[len(prefix):] if rest == "" { return false } return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-") } func asciiSet(s, allowed string) bool { for i := 0; i < len(s); i++ { if !strings.ContainsRune(allowed, rune(s[i])) { return false } } return true }
  6. #6nomaq_test.gno
  7. #7package nomaq import ( "chain" "strings" "testing" "gno.land/p/nt/avl/v0" "gno.land/p/nt/testutils/v0" ) const perk2 = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/perk2" func resetAll() { bySlug = avl.Tree{} byPkg = avl.Tree{} reserved = avl.Tree{} holderDisc = avl.Tree{} inited = false launchFee = 0 protocolBps = 0 genesisPkg = "" genesisShareBps = 0 discountPusher = "" var zero address admin = zero self = zero protocolSink = zero genesisSink = zero } func TestGenesisShareDefault(cur realm, t *testing.T) { resetAll() alice := testutils.TestAddress("alice") testing.SetRealm(testing.NewUserRealm(alice)) Init(cross(cur)) if GenesisShareBps() != 0 { t.Fatalf("share before genesis %d", GenesisShareBps()) } if ProtocolBps() != defaultProtocolBps { t.Fatalf("protocol %d", ProtocolBps()) } SetGenesis(cross(cur), perk2) if GenesisPkg() != perk2 { t.Fatalf("pkg %q", GenesisPkg()) } if GenesisSink() != chain.PackageAddress(perk2) { t.Fatalf("sink %s", GenesisSink()) } if GenesisShareBps() != defaultGenesisShareBps { t.Fatalf("share %d", GenesisShareBps()) } if GenesisShareBps() == 1100 { t.Fatalf("v5 fallback must not apply") } if !strings.Contains(Render(""), "holder share: 2500 bps") { t.Fatalf("render %q", Render("")) } } func TestSetGenesisShareBps(cur realm, t *testing.T) { resetAll() alice := testutils.TestAddress("alice") testing.SetRealm(testing.NewUserRealm(alice)) Init(cross(cur)) SetGenesisShareBps(cross(cur), 3000) if GenesisShareBps() != 0 { t.Fatalf("share before genesis %d", GenesisShareBps()) } SetGenesis(cross(cur), perk2) if GenesisShareBps() != 3000 { t.Fatalf("kept setter %d", GenesisShareBps()) } SetGenesisShareBps(cross(cur), 0) if GenesisShareBps() != 0 { t.Fatalf("zero share %d", GenesisShareBps()) } SetGenesisShareBps(cross(cur), maxGenesisShareBps) if GenesisShareBps() != 10000 { t.Fatalf("full share %d", GenesisShareBps()) } SetGenesisShareBps(cross(cur), 2500) if GenesisShareBps() != 2500 { t.Fatalf("restore %d", GenesisShareBps()) } } func TestSetGenesisDoesNotResetShare(cur realm, t *testing.T) { resetAll() alice := testutils.TestAddress("alice") testing.SetRealm(testing.NewUserRealm(alice)) Init(cross(cur)) SetGenesisShareBps(cross(cur), 0) SetGenesis(cross(cur), perk2) if GenesisShareBps() != 0 { t.Fatalf("set genesis clobbered zero %d", GenesisShareBps()) } SetGenesis(cross(cur), perk2) if GenesisShareBps() != 0 { t.Fatalf("second set genesis %d", GenesisShareBps()) } } func TestSetGenesisShareRejected(cur realm, t *testing.T) { resetAll() alice := testutils.TestAddress("alice") bob := testutils.TestAddress("bob") testing.SetRealm(testing.NewUserRealm(alice)) Init(cross(cur)) SetGenesis(cross(cur), perk2) got := revive(func() { SetGenesisShareBps(cross(cur), -1) }) if got == nil { t.Fatalf("expected negative reject") } got = revive(func() { SetGenesisShareBps(cross(cur), maxGenesisShareBps+1) }) if got == nil { t.Fatalf("expected above max reject") } if GenesisShareBps() != defaultGenesisShareBps { t.Fatalf("rejected call changed share %d", GenesisShareBps()) } testing.SetRealm(testing.NewUserRealm(bob)) got = revive(func() { SetGenesisShareBps(cross(cur), 2500) }) if got == nil { t.Fatalf("expected not admin") } testing.SetRealm(testing.NewCodeRealm(perk2)) got = revive(func() { SetGenesisShareBps(cross(cur), 2500) }) if got == nil { t.Fatalf("expected EOA only") } if GenesisShareBps() != defaultGenesisShareBps { t.Fatalf("rejected caller changed share %d", GenesisShareBps()) } } func TestAttachGenesisKeepsShare(cur realm, t *testing.T) { resetAll() alice := testutils.TestAddress("alice") testing.SetRealm(testing.NewUserRealm(alice)) Init(cross(cur)) SetGenesis(cross(cur), perk2) testing.SetRealm(testing.NewUserRealm(alice)) got := revive(func() { AttachGenesis(cross(cur)) }) if got == nil { t.Fatalf("expected genesis realm") } testing.SetRealm(testing.NewCodeRealm("gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/other")) got = revive(func() { AttachGenesis(cross(cur)) }) if got == nil { t.Fatalf("expected not admin") } if GenesisPkg() != perk2 { t.Fatalf("pkg changed %q", GenesisPkg()) } if GenesisShareBps() != defaultGenesisShareBps { t.Fatalf("share changed %d", GenesisShareBps()) } }
  8. #8/gno.MemPackageType
  9. #9 MPUserAll

Result log

msg:0,success:true,log:,events:[]