Transaction
516F56A58362B8…924EF257CAC7
Block 815,325 · index 0 · indexed
Summary
- Hash
- 516F56A58362B8DA2C50A5AF32986F48DFD3DFCEA373FE2EEA29924EF257CAC7
- Block
- 815,325
- Size
- 37645 bytes
- Gas used
- 49,768,435 / 100,000,000
- Fee
- 150000 ugnot
- Status
- success
Messages
- Attached funds
- 20000000 ugnot
Arguments · 23
- #1guardians
- #2allowlist.gno
- #3package guardians import ( "chain" "chain/runtime" "chain/runtime/unsafe" "crypto/sha256" "encoding/hex" "strings" "time" "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 !allowlistSaleOpenAt(time.Now().Unix()) { 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) mintAllocatedTokens(caller, quantity, 0) 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 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(address("g1lp547ww848djw0ammpqswegg7tc6xczqqtx07n"), 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/g1nmqhha0ua730vejxqhrelldm4pga5j5ptle5ea/guardians" gno = "0.9"
- #8guardians.gno
- #9package guardians import ( "gno.land/p/nt/grc721/v0" "gno.land/p/nt/ufmt/v0" ) const ( collectionName = "Guardians" collectionSymbol = "GUARDIANS" maxSupply = int64(3333) maxPublicMintBatch = 20 maxMintBatch = maxPublicMintBatch + 1 // One free claim plus 20 paid NFTs. ) // localAdmin is the standard prefunded gnodev account. It is intentionally // limited to local development and must be replaced by the deployment owner // before Pearl or Mainnet deployment. const localAdmin address = "g1nmqhha0ua730vejxqhrelldm4pga5j5ptle5ea" 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) { mintAllocatedTokens(to, 0, quantity) } func mintAllocatedTokens(to address, freeQuantity, paidQuantity int) { quantity := freeQuantity + paidQuantity assertMintingAllowed() assertMintQuantity(quantity) assertReserveCapacity(freeQuantity, paidQuantity) 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) } allowlistMintedSupply += int64(freeQuantity) } 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 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 import ( "chain" "chain/runtime/unsafe" "time" "gno.land/p/nt/ufmt/v0" ) // Mint preserves the pilot's one-button, free-first split. Each allowance is // checked independently and exact payment/forwarding is atomic with minting. 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 freeQuantity < 0 || freeQuantity > 1 || paidQuantity < 0 || paidQuantity > maxPublicMintBatch { panic("guardians: invalid free/paid quantity") } assertMintQuantity(freeQuantity + paidQuantity) caller := cur.Previous().Address() now := time.Now().Unix() if freeQuantity > 0 { if !allowlistSaleOpenAt(now) { panic("guardians: allowlist sale is not active") } if allowance != 1 || unitPrice != 0 || saleConfig.allowlistPrice != 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 { if !publicSaleOpenAt(now) { panic("guardians: public sale is not active") } assertSaleMint(paidQuantity, PublicMinted(caller), saleConfig.publicLimit) } required := int64(0) if paidQuantity > 0 { required = requiredPayment(saleConfig.publicPrice, paidQuantity) } sent := unsafe.OriginSend() assertExactPayment(sent, required) mintAllocatedTokens(caller, freeQuantity, paidQuantity) 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("Mint", "minter", caller.String(), "free", ufmt.Sprintf("%d", freeQuantity), "paid", ufmt.Sprintf("%d", paidQuantity), "payment", ufmt.Sprintf("%d", required)) }
- #14reserve.gno
- #15package guardians import ( "chain" "gno.land/p/nt/ufmt/v0" ) // The approved genesis list contains 369 independent one-token claims. // The Merkle manifest must be checked against this value before activation. const allowlistReservedSupply = int64(369) var ( allowlistMintedSupply int64 allowlistReserveReleased bool ) func AllowlistReservedSupply() int64 { return allowlistReservedSupply } func AllowlistMintedSupply() int64 { return allowlistMintedSupply } func AllowlistReserveReleased() bool { return allowlistReserveReleased } func AllowlistUnclaimedSupply() int64 { return allowlistReservedSupply - allowlistMintedSupply } func AllowlistReserveRemaining() int64 { if allowlistReserveReleased { return 0 } return AllowlistUnclaimedSupply() } func PublicMintableSupply() int64 { return maxSupply - token.TotalSupply() - AllowlistReserveRemaining() } // This one-way administrator action releases only the still-unclaimed stock. // It does NOT change the proof root, free price, or individual free entitlement. // No timer, public mint, or claim can trigger this release automatically. func ReleaseAllowlistReserve(cur realm) { assertAdmin(cur.Previous().Address()) if !saleConfigured || revealState != revealStateCommitted { panic("guardians: reserve release is not available") } if allowlistReserveReleased { panic("guardians: reserve already released") } released := AllowlistReserveRemaining() allowlistReserveReleased = true chain.Emit("AllowlistReserveReleased", "released", ufmt.Sprintf("%d", released), "freeEntitlementsPreserved", "true") } func assertReserveCapacity(freeQuantity, paidQuantity int) { if freeQuantity < 0 || paidQuantity < 0 { panic("guardians: invalid reserved allocation") } if int64(freeQuantity) > AllowlistUnclaimedSupply() { panic("guardians: whitelist allocation exhausted") } if token.TotalSupply()+int64(freeQuantity+paidQuantity) > maxSupply { panic("guardians: max supply reached") } // A free claim consumes one reserved slot and one supply slot together. // A paid mint must never consume the slots that remain protected afterwards. protectedAfter := AllowlistReserveRemaining() if !allowlistReserveReleased { protectedAfter -= int64(freeQuantity) } remainingAfter := maxSupply-token.TotalSupply()-int64(freeQuantity+paidQuantity) if remainingAfter < protectedAfter { panic("guardians: remaining supply is reserved for whitelist claims") } }
- #16reveal.gno
- #17package guardians 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)) } // Only the administrator can manually begin reveal after sale end or sellout. // Neither a deadline nor knowledge of the secret alone starts the reveal. 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") } // publicEnd=0 means still open, so it must never unlock an early reveal. ended := saleConfig.publicEnd > 0 && time.Now().Unix() >= saleConfig.publicEnd if !saleConfigured || (!ended && token.TotalSupply() < maxSupply) { 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) }
- #18sale.gno
- #19package guardians 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" salePhaseSoldOut = "SOLD_OUT" 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. An end timestamp // of zero means no announced deadline, not an already-ended sale. 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 !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()) } func SalePhaseAt(unixTime int64) string { return salePhaseAt(unixTime) } func salePhaseAt(unixTime int64) string { if token.TotalSupply() >= maxSupply { return salePhaseSoldOut } if salePaused || !saleConfigured { return salePhaseClosed } allowlistOpen := allowlistSaleOpenAt(unixTime) publicOpen := publicSaleOpenAt(unixTime) if allowlistOpen && publicOpen { return salePhaseOpen } if allowlistOpen { return salePhaseAllowlist } if publicOpen { return salePhasePublic } return salePhaseClosed } func publicSaleOpenAt(unixTime int64) bool { return saleConfigured && !salePaused && unixTime >= saleConfig.publicStart && (saleConfig.publicEnd == 0 || unixTime < saleConfig.publicEnd) } func allowlistSaleOpenAt(unixTime int64) bool { return saleConfigured && !salePaused && unixTime >= saleConfig.allowlistStart && (saleConfig.allowlistEnd == 0 || unixTime < saleConfig.allowlistEnd) && (saleConfig.publicEnd == 0 || unixTime < saleConfig.publicEnd) } // SetPublicEnd announces a deadline without pausing or changing price, wallet // limits, roots, or counters. A completed sale cannot be reopened this way. // Set end to the current block time to close immediately, or a future time // to announce an end countdown. Zero is reserved for initial configuration. func SetPublicEnd(cur realm, end int64) { assertAdmin(cur.Previous().Address()) if !saleConfigured || revealState != revealStateCommitted { panic("guardians: sale is not configurable") } now := time.Now().Unix() if saleConfig.publicEnd > 0 && now >= saleConfig.publicEnd { panic("guardians: closed sale cannot be reopened") } if end < now || end <= saleConfig.publicStart { panic("guardians: invalid public end timestamp") } saleConfig.publicEnd = end chain.Emit("PublicEndConfigured", "publicEnd", ufmt.Sprintf("%d", end)) } 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 } next := int64(0) for _, timestamp := range []int64{saleConfig.allowlistStart, saleConfig.allowlistEnd, saleConfig.publicStart, saleConfig.publicEnd} { if timestamp > now && (next == 0 || timestamp < next) { next = timestamp } } return next } // 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 !publicSaleOpenAt(time.Now().Unix()) { 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 && publicStart >= allowlistStart && (allowlistEnd == 0 || allowlistStart < allowlistEnd) && (publicEnd == 0 || publicStart < publicEnd) } 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 }
- #20snapshot.gno
- #21package guardians import ( "strconv" "time" "chain/runtime" "gno.land/p/nt/ufmt/v0" ) func LaunchVersion() string { return "GUARDIANS_MAINNET_V1" } // One read produces a consistent supply/reserve/timing snapshot and avoids // issuing thousands of owner queries as the collection approaches 3,333. func SaleSnapshot() string { return ufmt.Sprintf(`{"version":%s,"network":%s,"collectionId":%s,"admin":%s,"treasury":%s,"maxSupply":%d,"supply":%d,"configured":%t,"paused":%t,"phase":%s,"startsAt":%d,"endsAt":%d,"allowlistStartsAt":%d,"allowlistEndsAt":%d,"priceUgnot":%d,"freePriceUgnot":%d,"publicLimit":%d,"freeLimit":%d,"root":%s,"reserveCapacity":%d,"freeMintedSupply":%d,"reserveReleased":%t,"protectedRemaining":%d,"publicAvailable":%d,"revealState":%s,"revealProcessed":%d,"revealTarget":%d,"veiledURI":%s,"finalBaseURI":%s,"provenance":%s,"serverTime":%d}`, strconv.Quote(LaunchVersion()),strconv.Quote(runtime.ChainID()),strconv.Quote(CollectionID()),strconv.Quote(Admin().String()),strconv.Quote(SaleTreasury().String()),MaxSupply(),TotalSupply(),SaleConfigured(),SalePaused(),strconv.Quote(SalePhase()),PublicStart(),PublicEnd(),AllowlistStart(),AllowlistEnd(),PublicPrice(),AllowlistPrice(),PublicWalletLimit(),AllowlistWalletLimit(),strconv.Quote(AllowlistRoot()),AllowlistReservedSupply(),AllowlistMintedSupply(),AllowlistReserveReleased(),AllowlistReserveRemaining(),PublicMintableSupply(),strconv.Quote(RevealState()),RevealProcessed(),RevealTarget(),strconv.Quote(VeiledURI()),strconv.Quote(FinalBaseURI()),strconv.Quote(ProvenanceRoot()),time.Now().Unix()) } func WalletSnapshot(owner address) string { return ufmt.Sprintf(`{"freeMinted":%d,"paidMinted":%d,"balance":%d}`,AllowlistMinted(owner),PublicMinted(owner),BalanceOf(owner)) }
- #22/gno.MemPackageType
- #23 MPUserAll
Result log
msg:0,success:true,log:,events:[]