// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; interface ISeedShares { function sharesOf(address a) external view returns (uint256); } interface ISeedStaking { function stakers(address a) external view returns (uint256 amount, uint256 hypeDebt, uint256 ambrDebt, uint256 owedHype, uint256 owedAmbr, uint256 cooling, uint64 coolingUntil); } /// @title Ambrosia Seed /// @notice A free NFT: one per wallet, gas only. 7,777 at most: 5,555 open to anyone, and 2,222 kept for wallets /// that already own a paid share, so that people who take part can still get theirs. /// Minting starts at `openAt` (UTC), which the owner sets once it is announced. It carries no price and no promise. What it shows is what its /// holder does in Ambrosia, read live from the other contracts: /// Seed (0): holds the NFT /// Sprout (1): the holder owns at least one paid share /// Bloom (2): a Sprout whose holder also has $AMBR staked /// The picture is drawn on-chain from that level, so it changes by itself. /// The owner can only set the start time before minting starts, point the level at newer Ambrosia /// contracts, and close minting for good. It cannot /// mint, move or burn anyone's Seed. contract AmbrosiaSeed is ERC721 { using Strings for uint256; address public owner; ISeedShares public shares; ISeedStaking public staking; bool public closed; uint256 public totalSupply; uint256 public constant PUBLIC_CAP = 5_555; uint256 public constant HOLDER_CAP = 2_222; uint256 public publicMinted; uint256 public holderMinted; /// @notice Unix time minting starts. 0 = not scheduled yet. uint64 public openAt; mapping(address => bool) public hasMinted; /// @notice The Seed a wallet minted itself (0 if none). Rewards built later count one Seed per wallet. mapping(address => uint256) public seedOf; event Sources(address shares, address staking); event Closed(); event OpenAt(uint64 at); event OwnerChanged(address owner); error AlreadyMinted(); error MintClosed(); error NotOwner(); error ContractsCannotMint(); error SoldOut(); error NotOpenYet(); error AlreadyOpen(); constructor(address owner_, ISeedShares shares_, ISeedStaking staking_) ERC721("Ambrosia Seed", "SEED") { owner = owner_; shares = shares_; staking = staking_; emit Sources(address(shares_), address(staking_)); } /// @notice Free. One per wallet, ever. Only gas. function mint() external returns (uint256 id) { if (closed) revert MintClosed(); if (openAt == 0 || block.timestamp < openAt) revert NotOpenYet(); if (hasMinted[msg.sender]) revert AlreadyMinted(); if (msg.sender != tx.origin) revert ContractsCannotMint(); // a wallet signs its own mint; no batching through a contract // A wallet with a paid share uses the reserved half first, leaving the open half to newcomers. if (levelOfHolder(msg.sender) != 0 && holderMinted < HOLDER_CAP) ++holderMinted; else if (publicMinted < PUBLIC_CAP) ++publicMinted; else revert SoldOut(); hasMinted[msg.sender] = true; id = ++totalSupply; seedOf[msg.sender] = id; _mint(msg.sender, id); } /// @notice 0 Seed, 1 Sprout, 2 Bloom, for whoever holds the token now. function levelOf(uint256 id) public view returns (uint8) { return levelOfHolder(_ownerOf(id)); } /// @notice Seeds still open to anyone, and Seeds still reserved for wallets with a paid share. function left() external view returns (uint256 open, uint256 reserved) { return (PUBLIC_CAP - publicMinted, HOLDER_CAP - holderMinted); } function levelOfHolder(address a) public view returns (uint8) { if (a == address(0)) return 0; if (address(shares).code.length == 0) return 0; try shares.sharesOf(a) returns (uint256 s) { if (s == 0) return 0; } catch { return 0; } if (address(staking).code.length == 0) return 1; try staking.stakers(a) returns (uint256 amount, uint256, uint256, uint256, uint256, uint256, uint64) { return amount > 0 ? 2 : 1; } catch { return 1; } } // ------------------------------------------------------------------ owner function setSources(ISeedShares shares_, ISeedStaking staking_) external { if (msg.sender != owner) revert NotOwner(); shares = shares_; staking = staking_; emit Sources(address(shares_), address(staking_)); } /// @notice Schedule (or reschedule) the start. Once minting has started the time can no longer be moved. function setOpenAt(uint64 at) external { if (msg.sender != owner) revert NotOwner(); if (openAt != 0 && block.timestamp >= openAt) revert AlreadyOpen(); openAt = at; emit OpenAt(at); } function close() external { if (msg.sender != owner) revert NotOwner(); closed = true; emit Closed(); } function setOwner(address o) external { if (msg.sender != owner) revert NotOwner(); owner = o; emit OwnerChanged(o); } // ------------------------------------------------------------------ art /// @notice Colour of a Seed, fixed by its number: Amber 40%, Rose 25%, Jade 15%, Azure 10%, Violet 7%, Pearl 3%. function hueOf(uint256 id) public pure returns (uint8) { uint256 r = uint256(keccak256(abi.encode("ambrosia-seed", id))) % 100; if (r < 40) return 0; if (r < 65) return 1; if (r < 80) return 2; if (r < 90) return 3; if (r < 97) return 4; return 5; } function _palette(uint8 h) private pure returns (string memory name_, string memory light, string memory dark, string memory sky) { if (h == 0) return ("Amber", "#ffe6b3", "#e0852a", "#55305a"); if (h == 1) return ("Rose", "#ffd9df", "#e0587a", "#5a2648"); if (h == 2) return ("Jade", "#dcffe0", "#35b07c", "#1c4a48"); if (h == 3) return ("Azure", "#d6ecff", "#3d8fe6", "#22356a"); if (h == 4) return ("Violet", "#eadbff", "#8b58e6", "#3a2170"); return ("Pearl", "#ffffff", "#c3cfe6", "#454a66"); } function tokenURI(uint256 id) public view override returns (string memory) { _requireOwned(id); uint8 lv = levelOf(id); string memory stage = lv == 2 ? "Bloom" : lv == 1 ? "Sprout" : "Seed"; (string memory hue,,,) = _palette(hueOf(id)); bytes memory json = abi.encodePacked( '{"name":"Ambrosia ', stage, " #", id.toString(), '","description":"A free Ambrosia Seed. It sprouts when its holder owns a paid share and blooms when they stake $AMBR. Drawn and animated on-chain from live state.","attributes":[{"trait_type":"Stage","value":"', stage, '"},{"trait_type":"Colour","value":"', hue, '"},{"trait_type":"Level","value":', uint256(lv).toString(), '}],"image":"data:image/svg+xml;base64,', Base64.encode(bytes(svg(lv, id))), '"}' ); return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json))); } function svg(uint8 lv, uint256 id) public pure returns (string memory) { (string memory hue, string memory light, string memory dark, string memory sky) = _palette(hueOf(id)); return string( abi.encodePacked( _defs(light, dark, sky), _stars(id), _plot(lv), lv == 0 ? _seed() : lv == 1 ? _sprout() : _bloom(), _labels(lv, id, hue, light) ) ); } function _defs(string memory light, string memory dark, string memory sky) private pure returns (bytes memory) { return abi.encodePacked( '' ); } function _stars(uint256 id) private pure returns (bytes memory out) { bytes32 h = keccak256(abi.encode("stars", id)); for (uint256 i; i < 14; ++i) { uint256 x = 16 + (uint256(uint8(h[i])) * 368) / 255; uint256 y = 60 + (uint256(uint8(h[i + 14])) * 190) / 255; out = abi.encodePacked( out, '' ); } } function _plot(uint8 lv) private pure returns (bytes memory) { return abi.encodePacked( '', '', '', '' ); } function _seed() private pure returns (bytes memory) { return ''; } function _sprout() private pure returns (bytes memory) { return ''; } function _bloom() private pure returns (bytes memory) { bytes memory petals; for (uint256 i; i < 8; ++i) { petals = abi.encodePacked(petals, ''); } for (uint256 i; i < 8; ++i) { petals = abi.encodePacked(petals, ''); } return abi.encodePacked( '', petals, '', '' ); } function _labels(uint8 lv, uint256 id, string memory hue, string memory light) private pure returns (bytes memory) { return abi.encodePacked( 'AMBROSIA#', id.toString(), '', lv == 2 ? "BLOOM" : lv == 1 ? "SPROUT" : "SEED", '', hue, "" ); } }