# Tokens — the first thing the ledger does

The Tokens page (M6's "Tokens" and M7's "transaction building" together):
create a fungible token, send it, and for a managed-supply token grant, revoke
and hand the issuer role on. Everything is read back by an index over the
ledger chain; nothing is inferred from what this machine sent.

## What was built

| Layer | File | Does |
|---|---|---|
| Engine | `arcade/state.py` | Handlers for 55 (grant), 56 (revoke), 70 (change issuer), 200 (any data: valid, no-op) alongside 0, 4, 50, 54 |
| Index | `arcade/ledger.py` | `LedgerIndex`: follows the ledger chain from its start block, one SQLite file per network, stops loudly on an unknown type, unwinds reorgs; queries for properties, holders, balances, history |
| Building | `arcade/tokens.py` | `TokenSender.prepare` → signed, decoded, **not broadcast**; `broadcast` only after the caller has shown it. Payload builders that refuse what the engine would reject |
| Web | `arcade/web/app.py`, `templates/tokens.html`, `token.html`, `_token_confirm.html` | The page; every action is prepare → confirm → broadcast the same bytes |
| Watcher | `arcade/web/watcher.py` | Syncs the index when the ledger tip moves, 500 blocks a tick |

Rules and their sources are cited in the code; the ones not in M2 notes:

| Rule | Source |
|---|---|
| Grant: issuer only, managed only, cap so that total ever granted ≤ MAX | `tx.cpp:2158-2257` |
| Revoke: any holder, from own balance | `tx.cpp:2260-2326` |
| Change issuer: issuer only, needs a reference address | `tx.cpp:2329-2390` |
| Grant with no reference lands on the issuer | `tx.cpp:672-675` |
| Any data (200) is valid and changes nothing | `tx.cpp:2735-2747` |

## What the tests pin

- `tests/test_state.py` — each rule above, and that a grant and a change of
  issuer roll back with their block.
- `tests/test_ledger.py` — the index against a regtest node: a token appears,
  a send shows for both addresses, an unknown type (999) stops it **at the
  block and with the type in the reason**, a reorg is unwound, a chain with no
  start block is "disabled" rather than empty.
- `tests/test_tokens.py` — create / send / grant / revoke / hand-over built
  through the wallet and read back by the index, with the **engine's** sender
  and recipient; a long name goes Class B and reads back; every refusal
  happens before anything is spent.
- `tests/test_tokens_web.py` — the page against a regtest node: the fee on the
  confirm screen is the fee the node's mempool reports for that txid; prepare
  does not broadcast; a confirmation for a transaction the server no longer
  holds is shown again, not sent; a mainnet address is refused on testnet;
  balances after a send are what the page shows for both wallet addresses.
- `tests/test_web.py` — every token page renders with no node, every token
  POST is refused without the CSRF token, and the chain tag switches the
  page and is remembered by a fresh `AppState`.

## Seen in pixels, not in markup

The confirm screen was rendered in headless Firefox against the testnet node
before the first real transaction. It labelled every Class B data output
"change back to you": the wallet's decoder lists the sender's own key among a
multisig output's addresses, and the label keyed on that. The test that now
pins the labels (`test_a_long_name_goes_class_b_and_reads_back`) was written
after the screenshot, not before.

## Trying it on testnet

Open Tokens and click the red **main** tag: the page switches to testnet, and
the green **test** tag switches back. Both chains are indexed all the time,
so the switch shows a current index. The choice is remembered across restarts.

The first token created this way is **Arcade Test**, from
`nYW2BPLENpu2nGa7WCExvzxD3hQYueULFa`, transaction
`4b902684ca547b2433d7963c4428bbeee67c45fd74c812e1e3910e0cbc8bb7b5`, Class B
(the description pushed it past one OP_RETURN). 10,000 of it went to the
GX10's testnet address in
`26904d43d8d45dbc5dda67bf1cc95f8af2561c758f530f8ecf313116c9814da1`, Class C.
A second machine running the same version must show the same token, both
holders and both history entries — that is the cross-check.

The first airdrop was `examples/airdrop.py` through the bot RPC (D-017,
`bot-rpc.md`): 5 Arcade Test to every holder of #3 other than the sender —
one send, to the GX10, in
`585f0bdec7ab055619b1b72977b785913ccc504936c6d73a91c084f768ffdffa`, fee
0.00257 as the node's mempool reported it. GX10 should show 10,005.

## Not yet

- No token appears on the Overview page or in the Wallets page's balances.
- The history is per token or per address, not searchable.
- Send-to-owners (type 3), freezing and delegates are out of scope here (they
  are M4/M6 items and the engine records them as invalid, "not yet
  implemented").
