Transaction
B2BC756FD87DE8…D6879169D081
Block 756,886 · index 0 · indexed
Summary
- Hash
- B2BC756FD87DE887EBB3F803CAEA70D44FA008A52ADEB46D58B2D6879169D081
- Block
- 756,886
- Size
- 31932 bytes
- Gas used
- 42,499,184 / 100,000,000
- Fee
- 150000 ugnot
- Status
- success
Messages
- Attached funds
- 20000000 ugnot
Arguments · 19
- #1guardians_pilot_v1
- #2allowlist.gno
- #3package guardians_pilot_v1 import ( "chain" "chain/runtime" "chain/runtime/unsafe" "crypto/sha256" "encoding/hex" "strings" "gno.land/p/nt/ufmt/v0" ) const maxMerkleProofDepth = 32 var ( allowlistRoot string allowlistRootLocked bool ) func AllowlistRoot() string { return allowlistRoot } func AllowlistRootLocked() bool { return allowlistRootLocked } // SetAllowlistRoot accepts a lowercase/uppercase 32-byte SHA-256 hash in hex. // The first sale unpause permanently locks the root for this deployment. func SetAllowlistRoot(cur realm, root string) { assertAdmin(cur.Previous().Address()) if !salePaused { panic("guardians: pause sale before setting allowlist root") } if allowlistRootLocked { panic("guardians: allowlist root is locked") } decoded, ok := decodeMerkleHash(root) if !ok || len(decoded) != sha256.Size { panic("guardians: invalid allowlist root") } allowlistRoot = hex.EncodeToString(decoded) chain.Emit("AllowlistRootConfigured", "root", allowlistRoot) } // AllowlistLeaf binds a claim to this chain, collection, phase, address, // individual allowance and configured unit price. func AllowlistLeaf(owner address, allowance int, unitPrice int64) string { payload := runtime.ChainID() + "|" + CollectionID() + "|" + salePhaseAllowlist + "|" + owner.String() + "|" + ufmt.Sprintf("%d", allowance) + "|" + ufmt.Sprintf("%d", unitPrice) return hashBytes([]byte(payload)) } // VerifyAllowlist expects comma-separated 64-character hex sibling hashes. // Pair hashing is sorted, so proofs do not need left/right direction flags. func VerifyAllowlist(owner address, allowance int, unitPrice int64, proof string) bool { if allowlistRoot == "" || allowance < 1 || allowance > maxSupplyAsInt() { return false } computed := AllowlistLeaf(owner, allowance, unitPrice) siblings, ok := parseMerkleProof(proof) if !ok { return false } for _, sibling := range siblings { computed = hashMerklePair(computed, sibling) } return computed == allowlistRoot } func MintAllowlist(cur realm, quantity, allowance int, unitPrice int64, proof string) { if !cur.Previous().IsUserCall() { panic("guardians: only direct user calls can mint") } if SalePhase() != salePhaseOpen { panic("guardians: allowlist sale is not active") } if unitPrice != saleConfig.allowlistPrice { panic("guardians: allowlist price mismatch") } if allowance < 1 || allowance > saleConfig.allowlistLimit { panic("guardians: invalid allowlist allowance") } caller := cur.Previous().Address() if !VerifyAllowlist(caller, allowance, unitPrice, proof) { panic("guardians: invalid allowlist proof") } assertSaleMint(quantity, AllowlistMinted(caller), allowance) required := requiredPayment(unitPrice, quantity) sent := unsafe.OriginSend() assertExactPayment(sent, required) mintTokens(caller, quantity) allowlistMintedByWallet.Set(caller.String(), AllowlistMinted(caller)+quantity) forwardPayment(cur, sent) chain.Emit( "AllowlistMint", "minter", caller.String(), "quantity", ufmt.Sprintf("%d", quantity), "allowance", ufmt.Sprintf("%d", allowance), "paid", ufmt.Sprintf("%d", required), ) } func parseMerkleProof(proof string) ([]string, bool) { if proof == "" { return []string{}, true } parts := strings.Split(proof, ",") if len(parts) > maxMerkleProofDepth { return nil, false } for i, part := range parts { decoded, ok := decodeMerkleHash(part) if !ok { return nil, false } parts[i] = hex.EncodeToString(decoded) } return parts, true } func hashMerklePair(first, second string) string { left, leftOK := decodeMerkleHash(first) right, rightOK := decodeMerkleHash(second) if !leftOK || !rightOK { return "" } if second < first { left, right = right, left } combined := make([]byte, 0, sha256.Size*2) combined = append(combined, left...) combined = append(combined, right...) return hashBytes(combined) } func hashBytes(data []byte) string { sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) } func decodeMerkleHash(value string) ([]byte, bool) { if len(value) != sha256.Size*2 { return nil, false } decoded, err := hex.DecodeString(value) if err != nil || len(decoded) != sha256.Size { return nil, false } return decoded, true } func maxSupplyAsInt() int { return int(maxSupply) }
- #4extensions.gno
- #5package guardians_pilot_v1 import ( "gno.land/p/nt/grc721/enumerable/v0" "gno.land/p/nt/grc721/metadata/v0" "gno.land/p/nt/grc721/royalty/v0" "gno.land/p/nt/grc721/v0" ) const ( defaultRoyaltyBps = int64(400) // 4% creator royalty on marketplace sales. royaltyMaxBps = int64(1000) // Permanent collection cap: 10%. ) // Replace this value through SetVeiledURI before the first public mint. // The explicit placeholder prevents an unfinished CID from looking production-ready. var veiledURI = "ipfs://REPLACE_WITH_VEILED_METADATA_CID/metadata.json" var ( metadataView *metadata.Metadata metadataLedger *metadata.Ledger enumerableView *enumerable.Enumerable enumerableLedger *enumerable.Ledger royaltyView *royalty.Royalty royaltyLedger *royalty.Ledger ) // attachExtensions must run before the first mint so the enumerable index and // metadata hooks observe every token from genesis. func attachExtensions(coreLedger *grc721.PrivateLedger) { metadataView, metadataLedger = metadata.NewMetadata(coreLedger) enumerableView, enumerableLedger = enumerable.NewEnumerable(coreLedger) royaltyView, royaltyLedger = royalty.NewRoyalty(coreLedger, royaltyMaxBps) if err := royaltyLedger.SetDefaultRoyalty(localAdmin, defaultRoyaltyBps); err != nil { panic(err) } } // Metadata / veil facade. func VeiledURI() string { return veiledURI } func TokenURI(tokenID string) string { uri, err := metadataView.TokenURI(grc721.TokenID(tokenID)) if err != nil { panic(err) } return uri } // SetVeiledURI allows the final placeholder CID to be configured once before // minting begins. Existing token URIs are never silently rewritten. func SetVeiledURI(cur realm, uri string) { assertAdmin(cur.Previous().Address()) supply := token.TotalSupply() if !canConfigureVeiledURI(uri, supply) { if supply != 0 { panic("guardians: veiled uri is locked after the first mint") } panic("guardians: veiled uri cannot be empty") } veiledURI = uri } func validVeiledURI(uri string) bool { return uri != "" } func canConfigureVeiledURI(uri string, supply int64) bool { return supply == 0 && validVeiledURI(uri) } func setInitialTokenURI(tokenID grc721.TokenID) { if err := metadataLedger.SetTokenURI(tokenID, veiledURI); err != nil { panic(err) } } // Enumerable facade. func EnumerableSupply() int64 { return enumerableView.TotalSupply() } func TokenByIndex(index int64) string { tokenID, err := enumerableView.TokenByIndex(index) if err != nil { panic(err) } return tokenID.String() } func TokenOfOwnerByIndex(owner address, index int64) string { tokenID, err := enumerableView.TokenOfOwnerByIndex(owner, index) if err != nil { panic(err) } return tokenID.String() } // Royalty facade. This is EIP-2981-style royalty information. Actual payment // enforcement belongs in the project marketplace settlement realm. func RoyaltyMaxBps() int64 { return royaltyView.MaxBps() } func DefaultRoyalty() (address, int64) { info, ok := royaltyView.DefaultRoyalty() if !ok { return address(""), 0 } return info.Receiver, info.Bps } func RoyaltyInfo(tokenID string, salePrice int64) (address, int64) { receiver, amount, err := royaltyView.RoyaltyInfo(grc721.TokenID(tokenID), salePrice) if err != nil { panic(err) } return receiver, amount } func SetDefaultRoyalty(cur realm, receiver address, bps int64) { assertAdmin(cur.Previous().Address()) if !validRoyaltyBps(bps) { panic("guardians: royalty bps out of range") } if err := royaltyLedger.SetDefaultRoyalty(receiver, bps); err != nil { panic(err) } } func validRoyaltyBps(bps int64) bool { return bps >= 0 && bps <= royaltyMaxBps }
- #6gnomod.toml
- #7module = "gno.land/r/g1j4ehkkrqcr53a233wamtvqggt3skr86c6777xd/guardians_pilot_v1" gno = "0.9"
- #8guardians.gno
- #9package guardians_pilot_v1 import ( "gno.land/p/nt/grc721/v0" "gno.land/p/nt/ufmt/v0" ) const ( collectionName = "Guardians TEST" collectionSymbol = "GTEST" maxSupply = int64(10) maxMintBatch = 4 ) // The pilot owner is fixed independently of the production collection. const localAdmin address = "g1j4ehkkrqcr53a233wamtvqggt3skr86c6777xd" func PilotVersion() string { return "GUARDIANS_PILOT_V1" } var ( token *grc721.Token ledger *grc721.PrivateLedger callerTeller grc721.Teller ) func init(cur realm) { token, ledger = grc721.NewToken(collectionName, collectionSymbol, 0, cur) callerTeller = ledger.CallerTeller() attachExtensions(ledger) } // Collection getters. func Status() string { return "REVEAL_ENGINE_READY" } func Name() string { return token.GetName() } func Symbol() string { return token.GetSymbol() } func CollectionID() string { return token.ID() } func TotalSupply() int64 { return token.TotalSupply() } func MaxSupply() int64 { return maxSupply } func Admin() address { return localAdmin } // GRC-721 read facade. func BalanceOf(owner address) int64 { balance, err := token.BalanceOf(owner) if err != nil { panic(err) } return balance } func OwnerOf(tokenID string) address { owner, err := token.OwnerOf(grc721.TokenID(tokenID)) if err != nil { panic(err) } return owner } func GetApproved(tokenID string) address { approved, err := token.GetApproved(grc721.TokenID(tokenID)) if err != nil { panic(err) } return approved } func IsApprovedForAll(owner, operator address) bool { return token.IsApprovedForAll(owner, operator) } // GRC-721 write facade. CallerTeller derives the acting account from the // current cross-realm frame and keeps the PrivateLedger inside this realm. func Approve(cur realm, to address, tokenID string) { if err := callerTeller.Approve(0, cur, to, grc721.TokenID(tokenID)); err != nil { panic(err) } } func SetApprovalForAll(cur realm, operator address, approved bool) { if err := callerTeller.SetApprovalForAll(0, cur, operator, approved); err != nil { panic(err) } } func TransferFrom(cur realm, from, to address, tokenID string) { if cur.Previous().Address() != from { panic("guardians: operator transfers require marketplace settlement") } if err := callerTeller.TransferFrom(0, cur, from, to, grc721.TokenID(tokenID)); err != nil { panic(err) } } // AdminMint is for reserve/local-development minting and is permanently locked // after the first sale activation to prevent post-sale rare-token allocation. func AdminMint(cur realm, to address, quantity int) { assertAdmin(cur.Previous().Address()) assertMintingAllowed() if !salePaused || allowlistRootLocked { panic("guardians: admin mint is locked after sale activation") } mintTokens(to, quantity) } func mintTokens(to address, quantity int) { assertMintingAllowed() assertMintQuantity(quantity) if token.TotalSupply()+int64(quantity) > maxSupply { panic("guardians: max supply reached") } for i := 0; i < quantity; i++ { nextID := token.TotalSupply() + 1 tokenID := grc721.TokenID(ufmt.Sprintf("%d", nextID)) if err := ledger.Mint(to, tokenID); err != nil { panic(err) } setInitialTokenURI(tokenID) } } func Render(path string) string { if path != "" { return "404\n" } out := token.RenderHome() out += ufmt.Sprintf("* **Maximum supply**: %d\n", maxSupply) out += ufmt.Sprintf("* **Sale phase**: %s\n", SalePhase()) out += ufmt.Sprintf("* **Sale paused**: %t\n", SalePaused()) out += ufmt.Sprintf("* **Status**: %s\n", Status()) return out } func assertAdmin(caller address) { if !isAdmin(caller) { panic("guardians: restricted access") } } func isAdmin(caller address) bool { return caller == localAdmin } func assertMintQuantity(quantity int) { if !validMintQuantity(quantity) { panic(ufmt.Sprintf("guardians: quantity must be between 1 and %d", maxMintBatch)) } } func validMintQuantity(quantity int) bool { return quantity >= 1 && quantity <= maxMintBatch }
- #10marketplace.gno
- #11package guardians_pilot_v1 import ( "chain" "gno.land/p/nt/avl/v0" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/ufmt/v0" ) var ( authorizedMarketplace address settledMarketNonces avl.Tree ) func AuthorizedMarketplace() address { return authorizedMarketplace } // SetAuthorizedMarketplace is a one-time binding made before minting. Keeping // the PrivateLedger and Token handle private prevents other approved operators // from bypassing the official royalty settlement path. func SetAuthorizedMarketplace(cur realm, marketplace address) { assertAdmin(cur.Previous().Address()) if authorizedMarketplace.IsValid() { panic("guardians: authorized marketplace is locked") } if !marketplace.IsValid() { panic("guardians: invalid marketplace address") } if token.TotalSupply() != 0 || !salePaused { panic("guardians: configure marketplace before minting and sale activation") } authorizedMarketplace = marketplace chain.Emit("MarketplaceAuthorized", "marketplace", marketplace.String()) } // MarketplaceTransfer is the only operator-transfer route. The authorized // market supplies its consumed listing nonce and gross price for audit events. func MarketplaceTransfer(cur realm, seller, buyer address, tokenID string, grossPrice, nonce int64) { marketplace := cur.Previous().Address() if marketplace != authorizedMarketplace || !authorizedMarketplace.IsValid() { panic("guardians: caller is not authorized marketplace") } if grossPrice < 1 || nonce < 1 { panic("guardians: invalid marketplace settlement") } nonceKey := marketplace.String() + ":" + ufmt.Sprintf("%d", nonce) if settledMarketNonces.Has(nonceKey) { panic("guardians: marketplace nonce already settled") } if OwnerOf(tokenID) != seller { panic("guardians: marketplace seller is not token owner") } if !token.IsApprovedForAll(seller, marketplace) { panic("guardians: marketplace operator approval required") } settledMarketNonces.Set(nonceKey, true) if err := ledger.TransferFrom(marketplace, seller, buyer, grc721.TokenID(tokenID)); err != nil { panic(err) } chain.Emit( "MarketplaceSettlementTransfer", "marketplace", marketplace.String(), "tokenId", tokenID, "seller", seller.String(), "buyer", buyer.String(), "grossPrice", ufmt.Sprintf("%d", grossPrice), "nonce", ufmt.Sprintf("%d", nonce), ) }
- #12mint.gno
- #13package guardians_pilot_v1 import ( "chain" "chain/runtime/unsafe" "gno.land/p/nt/ufmt/v0" ) // Mint applies an explicit free/paid split in one atomic direct-user call. // The caller reviews the split and payment in Adena before signing. func Mint(cur realm, freeQuantity, paidQuantity, allowance int, unitPrice int64, proof string) { if !cur.Previous().IsUserCall() { panic("guardians: only direct user calls can mint") } if SalePhase() != salePhaseOpen { panic("guardians: sale is not active") } if freeQuantity < 0 || freeQuantity > 1 || paidQuantity < 0 || paidQuantity > 3 { panic("guardians: invalid free/paid quantity") } quantity := freeQuantity + paidQuantity assertMintQuantity(quantity) caller := cur.Previous().Address() if freeQuantity > 0 { if allowance != 1 || unitPrice != 0 || !VerifyAllowlist(caller, allowance, unitPrice, proof) { panic("guardians: invalid free-mint proof") } assertSaleMint(freeQuantity, AllowlistMinted(caller), saleConfig.allowlistLimit) } else if allowance != 0 || unitPrice != 0 || proof != "" { panic("guardians: unexpected free-mint arguments") } if paidQuantity > 0 { assertSaleMint(paidQuantity, PublicMinted(caller), saleConfig.publicLimit) } required := int64(0) if paidQuantity > 0 { required = requiredPayment(saleConfig.publicPrice, paidQuantity) } sent := unsafe.OriginSend() assertExactPayment(sent, required) mintTokens(caller, quantity) if freeQuantity > 0 { allowlistMintedByWallet.Set(caller.String(), AllowlistMinted(caller)+freeQuantity) } if paidQuantity > 0 { publicMintedByWallet.Set(caller.String(), PublicMinted(caller)+paidQuantity) } forwardPayment(cur, sent) chain.Emit("PilotMint", "minter", caller.String(), "free", ufmt.Sprintf("%d", freeQuantity), "paid", ufmt.Sprintf("%d", paidQuantity), "payment", ufmt.Sprintf("%d", required)) }
- #14reveal.gno
- #15package guardians_pilot_v1 import ( "chain" "chain/runtime" "crypto/sha256" "encoding/hex" "strconv" "strings" "time" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/ufmt/v0" ) const ( revealStateVeiled = "VEILED" revealStateCommitted = "REVEAL_COMMITTED" revealStateRevealing = "REVEALING" revealStateRevealed = "REVEALED" revealStateUnveiledFrozen = "UNVEILED_FROZEN" maxRevealBatch = 20 ) var ( revealState = revealStateVeiled provenanceRoot string revealCommitment string finalBaseURI string revealDeadline int64 revealSeedHash string permutationA int64 permutationB int64 revealTarget int64 revealProcessed int64 ) func RevealState() string { return revealState } func ProvenanceRoot() string { return provenanceRoot } func RevealCommitment() string { return revealCommitment } func FinalBaseURI() string { return finalBaseURI } func RevealDeadline() int64 { return revealDeadline } func RevealSeedHash() string { return revealSeedHash } func RevealTarget() int64 { return revealTarget } func RevealProcessed() int64 { return revealProcessed } func RevealOverdue() bool { return revealState == revealStateCommitted && time.Now().Unix() > revealDeadline } // ConfigureReveal permanently commits provenance, secret hash and final URI // layout before the first token can be minted. func ConfigureReveal(cur realm, provenance, commitment, baseURI string, deadline int64) { assertAdmin(cur.Previous().Address()) if revealState != revealStateVeiled { panic("guardians: reveal configuration already committed") } if token.TotalSupply() != 0 || !salePaused { panic("guardians: configure reveal before minting and sale activation") } normalizedProvenance := normalizeHash(provenance) if normalizedProvenance == "" { panic("guardians: invalid provenance root") } normalizedCommitment := normalizeHash(commitment) if normalizedCommitment == "" { panic("guardians: invalid reveal commitment") } if !validFinalBaseURI(baseURI) { panic("guardians: final base uri must be ipfs/https and end with slash") } if deadline <= time.Now().Unix() { panic("guardians: reveal deadline must be in the future") } provenanceRoot = normalizedProvenance revealCommitment = normalizedCommitment finalBaseURI = baseURI revealDeadline = deadline revealState = revealStateCommitted chain.Emit( "RevealConfigured", "provenance", provenanceRoot, "commitment", revealCommitment, "deadline", ufmt.Sprintf("%d", revealDeadline), ) } // ComputeRevealCommitment is deterministic but production secrets should be // committed with the offline helper, never exposed through a public RPC query. func ComputeRevealCommitment(secret string) string { payload := runtime.ChainID() + "|" + CollectionID() + "|REVEAL|" + secret return hashBytes([]byte(payload)) } // The pilot owner reveals manually after the full 24-hour sale window. // Selling out early never starts reveal, and no timer invokes this function. func Reveal(cur realm, secret string) { assertAdmin(cur.Previous().Address()) if revealState != revealStateCommitted { panic("guardians: reveal is not committed") } if len(secret) < 8 || len(secret) > 256 { panic("guardians: reveal secret length must be 8..256") } if !saleConfigured || time.Now().Unix() < saleConfig.publicEnd { panic("guardians: public sale has not ended") } if ComputeRevealCommitment(secret) != revealCommitment { panic("guardians: reveal secret does not match commitment") } if token.TotalSupply() == 0 { panic("guardians: cannot reveal an empty collection") } revealSeedHash = hashBytes([]byte("GUARDIANS|SEED|" + secret)) permutationA = derivePermutationA(secret) permutationB = deriveSeedNumber("B", secret) % maxSupply revealTarget = token.TotalSupply() revealProcessed = 0 revealState = revealStateRevealing chain.Emit( "RevealStarted", "seedHash", revealSeedHash, "target", ufmt.Sprintf("%d", revealTarget), ) } // ApplyRevealBatch updates sequentially minted tokens. Anyone may execute the // deterministic batch, so the project operator cannot block progress. func ApplyRevealBatch(startTokenID int64, quantity int) { if revealState != revealStateRevealing { panic("guardians: reveal batch is not active") } if quantity < 1 || quantity > maxRevealBatch { panic("guardians: reveal batch quantity must be 1..20") } if startTokenID != revealProcessed+1 { panic("guardians: reveal batch must continue sequentially") } end := startTokenID + int64(quantity) - 1 if end > revealTarget { panic("guardians: reveal batch exceeds target") } for tokenID := startTokenID; tokenID <= end; tokenID++ { metadataID := MetadataID(tokenID) uri := finalBaseURI + metadataFilename(metadataID) if err := metadataLedger.SetTokenURI(grc721.TokenID(ufmt.Sprintf("%d", tokenID)), uri); err != nil { panic(err) } } revealProcessed = end if revealProcessed == revealTarget { revealState = revealStateRevealed } chain.Emit( "RevealBatchApplied", "startTokenId", ufmt.Sprintf("%d", startTokenID), "endTokenId", ufmt.Sprintf("%d", end), "processed", ufmt.Sprintf("%d", revealProcessed), ) } // metadataFilename matches the immutable IPFS release layout (0001.json–3333.json). func metadataFilename(metadataID int64) string { serial := ufmt.Sprintf("%d", metadataID) for len(serial) < 4 { serial = "0" + serial } return serial + ".json" } func MetadataID(tokenID int64) int64 { if revealState != revealStateRevealing && revealState != revealStateRevealed && revealState != revealStateUnveiledFrozen { panic("guardians: metadata mapping is not revealed") } if tokenID < 1 || tokenID > revealTarget { panic("guardians: token id outside reveal target") } return (permutationA*(tokenID-1)+permutationB)%maxSupply + 1 } func FreezeMetadata(cur realm) { assertAdmin(cur.Previous().Address()) if revealState != revealStateRevealed || revealProcessed != revealTarget { panic("guardians: complete reveal before freezing metadata") } revealState = revealStateUnveiledFrozen chain.Emit("MetadataFrozen", "provenance", provenanceRoot, "seedHash", revealSeedHash) } func assertMintingAllowed() { if revealState != revealStateVeiled && revealState != revealStateCommitted { panic("guardians: minting is closed after reveal starts") } } func validFinalBaseURI(uri string) bool { return (strings.HasPrefix(uri, "ipfs://") || strings.HasPrefix(uri, "https://")) && strings.HasSuffix(uri, "/") } func derivePermutationA(secret string) int64 { candidate := deriveSeedNumber("A", secret) % maxSupply if candidate == 0 { candidate = 1 } for gcd(candidate, maxSupply) != 1 { candidate++ if candidate >= maxSupply { candidate = 1 } } return candidate } func deriveSeedNumber(label, secret string) int64 { hash := hashBytes([]byte("GUARDIANS|PERMUTATION|" + label + "|" + secret)) value, err := strconv.ParseInt(hash[:15], 16, 64) if err != nil { panic(err) } return value } func gcd(first, second int64) int64 { for second != 0 { first, second = second, first%second } return first } func normalizeHash(value string) string { decoded, err := hex.DecodeString(value) if err != nil || len(decoded) != sha256.Size { return "" } return hex.EncodeToString(decoded) }
- #16sale.gno
- #17package guardians_pilot_v1 import ( "chain" "chain/banker" "chain/runtime/unsafe" "time" "gno.land/p/nt/avl/v0" "gno.land/p/nt/ufmt/v0" ) const ( salePhaseClosed = "CLOSED" salePhaseAllowlist = "ALLOWLIST" salePhasePublic = "PUBLIC" salePhaseOpen = "OPEN" saleDenom = "ugnot" ) type saleConfiguration struct { allowlistStart int64 allowlistEnd int64 publicStart int64 publicEnd int64 allowlistPrice int64 publicPrice int64 allowlistLimit int publicLimit int treasury address } var ( saleConfig saleConfiguration saleConfigured bool salePaused = true allowlistMintedByWallet avl.Tree publicMintedByWallet avl.Tree ) // ConfigureSale stores timestamps as Unix seconds and prices in ugnot. // Configuration is only mutable while the sale is paused. func ConfigureSale( cur realm, allowlistStart, allowlistEnd int64, publicStart, publicEnd int64, allowlistPrice, publicPrice int64, allowlistLimit, publicLimit int, treasury address, ) { assertAdmin(cur.Previous().Address()) if !salePaused { panic("guardians: pause sale before configuration") } if !validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd) { panic("guardians: invalid sale schedule") } if !validAllowlistPrice(allowlistPrice) || !validSalePrice(publicPrice) { panic("guardians: invalid sale price") } if !validWalletLimit(allowlistLimit) || !validWalletLimit(publicLimit) { panic("guardians: invalid wallet limit") } if allowlistPrice != 0 || publicPrice != 1000000 || allowlistLimit != 1 || publicLimit != 3 { panic("guardians: pilot requires free 1 + paid 3 at 1 GNOT") } if treasury != address("g1ht2848ahr20tqq7086thg5h9vrw4836suzard8") { panic("guardians: unexpected pilot treasury") } if !treasury.IsValid() { panic("guardians: invalid sale treasury") } saleConfig = saleConfiguration{ allowlistStart: allowlistStart, allowlistEnd: allowlistEnd, publicStart: publicStart, publicEnd: publicEnd, allowlistPrice: allowlistPrice, publicPrice: publicPrice, allowlistLimit: allowlistLimit, publicLimit: publicLimit, treasury: treasury, } saleConfigured = true chain.Emit( "SaleConfigured", "allowlistStart", ufmt.Sprintf("%d", allowlistStart), "allowlistEnd", ufmt.Sprintf("%d", allowlistEnd), "publicStart", ufmt.Sprintf("%d", publicStart), "publicEnd", ufmt.Sprintf("%d", publicEnd), "treasury", treasury.String(), ) } func SetSalePaused(cur realm, paused bool) { assertAdmin(cur.Previous().Address()) if !paused && !saleConfigured { panic("guardians: configure sale before unpausing") } if !paused && allowlistRoot == "" { panic("guardians: configure allowlist root before unpausing") } if !paused && revealState != revealStateCommitted { panic("guardians: commit reveal configuration before unpausing") } if !paused { allowlistRootLocked = true } salePaused = paused chain.Emit("SalePauseChanged", "paused", ufmt.Sprintf("%t", paused)) } func SalePhase() string { return salePhaseAt(time.Now().Unix()) } // Activate once, after metadata and proofs are ready. Both purchase modes share // the same on-chain 24-hour window beginning in this activation transaction. func ActivatePilotSale(cur realm) { assertAdmin(cur.Previous().Address()) if saleConfigured { panic("guardians: pilot sale already configured") } start := time.Now().Unix() end := start + 86400 ConfigureSale(cur, start, end, start, end, 0, 1000000, 1, 3, address("g1ht2848ahr20tqq7086thg5h9vrw4836suzard8")) SetSalePaused(cur, false) } func SalePhaseAt(unixTime int64) string { return salePhaseAt(unixTime) } func salePhaseAt(unixTime int64) string { if salePaused || !saleConfigured { return salePhaseClosed } if unixTime >= saleConfig.publicStart && unixTime < saleConfig.publicEnd { return salePhaseOpen } return salePhaseClosed } func SalePaused() bool { return salePaused } func SaleConfigured() bool { return saleConfigured } func SaleTreasury() address { return saleConfig.treasury } func AllowlistStart() int64 { return saleConfig.allowlistStart } func AllowlistEnd() int64 { return saleConfig.allowlistEnd } func PublicStart() int64 { return saleConfig.publicStart } func PublicEnd() int64 { return saleConfig.publicEnd } func AllowlistPrice() int64 { return saleConfig.allowlistPrice } func PublicPrice() int64 { return saleConfig.publicPrice } func AllowlistWalletLimit() int { return saleConfig.allowlistLimit } func PublicWalletLimit() int { return saleConfig.publicLimit } func AllowlistMinted(owner address) int { return mintedCount(&allowlistMintedByWallet, owner) } func PublicMinted(owner address) int { return mintedCount(&publicMintedByWallet, owner) } func NextSaleTimestamp() int64 { now := time.Now().Unix() if !saleConfigured { return 0 } if now < saleConfig.allowlistStart { return saleConfig.allowlistStart } if now < saleConfig.allowlistEnd { return saleConfig.allowlistEnd } if now < saleConfig.publicStart { return saleConfig.publicStart } if now < saleConfig.publicEnd { return saleConfig.publicEnd } return 0 } // MintPublic accepts only direct Adena/EOA calls. This pairing is critical: // OriginSend describes actual receipt by this realm only for a direct user call. func MintPublic(cur realm, quantity int) { if !cur.Previous().IsUserCall() { panic("guardians: only direct user calls can mint") } if SalePhase() != salePhaseOpen { panic("guardians: public sale is not active") } caller := cur.Previous().Address() assertSaleMint(quantity, PublicMinted(caller), saleConfig.publicLimit) required := requiredPayment(saleConfig.publicPrice, quantity) sent := unsafe.OriginSend() assertExactPayment(sent, required) mintTokens(caller, quantity) publicMintedByWallet.Set(caller.String(), PublicMinted(caller)+quantity) forwardPayment(cur, sent) chain.Emit( "PublicMint", "minter", caller.String(), "quantity", ufmt.Sprintf("%d", quantity), "paid", ufmt.Sprintf("%d", required), ) } func assertSaleMint(quantity, alreadyMinted, walletLimit int) { assertMintQuantity(quantity) if alreadyMinted+quantity > walletLimit { panic("guardians: wallet mint limit exceeded") } } func requiredPayment(unitPrice int64, quantity int) int64 { if unitPrice < 0 || !validMintQuantity(quantity) { panic("guardians: invalid payment calculation") } return unitPrice * int64(quantity) } func assertExactPayment(sent chain.Coins, required int64) { if required == 0 { if len(sent) != 0 { panic("guardians: free mint must not include payment") } return } if !isExactPayment(sent, required) { panic(ufmt.Sprintf("guardians: exact payment required: %dugnot", required)) } } func isExactPayment(sent chain.Coins, required int64) bool { return len(sent) == 1 && sent[0].Denom == saleDenom && sent[0].Amount == required } func forwardPayment(cur realm, sent chain.Coins) { if len(sent) == 0 { return } b := banker.NewBanker(banker.BankerTypeRealmSend, cur) b.SendCoins(cur.Address(), saleConfig.treasury, sent) } func mintedCount(tree *avl.Tree, owner address) int { v := tree.Get(owner.String()) if v == nil { return 0 } return v.(int) } func validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd int64) bool { return allowlistStart > 0 && allowlistStart == publicStart && allowlistEnd == publicEnd && publicEnd- publicStart == 86400 } func validSalePrice(price int64) bool { return price > 0 && price <= 9223372036854775807/int64(maxMintBatch) } func validAllowlistPrice(price int64) bool { return price >= 0 && price <= 9223372036854775807/int64(maxMintBatch) } func validWalletLimit(limit int) bool { return limit > 0 && int64(limit) <= maxSupply }
- #18/gno.MemPackageType
- #19 MPUserAll
Result log
msg:0,success:true,log:,events:[]