Open a mystical chest. Receive a provably fair NFT. Own it forever — on the Sui blockchain.
Live on Sui Testnet · True On-Chain Randomness (sui::random) · 0.01 SUI per open · Open Source
| Feature | Details |
|---|---|
| True randomness | Uses sui::random — Sui's native VRF, cryptographically unpredictable every TX |
| 3 NFT tiers | Common (70%), Rare (25%), Epic (5%) |
| Fully on-chain | No backend, no admin keys, every result verifiable on Sui Explorer |
| Pay-to-play | 0.01 SUI per chest — sent directly to treasury via smart contract |
| Reveal modal | Animated reveal with confetti, sound effects, and tier-glow |
| Sound effects | Web Audio API — different melody per tier, no audio files needed |
| Cooldown timer | 8-second spam prevention with animated drain bar |
| My Collection | Fetches all your LootChest NFTs from the blockchain with live stats |
| Landing page | Scroll-reveal animations, floating reward chips, FAQ, How It Works |
| Responsive | Works on desktop and mobile |
| Layer | Technology |
|---|---|
| Blockchain | Sui Testnet |
| Smart Contract | Move 2024 (sui::random, sui::coin) |
| Frontend | React 18 + Vite |
| Wallet | @mysten/dapp-kit |
| Styling | Pure CSS — no Tailwind |
| Fonts | Cinzel Decorative + Rajdhani (Google Fonts) |
| Audio | Web Audio API (zero files) |
lootbox/
├── sources/
│ └── lootbox.move # Smart contract
├── frontend/
│ ├── src/
│ │ ├── App.jsx # dApp — mint, reveal modal, collection
│ │ ├── App.css # dApp styles
│ │ ├── Landing.jsx # Marketing landing page
│ │ ├── Landing.css # Landing styles
│ │ └── main.jsx # Root + Sui providers + routing
│ ├── .env # Your package ID (not committed)
│ └── .env.example # Template
├── Move.toml # Move package config
├── Published.toml # Deployed contract addresses
└── README.md
Package address (Testnet):
0x71c83839cc6f3595b971b26dab8415fdf64b1d811c3b4500d01400d734ee677e
public entry fun open_box(
payment: Coin<SUI>, // 0.01 SUI required
r: &Random, // Sui's on-chain VRF — object 0x8
ctx: &mut TxContext
) {
// 1. Verify payment >= 0.01 SUI (10_000_000 MIST)
assert!(coin::value(&payment) >= PRICE_MIST, EInsufficientPayment);
// 2. Split exact price → treasury, return change to sender
let treasury_coin = coin::split(&mut payment, PRICE_MIST, ctx);
transfer::public_transfer(treasury_coin, TREASURY);
// 3. Roll true random 0–99 via sui::random VRF
let mut generator = new_generator(r, ctx);
let roll = generator.generate_u8_in_range(0, 99);
// 4. Assign tier: 0–69 = Common, 70–94 = Rare, 95–99 = Epic
let tier = if (roll < 70) { 1 } else if (roll < 95) { 2 } else { 3 };
// 5. Mint LootNFT with name + image_url + tier → sender's wallet
// 6. Emit RewardEvent
}public struct LootNFT has key {
id: object::UID,
name: vector<u8>, // "Common NFT" | "Rare NFT" | "Epic NFT"
image_url: vector<u8>, // Hosted image URL
tier: u8, // 1 | 2 | 3
}
public struct RewardEvent has copy, drop {
reward: u8, // tier number
player: address,
}- Sui CLI installed
- Node.js v18+
- A Sui wallet browser extension (Sui Wallet, Martian, etc.)
- Testnet SUI from the faucet
git clone https://github.com/your-username/lootbox.git
cd lootbox# Switch to testnet
sui client switch --env testnet
# Deploy
sui client publish --gas-budget 100000000Copy the PackageID from the output.
cd frontend
cp .env.example .envEdit .env:
VITE_PACKAGE_ID=0xYOUR_NEW_PACKAGE_ID
VITE_NETWORK=testnet
VITE_RPC_URL=https://fullnode.testnet.sui.io:443
npm install
npm run dev- Go to the landing page → click Launch App
- Click Connect Wallet → select your Sui wallet
- Make sure you have testnet SUI (faucet.sui.io)
- Click Mint NFT — 0.01 SUI
- Approve the transaction in your wallet
- Watch the chest shake and open — your NFT reveal modal appears with confetti and sound!
- Click View NFT to see your NFT on Suiscan
- Switch to My Collection to see all your LootChest NFTs
| Tier | Emoji | Drop Rate | Name |
|---|---|---|---|
| Common | 70% | Common NFT | |
| Rare | 25% | Rare NFT | |
| Epic | 5% | Epic NFT |
The old approach used epoch + wallet address byte as a seed — this gave the same result all day for users with the same wallet byte, and was trivially predictable.
sui::random uses Sui's native Verifiable Random Function (VRF) — an on-chain shared object at address 0x8 that is updated every epoch with a distributed key generation protocol. When combined with your transaction data via new_generator(r, ctx), it produces a result that is:
- Different every single transaction
- Cryptographically unpredictable
- Impossible to manipulate (not even the contract deployer can influence it)
- Fully verifiable on-chain
| What | Where | How |
|---|---|---|
| Price | lootbox.move |
Change PRICE_MIST (1 SUI = 1_000_000_000 MIST) |
| Treasury | lootbox.move |
Change TREASURY address constant |
| Drop rates | lootbox.move |
Change the roll < 70 / roll < 95 thresholds |
| NFT images | lootbox.move |
Replace the image_url strings with your IPFS CIDs |
| Cooldown | App.jsx |
Change the COOLDOWN constant (seconds) |
| Package ID | frontend/.env |
Update VITE_PACKAGE_ID after each publish |
- Testnet only — Do not use real funds. Contract is on Sui Testnet.
- Placeholder images — NFT images use
placehold.co. Replace with real IPFS images. - Basic NFT metadata — No
displaystandard, no traits. Suiscan may not render the image automatically. - Pseudo-treasury — The treasury address is hardcoded. For production, use a multisig or DAO.
- Upload real NFT artwork to IPFS (Pinata)
- Implement Sui
displaystandard for proper NFT metadata - Kiosk compatibility for trading on Sui marketplaces
- "My Collection" rarity stats and filters
- Mainnet deployment
- Multiple chest types with different prices and odds
MIT — see LICENSE
- Sui Blockchain — Move smart contracts + sui::random
- Mysten dapp-kit — wallet integration
- Suiscan — explorer links
- Web Audio API — sound effects without any audio files