Ships

0x03166761eb6fd0bc6cb8660d00789f605cdf05cf

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,512 bytes
ABI entries
108
50 read · 18 write
License
none

Contract information

Address
0x03166761eb6fd0bc6cb8660d00789f605cdf05cf
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x7495e78132…e71f88f6b5

Token

Name
Symbol
Decimals
Holders
11

Read contract (50)

BRANCH_CHANCE()uint8
BRANCH_SHARE()uint8
CRAFT_SUCCESS_L1()uint16
CRAFT_SUCCESS_L2()uint16
CRAFT_SUCCESS_L3()uint16
CRAFT_SUCCESS_L4()uint16
MAX_CRAFT_PRICE()uint256
MAX_ITEM_PRICE()uint256
MAX_LEVEL()uint8
MAX_SHIP_PRICE()uint256
MAX_SIGNATURE_TTL()uint256
MIN_CRAFT_PRICE()uint256
MIN_ITEM_PRICE()uint256
MIN_SHIP_PRICE()uint256
PRICE_TIMELOCK()uint256
SHIP_ID()uint256
STUCK_CRAFT_TIMEOUT()uint256
balanceOf(address, uint256)uint256
balanceOfBatch(address[], uint256[])uint256[]
craftRequestedAt(address)uint256
craftRequests(uint256)address, uint8, uint8, bool, bool
craftSuccessFor(uint8)uint16
domainSeparator()bytes32
eip712Domain()bytes1, string, string, uint256, address, bytes32, uint256[]
equippedChanceLevel(address)uint8
equippedShareLevel(address)uint8
getEquipped(address)uint8, uint8
getLastCraft(address)tuple
getPlayerRaidStats(address)uint8, uint8, bool
guardian()address
hasShip(address)bool
isApprovedForAll(address, address)bool
itemBalance(address, uint8, uint8)uint256
itemPriceArg(uint8)uint256
lastCraft(address)uint256, uint8, uint8, bool, bool
nextRequestId()uint256
owner()address
paused()bool
pendingCraft(address)bool
pendingCraftId(address)uint256
pendingItemPrice(uint8)uint256, uint256
pendingOwner()address
pendingShipPrice()uint256, uint256
renounceOwnership()
shipPriceArg()uint256
supportsInterface(bytes4)bool
token()address
treasury()address
trustedSigner()address
uri(uint256)string

Events (18)

ApprovalForAllCraftAbandonedCraftRequestedCraftResolvedEIP712DomainChangedEquippedGuardianChangedItemBoughtOwnershipTransferStartedOwnershipTransferredPausedPriceChangeAnnouncedShipBoughtTransferBatchTransferSingleTrustedSignerChangedURIUnpaused

ABI

[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "uri_",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "token_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "trustedSigner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      }
    ],
    "name": "AddressEmptyCode",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "AddressInsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ECDSAInvalidSignature",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "length",
        "type": "uint256"
      }
    ],
    "name": "ECDSAInvalidSignatureLength",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "s",
        "type": "bytes32"
      }
    ],
    "name": "ECDSAInvalidSignatureS",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC1155InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC1155InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "idsLength",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "valuesLength",
        "type": "uint256"
      }
    ],
    "name": "ERC1155InvalidArrayLength",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "ERC1155InvalidOperator",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC1155InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC1155InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC1155MissingApprovalForAll",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedInnerCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidShortString",
    "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": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "str",
        "type": "string"
      }
    ],
    "name": "StringTooLong",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "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": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      }
    ],
    "name": "CraftAbandoned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "fromLevel",
        "type": "uint8"
      }
    ],
    "name": "CraftRequested",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "fromLevel",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "success",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "randomValue",
        "type": "uint256"
      }
    ],
    "name": "CraftResolved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "EIP712DomainChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      }
    ],
    "name": "Equipped",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "guardian",
        "type": "address"
      }
    ],
    "name": "GuardianChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "qty",
        "type": "uint256"
      }
    ],
    "name": "ItemBought",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "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": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "effectiveAt",
        "type": "uint256"
      }
    ],
    "name": "PriceChangeAnnounced",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "ShipBought",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256[]",
        "name": "ids",
        "type": "uint256[]"
      },
      {
        "indexed": false,
        "internalType": "uint256[]",
        "name": "values",
        "type": "uint256[]"
      }
    ],
    "name": "TransferBatch",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "TransferSingle",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "signer",
        "type": "address"
      }
    ],
    "name": "TrustedSignerChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "value",
        "type": "string"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      }
    ],
    "name": "URI",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BRANCH_CHANCE",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "BRANCH_SHARE",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CRAFT_SUCCESS_L1",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CRAFT_SUCCESS_L2",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CRAFT_SUCCESS_L3",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CRAFT_SUCCESS_L4",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_CRAFT_PRICE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_ITEM_PRICE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_LEVEL",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SHIP_PRICE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SIGNATURE_TTL",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_CRAFT_PRICE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_ITEM_PRICE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_SHIP_PRICE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PRICE_TIMELOCK",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SHIP_ID",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "STUCK_CRAFT_TIMEOUT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "accounts",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "ids",
        "type": "uint256[]"
      }
    ],
    "name": "balanceOfBatch",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "qty",
        "type": "uint256"
      }
    ],
    "name": "buyItem",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "buyShip",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "clearStuckCraft",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "fromLevel",
        "type": "uint8"
      }
    ],
    "name": "craft",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "craftRequestedAt",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "craftRequests",
    "outputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "fromLevel",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "fulfilled",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "fromLevel",
        "type": "uint8"
      }
    ],
    "name": "craftSuccessFor",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "domainSeparator",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "eip712Domain",
    "outputs": [
      {
        "internalType": "bytes1",
        "name": "fields",
        "type": "bytes1"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "version",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "chainId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "verifyingContract",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256[]",
        "name": "extensions",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      }
    ],
    "name": "equip",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "equippedChanceLevel",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "equippedShareLevel",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "getEquipped",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "chanceLevel",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "shareLevel",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "getLastCraft",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "requestId",
            "type": "uint256"
          },
          {
            "internalType": "uint8",
            "name": "branch",
            "type": "uint8"
          },
          {
            "internalType": "uint8",
            "name": "fromLevel",
            "type": "uint8"
          },
          {
            "internalType": "bool",
            "name": "success",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "exists",
            "type": "bool"
          }
        ],
        "internalType": "struct Ships.CraftResult",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "getPlayerRaidStats",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "chanceLevel",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "shareLevel",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "playerHasShip",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "guardian",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "hasShip",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "isApprovedForAll",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      }
    ],
    "name": "itemBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      }
    ],
    "name": "itemPriceArg",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastCraft",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "branch",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "fromLevel",
        "type": "uint8"
      },
      {
        "internalType": "bool",
        "name": "success",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nextRequestId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "pendingCraft",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "pendingCraftId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      }
    ],
    "name": "pendingItemPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "pending",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "effectiveAt",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingShipPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "pending",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "effectiveAt",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "randomValue",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "deadline",
        "type": "uint64"
      },
      {
        "internalType": "bytes",
        "name": "signature",
        "type": "bytes"
      }
    ],
    "name": "resolveCraft",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256[]",
        "name": "ids",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "values",
        "type": "uint256[]"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "safeBatchTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "safeTransferFrom",
    "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": "guardian_",
        "type": "address"
      }
    ],
    "name": "setGuardian",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      }
    ],
    "name": "setItemPrice",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      }
    ],
    "name": "setShipPrice",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "signer_",
        "type": "address"
      }
    ],
    "name": "setTrustedSigner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "newUri",
        "type": "string"
      }
    ],
    "name": "setURI",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "shipPriceArg",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "contract ITreasury",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "trustedSigner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "uri",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

// =============================================================================
//  ARGONAUT — Ships.sol  (КОРАБЛИ, ПРЕДМЕТЫ, КРАФТ, УРОВНИ)
// -----------------------------------------------------------------------------
//  ERC-1155: корабли и предметы игроков. Ведёт уровни двух веток прокачки
//  (шанс/доля), обрабатывает покупку корабля, покупку базовых предметов и крафт
//  с риском поломки. Реализует IShipStats — источник уровней для Raid.
//  Все платежи в ARG уходят в Treasury.
//
//  ID-СХЕМА:
//    ID 0        = Корабль (Ship). В v1 у игрока максимум 1 корабль (hasShip).
//    ID 1..5     = Предметы ветки ШАНС  (Compass), уровни 1..5.
//    ID 6..10    = Предметы ветки ДОЛЯ  (Idol),    уровни 1..5.
//    ID 11..15   = ЗАРЕЗЕРВИРОВАНО под ветку ПЕРЕЗАРЯДКА (фаза 1.5) — НЕ используется.
//
//  ЭКИПИРОВКА (v1, упрощённо): игрок держит один «активный» уровень на ветку.
//    equippedChanceLevel / equippedShareLevel. Крафт более высокого уровня
//    авто-экипирует его; equip() позволяет выбрать вручную.
//
//  ОПЛАТА (выбранный путь): игрок делает approve этому контракту. Ships берёт
//    token.safeTransferFrom(player -> Ships), делает forceApprove(Treasury) и
//    зовёт treasury.deposit(amount) (стандартный путь депозита, без спец-функций
//    в казне). Итог: средства сразу оказываются в Treasury, Ships не копит ARG.
//
//  КРАФТ И РАНДОМ: исход крафта решает оффчейн-рандом с СЕРВЕРНОЙ ПОДПИСЬЮ
//    (commit → resolve, EIP-712/ECDSA) — зеркально Raid.sol; целевая сеть
//    Robinhood Chain (chainId 4663), где Chainlink VRF отсутствует. craft()
//    СРАЗУ сжигает 2 предмета и берёт плату в казну, присваивает монотонный
//    requestId и эмитит CraftRequested. Исход приходит отдельной tx
//    resolveCraft, когда сервер (trustedSigner) подпишет результат. Это
//    закрывает эксплойт «simulate-then-revert»: в момент commit-tx результат
//    неизвестен, а предметы уже сгорели, откатить неудачу и повторить нельзя.
//
//  ВЛАДЕНИЕ: OZ Ownable2Step (двухшаговая передача). Раньше owner()/onlyOwner
//    приходили из Chainlink ConfirmedOwner (через VRFConsumerBaseV2Plus); после
//    удаления VRF единственная база владения — Ownable2Step. renounceOwnership
//    переопределён реверт'ом (нельзя случайно забрикать контракт).
//
//  Держит игровые NFT и маршрутизирует средства — требует ревью перед mainnet.
// =============================================================================

import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {ITreasury} from "./interfaces/ITreasury.sol";
import {IShipStats} from "./interfaces/IShipStats.sol";

contract Ships is ERC1155, Ownable2Step, EIP712, Pausable, ReentrancyGuard, IShipStats {
    using SafeERC20 for IERC20;

    // --------------------------------------------------------------------- //
    //  Константы ID / веток
    // --------------------------------------------------------------------- //
    uint256 public constant SHIP_ID = 0;
    uint8 public constant BRANCH_CHANCE = 0; // Compass, ID 1..5
    uint8 public constant BRANCH_SHARE = 1;  // Idol,    ID 6..10
    uint8 public constant MAX_LEVEL = 5;

    // --------------------------------------------------------------------- //
    //  Внешние зависимости — IMMUTABLE (owner не может подменить/перенаправить).
    // --------------------------------------------------------------------- //
    IERC20 public immutable token;
    ITreasury public immutable treasury;

    // --------------------------------------------------------------------- //
    //  Серверная подпись (EIP-712) вместо VRF
    // --------------------------------------------------------------------- //

    /// @notice Адрес доверенного подписанта результатов крафта (серверный ключ).
    ///         onlyOwner-настраиваемый; ротация мгновенно инвалидирует старые
    ///         подписи (recover != новый signer).
    address public trustedSigner;

    /// @notice Монотонный счётчик заявок крафта. Старт с 1 (первый requestId = 1),
    ///         поэтому 0 остаётся sentinel'ом «нет активной заявки» в
    ///         pendingCraftId. Заменяет VRF requestId.
    uint256 public nextRequestId;

    /// @notice «Хранитель» — может ТОЛЬКО pause() (не unpause), для мгновенной
    ///         остановки при подозрении на компрометацию ключа, без ожидания
    ///         двухшаговой смены owner. onlyOwner-настраиваемый.
    address public guardian;

    /// @dev EIP-712 typehash пакета, который подписывает сервер. Домен
    ///      ("ArgonautShips" — ОТЛИЧАЕТСЯ от Raid для разделения подписей) вносит
    ///      chainId и адрес контракта → защита от кросс-чейн/кросс-контракт replay.
    bytes32 private constant CRAFT_RESOLVE_TYPEHASH =
        keccak256("CraftResolve(uint256 requestId,address player,uint256 randomValue,uint64 deadline)");

    /// @notice Верхняя граница срока годности подписи (M2). Resolve не примет
    ///         deadline дальше block.timestamp + MAX_SIGNATURE_TTL — даже если
    ///         сервер подпишет его на годы вперёд. Серверный SIGNATURE_TTL (1 час)
    ///         укладывается с огромным запасом.
    uint256 public constant MAX_SIGNATURE_TTL = 1 days;

    // --------------------------------------------------------------------- //
    //  Цены (в ARG, 18 decimals) с ТАЙМЛОКОМ 1ч и жёсткими границами
    // --------------------------------------------------------------------- //

    /// @notice Таймлок на изменение цен: новая цена вступает в силу только через
    ///         1 час после объявления (защита игрока от мгновенного повышения).
    uint256 public constant PRICE_TIMELOCK = 1 hours;

    // Границы цен (в ARG, 18 decimals). Сеттеры не могут выйти за них.
    uint256 public constant MIN_SHIP_PRICE = 20_000 ether;
    uint256 public constant MAX_SHIP_PRICE = 2_000_000 ether;
    uint256 public constant MIN_ITEM_PRICE = 10_000 ether;
    uint256 public constant MAX_ITEM_PRICE = 1_000_000 ether;
    uint256 public constant MIN_CRAFT_PRICE = 5_000 ether;
    uint256 public constant MAX_CRAFT_PRICE = 15_000_000 ether;

    /// @notice Цена с отложенным применением. Пока `block.timestamp < effectiveAt`
    ///         действует `current`; после — `pending`. effectiveAt=0 => сразу current.
    struct TimedPrice {
        uint256 current;
        uint256 pending;
        uint64 effectiveAt;
    }

    TimedPrice private _shipPrice;
    mapping(uint8 => TimedPrice) private _itemPrices; // level(1..5)

    // --------------------------------------------------------------------- //
    //  Шансы крафта — КОНСТАНТЫ (правила игры, изменить нельзя)
    // --------------------------------------------------------------------- //
    uint16 public constant CRAFT_SUCCESS_L1 = 40; // Lv1 -> 2
    uint16 public constant CRAFT_SUCCESS_L2 = 30; // Lv2 -> 3
    uint16 public constant CRAFT_SUCCESS_L3 = 20; // Lv3 -> 4
    uint16 public constant CRAFT_SUCCESS_L4 = 15; // Lv4 -> 5

    // --------------------------------------------------------------------- //
    //  Состояние игроков
    // --------------------------------------------------------------------- //
    mapping(address => bool) public hasShip;
    mapping(address => uint8) public equippedChanceLevel;
    mapping(address => uint8) public equippedShareLevel;

    // --------------------------------------------------------------------- //
    //  Асинхронный крафт через VRF
    // --------------------------------------------------------------------- //

    /// @notice Таймаут «зависшего» крафта (сервер не зарезолвил) — игрок может сам
    ///         разблокироваться (предметы уже сожжены, награда НЕ выдаётся).
    uint256 public constant STUCK_CRAFT_TIMEOUT = 1 hours;

    struct CraftRequest {
        address player;
        uint8 branch;
        uint8 fromLevel;
        bool fulfilled;
        bool exists;
    }
    struct CraftResult {
        uint256 requestId;
        uint8 branch;
        uint8 fromLevel;
        bool success;
        bool exists;
    }

    mapping(uint256 => CraftRequest) public craftRequests; // requestId => запрос
    mapping(address => CraftResult) public lastCraft;      // player => последний результат
    mapping(address => uint256) public pendingCraftId;     // player => активный requestId (0 = нет)
    mapping(address => uint256) public craftRequestedAt;   // player => время запроса (для таймаута)

    // --------------------------------------------------------------------- //
    //  События
    // --------------------------------------------------------------------- //
    event ShipBought(address indexed player);
    event ItemBought(address indexed player, uint8 branch, uint8 level, uint256 qty);
    event TrustedSignerChanged(address indexed signer);
    event GuardianChanged(address indexed guardian);
    event CraftRequested(uint256 indexed requestId, address indexed player, uint8 branch, uint8 fromLevel);
    /// @notice randomValue включён для ПРОВЕРЯЕМОЙ ЧЕСТНОСТИ: игроки могут
    ///         пересчитать исход из раскрытого сервером seed (commit-reveal).
    event CraftResolved(
        uint256 indexed requestId,
        address indexed player,
        uint8 branch,
        uint8 fromLevel,
        bool success,
        uint256 randomValue
    );
    event CraftAbandoned(address indexed player, uint256 indexed requestId);
    event Equipped(address indexed player, uint8 branch, uint8 level);
    /// @notice Объявлено изменение цены. Вступит в силу в `effectiveAt` (через 1ч).
    event PriceChangeAnnounced(string name, uint8 level, uint256 newPrice, uint256 effectiveAt);

    // --------------------------------------------------------------------- //
    //  Конструктор
    // --------------------------------------------------------------------- //
    constructor(
        string memory uri_,
        address token_,
        address treasury_,
        address trustedSigner_,
        address initialOwner
    ) ERC1155(uri_) Ownable(initialOwner) EIP712("ArgonautShips", "1") {
        require(token_ != address(0) && treasury_ != address(0), "Ships: zero dependency");
        require(trustedSigner_ != address(0), "Ships: zero signer");
        token = IERC20(token_);
        treasury = ITreasury(treasury_);
        trustedSigner = trustedSigner_;

        // Стартовые цены в ARG (действуют сразу — effectiveAt=0). Владелец может
        // менять их сеттерами (с таймлоком 1ч и в пределах границ).
        _shipPrice.current = 200_000 ether;      // корабль
        _itemPrices[1].current = 100_000 ether;  // покупка предмета (компас/сосуд)
        _itemPrices[2].current = 70_000 ether;   // крафт Lv1->2
        _itemPrices[3].current = 190_000 ether;  // крафт Lv2->3
        _itemPrices[4].current = 500_000 ether;  // крафт Lv3->4
        _itemPrices[5].current = 1_300_000 ether;// крафт Lv4->5
        // Шансы крафта — константы (см. CRAFT_SUCCESS_L1..L4), не задаются здесь.
    }

    /// @dev Шанс успеха крафта (%) по исходному уровню — из констант.
    function _craftSuccess(uint8 fromLevel) internal pure returns (uint16) {
        if (fromLevel == 1) return CRAFT_SUCCESS_L1;
        if (fromLevel == 2) return CRAFT_SUCCESS_L2;
        if (fromLevel == 3) return CRAFT_SUCCESS_L3;
        if (fromLevel == 4) return CRAFT_SUCCESS_L4;
        return 0;
    }

    // --------------------------------------------------------------------- //
    //  Внутреннее: ID предмета и сбор оплаты
    // --------------------------------------------------------------------- //

    /// @dev Вычислить ERC-1155 ID предмета по ветке и уровню (level 1..5).
    function _itemId(uint8 branch, uint8 level) internal pure returns (uint256) {
        require(branch <= BRANCH_SHARE, "Ships: bad branch");
        require(level >= 1 && level <= MAX_LEVEL, "Ships: bad level");
        // Chance: 1..5 ; Share: 6..10
        return branch == BRANCH_CHANCE ? uint256(level) : uint256(5 + level);
    }

    /// @dev Забрать `amount` ARG у игрока и переслать в казну (deposit).
    function _collect(address player, uint256 amount) internal {
        if (amount == 0) return;
        token.safeTransferFrom(player, address(this), amount);
        token.forceApprove(address(treasury), amount);
        treasury.deposit(amount);
    }

    /// @dev Действующая цена: pending после наступления effectiveAt, иначе current.
    function _effectivePrice(TimedPrice storage p) internal view returns (uint256) {
        if (p.effectiveAt != 0 && block.timestamp >= p.effectiveAt) {
            return p.pending;
        }
        return p.current;
    }

    function _autoEquip(address player, uint8 branch, uint8 level) internal {
        if (branch == BRANCH_CHANCE) {
            if (level > equippedChanceLevel[player]) {
                equippedChanceLevel[player] = level;
                emit Equipped(player, branch, level);
            }
        } else {
            if (level > equippedShareLevel[player]) {
                equippedShareLevel[player] = level;
                emit Equipped(player, branch, level);
            }
        }
    }

    // --------------------------------------------------------------------- //
    //  Покупки
    // --------------------------------------------------------------------- //

    /// @notice Купить корабль (в v1 — один на игрока). Оплата ARG уходит в казну.
    function buyShip() external whenNotPaused nonReentrant {
        require(!hasShip[msg.sender], "Ships: already has ship");
        _collect(msg.sender, shipPriceArg());
        hasShip[msg.sender] = true;
        _mint(msg.sender, SHIP_ID, 1, "");
        emit ShipBought(msg.sender);
    }

    /// @notice Купить базовый предмет (ТОЛЬКО уровень 1). Уровни 2..5 — крафтом.
    function buyItem(uint8 branch, uint8 level, uint256 qty) external whenNotPaused nonReentrant {
        require(branch <= BRANCH_SHARE, "Ships: bad branch");
        require(level == 1, "Ships: craft only");
        require(qty > 0, "Ships: zero qty");
        _collect(msg.sender, itemPriceArg(1) * qty);
        _mint(msg.sender, _itemId(branch, 1), qty, "");
        emit ItemBought(msg.sender, branch, level, qty);
    }

    // --------------------------------------------------------------------- //
    //  Крафт (сердце прокачки)
    // --------------------------------------------------------------------- //

    /// @notice COMMIT крафта предмета уровня fromLevel+1 из двух предметов
    ///         fromLevel. СРАЗУ (до исхода): берёт плату в казну и сжигает 2
    ///         предмета — потеря необратима. Затем присваивает requestId и эмитит
    ///         CraftRequested; исход приходит отдельной tx resolveCraft, когда
    ///         сервер подпишет результат. Т.к. в момент commit результат
    ///         неизвестен, а предметы уже сгорели — эксплойт simulate-then-revert
    ///         не работает. Один активный крафт на игрока за раз.
    function craft(uint8 branch, uint8 fromLevel)
        external
        whenNotPaused
        nonReentrant
        returns (uint256 requestId)
    {
        require(branch <= BRANCH_SHARE, "Ships: bad branch");
        require(fromLevel >= 1 && fromLevel <= 4, "Ships: bad fromLevel");
        require(pendingCraftId[msg.sender] == 0, "Ships: craft pending");

        uint256 fromId = _itemId(branch, fromLevel);
        require(balanceOf(msg.sender, fromId) >= 2, "Ships: need 2 items");

        uint8 toLevel = fromLevel + 1;

        // Плата за крафт = цена целевого уровня, сразу в казну.
        _collect(msg.sender, itemPriceArg(toLevel));

        // Сжигаем 2 предмета исходного уровня СРАЗУ (потеря необратима).
        _burn(msg.sender, fromId, 2);

        // Если сгорел последний экземпляр экипированного уровня — снять экипировку
        // ветки уже сейчас (при успехе fulfillRandomWords авто-экипирует toLevel).
        if (balanceOf(msg.sender, fromId) == 0) {
            if (branch == BRANCH_CHANCE && equippedChanceLevel[msg.sender] == fromLevel) {
                equippedChanceLevel[msg.sender] = 0;
                emit Equipped(msg.sender, branch, 0);
            } else if (branch == BRANCH_SHARE && equippedShareLevel[msg.sender] == fromLevel) {
                equippedShareLevel[msg.sender] = 0;
                emit Equipped(msg.sender, branch, 0);
            }
        }

        // Присваиваем монотонный requestId (вместо VRF-запроса). Сервер слушает
        // CraftRequested и подписывает исход именно для этого requestId.
        requestId = ++nextRequestId;

        craftRequests[requestId] = CraftRequest({
            player: msg.sender,
            branch: branch,
            fromLevel: fromLevel,
            fulfilled: false,
            exists: true
        });
        pendingCraftId[msg.sender] = requestId;
        craftRequestedAt[msg.sender] = block.timestamp;

        emit CraftRequested(requestId, msg.sender, branch, fromLevel);
    }

    /// @notice RESOLVE крафта серверной подписью. PERMISSIONLESS: подать может кто
    ///         угодно (игрок, релеер, сервер) — исход зафиксирован подписью
    ///         trustedSigner и не зависит от отправителя. Идемпотентно защищён
    ///         флагом fulfilled (второй резолв ревертит). При успехе минтит предмет
    ///         следующего уровня и авто-экипирует.
    /// @param requestId   id заявки из CraftRequested.
    /// @param randomValue случайное значение, сгенерённое и подписанное сервером.
    /// @param deadline    срок годности подписи (unix-секунды).
    /// @param signature   EIP-712 подпись пакета CraftResolve ключом trustedSigner.
    function resolveCraft(
        uint256 requestId,
        uint256 randomValue,
        uint64 deadline,
        bytes calldata signature
    ) external whenNotPaused nonReentrant {
        require(block.timestamp <= deadline, "Ships: sig expired");
        require(deadline <= block.timestamp + MAX_SIGNATURE_TTL, "Ships: deadline too far"); // M2

        CraftRequest storage req = craftRequests[requestId];
        require(req.exists && !req.fulfilled, "Ships: bad request");

        // Проверка подписи. player берётся из зафиксированного ончейн req.player —
        // подпись валидна, только если сервер подписал именно этого игрока. Домен
        // EIP-712 ("ArgonautShips") связывает chainId и адрес контракта → нет
        // кросс-чейн / кросс-контракт replay (в т.ч. подпись домена Raid не пройдёт).
        _verifySignature(requestId, req.player, randomValue, deadline, signature);

        req.fulfilled = true;
        pendingCraftId[req.player] = 0; // CEI: снимаем блокировку до внешних вызовов (mint hook)

        bool success = (randomValue % 100) < _craftSuccess(req.fromLevel);

        lastCraft[req.player] = CraftResult({
            requestId: requestId,
            branch: req.branch,
            fromLevel: req.fromLevel,
            success: success,
            exists: true
        });

        if (success) {
            uint8 toLevel = req.fromLevel + 1;
            _mint(req.player, _itemId(req.branch, toLevel), 1, "");
            _autoEquip(req.player, req.branch, toLevel);
        }

        emit CraftResolved(requestId, req.player, req.branch, req.fromLevel, success, randomValue);
    }

    /// @dev Проверка EIP-712 подписи резолва ключом trustedSigner. Домен
    ///      (chainId + verifyingContract) закрывает кросс-чейн/кросс-контракт replay.
    function _verifySignature(
        uint256 requestId,
        address player,
        uint256 randomValue,
        uint64 deadline,
        bytes calldata signature
    ) private view {
        bytes32 structHash = keccak256(
            abi.encode(CRAFT_RESOLVE_TYPEHASH, requestId, player, randomValue, deadline)
        );
        address rec = ECDSA.recover(_hashTypedDataV4(structHash), signature);
        require(rec != address(0) && rec == trustedSigner, "Ships: bad signer");
    }

    /// @notice Разблокировать «зависший» крафт, если сервер не зарезолвил за
    ///         таймаут. PERMISSIONLESS (только свой). Предмет НЕ выдаётся (2
    ///         предмета уже сожжены, плата в казне) — защита доступности, не возврат.
    function clearStuckCraft() external {
        uint256 reqId = pendingCraftId[msg.sender];
        require(reqId != 0, "Ships: no pending craft");
        CraftRequest storage req = craftRequests[reqId];
        require(!req.fulfilled, "Ships: already fulfilled");
        require(
            block.timestamp >= craftRequestedAt[msg.sender] + STUCK_CRAFT_TIMEOUT,
            "Ships: not stuck yet"
        );
        req.fulfilled = true; // поздний resolveCraft будет отклонён
        pendingCraftId[msg.sender] = 0;
        emit CraftAbandoned(msg.sender, reqId);
    }

    /// @notice Вручную экипировать имеющийся предмет ветки (выбор активного уровня).
    function equip(uint8 branch, uint8 level) external whenNotPaused {
        require(branch <= BRANCH_SHARE, "Ships: bad branch");
        require(level >= 1 && level <= MAX_LEVEL, "Ships: bad level");
        require(balanceOf(msg.sender, _itemId(branch, level)) >= 1, "Ships: item not owned");
        if (branch == BRANCH_CHANCE) {
            equippedChanceLevel[msg.sender] = level;
        } else {
            equippedShareLevel[msg.sender] = level;
        }
        emit Equipped(msg.sender, branch, level);
    }

    // --------------------------------------------------------------------- //
    //  Сеттеры подписанта / хранителя (onlyOwner). token/treasury — immutable.
    // --------------------------------------------------------------------- //

    /// @notice Сменить доверенного подписанта (ротация ключа). Non-zero.
    function setTrustedSigner(address signer_) external onlyOwner {
        require(signer_ != address(0), "Ships: zero signer");
        trustedSigner = signer_;
        emit TrustedSignerChanged(signer_);
    }

    /// @notice Назначить «хранителя» (может только pause()). address(0) — снять.
    function setGuardian(address guardian_) external onlyOwner {
        guardian = guardian_;
        emit GuardianChanged(guardian_);
    }

    // --------------------------------------------------------------------- //
    //  Сеттеры цен/параметров (onlyOwner)
    // --------------------------------------------------------------------- //

    /// @notice Объявить новую цену корабля. Вступит в силу через PRICE_TIMELOCK (1ч).
    ///         До этого действует прежняя (уже действующая) цена.
    function setShipPrice(uint256 newPrice) external onlyOwner {
        require(newPrice >= MIN_SHIP_PRICE && newPrice <= MAX_SHIP_PRICE, "Ships: ship price out of range");
        // Зафиксировать текущую действующую цену как current, объявить pending.
        _shipPrice.current = _effectivePrice(_shipPrice);
        _shipPrice.pending = newPrice;
        _shipPrice.effectiveAt = uint64(block.timestamp + PRICE_TIMELOCK);
        emit PriceChangeAnnounced("ship", 0, newPrice, _shipPrice.effectiveAt);
    }

    /// @notice Объявить новую цену предмета/крафта уровня (таймлок 1ч).
    ///         level 1 = цена покупки предмета (границы предметов);
    ///         level 2..5 = плата за крафт целевого уровня (границы крафта).
    function setItemPrice(uint8 level, uint256 newPrice) external onlyOwner {
        require(level >= 1 && level <= MAX_LEVEL, "Ships: bad level");
        if (level == 1) {
            require(newPrice >= MIN_ITEM_PRICE && newPrice <= MAX_ITEM_PRICE, "Ships: item price out of range");
        } else {
            require(newPrice >= MIN_CRAFT_PRICE && newPrice <= MAX_CRAFT_PRICE, "Ships: craft price out of range");
        }
        TimedPrice storage tp = _itemPrices[level];
        tp.current = _effectivePrice(tp);
        tp.pending = newPrice;
        tp.effectiveAt = uint64(block.timestamp + PRICE_TIMELOCK);
        emit PriceChangeAnnounced("item", level, newPrice, tp.effectiveAt);
    }

    function setURI(string calldata newUri) external onlyOwner {
        _setURI(newUri);
    }

    // Пауза: owner ИЛИ guardian на pause; unpause только owner.
    function pause() external {
        require(msg.sender == owner() || msg.sender == guardian, "Ships: not authorized");
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    // Владение: renounceOwnership отключён (нельзя забрикать контракт).
    function renounceOwnership() public view override onlyOwner {
        revert("Ships: renounce disabled");
    }

    // --------------------------------------------------------------------- //
    //  View
    // --------------------------------------------------------------------- //

    /// @inheritdoc IShipStats
    function getPlayerRaidStats(address player)
        external
        view
        override
        returns (uint8 chanceLevel, uint8 shareLevel, bool playerHasShip)
    {
        return (equippedChanceLevel[player], equippedShareLevel[player], hasShip[player]);
    }

    function getEquipped(address player) external view returns (uint8 chanceLevel, uint8 shareLevel) {
        return (equippedChanceLevel[player], equippedShareLevel[player]);
    }

    function itemBalance(address player, uint8 branch, uint8 level) external view returns (uint256) {
        return balanceOf(player, _itemId(branch, level));
    }

    /// @notice Шанс успеха крафта (%) по исходному уровню — из констант. Фронт читает это.
    function craftSuccessFor(uint8 fromLevel) external pure returns (uint16) {
        return _craftSuccess(fromLevel);
    }

    /// @notice Действующая цена корабля (учитывает таймлок). Фронт читает её.
    function shipPriceArg() public view returns (uint256) {
        return _effectivePrice(_shipPrice);
    }

    /// @notice Действующая цена предмета/крафта уровня (учитывает таймлок).
    function itemPriceArg(uint8 level) public view returns (uint256) {
        return _effectivePrice(_itemPrices[level]);
    }

    /// @notice Информация об анонсированном изменении цены (для UI/прозрачности).
    ///         effectiveAt=0 или уже наступил => изменения в очереди нет.
    function pendingShipPrice() external view returns (uint256 pending, uint256 effectiveAt) {
        return (_shipPrice.pending, _shipPrice.effectiveAt);
    }

    function pendingItemPrice(uint8 level) external view returns (uint256 pending, uint256 effectiveAt) {
        return (_itemPrices[level].pending, _itemPrices[level].effectiveAt);
    }

    /// @notice Есть ли у игрока незавершённый крафт (ждём подпись сервера).
    function pendingCraft(address player) external view returns (bool) {
        return pendingCraftId[player] != 0;
    }

    /// @notice Последний результат крафта игрока (для фронта).
    function getLastCraft(address player) external view returns (CraftResult memory) {
        return lastCraft[player];
    }

    /// @notice EIP-712 domain separator — сервер использует его для подписи.
    function domainSeparator() external view returns (bytes32) {
        return _domainSeparatorV4();
    }

    /// @dev Разрешаем конфликт наследования supportsInterface. Только ERC1155
    ///      реализует ERC165 в цепочке (у Ownable/EIP712 его нет) → override(ERC1155).
    function supportsInterface(bytes4 interfaceId) public view override(ERC1155) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}
Chain explorer3621msChain node92ms