---
name: eternal-beings-operator
description: Inspect and operate Eternal Beings safely from one self-contained file, including live instance discovery, epoch/phase status, commit-reveal-claim Mint, Being queries, Hunt and ORE, NFT Devour, CryptoPunk Devour, Fusion, approvals, cooldowns, proofs, secure signing, and irreversible-action confirmation. Use when an agent must explain, prepare, simulate, sign, or execute an Eternal Beings action.
---

# Eternal Beings Operator

Use this file as the complete operating skill. Treat verified on-chain state as the source of truth.

## Official mainnet instance

```json
{
  "status": "live-mainnet",
  "network": "Ethereum Mainnet",
  "chainId": 1,
  "rpcUrl": "https://ethereum-rpc.publicnode.com",
  "eventRpcUrl": "https://rpc.mevblocker.io",
  "explorerUrl": "https://etherscan.io",
  "gameContract": "0xC6D9Ea961C1E1E5F99D36970FcC824b8faA144f3",
  "beingNftContract": "0xC6D9Ea961C1E1E5F99D36970FcC824b8faA144f3",
  "oreContract": "0x49C179aB4CE7d3423ef7Fa19447E2Ff4557CeDA1",
  "rendererContract": "0x72445EE217f146b85A063403774513bD7F7e3bB1",
  "deploymentBlock": 25531518,
  "deploymentTransaction": "0x2d2db8f47d093206451f6bbf052c9233f23ceff79f4a4cc72e2ed4583de547a4",
  "topCollectionsRoot": "0xcfd388334a04e188055199c93b09e9b65ff5e742380b7aecbd5d46c05e51358f"
}
```

Before using these values, verify chain ID, deployed bytecode, Etherscan Exact Match source, `ore()`, `renderer()`, `topCollectionsRoot()`, and immutable constants. Stop if any value differs.

## Signing and private keys

Allow transaction signing when the user authorizes it.

Use, in preference order:

1. injected browser wallet;
2. hardware wallet or multisig;
3. local encrypted keystore or OS secret manager;
4. local environment variable or secret file excluded from source control.

A private key may be used by an agent-controlled local signer only when the user explicitly authorizes that signing method. Never ask the user to paste a private key or seed phrase into chat. Never print, log, upload, return, embed, commit, or place a private key in a command line, website, Skill, report, screenshot, or transaction summary.

Before every value-moving or irreversible write, show the network, contract, function, arguments, estimated gas/value, assets affected, and permanent effect. Ask for explicit confirmation immediately before signing. A previous general authorization does not cover a later burn, permanent lock, broad approval, transfer, or mainnet transaction.

## Mandatory workflow

1. Verify network, address, bytecode, source, and ABI.
2. Read all prerequisite state from the contract.
3. Determine the exact current block-based phase; do not rely only on wall-clock time.
4. Prepare the exact call and simulate or estimate it.
5. Explain expected events, cost, restrictions, and irreversible effects.
6. Obtain confirmation at the signing boundary when required.
7. Sign with the authorized local signer.
8. Wait for the receipt and verify post-state.
9. Return the transaction hash, result, and next eligible action/block.

## Production Mint profile

Read constants from the deployed contract; do not assume values from any rehearsal deployment.

- `MAX_BEINGS = 9,999`
- `EPOCH_BLOCKS = 7,200`
- `COMMIT_BLOCKS = 4,800`
- `MINTS_PER_EPOCH = 333`
- At 12 seconds per block: approximately 24 hours per epoch, 16 hours Commit, 8 hours Reveal.
- Minimum full distribution is 31 epochs: 30 full tranches release 9,990 Beings and the final capped tranche releases 9. Missed claims or low demand can extend it.
- One wallet can successfully Mint only one Being.

## Phase calculation

```text
epoch = currentEpoch()
start = epochStart(epoch)
offset = currentBlock - start

offset < COMMIT_BLOCKS                  => COMMIT
COMMIT_BLOCKS <= offset < EPOCH_BLOCKS => REVEAL
previous completed epochs may be claimable during a new COMMIT epoch
```

There is no exclusive global Claim phase.

## Commit

Read `currentEpoch()`, `epochStart(epoch)`, `hasMintedBeing(wallet)`, and `commitments(epoch,wallet)`.

Generate a cryptographically random `bytes32` secret locally:

```solidity
commitment = keccak256(abi.encodePacked(wallet, epoch, secret))
```

Securely store chain ID, game address, wallet, epoch, and secret. Simulate and call:

```solidity
commitMint(bytes32 commitment)
```

Expect `Committed(epoch,user,commitment)`. Never expose the unrevealed secret.

## Reveal

Require block in `[epochStart+COMMIT_BLOCKS, epochStart+EPOCH_BLOCKS)`. Load the exact secret, recompute the commitment locally, and check `revealed(epoch,wallet)`.

Simulate and call:

```solidity
revealMint(uint256 epoch, bytes32 secret)
```

Expect `Revealed(epoch,user)`. A missed Reveal cannot be recovered for that epoch, but a wallet that has not minted may enter a later epoch.

## Claim

Require block `>= epochStart(epoch)+EPOCH_BLOCKS`. Read:

- `revealed(epoch,wallet)`
- `claimedEpoch(epoch,wallet)`
- `hasMintedBeing(wallet)`
- `totalMinted()` and `releasedMintCap(epoch)`
- `revealedCount(epoch)`
- `epochClaimLimit(epoch)`
- `epochClaimedCount(epoch)`

Simulate and call:

```solidity
claimMint(uint256 epoch)
```

Expect `BeingMinted` and `MetadataUpdate`.

Reverts: `claim` means too early; `not revealed`; `claimed`; `minted`; `quota` or `filled`; `not winner`; `sold out`. If not selected, the wallet may enter a later epoch.

## Query a Being

Read:

- `ownerOf(tokenId)`
- `getBeing(tokenId)`
- `tokenURI(tokenId)`
- `hunts(tokenId)`
- `cooldownUntil(tokenId)`
- `getApproved(tokenId)`
- `isApprovedForAll(owner,operator)`

`tokenURI` returns on-chain JSON and SVG. Report Mass, Complexity, Devours, Fusions, premium Devours, Power, Skill, scars, stage, lineage, and genome.

## Hunt and ORE

Read owner, active Hunt, cooldown, `availableEmission()`, and ORE address.

Enter:

```solidity
enterHunt(uint256 tokenId)
```

Confirm that the Being temporarily transfers to the game. Wait at least one block. Read the generated Hunt and resolve:

```solidity
resolveHunt(uint256 tokenId)
```

Only the recorded hunter can resolve. Verify the Being returned, cooldown updated, metadata changed, and any ORE reward. Read `totalEmitted()`, `emittedCap()`, and `availableEmission()`.

## Devour an unknown ERC-721

Read `UNKNOWN_HOLD_BLOCKS`, `UNKNOWN_MIN_TOTAL_SUPPLY`, `UNKNOWN_COLLECTION_DEVOUR_LIMIT`, ownership, codehash, ERC-721 support, observation, collection count, Being Hunt, and cooldown.

Call:

```solidity
observeExternal(address nft, uint256 externalTokenId)
```

Wait until ready. Use exact-token approval when possible. Immediately before approval and Devour, state that the external NFT will be locked forever with no rescue or withdrawal. Confirm, then call:

```solidity
devourExternal(uint256 beingId, address nft, uint256 externalTokenId)
```

Verify game ownership of the external NFT, `devouredExternal=true`, Being state, and `MetadataUpdate`.

## Devour a curated Top-100 NFT

Never invent a proof. Obtain the tier and Merkle proof for the exact deployment root. Verify:

```solidity
verifyCollectionTier(address nft, uint8 tier, bytes32[] proof)
nutritionForTier(uint8 tier)
```

Verify ownership, Being state, and exact-token approval. Confirm permanent lock immediately before signing:

```solidity
devourTieredExternal(
  uint256 beingId,
  address nft,
  uint256 externalTokenId,
  uint8 tier,
  bytes32[] proof
)
```

## Devour a CryptoPunk-style asset

Verify tier proof, Punk ownership, and observation expiry. Call:

```solidity
observeCryptoPunk(address punkContract, uint256 punkId, uint8 tier, bytes32[] proof)
```

Before transferring the Punk, explain that it will be locked forever. Obtain confirmation, transfer it to the game through the Punk contract, verify game custody, then call before expiry:

```solidity
devourCryptoPunk(uint256 beingId, address punkContract, uint256 punkId)
```

## Fuse two Beings

Read ownership and complete state for both token IDs, both Hunts, both cooldowns, and `aliveSupply`. Clearly identify the parent and sacrifice.

Explain that the sacrifice will be burned forever and cannot be recovered. Obtain confirmation immediately before:

```solidity
fuseBeing(uint256 parentId, uint256 sacrificeId)
```

Verify `Fused`, parent metadata/state, absence of sacrifice, and decreased `aliveSupply`.

Mutation has no standalone player function. It may occur during Devour or Fusion.

## Approval rules

Prefer `approve(spender,tokenId)` over `setApprovalForAll`. If broad approval is required, explain its scope and risk and request separate confirmation. After the action, offer to revoke any approval that is no longer needed.

## Hard safety invariants

- No owner, upgrade, pause, rescue, withdrawal, or admin Mint exists.
- Devoured external NFTs and CryptoPunks remain locked forever.
- Fusion burns the sacrifice.
- Hunt temporarily transfers the Being to the game.
- ORE issuance is capped by `availableEmission()`.
- Never continue on an address, chain, ABI, proof, ownership, phase, signer, simulation, or bytecode mismatch.
+
# Compact verified ABI

Use these human-readable ABI fragments with ethers v6. Before signing, compare them with the verified deployed source.

```text
constructor(bytes32 topCollectionsRoot,address royaltyReceiver,address renderer)

event Approval(address indexed owner,address indexed approved,uint256 indexed tokenId)
event ApprovalForAll(address indexed owner,address indexed operator,bool approved)
event BeingMinted(address indexed user,uint256 indexed tokenId,bytes32 genome)
event Committed(uint256 indexed epoch,address indexed user,bytes32 commitment)
event CryptoPunkObserved(address indexed owner,address indexed punkContract,uint256 indexed punkId,uint8 tier)
event Devoured(uint256 indexed beingId,address indexed nft,uint256 indexed externalTokenId,uint8 tier,bytes32 genome)
event ExternalObserved(address indexed owner,address indexed nft,uint256 indexed externalTokenId,uint64 readyBlock)
event Fused(uint256 indexed parentId,uint256 indexed sacrificeId,bytes32 genome)
event HuntEntered(uint256 indexed tokenId,address indexed owner,uint16 sceneId,uint32 difficulty,uint64 endurance)
event HuntResolved(uint256 indexed tokenId,address indexed owner,uint256 reward,bool powerUp,bool skillUp)
event MetadataUpdate(uint256 indexed tokenId)
event Mutation(uint256 indexed tokenId,uint8 mutationKind,bytes32 genome)
event Revealed(uint256 indexed epoch,address indexed user)
event Transfer(address indexed from,address indexed to,uint256 indexed tokenId)

function BASE_HUNT_RATE() view returns (uint256)
function BPS_DENOMINATOR() view returns (uint96)
function COMMIT_BLOCKS() view returns (uint256)
function COOLDOWN_BLOCKS() view returns (uint256)
function CRYPTOPUNK_OBSERVATION_BLOCKS() view returns (uint256)
function EMISSION_PER_BLOCK() view returns (uint256)
function EPOCH_BLOCKS() view returns (uint256)
function MAX_BEINGS() view returns (uint256)
function MAX_ENDURANCE() view returns (uint256)
function MINTS_PER_EPOCH() view returns (uint256)
function ROYALTY_BPS() view returns (uint96)
function TOKEN_MAX_SUPPLY() view returns (uint256)
function UNKNOWN_COLLECTION_DEVOUR_LIMIT() view returns (uint32)
function UNKNOWN_HOLD_BLOCKS() view returns (uint256)
function UNKNOWN_MIN_TOTAL_SUPPLY() view returns (uint256)
function aliveSupply() view returns (uint256)
function approve(address spender,uint256 tokenId)
function availableEmission() view returns (uint256)
function balanceOf(address owner) view returns (uint256)
function claimMint(uint256 epoch)
function claimedEpoch(uint256 epoch,address user) view returns (bool)
function collectionTier(address nft) pure returns (uint8)
function commitMint(bytes32 commitment)
function commitments(uint256 epoch,address user) view returns (bytes32)
function cooldownUntil(uint256 tokenId) view returns (uint256)
function cryptoPunkDeposits(address punk,uint256 id) view returns (address owner,uint64 blockNumber,uint8 tier,bytes32 codehash)
function currentEpoch() view returns (uint256)
function devourCryptoPunk(uint256 beingId,address punk,uint256 punkId)
function devourExternal(uint256 beingId,address nft,uint256 externalTokenId)
function devourTieredExternal(uint256 beingId,address nft,uint256 externalTokenId,uint8 tier,bytes32[] proof)
function devouredExternal(address nft,uint256 externalTokenId) view returns (bool)
function emissionStartBlock() view returns (uint256)
function emittedCap() view returns (uint256)
function enterHunt(uint256 tokenId)
function epochClaimLimit(uint256 epoch) view returns (uint32)
function epochClaimedCount(uint256 epoch) view returns (uint32)
function epochSeed(uint256 epoch) view returns (bytes32)
function epochStart(uint256 epoch) view returns (uint256)
function externalObservations(address nft,uint256 id) view returns (address owner,uint64 blockNumber,bytes32 codehash)
function fuseBeing(uint256 parentId,uint256 sacrificeId)
function getApproved(uint256 tokenId) view returns (address)
function getBeing(uint256 tokenId) view returns ((uint128 mass,uint128 complexity,uint64 devours,uint64 fusions,uint64 premiumDevours,uint64 markLuck,uint64 huntNonce,uint32 power,uint32 skill,uint32 scars,uint16 stage,uint8 originClass,uint8 originSymbol,uint8 mutationBias,uint8 lineageMask,bytes32 genome))
function hasMintedBeing(address user) view returns (bool)
function hasRevealedAny(address user) view returns (bool)
function hunts(uint256 tokenId) view returns (address owner,uint64 startBlock,uint64 endurance,uint32 difficulty,uint16 sceneId,uint16 tokenMultiplier,uint16 powerRate,uint16 skillRate,bytes32 seed)
function isApprovedForAll(address owner,address operator) view returns (bool)
function name() view returns (string)
function nutritionForTier(uint8 tier) pure returns (uint32 nutrition,uint32 complexityGain)
function observeCryptoPunk(address punk,uint256 punkId,uint8 tier,bytes32[] proof)
function observeExternal(address nft,uint256 externalTokenId)
function ore() view returns (address)
function ownerOf(uint256 tokenId) view returns (address)
function releasedMintCap(uint256 epoch) pure returns (uint256)
function renderer() view returns (address)
function resolveHunt(uint256 tokenId)
function revealMint(uint256 epoch,bytes32 secret)
function revealed(uint256 epoch,address user) view returns (bool)
function revealedCount(uint256 epoch) view returns (uint32)
function royaltyInfo(uint256 tokenId,uint256 salePrice) view returns (address receiver,uint256 royaltyAmount)
function royaltyReceiver() view returns (address)
function safeTransferFrom(address from,address to,uint256 tokenId)
function safeTransferFrom(address from,address to,uint256 tokenId,bytes data)
function setApprovalForAll(address operator,bool approved)
function startBlock() view returns (uint256)
function supportsInterface(bytes4 interfaceId) pure returns (bool)
function symbol() view returns (string)
function tokenURI(uint256 tokenId) view returns (string)
function topCollectionsRoot() view returns (bytes32)
function totalEmitted() view returns (uint256)
function totalMinted() view returns (uint256)
function transferFrom(address from,address to,uint256 tokenId)
function unknownCollectionDevours(address nft) view returns (uint32)
function verifyCollectionTier(address nft,uint8 tier,bytes32[] proof) view returns (bool)
```

The ORE address returned by `ore()` exposes minimal ERC-20-style reads and writes:

```text
function name() view returns (string)
function symbol() view returns (string)
function decimals() view returns (uint8)
function totalSupply() view returns (uint256)
function balanceOf(address) view returns (uint256)
function allowance(address,address) view returns (uint256)
function transfer(address,uint256) returns (bool)
function approve(address,uint256) returns (bool)
function transferFrom(address,address,uint256) returns (bool)
```

# Event interpretation

- `Committed`: store epoch and secret; next action is Reveal inside the same epoch.
- `Revealed`: commitment matched; next action is Claim after the epoch ends.
- `BeingMinted`: Claim succeeded; record tokenId and genome.
- `HuntEntered`: Being custody moved to the game; only the recorded hunter can resolve.
- `HuntResolved`: verify reward, stat flags, Being return, cooldown, and ORE balance.
- `ExternalObserved`: record `readyBlock`; do not approve or Devour before prerequisites.
- `CryptoPunkObserved`: record observation block and expiry.
- `Devoured`: verify permanent external-asset custody and updated Being.
- `Fused`: verify sacrifice burn and parent state.
- `Mutation`: mutation occurred inside Devour or Fusion.
- `MetadataUpdate`: refresh `tokenURI`.
- `Transfer`: use zero-address mint/burn semantics and verify expected custody.

# Error and recovery table

| Error | Meaning | Recovery |
|---|---|---|
| `commit` | Not in Commit window | Wait for next Commit window |
| `committed` | Wallet already committed this epoch | Use the already saved secret |
| `zero` | Zero commitment | Generate a nonzero random secret |
| `reveal` | Not in Reveal window | Wait if early; use a later epoch if missed |
| `bad secret` | Wallet/epoch/secret mismatch | Stop and locate the correct saved record |
| `revealed` | Already revealed | Proceed to Claim after epoch |
| `claim` | Claim too early | Wait until epoch end |
| `not revealed` | Reveal absent | Enter a later epoch |
| `claimed` | Already claimed that epoch | Verify token ownership/events |
| `minted` | Wallet already minted its lifetime Being | Do not retry |
| `not winner` | Oversubscription draw rejected wallet | Enter a later epoch |
| `quota`, `filled`, `sold out` | Allocation unavailable | Check later epoch or final supply |
| `not owner`, `ext owner` | Signer does not own required asset | Switch signer or stop |
| `not approved` | ERC-721 approval missing | Approve exact token after confirmation |
| `already hunting`, `hunting` | Being is in Hunt | Resolve before evolution |
| `not hunter` | Signer did not enter this Hunt | Use recorded hunter |
| `cooldown`, `evo` | Evolution cooldown active | Wait until `cooldownUntil` |
| `not erc721`, `supply too low` | Unknown collection is ineligible | Stop; do not transfer the NFT |
| `not observed`, `hold more` | Observation absent or hold incomplete | Observe or wait |
| `code changed` | NFT codehash differs from observation | Stop and re-audit |
| `collection filled` | Unknown collection cap reached | Cannot Devour through unknown path |
| `bad tier`, `bad proof` | Invalid curated tier/proof | Regenerate against exact root |
| `obs expired` | Punk observation expired | Observe again if still owned |
| `punk not locked` | Game does not own the Punk | Stop; verify transfer |
| `same id` | Parent equals sacrifice | Choose distinct Beings |
| `parent hunting`, `sacrifice hunting` | Fusion asset is in Hunt | Resolve it first |
| `reentrant` | Nested write blocked | Stop and inspect call path |
| `max supply`, `balance`, `allowance` | Token constraint failed | Re-read amount/balance/approval |

# Automatic next-action decision

```text
verify instance and wallet
read currentBlock, currentEpoch, epochStart, EPOCH_BLOCKS, COMMIT_BLOCKS
read hasMintedBeing(wallet)

if hasMinted:
    report token/Being state and gameplay actions
else:
    scan revealed-but-unclaimed completed epochs
    if a claimable epoch exists:
        simulate claimMint(epoch)
        if simulation succeeds: propose CLAIM
        if "not winner": propose a new COMMIT when open
    else:
        commitment = commitments(currentEpoch,wallet)
        didReveal = revealed(currentEpoch,wallet)
        offset = currentBlock - epochStart(currentEpoch)

        if offset < COMMIT_BLOCKS:
            if commitment == 0: propose COMMIT
            else: verify saved secret and wait for REVEAL
        else:
            if commitment != 0 and !didReveal:
                verify saved secret; propose REVEAL
            else if didReveal:
                wait for CLAIM
            else:
                wait for next epoch COMMIT
```

For gameplay:

```text
read ownerOf, getBeing, hunts, cooldownUntil
if Hunt active and signer is hunter: propose RESOLVE after ≥1 block
else if cooldown active: report ready block/time
else:
    offer HUNT
    offer DEVOUR only after validating external asset/proof/observation
    offer FUSION only after validating both Beings
always mark permanent lock/burn actions as critical confirmation
```

# Mint secret storage and recovery

Store a record keyed by `chainId + gameContract + wallet + epoch`:

```json
{
  "version": 1,
  "chainId": 1,
  "gameContract": "0x...",
  "wallet": "0x...",
  "epoch": 0,
  "secret": "0x...32-bytes...",
  "commitment": "0x...32-bytes...",
  "commitTx": "0x...",
  "createdAt": "ISO-8601"
}
```

Encrypt this record at rest when possible. Never upload it. Verify that the locally recomputed commitment equals `commitments(epoch,wallet)` before Reveal.

The official web interface does not expose backup/import controls. For new Commits it asks the connected wallet to sign one canonical, free off-chain message containing chain ID, Game contract, wallet, and epoch, then derives `secret = keccak256(signature)`. During Reveal, the same wallet signs the identical message to reconstruct the secret. The browser may cache the derived secret as a transparent fallback, but the player does not upload or manually enter it. Always compare the reconstructed commitment with `commitments(epoch,wallet)` before sending Reveal.

Recovery order:

1. reconstruct from the same wallet and exact canonical message when the Commitment used wallet-signature derivation;
2. check the authorized application's protected local record for legacy random-secret Commits;
3. match chain, game, wallet, epoch, and commitment;
4. refuse Reveal if any field mismatches;
5. if a legacy random secret no longer exists, explain that the epoch cannot be recovered.

After confirmed Reveal, retain the record until Claim succeeds or the user deliberately archives it. Do not delete secrets automatically while a transaction is pending.

# Top-100 Merkle proof procedure

1. Read `topCollectionsRoot()` from the deployed game.
2. Obtain the curated dataset that produced that exact root.
3. Normalize the collection address and confirm its intended tier 1–5.
4. Compute `leaf = keccak256(abi.encodePacked(collectionAddress, tier))`.
5. Build a sorted-pair Merkle proof: at every level hash the numerically smaller bytes32 value first.
6. Call `verifyCollectionTier(collection,tier,proof)`.
7. Continue only if it returns `true`.
8. Read `nutritionForTier(tier)` and explain expected base gains.
9. Verify NFT ownership and exact-token approval.
10. Confirm permanent lock, simulate Devour, then sign.

Never reuse a proof from a different root or deployment.

# Mainnet production constants

| Parameter | Mainnet profile |
|---|---:|
| EPOCH_BLOCKS | 7,200 |
| COMMIT_BLOCKS | 4,800 |
| MINTS_PER_EPOCH | 333 |
| MAX_ENDURANCE | 7,200 |
| COOLDOWN_BLOCKS | 40 |
| UNKNOWN_HOLD_BLOCKS | 7,200 |
| CRYPTOPUNK_OBSERVATION_BLOCKS | 7,200 |
| UNKNOWN_MIN_TOTAL_SUPPLY | 1,000 |
| UNKNOWN_COLLECTION_DEVOUR_LIMIT | 100 |
| ROYALTY_BPS | 300 |

Never identify a profile by network name alone. Read every constant from the deployed contract.

# ethers v6 read and signing template

Use an injected wallet:

```js
import { BrowserProvider, Contract } from "ethers";

const provider = new BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const network = await provider.getNetwork();
if (Number(network.chainId) !== EXPECTED_CHAIN_ID) throw new Error("wrong chain");

const code = await provider.getCode(GAME_ADDRESS);
if (code === "0x") throw new Error("no contract bytecode");

const game = new Contract(GAME_ADDRESS, ABI, signer);
const wallet = await signer.getAddress();
```

Use an encrypted local keystore without revealing the key:

```js
import { JsonRpcProvider, Wallet } from "ethers";
import { readFile } from "node:fs/promises";

const provider = new JsonRpcProvider(RPC_URL);
const encryptedJson = await readFile(AUTHORIZED_KEYSTORE_PATH, "utf8");
const password = await obtainPasswordFromSecureLocalPrompt();
const signer = (await Wallet.fromEncryptedJson(encryptedJson, password)).connect(provider);
```

Never hardcode or print `password`, `signer.privateKey`, a mnemonic, or decrypted keystore JSON.

# ethers v6 simulation and transaction pattern

```js
async function confirmedWrite(contract, functionName, args, describeAndConfirm) {
  const fn = contract.getFunction(functionName);

  // Revert check without changing state.
  await fn.staticCall(...args);

  // Cost check.
  const gas = await fn.estimateGas(...args);

  // Application must show chain, contract, function, args, value,
  // estimated gas, affected assets, and irreversible effect.
  const approved = await describeAndConfirm({ functionName, args, gas });
  if (!approved) throw new Error("user declined");

  const tx = await fn(...args, { gasLimit: gas * 120n / 100n });
  const receipt = await tx.wait();
  if (receipt.status !== 1) throw new Error("transaction failed");
  return receipt;
}
```

# ethers v6 Mint example

```js
import { randomBytes, hexlify, solidityPackedKeccak256 } from "ethers";

const epoch = await game.currentEpoch();
const secret = hexlify(randomBytes(32));
const commitment = solidityPackedKeccak256(
  ["address", "uint256", "bytes32"],
  [wallet, epoch, secret]
);

// Save the encrypted secret record before sending.
await saveSecretSecurely({ chainId: EXPECTED_CHAIN_ID, game: GAME_ADDRESS, wallet, epoch, secret, commitment });
await game.commitMint.staticCall(commitment);
const commitReceipt = await (await game.commitMint(commitment)).wait();

// Later, recompute and compare before Reveal.
const stored = await loadSecretSecurely(EXPECTED_CHAIN_ID, GAME_ADDRESS, wallet, epoch);
const expected = solidityPackedKeccak256(["address","uint256","bytes32"], [wallet, epoch, stored.secret]);
if (expected !== await game.commitments(epoch, wallet)) throw new Error("secret mismatch");
await game.revealMint.staticCall(epoch, stored.secret);
const revealReceipt = await (await game.revealMint(epoch, stored.secret)).wait();

// After epoch end.
await game.claimMint.staticCall(epoch);
const claimReceipt = await (await game.claimMint(epoch)).wait();
```

# ethers v6 Hunt, Devour, and Fusion examples

```js
// Hunt
await confirmedWrite(game, "enterHunt", [tokenId], confirmHuntCustody);
await waitForAtLeastOneBlock();
await confirmedWrite(game, "resolveHunt", [tokenId], confirmResolve);

// Unknown ERC-721 Devour
await confirmedWrite(game, "observeExternal", [nft, externalId], confirmObservation);
await waitUntilBlock(Number(observation.blockNumber) + Number(await game.UNKNOWN_HOLD_BLOCKS()));
const external = new Contract(nft, ["function approve(address,uint256)"], signer);
await confirmedWrite(external, "approve", [GAME_ADDRESS, externalId], confirmExactApproval);
await confirmedWrite(game, "devourExternal", [beingId, nft, externalId], confirmPermanentLock);

// Tiered Devour
if (!await game.verifyCollectionTier(nft, tier, proof)) throw new Error("bad proof");
await confirmedWrite(game, "devourTieredExternal", [beingId, nft, externalId, tier, proof], confirmPermanentLock);

// Fusion
await confirmedWrite(game, "fuseBeing", [parentId, sacrificeId], confirmPermanentBurn);
```

# Final transaction report

Return:

- network and block;
- signer address, never signer secret;
- target contract and function;
- transaction hash and receipt status;
- matched events;
- ownership/custody changes;
- Being and ORE state changes;
- irreversible effects completed;
- cooldown or next eligible block;
- any approval that should be revoked.
