SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,745Head age6 minLast interval4 minFinality23,734Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

height 23745 of 23745 on the network. 9 peer(s) connected, 77 in its peer directory; 1041 block(s) followed and 865 pulled in by sync since this follower started. Caught up: this follower holds every block any peer has claimed, so every panel below is the whole chain. Genesis 1cf8655125cc1bbb…, producers genesis.

What this follower is, locally

Following 7 seed(s) — their addresses are not published here, for the reason the peer table gives. Its store is held under its own Cerberus lease — no other node’s store is opened, touched or stopped.

NFTCollection for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
NFT collection
Holds
its own ownership map
The property that matters
only the holder of a token can move it

Edit the draft

Set the constants the template marks for editing, and name a deployer to see where it would land.

string, shipped as "Example Collection"

string, shipped as "EXC"

uint, shipped as 10000

The form asks this explorer to compile your draft and show it back. It signs nothing, stores nothing and reaches no node.

The draft as shipped

Contract
NFTCollection
Compiler
areslang-1.3.0
Bytecode SHA-256
1b891ed5ebdcfc068929f09855b52f5671ad161dd6dfee6c87dedc0b9fb94afa
Bytecode
2,327 bytes
Source SHA-256
4ea6f77f21aaf07b8dd0c2c2fe9bde21456c6f57b7735ba5b6d5ca8bfe0bd2f6
The catalogue text?
Yes, byte for byte: matches the published vector
Ready to deploy?
not yet a deployment refuses while these remain: collectionName = "Example Collection", collectionSymbol = "EXC"
Would land at
name a deployer to see it

Deploy it to your fork

Only the last step signs, and only it reaches a node.

  1. Save the draft. Download it above, or start from the catalogue text with strblk contract template nft-collection > nft-collection.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy nft-collection.ares --dry-run --address <your address on the fork> compiles it locally and prints its digest and address. They must match this page; if they do not, the file is not this draft.
  3. Find your fork's RPC. It is the rpc (Iris, wallets) line of the fork's banner, and the rpcUrl of each node in <data root>/fork.json.
  4. Deploy it. strblk wallet deploy nft-collection.ares --node <your fork's RPC>, from a wallet whose account holds a proved STR.domain on your fork. Before it signs, the wallet prints the network: the realm reads unregistered and the genesis is your fork's hash. Check that hash; it is what the signature binds.

Measured gas

Charged by the STARW VM under areslang-1.3.0, the same on your fork. Measured, not estimated.

Entry pointGas
mint1,223
transferToken1,110
burnToken804
setMinter471
closeMinting470
setPaused363
Declare at least
12,230 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionmintto: addressuintnonpayable
functiontransferTokento: address, tokenId: uintboolnonpayable
functionburnTokentokenId: uintboolnonpayable
functionproposeOwnernext: addressnonpayable
functionacceptOwnershipnonpayable
functionsetPausednext: boolnonpayable
functionsetMinternext: addressnonpayable
functioncloseMintingnonpayable
functionnamestringview
functionsymbolstringview
functionownerOftokenId: uintaddressview
functionbalanceOfaccount: addressuintview
functiontotalMinteduintview
functionmaxSupplyuintview
functionowneraddressview
functionminteraddressview
functionpausedboolview
eventTransferfrom: address, to: address, tokenId: uint
eventOwnerProposedcurrent: address, pending: address
eventOwnerChangedprevious: address, next: address
eventPausedChangedpaused: bool, by: address
eventMinterChangedprevious: address, next: address

Your draft's source

templates/nft-collection.ares, as shipped

// =========================================================================================
// NFTCollection — a non-fungible collection whose tokens are owned by an address.
//
// THIS IS NOT A DOMAIN WNFT, AND IT CANNOT BE MADE INTO ONE
//   Read this before you name your collection. The native WNFT is one per STR.domain, minted
//   by the domain registry, backed by 1000 STR locked, and NON-TRANSFERABLE — because a
//   domain WNFT is bound to its holder and transferring it would transfer an identity.
//   Nothing in this contract is any of that. It is a different object with a similar name,
//   and a template that lets a user believe they are minting a domain WNFT is the most
//   dangerous mislabelling available on this chain. Do not use "WNFT" in your collection
//   name, symbol or marketing.
//
//   A DOMAIN-BOUND NFT — one whose ownership is tied to a proved STR.domain rather than to a
//   bare address — IS NOT WRITABLE TODAY. `AresHost` exposes selfAddress, blockTimestampMs,
//   blockHeight, balanceOf and transfer, and no domain accessor at all: a contract cannot ask
//   whether an address holds a proved domain or which domain it holds. Storing a domain
//   string in the token and treating it as proof verifies nothing — a string is a string, and
//   case folding (STR.Treasury beside STR.treasury) is an impersonation primitive the
//   contract cannot even detect, because `normalizeStrDomain` is not reachable from AresLang.
//   This template therefore stores no domain at all. That is the honest version.
//
// WHAT IT GUARANTEES
//   * Only the holder of a token can move it. `transferToken` reads `tokenOwner[id]` and
//     compares it to `caller`; there is no `from` argument and no operator or approval
//     mechanism, so there is no second authority that could move a token.
//   * An id is minted at most once. Ids are assigned by the contract, in sequence, and no
//     function writes an owner for an id that was not minted.
//   * The supply cap is fixed at deployment and no function raises it.
//   * Minting closes permanently with `closeMinting`, one-way, so "the collection is
//     complete" can be a fact and not a promise.
//   * The contract owner cannot take a token from a holder. There is no such function; the
//     owner can only pass the minter role, close minting, pause, and hand over ownership.
//   * There is an emergency stop, and it is declared. `setPaused` freezes mint, transferToken
//     and burnToken; it freezes no view. A pause moves nothing and destroys nothing — every
//     token stays with its holder, readable in `ownerOf`, and moves again when the owner
//     unpauses. `paused()` is in the ABI so a buyer can read the state before committing.
//
// WHAT IT DELIBERATELY DOES NOT HAVE
//   No approve, no operator, no transferFrom. Those exist so another CONTRACT can move a
//   token on your behalf, and AresLang has no cross-contract calls, so they would be stored
//   permissions nothing could use — waiting to become live on the day a call opcode ships.
//   No enumeration of a holder's tokens. That would be iteration over caller-controlled data,
//   which is how a contract becomes permanently uncallable at a size discovered in
//   production. `ownedCount` is a counter; an indexer builds the list from Transfer events.
//
// HOW IT IS TYPICALLY MISUSED
//   * Transferring a token to a contract address. A contract cannot be `caller`, so a token
//     owned by a contract can never move again. PERMANENT LOSS, with no recovery path, and
//     this contract cannot detect the case to refuse it.
//   * Treating `ownerOf` as a person. Holding is not identity — identity is the WNFT, is
//     non-transferable, and is minted only by the domain registry.
//   * Ignoring who holds the pause. It is the contract owner, there is no `renounceOwnership`,
//     and the code is immutable — so an owner who pauses and then loses the key leaves a
//     collection nobody can ever move again. Nothing is confiscated and nothing is destroyed,
//     but a permanently paused collection is a permanently unsellable one. Hand ownership to
//     a key you will still hold in five years, and read `owner()` before you buy.
//   * Under-declaring the gas limit. MEASURED: mint 1,223, transferToken 1,110,
//     burnToken 804, setPaused 363, setMinter 471, closeMinting 470. Declare at least 13,000.
// =========================================================================================

contract NFTCollection {
  // ---- edit these before deploying -------------------------------------------------------
  state collectionName: string = "Example Collection";
  state collectionSymbol: string = "EXC";
  // Fixed at deployment. 0 would mean uncapped; declare a real cap unless you mean uncapped.
  state maxTokens: uint = 10000;
  // -------------------------------------------------------------------------------------

  state contractOwner: address = caller;
  state pendingOwner: address = "";
  state mintAuthority: address = caller;
  state frozen: bool = false;
  state mintedCount: uint = 0;

  state tokenOwner: map<uint, address>;
  state ownedCount: map<address, uint>;

  // A mint is Transfer("", to, id) and a move is Transfer(from, to, id). One event, every
  // movement, so an indexer summing Transfer arrives at exactly the owners in storage.
  event Transfer(from: address, to: address, tokenId: uint);
  event OwnerProposed(current: address, pending: address);
  event OwnerChanged(previous: address, next: address);
  event PausedChanged(paused: bool, by: address);
  event MinterChanged(previous: address, next: address);

  fn mint(to: address) -> uint {
    require(frozen == false, "paused");
    require(len(mintAuthority) > 0, "mint closed");
    require(caller == mintAuthority, "not minter");
    require(len(to) > 0, "empty recipient");
    // A position at this contract's own address could never move again: a contract cannot be
    // `caller`, and the value ledger refuses a contract that pays itself. Every `transfer` in
    // this catalogue has refused this since it was written; the ISSUANCE path did not, so the
    // guard sat on the call a holder makes and was absent from the call an operator makes.
    // MEASURED 2026-10-07: minting here succeeded and produced a position nothing can spend.
    require(to != self, "self is not payable");
    if (maxTokens > 0) {
      require(mintedCount < maxTokens, "supply cap");
    }
    let id = mintedCount + 1;
    mintedCount = id;
    tokenOwner[id] = to;
    ownedCount[to] = ownedCount[to] + 1;
    emit Transfer("", to, id);
    return id;
  }

  fn transferToken(to: address, tokenId: uint) -> bool {
    require(frozen == false, "paused");
    require(len(to) > 0, "empty recipient");
    require(tokenId > 0 && tokenId <= mintedCount, "no such token");
    // The whole security property of this contract is on this line.
    require(tokenOwner[tokenId] == caller, "not the token owner");
    require(to != caller, "self transfer");
    tokenOwner[tokenId] = to;
    ownedCount[caller] = ownedCount[caller] - 1;
    ownedCount[to] = ownedCount[to] + 1;
    emit Transfer(caller, to, tokenId);
    return true;
  }

  // The holder destroys their own token and nobody else's.
  fn burnToken(tokenId: uint) -> bool {
    require(frozen == false, "paused");
    require(tokenId > 0 && tokenId <= mintedCount, "no such token");
    require(tokenOwner[tokenId] == caller, "not the token owner");
    tokenOwner[tokenId] = "";
    ownedCount[caller] = ownedCount[caller] - 1;
    emit Transfer(caller, "", tokenId);
    return true;
  }

  fn proposeOwner(next: address) {
    require(caller == contractOwner, "not owner");
    pendingOwner = next;
    emit OwnerProposed(contractOwner, next);
  }

  fn acceptOwnership() {
    require(caller == pendingOwner, "not the pending owner");
    emit OwnerChanged(contractOwner, caller);
    contractOwner = caller;
    pendingOwner = "";
  }

  // The emergency stop the other two asset templates have and this one did not. It covers
  // mint, transferToken and burnToken — every function that writes a token — and no view,
  // because a stopped collection must stay auditable at exactly the moment somebody needs to
  // audit it. It takes nothing from a holder and destroys nothing: a paused token is still
  // theirs, in `ownerOf`, and moves again the moment the owner unpauses.
  fn setPaused(next: bool) {
    require(caller == contractOwner, "not owner");
    frozen = next;
    emit PausedChanged(next, caller);
  }

  fn setMinter(next: address) {
    require(caller == contractOwner, "not owner");
    require(len(mintAuthority) > 0, "mint closed");
    emit MinterChanged(mintAuthority, next);
    mintAuthority = next;
  }

  fn closeMinting() {
    require(caller == contractOwner, "not owner");
    require(len(mintAuthority) > 0, "mint closed");
    emit MinterChanged(mintAuthority, "");
    mintAuthority = "";
  }

  view fn name() -> string {
    return collectionName;
  }

  view fn symbol() -> string {
    return collectionSymbol;
  }

  // "" means the id was never minted, or was burned.
  view fn ownerOf(tokenId: uint) -> address {
    return tokenOwner[tokenId];
  }

  view fn balanceOf(account: address) -> uint {
    return ownedCount[account];
  }

  view fn totalMinted() -> uint {
    return mintedCount;
  }

  view fn maxSupply() -> uint {
    return maxTokens;
  }

  view fn owner() -> address {
    return contractOwner;
  }

  view fn minter() -> address {
    return mintAuthority;
  }

  // Required even on a collection that never pauses. A missing function is indistinguishable,
  // to a wallet, from a contract whose answer is inconvenient.
  view fn paused() -> bool {
    return frozen;
  }
}