HuskNFT

0xcef60c5df1a96e3349f7fdfcdb575fb7d13fd9fe

Verification
Verified
v0.8.27+commit.40a35a09
Type
Contract
19,573 bytes
ABI entries
135
50 read · 39 write
License
none

Contract information

Address
0xcef60c5df1a96e3349f7fdfcdb575fb7d13fd9fe
Chain
Robinhood Chain (4663)
Compiler
v0.8.27+commit.40a35a09
Optimization
Enabled
Creation tx
0xaae8f8e3da…f248a15fce

Token

Name
Husks
Symbol
HUSKS
Decimals
Holders
1,069

Read contract (50)

KEEPER_BOUNTY_FLOOR()uint256
MAX_PER_WALLET()uint256
MAX_ROYALTY_BPS()uint96
MAX_SUPPLY_CEILING()uint256
accountImplementation()address
allowlistPrice()uint256
allowlistRoot()bytes32
allowlisted(address)bool
arenaContract()address
balanceOf(address)uint256
contractURI()string
dailyLifeSupport()uint256
eoaOnlyMint()bool
freeMintThreshold()uint256
fuelOracle()address
fuelOracleBand()uint256
fuelPerDay()uint256
fuelToken()address
getApproved(uint256)address
getHusk(uint256)tuple
getHuskAccount(uint256)address
getHusks(uint256[])tuple[]
huskAccounts(uint256)address
huskToken()address
isAllowlisted(address, bytes32[])bool
isApprovedForAll(address, address)bool
isFreeMinter(address)bool
lastLook()uint32
manualFuelPerDay()uint256
maxSupply()uint256
mintPhase()uint8
mintPrice()uint256
mintPriceFor(address)uint256
mintRouter(address)bool
mintedPerWallet(address)uint256
name()string
owner()address
ownerOf(uint256)address
rebornDeposit()uint256
registry()address
renderer()address
rendererFrozen()bool
revealDeposit()uint256
royaltyInfo(uint256, uint256)address, uint256
supportsInterface(bytes4)bool
symbol()string
tokenURI(uint256)string
tokensOfOwner(address)uint256[]
totalMinted()uint256
treasury()address

Events (23)

AllowlistPriceSetAllowlistRootSetAllowlistSetApprovalApprovalForAllArenaContractSetBatchMetadataUpdateContractURISetFreeMintConfigSetFuelConfigSetFuelOracleBandSetFuelOracleSetHuskSummonedLifecycleCostsSetLookRevealedMaxSupplyIncreasedMetadataUpdateMintPhaseSetMintRouterSetOwnershipTransferredRendererFrozenRendererSetTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_registry",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_accountImpl",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_treasury",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_mintPrice",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_maxSupply",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AllowlistClosed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "numerator",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "denominator",
        "type": "uint256"
      }
    ],
    "name": "ERC2981InvalidDefaultRoyalty",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC2981InvalidDefaultRoyaltyReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numerator",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "denominator",
        "type": "uint256"
      }
    ],
    "name": "ERC2981InvalidTokenRoyalty",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC2981InvalidTokenRoyaltyReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721IncorrectOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721InsufficientApproval",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOperator",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721NonexistentToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InsufficientPayment",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MaxSupplyReached",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MintCapReached",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAllowlisted",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PublicMintClosed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "length",
        "type": "uint256"
      }
    ],
    "name": "StringsInsufficientHexLength",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenDoesNotExist",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "price",
        "type": "uint256"
      }
    ],
    "name": "AllowlistPriceSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "root",
        "type": "bytes32"
      }
    ],
    "name": "AllowlistRootSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "status",
        "type": "bool"
      }
    ],
    "name": "AllowlistSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "approved",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "ApprovalForAll",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "arena",
        "type": "address"
      }
    ],
    "name": "ArenaContractSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "_fromTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "_toTokenId",
        "type": "uint256"
      }
    ],
    "name": "BatchMetadataUpdate",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "ContractURISet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "huskToken",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "threshold",
        "type": "uint256"
      }
    ],
    "name": "FreeMintConfigSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "perDay",
        "type": "uint256"
      }
    ],
    "name": "FuelConfigSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "band",
        "type": "uint256"
      }
    ],
    "name": "FuelOracleBandSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "oracle",
        "type": "address"
      }
    ],
    "name": "FuelOracleSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "necromancer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "tbaAddress",
        "type": "address"
      }
    ],
    "name": "HuskSummoned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "revealDeposit",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "dailyLifeSupport",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "rebornDeposit",
        "type": "uint256"
      }
    ],
    "name": "LifecycleCostsSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint32",
        "name": "look",
        "type": "uint32"
      }
    ],
    "name": "LookRevealed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldMax",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newMax",
        "type": "uint256"
      }
    ],
    "name": "MaxSupplyIncreased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "_tokenId",
        "type": "uint256"
      }
    ],
    "name": "MetadataUpdate",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "enum HuskNFT.Phase",
        "name": "phase",
        "type": "uint8"
      }
    ],
    "name": "MintPhaseSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "MintRouterSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "RendererFrozen",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "renderer",
        "type": "address"
      }
    ],
    "name": "RendererSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "KEEPER_BOUNTY_FLOOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PER_WALLET",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_ROYALTY_BPS",
    "outputs": [
      {
        "internalType": "uint96",
        "name": "",
        "type": "uint96"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SUPPLY_CEILING",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accountImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "allowlistPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "allowlistRoot",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "allowlisted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "arenaContract",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "contractURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "dailyLifeSupport",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "eoaOnlyMint",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "freeMintThreshold",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "freezeRenderer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "fuelOracle",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "fuelOracleBand",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "fuelPerDay",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "fuelToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getApproved",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getHusk",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "tokenId",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "account",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "owner",
            "type": "address"
          },
          {
            "internalType": "bool",
            "name": "revealed",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "pendingReveal",
            "type": "bool"
          },
          {
            "internalType": "uint16",
            "name": "generation",
            "type": "uint16"
          },
          {
            "internalType": "uint8",
            "name": "level",
            "type": "uint8"
          },
          {
            "internalType": "uint32",
            "name": "wins",
            "type": "uint32"
          },
          {
            "internalType": "uint32",
            "name": "losses",
            "type": "uint32"
          },
          {
            "internalType": "uint16",
            "name": "currentStreak",
            "type": "uint16"
          },
          {
            "internalType": "uint256",
            "name": "accountBalance",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "tokenURI",
            "type": "string"
          }
        ],
        "internalType": "struct HuskNFT.HuskView",
        "name": "v",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getHuskAccount",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256[]",
        "name": "tokenIds",
        "type": "uint256[]"
      }
    ],
    "name": "getHusks",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "tokenId",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "account",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "owner",
            "type": "address"
          },
          {
            "internalType": "bool",
            "name": "revealed",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "pendingReveal",
            "type": "bool"
          },
          {
            "internalType": "uint16",
            "name": "generation",
            "type": "uint16"
          },
          {
            "internalType": "uint8",
            "name": "level",
            "type": "uint8"
          },
          {
            "internalType": "uint32",
            "name": "wins",
            "type": "uint32"
          },
          {
            "internalType": "uint32",
            "name": "losses",
            "type": "uint32"
          },
          {
            "internalType": "uint16",
            "name": "currentStreak",
            "type": "uint16"
          },
          {
            "internalType": "uint256",
            "name": "accountBalance",
            "type": "uint256"
          },
          {
            "internalType": "string",
            "name": "tokenURI",
            "type": "string"
          }
        ],
        "internalType": "struct HuskNFT.HuskView[]",
        "name": "out",
        "type": "tuple[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "huskAccounts",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "huskToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "isAllowlisted",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "isApprovedForAll",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "who",
        "type": "address"
      }
    ],
    "name": "isFreeMinter",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lastLook",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "manualFuelPerDay",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "quantity",
        "type": "uint256"
      }
    ],
    "name": "mint",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      },
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "quantity",
        "type": "uint256"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "mintAllowlist",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      },
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintPhase",
    "outputs": [
      {
        "internalType": "enum HuskNFT.Phase",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "mintPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "who",
        "type": "address"
      }
    ],
    "name": "mintPriceFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "mintRouter",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "quantity",
        "type": "uint256"
      }
    ],
    "name": "mintTo",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      },
      {
        "internalType": "address[]",
        "name": "",
        "type": "address[]"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "mintedPerWallet",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "notifyMetadataUpdate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ownerOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rebornDeposit",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "refreshMetadata",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fromTokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "toTokenId",
        "type": "uint256"
      }
    ],
    "name": "refreshMetadataRange",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "registry",
    "outputs": [
      {
        "internalType": "contract ERC6551Registry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renderer",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rendererFrozen",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint32",
        "name": "look",
        "type": "uint32"
      }
    ],
    "name": "reportLook",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "revealDeposit",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "salePrice",
        "type": "uint256"
      }
    ],
    "name": "royaltyInfo",
    "outputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "accounts",
        "type": "address[]"
      },
      {
        "internalType": "bool",
        "name": "status",
        "type": "bool"
      }
    ],
    "name": "setAllowlist",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "price",
        "type": "uint256"
      }
    ],
    "name": "setAllowlistPrice",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "root",
        "type": "bytes32"
      }
    ],
    "name": "setAllowlistRoot",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "setApprovalForAll",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_arena",
        "type": "address"
      }
    ],
    "name": "setArenaContract",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "uri",
        "type": "string"
      }
    ],
    "name": "setContractURI",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setDailyLifeSupport",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "_on",
        "type": "bool"
      }
    ],
    "name": "setEoaOnlyMint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "setFreeMintThreshold",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "oracle",
        "type": "address"
      }
    ],
    "name": "setFuelOracle",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "band",
        "type": "uint256"
      }
    ],
    "name": "setFuelOracleBand",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "perDay",
        "type": "uint256"
      }
    ],
    "name": "setFuelPerDay",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "setFuelToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "setHuskToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newMax",
        "type": "uint256"
      }
    ],
    "name": "setMaxSupply",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "enum HuskNFT.Phase",
        "name": "phase",
        "type": "uint8"
      }
    ],
    "name": "setMintPhase",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_price",
        "type": "uint256"
      }
    ],
    "name": "setMintPrice",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "allowed",
        "type": "bool"
      }
    ],
    "name": "setMintRouter",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setRebornDeposit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_renderer",
        "type": "address"
      }
    ],
    "name": "setRenderer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setRevealDeposit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint96",
        "name": "bps",
        "type": "uint96"
      }
    ],
    "name": "setRoyalty",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_treasury",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "summon",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "tba",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "quantity",
        "type": "uint256"
      }
    ],
    "name": "summonBatch",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "tokenIds",
        "type": "uint256[]"
      },
      {
        "internalType": "address[]",
        "name": "tbas",
        "type": "address[]"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "tokenURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "tokensOfOwner",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "ids",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalMinted",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/interfaces/IERC4906.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "./ERC6551Registry.sol";
import "./HuskAccount.sol";
import "./IFuelOracle.sol";
import {IHuskRenderer} from "./render/IHuskRenderer.sol";

/// @dev Minimal $HUSK balance read for the free-mint tier.
interface IHuskBalance {
    function balanceOf(address account) external view returns (uint256);
}

/**
 * @title HuskNFT
 * @notice ERC-721 that auto-deploys Token Bound Accounts on mint.
 *         Each NFT represents ownership of a Husk — an autonomous AI fighter.
 *
 *         Summon → Creates NFT → Deploys TBA → Initializes brain + art + stats
 */
contract HuskNFT is ERC721, ERC2981, IERC4906, Ownable {
    using Strings for uint256;

    // ─────────────────────── Config ───────────────────────

    uint256 public mintPrice;      // PUBLIC-phase price per Husk
    uint256 public maxSupply;      // mint cap; raised only via setMaxSupply (increase-only, ceiling-bound)
    /// @notice Hard ceiling on maxSupply. setMaxSupply can only ever raise the cap up to this bound, which
    ///         is far below MercenaryHusk.MERC_BASE (1e9) so permanent + mercenary ids can never collide,
    ///         and so holders' worst-case dilution is a KNOWN, immutable bound rather than open-ended.
    uint256 public constant MAX_SUPPLY_CEILING = 1_000_000;
    uint256 public totalMinted;

    // ── Mint phases + whitelist ──
    enum Phase { Closed, Allowlist, Public }
    /// @notice Current mint phase. Starts Closed; owner opens Allowlist then Public.
    Phase public mintPhase;
    /// @notice Merkle root of the allowlist (leaf = keccak256(abi.encodePacked(minter))).
    bytes32 public allowlistRoot;
    /// @notice Direct on-chain allowlist (owner-managed from the admin panel). A wallet may mint in the
    ///         Allowlist phase if it is either here OR proven by the merkle root — so small lists can be
    ///         managed live by address without rebuilding a root, and large drops still use the root.
    mapping(address => bool) public allowlisted;
    /// @notice Price per Husk during the Allowlist phase (may differ from the public price).
    uint256 public allowlistPrice;

    /// @notice Free-mint tier: a wallet holding >= freeMintThreshold of `huskToken` ($HUSK) mints at
    ///         price 0 — a growth flywheel that turns "I want a free mint" into "go buy $HUSK". Both are
    ///         owner-settable; set huskToken to address(0) OR threshold to 0 to disable the free tier.
    ///         All three tiers (free holder / allowlist / public) are live at once whenever mint is open.
    address public huskToken;
    uint256 public freeMintThreshold;
    event FreeMintConfigSet(address huskToken, uint256 threshold);

    /// @notice Per-wallet mint cap (primary mints only; secondary transfers are unrestricted).
    uint256 public constant MAX_PER_WALLET = 10;
    mapping(address => uint256) public mintedPerWallet;

    /// @notice When true, only EOAs (or allowlisted routers) may mint. Stops a contract factory
    ///         from minting from many fresh child addresses in ONE transaction to blow past the
    ///         per-wallet cap. It does NOT stop a scripted attacker rotating funded EOAs (inherent
    ///         to any free mint). The owner can relax it, or allowlist specific aggregator routers.
    bool public eoaOnlyMint = true;

    /// @notice Aggregator/relayer contracts blessed to mint on a user's behalf via mintTo() even
    ///         while eoaOnlyMint is on. Their mints still count against the RECIPIENT's per-wallet
    ///         cap, so a blessed router can't over-mint any single user.
    mapping(address => bool) public mintRouter;

    /// @notice Collection-level metadata URI (OpenSea/marketplace collection page). Owner-settable.
    string private _contractURI;

    /// @notice Secondary-sale royalty, capped so it can never be rugged upward.
    uint96 public constant MAX_ROYALTY_BPS = 1000; // 10%

    ERC6551Registry public immutable registry;
    address public immutable accountImplementation;
    address public treasury;
    address public arenaContract;

    // Art renderer (sprite classes). Swappable for art seasons until frozen, then immutable.
    address public renderer;
    bool public rendererFrozen;

    // tokenId → TBA address
    mapping(uint256 => address) public huskAccounts;

    // Uniqueness registry: (class << 8 | colourway) + 1 of the most recent reveal (0 = none yet).
    // Each emerging Husk avoids this look, so no reveal ever repeats the one before it.
    uint32 public lastLook;

    // ── Fuel: an ERC-20 that can prepay life support (as well as ETH) ──
    /// @notice Owner-set fuel token (e.g. $HUSK once deployed). address(0) = disabled, ETH-only.
    ///         MUST be a trusted, standard ERC-20 (no fee-on-transfer / callback tokens): feedFuel
    ///         moves it with a plain transferFrom to the burn address.
    address public fuelToken;
    /// @notice Manual $HUSK per day of life support (0 = disabled). Used when no oracle is set — e.g.
    ///         while the token is still on its launchpad bonding curve (no Uniswap pool yet to peg to).
    uint256 public manualFuelPerDay;
    /// @notice Optional price oracle: once $HUSK graduates to a Uniswap pool, point this at a reader
    ///         (e.g. UniV3FuelOracle) and `fuelPerDay()` auto-tracks it. 0 = use the manual rate.
    ///         Swappable, so a v2/v4 graduation just means a different reader — the game never changes.
    address public fuelOracle;
    /// @notice Consumer-side sanity band (defense-in-depth). The oracle's rate is trusted only while it
    ///         stays within [manualFuelPerDay / band, manualFuelPerDay * band]; a reading outside that is
    ///         treated as a manipulated/thin pool and ignored in favour of the manual rate. So even a
    ///         buggy or un-clamped oracle against a shallow freshly-graduated pool can never price $HUSK
    ///         life support to dust (or absurdly high). Default 8x; tighten as the manual anchor tracks.
    uint256 public fuelOracleBand = 8;

    /// @notice $HUSK charged per day of life support. Returns the oracle's live price-pegged rate ONLY
    ///         when an oracle is set, a manual anchor exists, and the reading sits inside the sanity band;
    ///         otherwise the manual rate. try/catch + band + zero-guard so a broken, hostile, or wildly
    ///         mispriced oracle can never brick feeding or price life support to dust — it just falls back
    ///         to the manual number. The oracle only ever AUGMENTS a configured manual anchor (manual == 0
    ///         keeps fuel disabled regardless of the oracle), which is what makes the band meaningful.
    function fuelPerDay() public view returns (uint256) {
        uint256 manual = manualFuelPerDay;
        address o = fuelOracle;
        if (o != address(0) && manual > 0) {
            try IFuelOracle(o).huskPerDay() returns (uint256 v) {
                uint256 band = fuelOracleBand;
                // within [manual/band, manual*band] -> trust the live oracle; else ignore it
                if (v >= manual / band && v <= manual * band) return v;
            } catch {}
        }
        return manual;
    }

    // ── Owner-tunable lifecycle costs (read by HuskAccount; re-price the game live, no redeploy) ──
    /// @notice Built-in defaults match the original constants; the deploy script sets the launch tier
    ///         (cheaper). Reveal/reborn are floored at HuskAccount.EMERGE_BOUNTY (0.001) by the setters
    ///         so a reveal always covers the keeper. Life support may be set to anything (0 = free).
    uint256 public constant KEEPER_BOUNTY_FLOOR = 0.001 ether; // matches HuskAccount.EMERGE_BOUNTY
    uint256 public revealDeposit = 0.01 ether;      // min ETH to awaken (fund-to-reveal; seeds upkeep)
    uint256 public dailyLifeSupport = 0.001 ether;  // upkeep drawn per day from a living Husk
    uint256 public rebornDeposit = 0.05 ether;      // min ETH to revive a starved Husk

    // ─────────────────────── Events ───────────────────────

    event HuskSummoned(
        address indexed necromancer,
        uint256 indexed tokenId,
        address tbaAddress
    );
    event ArenaContractSet(address arena);
    event RendererSet(address renderer);
    event RendererFrozen();
    event LookRevealed(uint256 indexed tokenId, uint32 look);
    event MintRouterSet(address indexed router, bool allowed);
    event ContractURISet();
    event MintPhaseSet(Phase phase);
    event MaxSupplyIncreased(uint256 oldMax, uint256 newMax);
    event AllowlistRootSet(bytes32 root);
    event AllowlistSet(address indexed account, bool status);
    event AllowlistPriceSet(uint256 price);
    event FuelConfigSet(address token, uint256 perDay);
    event FuelOracleSet(address oracle);
    event FuelOracleBandSet(uint256 band);
    event LifecycleCostsSet(uint256 revealDeposit, uint256 dailyLifeSupport, uint256 rebornDeposit);

    // ─────────────────────── Errors ───────────────────────

    error InsufficientPayment();
    error MaxSupplyReached();
    error TokenDoesNotExist();
    error MintCapReached();
    error PublicMintClosed();
    error AllowlistClosed();
    error NotAllowlisted();

    // ─────────────────────── Constructor ─────────────────────

    constructor(
        address _registry,
        address _accountImpl,
        address _treasury,
        uint256 _mintPrice,
        uint256 _maxSupply
    ) ERC721("Husks", "HUSKS") Ownable(msg.sender) {  // NFT symbol HUSKS — distinct from the $HUSK ERC-20 (no ticker collision)
        registry = ERC6551Registry(_registry);
        accountImplementation = _accountImpl;
        treasury = _treasury;
        mintPrice = _mintPrice;      // public price
        allowlistPrice = _mintPrice; // default allowlist price = public price (owner can discount)
        maxSupply = _maxSupply;
        mintPhase = Phase.Closed;    // deploy locked; owner opens Allowlist then Public
        _setDefaultRoyalty(_treasury, 500); // 5% secondary royalty (ERC-2981) to the treasury
    }

    // ──────────────────── Summoning ────────────────────

    // Standard-named entrypoints so third-party mint aggregators auto-detect the mint, plus the
    // game-flavoured summon()/summonBatch() aliases. All routes share _summonTo, which keys the
    // per-wallet cap on the RECIPIENT (so a shared router can't brick at 5 and can't over-mint any
    // one user), enforces the 10k supply cap, deploys+initializes each token's ERC-6551 account,
    // and forwards the (free-mint: zero) fee to the treasury.

    /// @notice Allowlist mint — mints to the caller at the allowlist price, gated by a Merkle proof.
    ///         Open during the Allowlist phase and (for early supporters) the Public phase too.
    function mintAllowlist(uint256 quantity, bytes32[] calldata proof)
        external
        payable
        returns (uint256[] memory, address[] memory)
    {
        if (mintPhase == Phase.Closed) revert AllowlistClosed();
        _requireEOA();
        // Free-tier $HUSK holders mint free even via the WL path; otherwise must be on the allowlist.
        uint256 price;
        if (isFreeMinter(msg.sender)) {
            price = 0;
        } else {
            if (!allowlisted[msg.sender] && !isAllowlisted(msg.sender, proof)) revert NotAllowlisted();
            price = allowlistPrice;
        }
        return _mintCore(msg.sender, quantity, price);
    }

    /// @notice Is `account` on the allowlist for `proof`? (leaf = keccak256(abi.encodePacked(account)))
    function isAllowlisted(address account, bytes32[] calldata proof) public view returns (bool) {
        if (allowlistRoot == bytes32(0)) return false;
        return MerkleProof.verify(proof, allowlistRoot, keccak256(abi.encodePacked(account)));
    }

    /// @notice Does `who` qualify for a FREE mint (holds >= freeMintThreshold of the $HUSK token)?
    function isFreeMinter(address who) public view returns (bool) {
        return huskToken != address(0)
            && freeMintThreshold != 0
            && IHuskBalance(huskToken).balanceOf(who) >= freeMintThreshold;
    }

    /// @notice The price `who` pays via mint()/summon(): 0 if a qualifying $HUSK holder, else the
    ///         on-chain-allowlist price if allowlisted, else the public price. (Merkle-proof allowlisters
    ///         pay allowlistPrice via mintAllowlist.) Lets the UI show + send the exact tier price.
    function mintPriceFor(address who) external view returns (uint256) {
        if (isFreeMinter(who)) return 0;
        if (allowlisted[who]) return allowlistPrice;
        return mintPrice;
    }

    /// @notice Standard mint entrypoint (mints to the caller). Aggregator-friendly name.
    function mint(uint256 quantity) external payable returns (uint256[] memory, address[] memory) {
        _requireEOA();
        return _summonTo(msg.sender, quantity);
    }

    /// @notice Mint on behalf of a recipient — lets an allowlisted aggregator/relayer mint for a
    ///         user. The cap counts against `to`, and (when eoaOnlyMint is on) the caller must be
    ///         the recipient's EOA or an allowlisted router.
    function mintTo(address to, uint256 quantity)
        external
        payable
        returns (uint256[] memory, address[] memory)
    {
        require(to != address(0), "Mint to zero");
        _requireEOA();
        return _summonTo(to, quantity);
    }

    /// @notice Summon one Husk to yourself (alias for mint(1)).
    function summon() external payable returns (uint256 tokenId, address tba) {
        _requireEOA();
        (uint256[] memory ids, address[] memory tbas) = _summonTo(msg.sender, 1);
        return (ids[0], tbas[0]);
    }

    /// @notice Summon up to 5 Husks to yourself in one tx (alias for mint(quantity)).
    function summonBatch(uint256 quantity)
        external
        payable
        returns (uint256[] memory tokenIds, address[] memory tbas)
    {
        _requireEOA();
        return _summonTo(msg.sender, quantity);
    }

    /// @dev Tiered mint, keyed on the RECIPIENT — all three tiers are live at once once mint is open:
    ///      $HUSK holder → free · on-chain allowlisted → allowlist price · everyone else → public price
    ///      (public tier is Public-phase only; holders + allowlist can also go in the Allowlist phase).
    ///      The merkle-proof allowlist path lives in mintAllowlist().
    function _summonTo(address to, uint256 quantity)
        internal
        returns (uint256[] memory, address[] memory)
    {
        if (mintPhase == Phase.Closed) revert PublicMintClosed();
        uint256 price;
        if (isFreeMinter(to)) {
            price = 0;                        // holds >= threshold $HUSK — free
        } else if (allowlisted[to]) {
            price = allowlistPrice;           // on-chain allowlist — WL price
        } else {
            if (mintPhase != Phase.Public) revert PublicMintClosed(); // pure public: Public phase only
            price = mintPrice;
        }
        return _mintCore(to, quantity, price);
    }

    /// @dev Mints `quantity` Husks to `to` at `price` each. Neural weights/stats/art are a pure
    ///      function of the per-token seed (entropy only — real identity is rolled later at emerge).
    function _mintCore(address to, uint256 quantity, uint256 price)
        internal
        returns (uint256[] memory tokenIds, address[] memory tbas)
    {
        require(quantity > 0 && quantity <= MAX_PER_WALLET, "1-10 per tx");
        if (msg.value < price * quantity) revert InsufficientPayment();
        if (totalMinted + quantity > maxSupply) revert MaxSupplyReached();
        if (mintedPerWallet[to] + quantity > MAX_PER_WALLET) revert MintCapReached();
        mintedPerWallet[to] += quantity;

        tokenIds = new uint256[](quantity);
        tbas = new address[](quantity);

        for (uint256 i = 0; i < quantity; i++) {
            uint256 tokenId = totalMinted;
            totalMinted++;

            _mint(to, tokenId);

            address tba = registry.createAccount(
                accountImplementation, block.chainid, address(this), tokenId, 0
            );
            huskAccounts[tokenId] = tba;

            uint256 seed = uint256(
                keccak256(abi.encodePacked(tokenId, to, block.prevrandao, block.timestamp, i))
            );
            HuskAccount(payable(tba)).initialize(seed);

            tokenIds[i] = tokenId;
            tbas[i] = tba;
            emit HuskSummoned(to, tokenId, tba);
        }

        // Forward the fee (zero on a free mint) to the treasury.
        if (msg.value > 0) {
            (bool success, ) = treasury.call{value: msg.value}("");
            require(success, "Treasury transfer failed");
        }
    }

    // ──────────────────── On-Chain Metadata ────────────────────

    /**
     * @notice Fully on-chain tokenURI with SVG art and JSON metadata.
     */
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        if (huskAccounts[tokenId] == address(0)) revert TokenDoesNotExist();

        HuskAccount husk = HuskAccount(payable(huskAccounts[tokenId]));

        string memory name = _nameString(husk.huskName(), tokenId);

        // Art + traits both come from the (freezable) renderer. Wrap BOTH so a renderer defect that
        // reverts or gas-bombs for some seed can never PERMANENTLY brick metadata reads for
        // marketplaces (especially once the renderer is frozen) — degrade to a minimal but valid JSON
        // instead of reverting the whole tokenURI. Transfers never call tokenURI, so this is read-path
        // resilience only. (audit L-4)
        string memory svgBase64;
        try husk.renderSVG() returns (string memory svg) {
            svgBase64 = Base64.encode(bytes(svg));
        } catch {
            return _fallbackURI(name);
        }

        // Renderer traits fragment (leading comma). Dormant => ",{Status:Dormant}"; active => class+colourway.
        bytes memory rTraits;
        if (renderer != address(0)) {
            try IHuskRenderer(renderer).traits(husk.getRenderState()) returns (string memory t) {
                rTraits = bytes(t);
            } catch {
                return _fallbackURI(name);
            }
        }

        string memory attributes;
        string memory description;
        if (husk.isActive()) {
            // Revealed: full stat sheet + class traits.
            attributes = string(abi.encodePacked(_statAttributes(husk), rTraits));
            description = "On-chain AI fighter with INT8 neural brain. Born in The Trench.";
        } else {
            // Dormant/pre-reveal: total blackout — no stats, no class, just a sealed coffin.
            // rTraits carries a leading comma; strip it so the attributes array stays valid JSON.
            attributes = _dropLeadingComma(rTraits);
            description = "A dormant Husk sealed in its coffin. Fund it to awaken and reveal the fighter within.";
        }

        string memory json = string(
            abi.encodePacked(
                '{"name":"', name,
                '","description":"', description, '",',
                '"image":"data:image/svg+xml;base64,', svgBase64, '",',
                '"attributes":[', attributes, ']}'
            )
        );

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

    /// @dev Minimal, self-contained metadata used when the renderer reverts, so tokenURI always returns
    ///      valid JSON (name + Status) instead of bricking. No external calls. (audit L-4)
    function _fallbackURI(string memory name) internal pure returns (string memory) {
        bytes memory json = abi.encodePacked(
            '{"name":"', name,
            '","description":"Metadata temporarily unavailable (renderer error).",',
            '"attributes":[{"trait_type":"Status","value":"Unrenderable"}]}'
        );
        return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
    }

    /// @dev Full stat-sheet attributes (no leading/trailing comma). Split out to ease stack pressure.
    function _statAttributes(HuskAccount husk) internal view returns (string memory) {
        (
            uint16 atk, uint16 def, uint16 spd, uint16 hp,
            uint8 lvl, uint32 wins, uint32 losses,
            uint16 streak, , , ,
        ) = husk.getFullStats();
        return string(
            abi.encodePacked(
                _trait("Attack", atk),  ',',
                _trait("Defense", def),  ',',
                _trait("Speed", spd),    ',',
                _trait("Health", hp),    ',',
                _trait("Level", uint256(lvl)), ',',
                _trait("Wins", uint256(wins)), ',',
                _trait("Losses", uint256(losses)), ',',
                _trait("Best Streak", uint256(streak))
            )
        );
    }

    /// @dev Drop a single leading comma so a lone traits fragment forms a valid attributes array.
    function _dropLeadingComma(bytes memory b) internal pure returns (string memory) {
        if (b.length == 0 || b[0] != ',') return string(b);
        bytes memory out = new bytes(b.length - 1);
        for (uint256 i = 1; i < b.length; i++) out[i - 1] = b[i];
        return string(out);
    }

    function _trait(string memory traitType, uint256 value)
        internal
        pure
        returns (string memory)
    {
        return string(
            abi.encodePacked(
                '{"trait_type":"', traitType,
                '","value":', value.toString(), '}'
            )
        );
    }

    /// @dev bytes16 name → trimmed string; strips control chars, quotes and backslashes
    ///      so a hostile name can never corrupt the metadata JSON.
    function _nameString(bytes16 nameBytes, uint256 tokenId) internal pure returns (string memory) {
        if (nameBytes == bytes16(0)) return string(abi.encodePacked("HUSK-", tokenId.toString()));
        bytes memory raw = new bytes(16);
        uint256 n;
        for (uint256 i = 0; i < 16; i++) {
            bytes1 ch = nameBytes[i];
            if (ch == 0) break;
            if (ch < 0x20 || ch > 0x7E || ch == '"' || ch == 0x5C) continue;
            raw[n++] = ch;
        }
        if (n == 0) return string(abi.encodePacked("HUSK-", tokenId.toString()));
        bytes memory out = new bytes(n);
        for (uint256 i = 0; i < n; i++) out[i] = raw[i];
        return string(out);
    }

    /// @notice Called by a token's own bound account when its identity is revealed (emerge).
    ///         Records the revealed look so the NEXT reveal can be guaranteed to differ.
    function reportLook(uint256 tokenId, uint32 look) external {
        require(huskAccounts[tokenId] == msg.sender, "Only the token's account");
        lastLook = look + 1; // +1 so 0 always means "no reveal yet"
        emit LookRevealed(tokenId, look);
    }

    // ──────────────────── Admin ────────────────────

    function setArenaContract(address _arena) external onlyOwner {
        arenaContract = _arena;
        emit ArenaContractSet(_arena);
    }

    /// @notice Point tokenURI at an art renderer. Blocked forever once frozen.
    function setRenderer(address _renderer) external onlyOwner {
        require(!rendererFrozen, "Renderer frozen");
        renderer = _renderer;
        emit RendererSet(_renderer);
    }

    /// @notice One-way latch: permanently lock the current renderer (art becomes immutable).
    function freezeRenderer() external onlyOwner {
        require(!rendererFrozen, "Already frozen");
        rendererFrozen = true;
        emit RendererFrozen();
    }

    /// @dev EOA-or-allowlisted-router mint guard (see eoaOnlyMint). Blocks unknown contract callers.
    function _requireEOA() internal view {
        if (eoaOnlyMint) {
            require(msg.sender == tx.origin || mintRouter[msg.sender], "EOA only");
        }
    }

    function setEoaOnlyMint(bool _on) external onlyOwner {
        eoaOnlyMint = _on;
    }

    /// @notice Allowlist (or remove) an aggregator/relayer contract for mintTo().
    function setMintRouter(address router, bool allowed) external onlyOwner {
        mintRouter[router] = allowed;
        emit MintRouterSet(router, allowed);
    }

    // ──────────────── Fuel token (ERC-20 life support) ────────────────

    /// @notice Set the ERC-20 that can prepay life support (e.g. $HUSK). address(0) disables the
    ///         token path (ETH-only). ONLY set a trusted, standard ERC-20 — feedFuel assumes plain
    ///         transferFrom semantics (no fee-on-transfer, no transfer hooks).
    function setFuelToken(address token) external onlyOwner {
        fuelToken = token;
        emit FuelConfigSet(token, manualFuelPerDay);
    }

    /// @notice Set the MANUAL $HUSK-per-day rate (used when no oracle is set). 0 disables.
    function setFuelPerDay(uint256 perDay) external onlyOwner {
        manualFuelPerDay = perDay;
        emit FuelConfigSet(fuelToken, perDay);
    }

    /// @notice Point $HUSK life-support pricing at a live oracle (e.g. a Uniswap v3 TWAP reader) once
    ///         the token has a real pool — `fuelPerDay()` then tracks it. Set to 0 to fall back to the
    ///         manual rate (the state during the launchpad bonding curve, before a pool exists).
    function setFuelOracle(address oracle) external onlyOwner {
        fuelOracle = oracle;
        emit FuelOracleSet(oracle);
    }

    /// @notice Tune the oracle sanity band: the live rate is trusted only within [manual/band, manual*band].
    ///         Must be >= 1 (band == 1 pins the oracle to exactly the manual rate). Tighten it toward the
    ///         real volatility once the manual anchor is nudged near the pool peg. Does not affect the ETH
    ///         life-support path, which never reads the oracle.
    function setFuelOracleBand(uint256 band) external onlyOwner {
        require(band >= 1, "band < 1");
        fuelOracleBand = band;
        emit FuelOracleBandSet(band);
    }

    // ──────────────── Lifecycle cost tuning (owner) ────────────────

    /// @notice Min ETH to awaken a Husk (fund-to-reveal; the deposit seeds its upkeep). Floored at the
    ///         keeper bounty so emerge() can always pay a keeper.
    function setRevealDeposit(uint256 v) external onlyOwner {
        require(v >= KEEPER_BOUNTY_FLOOR, "below keeper bounty");
        revealDeposit = v;
        emit LifecycleCostsSet(revealDeposit, dailyLifeSupport, rebornDeposit);
    }

    /// @notice ETH drawn per day of life support (0 = free upkeep — Husks never starve; not advised,
    ///         it neuters the death/rebirth loop).
    function setDailyLifeSupport(uint256 v) external onlyOwner {
        dailyLifeSupport = v;
        emit LifecycleCostsSet(revealDeposit, dailyLifeSupport, rebornDeposit);
    }

    /// @notice Min ETH to revive a starved Husk into a new fighter. Floored at the keeper bounty.
    function setRebornDeposit(uint256 v) external onlyOwner {
        require(v >= KEEPER_BOUNTY_FLOOR, "below keeper bounty");
        rebornDeposit = v;
        emit LifecycleCostsSet(revealDeposit, dailyLifeSupport, rebornDeposit);
    }

    // ──────────────── Mint phase + whitelist admin ────────────────

    /// @notice Advance the mint phase: Closed → Allowlist → Public (can be set to any value).
    function setMintPhase(Phase phase) external onlyOwner {
        mintPhase = phase;
        emit MintPhaseSet(phase);
    }

    /// @notice Set the whitelist Merkle root (leaf = keccak256(abi.encodePacked(minter))).
    function setAllowlistRoot(bytes32 root) external onlyOwner {
        allowlistRoot = root;
        emit AllowlistRootSet(root);
    }

    /// @notice Add or remove wallets on the DIRECT on-chain allowlist (batched). Owner-only, driven from
    ///         the admin panel — no merkle root rebuild needed to manage a small list live.
    function setAllowlist(address[] calldata accounts, bool status) external onlyOwner {
        for (uint256 i = 0; i < accounts.length; i++) {
            allowlisted[accounts[i]] = status;
            emit AllowlistSet(accounts[i], status);
        }
    }

    /// @notice Set the allowlist-phase price (may be a discount on, or equal to, the public price).
    function setAllowlistPrice(uint256 price) external onlyOwner {
        allowlistPrice = price;
        emit AllowlistPriceSet(price);
    }

    // ──────────────── ERC-4906: dynamic-art metadata refresh ────────────────

    /// @notice Signal marketplaces to re-pull a token's metadata after its art changed. Called by
    ///         the token's OWN account (on reveal/level-up) and the arena (after a fight). Reverts
    ///         are swallowed by callers so a refresh can never brick a core action.
    function notifyMetadataUpdate(uint256 tokenId) external {
        require(
            huskAccounts[tokenId] == msg.sender || msg.sender == arenaContract,
            "Only token account or arena"
        );
        emit MetadataUpdate(tokenId);
    }

    /// @notice Permissionless retroactive refresh — anyone (a keeper, the dApp) can poke a token
    ///         or a range so a marketplace re-pulls the current on-chain art. Purely a hint.
    function refreshMetadata(uint256 tokenId) external {
        if (huskAccounts[tokenId] == address(0)) revert TokenDoesNotExist();
        emit MetadataUpdate(tokenId);
    }

    function refreshMetadataRange(uint256 fromTokenId, uint256 toTokenId) external {
        require(toTokenId < totalMinted && fromTokenId <= toTokenId, "bad range");
        emit BatchMetadataUpdate(fromTokenId, toTokenId);
    }

    // ──────────────── Collection metadata ────────────────

    /// @notice Collection-level metadata for marketplace collection pages. If unset, returns an
    ///         on-chain default carrying the ERC-2981 royalty (bps + recipient) for legacy readers.
    function contractURI() external view returns (string memory) {
        if (bytes(_contractURI).length != 0) return _contractURI;
        (address recv, uint256 bps) = royaltyInfo(0, 10000);
        bytes memory json = abi.encodePacked(
            '{"name":"Husks","description":"Fully on-chain AI fighters that live in their own wallets. Born in a coffin, revealed on funding, scarred by battle.",',
            '"seller_fee_basis_points":', bps.toString(),
            ',"fee_recipient":"', Strings.toHexString(recv), '"}'
        );
        return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
    }

    function setContractURI(string calldata uri) external onlyOwner {
        _contractURI = uri;
        emit ContractURISet();
    }

    function setMintPrice(uint256 _price) external onlyOwner {
        mintPrice = _price;
    }

    /// @notice Point the free-mint tier at the $HUSK token. Set to address(0) to disable the free tier.
    function setHuskToken(address token) external onlyOwner {
        huskToken = token;
        emit FreeMintConfigSet(token, freeMintThreshold);
    }

    /// @notice Minimum $HUSK balance (base units, 18 decimals) that earns a free mint. 0 disables it.
    function setFreeMintThreshold(uint256 amount) external onlyOwner {
        freeMintThreshold = amount;
        emit FreeMintConfigSet(huskToken, amount);
    }

    /// @notice Raise the mint cap. INCREASE-ONLY (can never shrink the collection or drop below what's
    ///         already minted) and bounded by the immutable MAX_SUPPLY_CEILING (which sits far below the
    ///         mercenary id space at 1e9), so the collection can grow if the game needs more supply while
    ///         holders keep a hard, known ceiling on dilution. Every change is event-logged for transparency.
    function setMaxSupply(uint256 newMax) external onlyOwner {
        require(newMax > maxSupply, "increase only");        // never shrink (also keeps newMax > totalMinted)
        require(newMax <= MAX_SUPPLY_CEILING, "above ceiling");
        uint256 old = maxSupply;
        maxSupply = newMax;
        emit MaxSupplyIncreased(old, newMax);
    }

    /// @notice Migrate the treasury. Moves the mint-fee/life-support destination AND re-points the
    ///         ERC-2981 royalty receiver to match (keeping the current bps), so secondary royalties
    ///         never keep flowing to an abandoned address after a migration.
    function setTreasury(address _treasury) external onlyOwner {
        require(_treasury != address(0), "Zero address"); // don't burn future fees
        treasury = _treasury;
        (, uint256 bps) = royaltyInfo(0, 10000); // current rate in bps (denominator 10000)
        _setDefaultRoyalty(_treasury, uint96(bps));
    }

    /// @notice Adjust the ERC-2981 secondary royalty rate (receiver stays the treasury).
    ///         Hard-capped at 10% so holders can never have the royalty rugged upward.
    function setRoyalty(uint96 bps) external onlyOwner {
        require(bps <= MAX_ROYALTY_BPS, "Royalty above hard cap");
        _setDefaultRoyalty(treasury, bps);
    }

    /// @dev ERC721 + ERC2981 + ERC-4906. IERC4906 has no functions, so its interfaceId is the
    ///      literal 0x49064906 (NOT type(IERC4906).interfaceId, which would compute to 0).
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC2981, IERC165)
        returns (bool)
    {
        return interfaceId == bytes4(0x49064906) || super.supportsInterface(interfaceId);
    }

    // ──────────────────── View Helpers ────────────────────

    function getHuskAccount(uint256 tokenId) external view returns (address) {
        return huskAccounts[tokenId];
    }

    /// @notice All token ids owned by `owner`. O(totalMinted) — a free view call, not a tx. Lets a
    ///         frontend show "my Husks" without ERC721Enumerable's per-transfer gas cost.
    function tokensOfOwner(address owner) external view returns (uint256[] memory ids) {
        uint256 bal = balanceOf(owner);
        ids = new uint256[](bal);
        uint256 found;
        for (uint256 t = 0; t < totalMinted && found < bal; t++) {
            if (_ownerOf(t) == owner) ids[found++] = t;
        }
    }

    /// @notice Everything a UI needs to render one Husk card, in a single call (owner, its wallet,
    ///         reveal/live state, class, level, record, and its wallet's ETH — the "life force").
    struct HuskView {
        uint256 tokenId;
        address account;          // the token-bound wallet
        address owner;
        bool revealed;            // isActive — coffin opened?
        bool pendingReveal;       // awaken/reborn committed, awaiting emerge
        uint16 generation;        // 0 = original, 1+ = reborn
        uint8 level;
        uint32 wins;
        uint32 losses;
        uint16 currentStreak;
        uint256 accountBalance;   // ETH held by the Husk's wallet (upkeep runway)
        string tokenURI;          // full on-chain metadata (coffin while dormant, character when live)
    }

    function getHusk(uint256 tokenId) external view returns (HuskView memory v) {
        address acctAddr = huskAccounts[tokenId];
        require(acctAddr != address(0), "TokenDoesNotExist");
        HuskAccount a = HuskAccount(payable(acctAddr));
        v.tokenId = tokenId;
        v.account = acctAddr;
        v.owner = ownerOf(tokenId);
        v.revealed = a.isActive();
        v.pendingReveal = a.revealCommitBlock() != 0;
        v.generation = a.generation();
        v.level = a.level();
        v.wins = a.winCount();
        v.losses = a.lossCount();
        v.currentStreak = a.currentStreak();
        v.accountBalance = acctAddr.balance;
        v.tokenURI = tokenURI(tokenId);
    }

    /// @notice Batched getHusk for a list of ids (one RPC round-trip for a whole wallet/gallery).
    function getHusks(uint256[] calldata tokenIds) external view returns (HuskView[] memory out) {
        out = new HuskView[](tokenIds.length);
        for (uint256 i = 0; i < tokenIds.length; i++) out[i] = this.getHusk(tokenIds[i]);
    }
}
Chain explorer3184msChain node107ms