//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "../interfaces/IN.sol";
import "./SeedPhrasePricing.sol";
import "../libraries/Randomize.sol";
import "../libraries/NilProtocolUtils.sol";
import "../libraries/SeedPhraseUtils.sol";

/**
 * @title Seed Phrases
 * @author maximonee (twitter.com/maximonee_)
 * @notice This contract provides minting for the Seed Phrases NFT by Sean Elliott (twitter.com/seanelliottoc)
 */
contract SeedPhrase is SeedPhrasePricing {
    using Strings for uint256;
    using Strings for uint16;
    using Strings for uint8;
    using Counters for Counters.Counter;
    using Randomize for Randomize.Random;

    Counters.Counter private _tokenIds;
    Counters.Counter private _doublePanelTokens;

    // TODO: Check if this is the correct way to store double tokens
    // Idea is to map double panel tokens (let's say they go from 3000 - 3100)
    // In the tokenUri we can check if a token is >= 3000
    // If it is check that it exists in this mapping which should contain the original two (now burned tokens)
    mapping(uint256 => uint256[2]) burnedTokensPairings;

    event Burnt(address to, uint256 firstBurntToken, uint256 secondBurntToken, uint256 doublePaneledToken);

    DerivativeParameters params = DerivativeParameters(false, false, 0, 2048, 4);

    constructor(
        address _n,
        address masterMint,
        address dao
    ) SeedPhrasePricing("NFT #1337", "LEET", IN(_n), params, 40000000000000000, 80000000000000000, masterMint, dao) {
        // Start token IDs at 1
        _tokenIds.increment();

        preSaleLimits[PreSaleType.N] = 1044;
        preSaleLimits[PreSaleType.GenesisSketch] = 40;
        preSaleLimits[PreSaleType.GM] = 500;
    }

    mapping(address => bool) genesisSketchAddresses;
    mapping(address => bool) gmAddresses;
    mapping(PreSaleType => uint256) preSaleLimits;

    bool public preSaleActive = false;
    bool public publicSaleActive = false;
    bool public isSaleHalted = false;

    uint8 public constant MAX_PUBLIC_MINT = 8;
    uint8 public constant MAX_BURNS = 100;
    uint16 public constant OWNER_LIMIT = 4;

    // Time TBD
    uint32 preSaleLaunchTime = 1633723200;
    // Time TBD
    uint32 publicSaleLaunchTime = 1633723200;

    // NFT creation vars
    uint8 constant gridSize = 16;
    uint16 constant viewBox = 1600;
    uint16 constant segmentSize = 100;
    uint16 constant radius = 50;
    uint16 constant r = 40;
    uint16 constant padding = 10;
    uint16 constant panelWidth = segmentSize * 4;
    uint16 constant panelHeight = segmentSize * 10;
    uint16 constant singlePanelX = (segmentSize * 6);
    uint16 constant doublePanel1X = (segmentSize * 3);
    uint16 constant doublePanel2X = doublePanel1X + (segmentSize * 6);
    uint16 constant panelY = (segmentSize * 3);
    uint8 constant strokeWeight = 7;

    // Map token to their unique seed
    mapping(uint256 => bytes32) internal tokenSeed;

    struct Colors {
        string background;
        string panel;
        string panelStroke;
        string selectedCircleStroke;
        string selectedCircleFill;
        string negativeCircleStroke;
        string negativeCircleFill;
        string blackOrWhite;
        string bOrWStroke;
    }

    struct Attrs {
        bool showStroke;
        bool showPanel;
        bool backgroundCircles;
        bool showGrid;
    }

    enum PreSaleType {
        N,
        GenesisSketch,
        GM,
        None
    }

    function getTokenSeed(uint256 tokenId) public view returns (bytes32) {
        return tokenSeed[tokenId];
    }

    function tokenSVG(uint256 tokenId) public view virtual returns (string memory, string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        return _render(tokenId, getTokenSeed(tokenId));
    }

    function tokenSVG(uint256 tokenId) public view virtual returns (string memory svg, bytes memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        Randomize.Random memory random = Randomize.Random({ seed: uint256(getTokenSeed(tokenId)), offsetBit: 0 });
        (SeedPhraseUtils.Attrs memory attributes, bytes memory traits) = tokenAttributes(tokenId, random);

        return (_render(tokenId, random, attributes), traits);
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        SeedPhraseUtils.Attrs memory attributes = tokenAttributes(tokenId);

        // We might also want to return the attributes string from tokenSVG also (as all the info is already in there)
        (string memory output, bytes memory traits) = tokenSVG(tokenId);

        string memory json = NilProtocolUtils.base64encode(
            bytes(
                string(
                    abi.encodePacked(
                        '{"name": "Seed Phrases #',
                        NilProtocolUtils.stringify(tokenId),
                        '", "image": "data:image/svg+xml;base64,',
                        NilProtocolUtils.base64encode(bytes(output)),
                        '", "attributes": ',
                        traits,
                        "}"
                    )
                )
            )
        );
        output = string(abi.encodePacked("data:application/json;base64,", json));

        return output;
    }

    /**
    Updates the presale state for n holders
     */
    function setPreSaleState(bool _preSaleActiveState) public onlyAdmin {
        preSaleActive = _preSaleActiveState;
    }

    /**
    Updates the public sale state for non-n holders
     */
    function setPublicSaleState(bool _publicSaleActiveState) public onlyAdmin {
        publicSaleActive = _publicSaleActiveState;
    }

    /**
    Give the ability to halt the sale if necessary due to automatic sale enablement based on time
     */
    function setSaleHaltedState(bool _saleHaltedState) public onlyAdmin {
        isSaleHalted = _saleHaltedState;
    }

    function setGenesisSketchAllowList(address[] calldata addresses) external onlyAdmin {
        for (uint256 i = 0; i < addresses.length; i++) {
            genesisSketchAddresses[addresses[i]] = true;
        }
    }

    function setGmAllowList(address[] calldata addresses) external onlyAdmin {
        for (uint256 i = 0; i < addresses.length; i++) {
            gmAddresses[addresses[i]] = true;
        }
    }

    function isPreSaleActive() public view returns (bool) {
        if ((block.timestamp >= preSaleLaunchTime || preSaleActive) && !isSaleHalted) {
            return true;
        }

        return false;
    }

    function isPublicSaleActive() public view returns (bool) {
        if ((block.timestamp >= publicSaleLaunchTime || publicSaleActive) && !isSaleHalted) {
            return true;
        }

        return false;
    }

    function canMintPresale(address addr, uint256 amount) internal view returns (bool, PreSaleType) {
        if (genesisSketchAddresses[addr] && preSaleLimits[PreSaleType.GenesisSketch] - amount > 0) {
            return (true, PreSaleType.GenesisSketch);
        }

        if (gmAddresses[addr] && preSaleLimits[PreSaleType.GM] - amount > 0) {
            return (true, PreSaleType.GM);
        }

        if (n.balanceOf(addr) > 0 && preSaleLimits[PreSaleType.N] - amount > 0) {
            return (true, PreSaleType.N);
        }

        return (false, PreSaleType.None);
    }

    /**
     * @notice Allow a n token holder to bulk mint tokens with id of their n tokens' id
     * @param recipient Recipient of the mint
     * @param tokenIds Ids to be minted
     * @param paid Amount paid for the mint
     */
    function mintWithN(
        address recipient,
        uint256[] calldata tokenIds,
        uint256 paid
    ) public virtual override nonReentrant {
        uint256 maxTokensToMint = tokenIds.length;
        (bool preSaleEligible, PreSaleType presaleType) = canMintPresale(recipient, maxTokensToMint);

        require(isPreSaleActive() && preSaleEligible, "SeedPhrase:SALE_NOT_ACTIVE");
        require(
            balanceOf(recipient) + maxTokensToMint <= _getMaxMintPerWallet(),
            "NilPass:MINT_ABOVE_MAX_MINT_ALLOWANCE"
        );
        require(totalSupply() + maxTokensToMint <= params.maxTotalSupply, "NilPass:MAX_ALLOCATION_REACHED");

        require(paid == getNextPriceForNHoldersInWei(maxTokensToMint), "NilPass:INVALID_PRICE");

        for (uint256 i = 0; i < maxTokensToMint; i++) {
            uint256 tokenId = _tokenIds.current();

            tokenSeed[tokenId] = SeedPhraseUtils.generateSeed(tokenId);

            _safeMint(recipient, tokenId);
            _tokenIds.increment();
        }

        if (preSaleEligible) {
            preSaleLimits[presaleType] -= maxTokensToMint;
        }
    }

    /**
     * @notice Allow anyone to mint a token with the supply id if this pass is unrestricted.
     *         n token holders can use this function without using the n token holders allowance,
     *         this is useful when the allowance is fully utilized.
     * @param recipient Recipient of the mint
     * @param amount Amount of tokens to mint
     * @param paid Amount paid for the mint
     */
    function mint(
        address recipient,
        uint8 amount,
        uint256 paid
    ) public virtual override nonReentrant {
        (bool preSaleEligible, PreSaleType presaleType) = canMintPresale(recipient, amount);

        require(isPublicSaleActive() || (isPreSaleActive() && preSaleEligible), "SeedPhrase:SALE_NOT_ACTIVE");
        require(!params.supportsTokenId, "NilPass:NON_TOKENID_MINTING_DISABLED");
        require(balanceOf(msg.sender) + amount <= _getMaxMintPerWallet(), "NilPass:MINT_ABOVE_MAX_MINT_ALLOWANCE");
        require(totalSupply() + amount <= params.maxTotalSupply, "NilPass:MAX_ALLOCATION_REACHED");

        require(paid == getNextPriceForOpenMintInWei(amount), "NilPass:INVALID_PRICE");

        for (uint256 i = 0; i < amount; i++) {
            uint256 tokenId = _tokenIds.current();
            tokenSeed[tokenId] = SeedPhraseUtils.generateSeed(tokenId);

            _safeMint(recipient, tokenId);
            _tokenIds.increment();
        }

        if (preSaleEligible) {
            preSaleLimits[presaleType] -= amount;
        }
    }

    /**
     * @notice Allow anyone to burn two single panels they own in order to mint
     *         a double paneled token.
     * @param firstTokenId Token ID of the first token
     * @param secondTokenId Token ID of the second token
     */
    function burnForDoublePanel(uint256 firstTokenId, uint256 secondTokenId) public nonReentrant {
        require(_doublePanelTokens.current() < MAX_BURNS, "SeedPhrase:MAX_BURNS_REACHED");
        require(
            ownerOf(firstTokenId) == msg.sender && ownerOf(secondTokenId) == msg.sender,
            "SeedPhrase:INCORRECT_OWNER"
        );
        require(firstTokenId != secondTokenId, "SeedPhrase:EQUAL_TOKENS");
        // Ensure two owned tokens are in Burnable token pairings
        require(
            doubleWordPairings[abi.encode(string(firstTokenId), "-", string(secondTokenId))],
            "SeedPhrase:INVALID_TOKEN_PAIRING"
        );

        _burn(firstTokenId);
        _burn(secondTokenId);

        // Any Token ID of 3000 or greater indicates it is a double panel e.g. 3000, 3001, 3002...
        uint256 doublePaneledTokenId = 3000 + _doublePanelTokens;

        _mint(msg.sender, doublePaneledTokenId);

        // Add burned tokens to burnedTokensPairings mapping so we can use them to render the double panels later
        burnedTokensPairings[doublePaneledTokenId] = [firstTokenId, secondTokenId];
        _doublePanelTokens.increment();

        emit Burnt(msg.sender, firstTokenId, secondTokenId, doublePaneledTokenId);
    }

    /**
     * @notice Calculate the total available number of mints
     * @return total mint available
     */
    function totalMintsAvailable() public view override returns (uint256) {
        return derivativeParams.maxTotalSupply - totalSupply();
    }

    function _getMaxMintPerWallet() internal view returns (uint128) {
        return isPublicSaleActive() ? MAX_PUBLIC_MINT : params.maxMintAllowance;
    }

    function canMint(address account) public view virtual override returns (bool) {
        uint256 balance = balanceOf(account);

        if (isPublicSaleActive() && (totalMintsAvailable() > 0) && balance < MAX_PUBLIC_MINT) {
            return true;
        }

        if (isPreSaleActive()) {
            (bool preSaleEligible, ) = canMintPresale(account, 1);
            if (preSaleEligible) {
                return true;
            }
        }

        return false;
    }

    function tokenAttributes(uint256 tokenId, Randomize.Random memory random)
        public
        view
        virtual
        returns (SeedPhraseUtils.Attrs memory attributes, bytes memory traits)
    {
        attributes = SeedPhraseUtils.Attrs({
            showStroke: random.boolPercentage(70), // Show stroke 70% of the time
            showPanel: random.boolPercentage(80), // Show panel 80% of the time
            backgroundCircles: random.boolPercentage(2), // Show background circles 2% of the time
            doublePanel: (tokenId >= 3000), // To render a double parameter second token must not be 0
            bigBackgroundCircle: random.boolPercentage(15),
            border: false,
            backgroundSquare: false,
            showGrid: false,
            greyscale: false
        });

        // Only give option of background square if bigBackgroundCircle is not true
        if (!attributes.bigBackgroundCircle) {
            attributes.backgroundSquare = random.boolPercentage(15);
            // Border should only be on if not using background shapes
            if (!attributes.backgroundSquare) {
                attributes.border = random.boolPercentage(40);
            }
        }
        if (attributes.showStroke) {
            attributes.greyscale = random.boolPercentage(2); // Greyscale should only be seen if stroke is shown
            if (!attributes.backgroundCircles) {
                attributes.showGrid = random.boolPercentage(3); // Only set use grid if stroke is true and background circles is not true
            }
        }
        // Start Traits JSON
        traits = abi.encodePacked('[{"trait_type": "BIP39", "value": "', tokenId.toString(), '"},');

        if (!attributes.showStroke) {
            traits = abi.encodePacked(traits, '{"value": "No Stroke"},');
        } else if (attributes.greyscale) {
            traits = abi.encodePacked(traits, '{"value": "Greyscale"},');
        }
        if (!attributes.showPanel) {
            traits = abi.encodePacked(traits, '{"value": "No Panel"},');
        }
        if (attributes.backgroundCircles) {
            traits = abi.encodePacked(traits, '{"value": "Background Circles"},');
        } else if (attributes.showGrid) {
            traits = abi.encodePacked(traits, '{"value": "Grid"},');
        }
        if (attributes.bigBackgroundCircle) {
            traits = abi.encodePacked(traits, '{"value": "Giant Circle Background"},');
        } else if (attributes.backgroundSquare) {
            traits = abi.encodePacked(traits, '{"value": "Window Pane"},');
        } else if (attributes.border) {
            traits = abi.encodePacked(traits, '{"value": "Border"},');
        }

        traits = abi.encodePacked(traits, "]");
    }

    /// @param tokenId the tokenId (only double panels will have two)
    /// @return the json
    function _render(
        uint256 tokenId,
        Randomize.Random memory random,
        SeedPhraseUtils.Attrs memory attributes
    ) internal view virtual returns (string memory) {
        // Get color pallet
        SeedPhraseUtils.Colors memory pallet = SeedPhraseUtils._getPalette(random, attributes);

        //  Start SVG (viewbox & static patterns)
        bytes memory svg = abi.encodePacked(
            "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ",
            viewBox.toString(),
            " ",
            viewBox.toString(),
            "'><path fill='",
            pallet.background,
            "' ",
            SeedPhraseUtils._getStrokeStyle(attributes.border, pallet.blackOrWhite, "0.3", 50),
            " d='M0 0h",
            viewBox.toString(),
            "v",
            viewBox.toString(),
            "H0z'/>",
            "  <pattern id='panelCircles' x='0' y='0' width='.25' height='.1' patternUnits='objectBoundingBox'>",
            "<circle cx='",
            radius.toString(),
            "' cy='",
            radius.toString(),
            "' r='",
            r.toString(),
            "' fill='",
            pallet.negativeCircleFill,
            "' ",
            SeedPhraseUtils._getStrokeStyle(attributes.showStroke, pallet.negativeCircleStroke, "1", strokeWeight),
            " /></pattern>"
        );
        // Render optional patterns (grid OR background circles)
        if (attributes.backgroundCircles) {
            svg = abi.encodePacked(
                svg,
                "<pattern id='backgroundCircles' x='0' y='0' width='",
                segmentSize.toString(),
                "' height='",
                segmentSize.toString(),
                "' patternUnits='userSpaceOnUse'><circle cx='",
                radius.toString(),
                "' cy='",
                radius.toString(),
                "' r='",
                r.toString(),
                "' fill='",
                pallet.blackOrWhite,
                "' style='fill-opacity: ",
                pallet.dynamicOpacity,
                ";'></circle></pattern><path fill='url(#backgroundCircles)' d='M0 0h",
                viewBox.toString(),
                "v",
                viewBox.toString(),
                "H0z'/>"
            );
        } else if (attributes.showGrid) {
            svg = abi.encodePacked(
                svg,
                "<pattern id='grid' x='0' y='0' width='",
                segmentSize.toString(),
                "' height='",
                segmentSize.toString(),
                "' patternUnits='userSpaceOnUse'><rect x='0' y='0' width='",
                segmentSize.toString(),
                "' height='",
                segmentSize.toString(),
                "' fill='none' ",
                SeedPhraseUtils._getStrokeStyle(true, pallet.blackOrWhite, pallet.dynamicOpacity, strokeWeight),
                "'/></pattern><path fill='url(#grid)' d='M0 0h",
                viewBox.toString(),
                "v",
                viewBox.toString(),
                "H0z'/>"
            );
        }
        if (attributes.bigBackgroundCircle) {
            (uint16 shapeSize, uint16 stroke) = SeedPhraseUtils._backgroundShapeSizing(random, attributes);
            uint16 centerCircle = (viewBox / 2);
            svg = abi.encodePacked(
                svg,
                "<circle cx='",
                centerCircle.toString(),
                "' cy='",
                centerCircle.toString(),
                "' r='",
                (shapeSize / 2).toString(),
                "' fill='",
                pallet.backgroundCircle,
                "' stroke='",
                pallet.negativeCircleStroke,
                "' style='stroke-width:",
                stroke.toString(),
                ";stroke-opacity:0.3'/>"
            );
        } else if (attributes.backgroundSquare) {
            (uint16 shapeSize, uint16 stroke) = SeedPhraseUtils._backgroundShapeSizing(random, attributes);
            uint16 centerSquare = ((viewBox - shapeSize) / 2);
            svg = abi.encodePacked(
                svg,
                "<rect x='",
                centerSquare.toString(),
                "' y='",
                centerSquare.toString(),
                "' width='",
                shapeSize.toString(),
                "' height='",
                shapeSize.toString(),
                "' fill='",
                pallet.backgroundCircle,
                "' stroke='",
                pallet.negativeCircleStroke,
                "' style='stroke-width:",
                stroke.toString(),
                ";stroke-opacity:0.3'/>"
            );
        }

        // Double panel (only if holder has burned two tokens from the defined pairings)
        // TODO: Add further checking here to confirm we are safe to render a double panel
        if (attributes.doublePanel) {
            uint256[2] memory tokens = burnedTokensPairings[tokenId];
            (uint8[4] memory firstBipIndexArray, string memory firstBipIndexStr) = SeedPhraseUtils.transformTokenId(
                tokens[0]
            );
            (uint8[4] memory secondBipIndexArray, string memory secondBipIndexStr) = SeedPhraseUtils.transformTokenId(
                tokens[1]
            );

            svg = abi.encodePacked(
                svg,
                _renderSinglePanel(firstBipIndexArray, attributes, pallet, doublePanel1X, false),
                _renderSinglePanel(secondBipIndexArray, attributes, pallet, doublePanel2X, true)
            );

            // Create text
            bytes memory combinedText = abi.encodePacked(firstBipIndexStr, " - #", secondBipIndexStr);
            svg = abi.encodePacked(
                svg,
                SeedPhraseUtils._renderText(string(combinedText), pallet.blackOrWhite),
                "</svg>"
            );
        }
        // Single Panel
        else {
            (uint8[4] memory bipIndexArray, string memory bipIndexStr) = SeedPhraseUtils.transformTokenId(tokenId);
            svg = abi.encodePacked(svg, _renderSinglePanel(bipIndexArray, attributes, pallet, singlePanelX, false));

            // Add closing text and svg element
            svg = abi.encodePacked(svg, SeedPhraseUtils._renderText(bipIndexStr, pallet.blackOrWhite), "</svg>");
        }

        return string(svg);
    }

    function _renderSinglePanel(
        uint8[4] memory bipIndexArray,
        SeedPhraseUtils.Attrs memory attributes,
        SeedPhraseUtils.Colors memory pallet,
        uint16 panelX,
        bool secondPanel
    ) internal pure returns (bytes memory panelSvg) {
        // Draw panels
        bool squareEdges = (attributes.doublePanel && attributes.backgroundSquare);
        if (attributes.showPanel) {
            panelSvg = abi.encodePacked(
                "<rect x='",
                (panelX - padding).toString(),
                "' y='",
                (panelY - padding).toString(),
                "' width='",
                (panelWidth + (padding * 2)).toString(),
                "' height='",
                (panelHeight + (padding * 2)).toString(),
                "' rx='",
                (squareEdges ? 0 : radius).toString(),
                "' fill='",
                (secondPanel ? pallet.panel2 : pallet.panel),
                "' ",
                SeedPhraseUtils._getStrokeStyle(attributes.showStroke, pallet.panelStroke, "1", strokeWeight),
                "/>"
            );
        }
        // Fill panel with negative circles, should resemble M600 300h400v1000H600z
        panelSvg = abi.encodePacked(
            panelSvg,
            "<path fill='url(#panelCircles)' d='M",
            panelX.toString(),
            " ",
            panelY.toString(),
            "h",
            panelWidth.toString(),
            "v",
            panelHeight.toString(),
            "H",
            panelX.toString(),
            "z'/>"
        );
        // Draw selected circles
        panelSvg = abi.encodePacked(
            panelSvg,
            _renderSelectedCircles(bipIndexArray, pallet, attributes.showStroke, panelX, secondPanel)
        );
    }

    function _renderSelectedCircles(
        uint8[4] memory bipIndexArray,
        SeedPhraseUtils.Colors memory pallet,
        bool showStroke,
        uint16 panelX,
        bool secondPanel
    ) internal pure returns (bytes memory svg) {
        for (uint8 i = 0; i < bipIndexArray.length; i++) {
            svg = abi.encodePacked(
                svg,
                "<circle cx='",
                (panelX + (segmentSize * i) + radius).toString(),
                "' cy='",
                (panelY + (segmentSize * bipIndexArray[i]) + radius).toString(),
                "' r='",
                (radius - padding).toString(),
                ".1' fill='", // Increase the size a tiny bit here (0.1) to hide negative circle outline
                (secondPanel ? pallet.selectedCircleFill2 : pallet.selectedCircleFill),
                "' ",
                SeedPhraseUtils._getStrokeStyle(showStroke, pallet.selectedCircleStroke, "1", strokeWeight),
                " />"
            );
        }
    }

    function validPairingKey(string memory key) public view returns (bool) {
        return pairings[key];
    }

    // TODO: Make this function only accessible by admin (not sure how to do this)
    function ammendPairings(string memory key, bool value) private {
        pairings[key] = value;
    }

    // Mapping should be a string containing both tokens e.g. "tokenId1-tokenId2"
    mapping(string => bool) doubleWordPairings;
}
░██████╗███████╗███████╗██████╗░  ██████╗░██╗░░██╗██████╗░░█████╗░░██████╗███████╗██╔════╝██╔════╝██╔════╝██╔══██╗  ██╔══██╗██║░░██║██╔══██╗██╔══██╗██╔════╝██╔════╝╚█████╗░█████╗░░█████╗░░██║░░██║  ██████╔╝███████║██████╔╝███████║╚█████╗░█████╗░░░╚═══██╗██╔══╝░░██╔══╝░░██║░░██║  ██╔═══╝░██╔══██║██╔══██╗██╔══██║░╚═══██╗██╔══╝░░██████╔╝███████╗███████╗██████╔╝  ██║░░░░░██║░░██║██║░░██║██║░░██║██████╔╝███████╗╚═════╝░╚══════╝╚══════╝╚═════╝░  ╚═╝░░░░░╚═╝░░╚═╝╚═╝░░╚═╝╚═╝░░╚═╝╚═════╝░╚══════╝
Welcome to ‘Seed Phrase’
Seed Phrase is a Crypto Native’ fully on-chain collection (YES, even the art generation 😎). Every decision in this project has been made with the available blockchain technologies in mind.
My intention for this project was simple - pay homage to those before me and inspire those who will be here after.


Check out the collection on OpenSea





seed phrase





    // Price per SEED

    var

    priceETH = 0.06

    // Remaining Supply

    var

    remaining = 2048

>>>

<<<


🙋 Click above to MINT your SEED now.

STATUS:ACTIVE







BIP-39 DECODER


This tool can be used to find the corresponding word for any given number in the BIP-0039 index (1-2048). Alternatively type a word to find its ID.
SEARCH

Start by entering a number from 1-2048 or a word from the BIP-0039 index


CRYPTO NATIVE CONCEPT


A 'Seed', ‘Seed Phrase’ or ‘mnemonic phrase’ is something most degens... sorry, 'crypto natives', will be familiar with! But for those who might not know... let’s educate 🤓
BIP-0039 is the most commonly used word list to generate a ‘seed phrase’. Think of it as a dictionary that only holds ‘2048’ words. Wallet software uses this list to generate a string of 12 - 24 unique words which is then used to safeguard our digital assets from external attacks.
Example: [ stay, curious, identify, truth, have, trust, empower, people, help, educate, grow, together ]
The Seed Phrase collection is inspired by BIP-0039, utilising all 2048 words in its list! Click here to learn more and to see all of the available words.






THE ART


Backstory

Inspired by the many unique ways in which people chose to store their Seed Phrase, I was determined to create a way to represent the words from BIP-0039 through Art. I started this project back in early June by spray painting dots onto paper that represented individual words from the list.
I quickly realised that was a terrible idea 🤷‍♂️😂 and instead I should harness the power of Cryptography, this is where I began to educate myself on creative coding. After months of experimenting with my initial project: 'Genesis Sketches', and one community airdrop later, I came up with the aesthetic for ‘Seed Phrase’.

...

Aesthetic

Seed Phrase’s aesthetic takes inspiration from the many ways crypto natives have adopted to hide their pass-codes.
By attaching a simple four digit code starting from 0001 (which means 'abandon') all the way up to 2048 (which means 'zoo'), each piece in the collection will translate to a verifiably unique word from the BIP-0039 word list.
I came up with a way to represent this visually through code 🧠. That's where the idea of a 4x10 circular grid came in. See if you can work out how these circles translate to the BIP-0039 index, here's an "Example" [0628] for you 🤓.
Example [0628]

You might notice how this idea resembles the engraved metal plates people often use to store/decipher their personal recovery phrases.

...

Rarities

There are a number of things that can determine what makes each piece rare, such as...

1. COLOURS


There are 26 hand picked colour palettes, each containing 6 colors. At mint, a palette will be randomly selected and its order will be scrambled. If my maths is correct, this means there are 720 different colour combinations for each palette (6*5*4*3*2*1 = 720). Basically... A LOT of different colour options!


However... 1 pallet is determined to be slightly rarer than the other 25. This pallet is completely Monochrome. If you are lucky enough to get this you can officially say "LOOKS RARE"!


2. ATTRIBUTES / TRAITS


These are determined at mint and will control certain aspects of the design (e.g. border or no border). Unlike the colours, these DO NOT have an equal weighting 💪.


The ranking system is as follows:

    👍🏻 Basic (common)

    • OG Stroke
    • OG Panel
    • Border

    👌 Kinda' Rare (20-30% chance)

    • No Panel
    • No Stroke
    • Group Square
    • Group Circle

    💎 Rare (5-10% chance)

    • Caged
    • Bubblewrap

    🚀 Super Rare (1-3%)

    • Monochrome


3. THE WORD IT REPRESENTS


Each piece visually represents ONE unique word from the BIP-0039 word list. Although you might share similar traits with other pieces, only YOU will own YOUR word.


Naturally there will be words that resonate with the community more than others, for example "Love" [1060] might be considered 'more rare' than say "Tooth" [1831]. Did I just RUG tooth 👀
If you haven't guessed already, the selected dots and their positions in the art (see examples at the top of the page) are what depict a number from the BIP-0039 index (1-2048) and therefore connects it to the unique word it represents.


SOURCE OF RANDOM


A common practice in the space has been to use solely block data as a source of randomness (such as the block hash), unfortunately this approach has some downsides...


"A miner has the choice to publish a block or not. If they mine a block that they don’t like they can throw it out! If a group of miners do this, your numbers aren’t random any more!"

read more here

With blockchain constantly changing, we chose to adapt with the times by incorporating Chainlink VRF into our RNG (random number generator) to provide a provably-fair and verifiable source of randomness.
As I mentioned above, some decisions are weighted, meaning they are more/less likely to happen. This is how we create traits that are more rare than others, cough cough... Monochrome.







FULLY ON-CHAIN


Fully ‘on-chain’ implies that your ‘SEED’ (NFT), its metadata, attributes and the Art are generated directly from the smart contract and stored immutably on the Ethereum blockchain.
Come again?!
A large majority of NFTs store only the ownership information on-chain, whereas everything else (metadata, Artwork) lives on external storage off-chain! This is for a few reasons, one contributing factor may be the size or the complexity of the artwork (e.g. 3D rendering, photography/video).
The risk with off-chain storage is that the third-party vendor is solely responsible for your data:
  • Servers may go offline
  • They could be hacked
  • They could go out of business
  • Their servers could be destroyed

Any of the above could potentially render your asset worthless! Note, protocols such as Arweave are solving this storage issue.

DEFLATIONARY


The burn mechanism will allow the collector to swap (AKA burn) TWO single 'SEEDS' for ONE new outcome (super rare). This decreases the overall collection size. For example if 200 'SEEDS' were burned the overall collection supply would decrease by 100 tokens meaning the collection is deflationary.
How does this work?
  • 200 single SEEDs are burned (destroyed)
  • 100 NEW SUPER RARE outcomes are minted (created)
  • This brings the supply from 2048 down to 1948

Not all SEEDS will be eligible for burning, however there will be a community driven aspect giving YOU the power to vote towards which new outcomes will be added to the 'Burnable Pairings List' find out more about this on the BURN PAGE.