File size: 5,353 Bytes
9bf0b92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title FORGE ART — On-Chain SACM Agent Generative NFT
 * @notice Each token is a unique piece of ASCII art generated by a SnapKitty
 *         SACM agent, sealed to the WORM chain, stored entirely on-chain.
 *         No IPFS. No external dependencies. Art lives on Base forever.
 *
 * @dev ERC-721 with fully on-chain SVG tokenURI.
 *      Mint is gated by MINTER_ROLE — only the FORGE settlement bridge mints.
 *      Each mint records: agent name, WORM chain tip, SVG art, token seed.
 *      Art is deterministically reproducible from (agent, wormTip, tokenId).
 *
 * @custom:deployed-by SnapKitty Collective LLC — EIN 41-5105572
 * @custom:holding-entity Bel Esprit D'Accord Trust — EIN 41-6630640
 * @custom:legal UTILITY COLLECTIBLE ONLY. Not a security or investment instrument.
 */

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract FORGEART is ERC721, ERC721Burnable, AccessControl {

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant ARCHITECT_ROLE = keccak256("ARCHITECT_ROLE");

    uint256 public nextTokenId;
    uint256 public constant MAX_SUPPLY = 2_100;  // 1/10 of FORGE cap — scarce

    struct ArtToken {
        string agent;       // NOVA | ENKI | CIPHER | VAULT
        string wormTip;     // WORM chain tip at mint time (provenance)
        string tokenSeed;   // deterministic seed for off-chain reproduction
        string svgArt;      // full SVG stored on-chain
        uint256 mintedAt;
    }

    mapping(uint256 => ArtToken) public artTokens;
    mapping(string => uint256) public agentMintCount;

    event ArtMinted(
        uint256 indexed tokenId,
        address indexed recipient,
        string agent,
        string tokenSeed,
        string wormTip
    );

    constructor(address architect, address minter) ERC721("FORGE ART", "FART") {
        require(architect != address(0), "FORGEART: zero architect");
        require(minter != address(0),   "FORGEART: zero minter");
        _grantRole(DEFAULT_ADMIN_ROLE, architect);
        _grantRole(ARCHITECT_ROLE, architect);
        _grantRole(MINTER_ROLE, minter);
    }

    /**
     * @notice Mint a new FORGE ART token with fully on-chain SVG art.
     * @param recipient  Address receiving the NFT
     * @param agent      Agent name — "NOVA", "ENKI", "CIPHER", or "VAULT"
     * @param wormTip    WORM chain tip at time of sealing (provenance anchor)
     * @param tokenSeed  Deterministic seed — keccak256(agent:wormTip:tokenId)
     * @param svgArt     Complete SVG string to store on-chain
     */
    function mintArt(
        address recipient,
        string calldata agent,
        string calldata wormTip,
        string calldata tokenSeed,
        string calldata svgArt
    )
        external
        onlyRole(MINTER_ROLE)
    {
        require(nextTokenId < MAX_SUPPLY, "FORGEART: max supply reached");
        require(recipient != address(0), "FORGEART: zero recipient");
        require(bytes(agent).length > 0, "FORGEART: empty agent");
        require(bytes(svgArt).length > 0, "FORGEART: empty art");

        uint256 tokenId = nextTokenId++;
        artTokens[tokenId] = ArtToken({
            agent: agent,
            wormTip: wormTip,
            tokenSeed: tokenSeed,
            svgArt: svgArt,
            mintedAt: block.timestamp
        });
        agentMintCount[agent]++;

        _safeMint(recipient, tokenId);

        emit ArtMinted(tokenId, recipient, agent, tokenSeed, wormTip);
    }

    /**
     * @notice Returns fully on-chain tokenURI as a base64-encoded data URI.
     */
    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        _requireOwned(tokenId);
        ArtToken storage art = artTokens[tokenId];

        string memory svgBase64 = Base64.encode(bytes(art.svgArt));

        string memory json = string(abi.encodePacked(
            '{"name":"FORGE ART #', Strings.toString(tokenId), ' - ', art.agent,
            '","description":"On-chain generative ASCII art sealed to the SnapKitty SACM WORM chain. Agent: ', art.agent,
            '. Every pixel is a proof of productive work.","image":"data:image/svg+xml;base64,', svgBase64,
            '","attributes":[',
                '{"trait_type":"Agent","value":"', art.agent, '"},',
                '{"trait_type":"WORM Tip","value":"', art.wormTip, '"},',
                '{"trait_type":"Token Seed","value":"', art.tokenSeed, '"},',
                '{"trait_type":"Minted At","value":', Strings.toString(art.mintedAt), '}',
            ']}'
        ))  ;

        return string(abi.encodePacked(
            'data:application/json;base64,',
            Base64.encode(bytes(json))
        ));
    }

    function remainingSupply() external view returns (uint256) {
        return MAX_SUPPLY - nextTokenId;
    }

    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, AccessControl)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}