@artblocks/abx-cli 0.1.0-alpha.3 → 0.1.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3663 -0
- package/assets/renderer-scaffold/README.md +2 -2
- package/assets/renderer-scaffold/src/MyRenderer.sol +2 -2
- package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +1 -1
- package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +17 -0
- package/assets/renderer-scaffold/test/MyRenderer.t.sol +51 -2
- package/dist/commands/deploy.d.ts +242 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +4764 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/project.d.ts +257 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +1299 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/reads.d.ts +64 -0
- package/dist/commands/reads.d.ts.map +1 -0
- package/dist/commands/reads.js +701 -0
- package/dist/commands/reads.js.map +1 -0
- package/dist/commands/scaffold.d.ts +73 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +674 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/service.d.ts +41 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +618 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/storage.d.ts +51 -0
- package/dist/commands/storage.d.ts.map +1 -0
- package/dist/commands/storage.js +370 -0
- package/dist/commands/storage.js.map +1 -0
- package/dist/commands/submit-app.d.ts +58 -0
- package/dist/commands/submit-app.d.ts.map +1 -0
- package/dist/commands/submit-app.js +512 -0
- package/dist/commands/submit-app.js.map +1 -0
- package/dist/config.d.ts +90 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +285 -11
- package/dist/config.js.map +1 -1
- package/dist/conformance.d.ts +31 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +393 -0
- package/dist/conformance.js.map +1 -0
- package/dist/deps.d.ts +6 -39
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +4 -68
- package/dist/deps.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/flag-allowlists.d.ts +53 -0
- package/dist/flag-allowlists.d.ts.map +1 -0
- package/dist/flag-allowlists.js +157 -0
- package/dist/flag-allowlists.js.map +1 -0
- package/dist/flags.d.ts +45 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +117 -1
- package/dist/flags.js.map +1 -1
- package/dist/jsonout.d.ts +37 -0
- package/dist/jsonout.d.ts.map +1 -0
- package/dist/jsonout.js +68 -0
- package/dist/jsonout.js.map +1 -0
- package/dist/kind.d.ts +61 -0
- package/dist/kind.d.ts.map +1 -0
- package/dist/kind.js +111 -0
- package/dist/kind.js.map +1 -0
- package/dist/main.js +699 -4736
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts +17 -2
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +241 -54
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +179 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +756 -0
- package/dist/output.js.map +1 -0
- package/dist/ownerops.d.ts +302 -51
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +1780 -357
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +81 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +535 -0
- package/dist/preview.js.map +1 -0
- package/dist/prompt.d.ts +17 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +19 -0
- package/dist/prompt.js.map +1 -0
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +16 -8
- package/dist/provision.js.map +1 -1
- package/dist/remote.d.ts +153 -52
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +406 -46
- package/dist/remote.js.map +1 -1
- package/dist/riskgate.d.ts +58 -0
- package/dist/riskgate.d.ts.map +1 -0
- package/dist/riskgate.js +212 -0
- package/dist/riskgate.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +52 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/schema.d.ts +36 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +121 -26
- package/dist/schema.js.map +1 -1
- package/dist/script-chunks.d.ts +8 -0
- package/dist/script-chunks.d.ts.map +1 -0
- package/dist/script-chunks.js +35 -0
- package/dist/script-chunks.js.map +1 -0
- package/dist/served.d.ts +30 -0
- package/dist/served.d.ts.map +1 -0
- package/dist/served.js +112 -0
- package/dist/served.js.map +1 -0
- package/dist/signer.d.ts +13 -0
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +84 -15
- package/dist/signer.js.map +1 -1
- package/dist/update-check.d.ts +83 -4
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +140 -19
- package/dist/update-check.js.map +1 -1
- package/package.json +10 -6
- package/skill/SKILL.md +350 -150
- package/skill/reference/code-projects.md +183 -26
- package/skill/reference/creator-token.md +99 -0
- package/skill/reference/decisions.md +174 -0
- package/skill/reference/hosting.md +63 -19
- package/skill/reference/operating.md +116 -12
- package/skill/reference/setup.md +55 -6
- package/skill/reference/troubleshooting.md +42 -5
- package/dist/inspect.d.ts +0 -48
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js +0 -184
- package/dist/inspect.js.map +0 -1
- package/dist/migrate.d.ts +0 -65
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -180
- package/dist/migrate.js.map +0 -1
- package/dist/onchain-uri.d.ts +0 -97
- package/dist/onchain-uri.d.ts.map +0 -1
- package/dist/onchain-uri.js +0 -243
- package/dist/onchain-uri.js.map +0 -1
- package/dist/upload.d.ts +0 -28
- package/dist/upload.d.ts.map +0 -1
- package/dist/upload.js +0 -41
- package/dist/upload.js.map +0 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Decisions — configuring a real launch
|
|
2
|
+
|
|
3
|
+
[← back to SKILL.md](../SKILL.md#decisions-real-launch)
|
|
4
|
+
|
|
5
|
+
The six decisions a real launch actually turns on. SKILL.md carries the router and the rules that
|
|
6
|
+
prevent an irreversible mistake; this is the depth — read it once you are configuring a real deploy,
|
|
7
|
+
not while you are still working out what the creator wants.
|
|
8
|
+
|
|
9
|
+
Master call is **custody × mutability**:
|
|
10
|
+
|
|
11
|
+
| | **Mutable** (name/desc/traits may change) | **Immutable** (never changes) |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Small static** (≲ 40 KB/file — the READ ceiling, see below) | **on-chain renderer** — `--onchain-image --compress fastlz`. No host, mutable via `set-field`, permanent. | on-chain renderer + `lock-field` + `lock-uri` once it resolves. |
|
|
14
|
+
| **Bigger / dynamic** (most PNG/JPEG) | **image off-chain, JSON on-chain, no server** — `--onchain-uri --backend arweave` (or `ipfs`). Renderer assembles JSON pointing at the bytes; many files → one `url-template` (O(1)). For metadata you edit often, a **resolver** instead — a managed provider or your own (`abx deploy-resolver`), [hosting.md](reference/hosting.md). Not fully on-chain (~200 gas/byte). | image off-chain (Arweave = permanent) + on-chain renderer + `lock-field`/`lock-uri`. Or a frozen `ipfs://` override + `lock-uri`. |
|
|
15
|
+
|
|
16
|
+
**Four patterns, by where bytes live × how `tokenURI` resolves:**
|
|
17
|
+
1. **Fully on-chain** (`--onchain-image`) — bytes + JSON on-chain. Small content only, and the binding limit is READ gas, not storage cost (below).
|
|
18
|
+
2. **Image off-chain, JSON on-chain, no server** (`--onchain-uri --backend arweave|ipfs|cloud`) — the sweet spot for static content. Arweave/IPFS (permanent, content-addressed) or your S3/CDN (`--backend cloud --public-base <url>`; centralized, mutable). Many files → one `url-template`.
|
|
19
|
+
3. **Remote resolver** (`--public-base-url` + a node) — for mutable/dynamic metadata; **self-hosted** (`abx deploy-resolver`, you run it) or a **managed provider** (an API key, they run it). Same interface — swap with one re-point.
|
|
20
|
+
4. **Inline SVG on-chain** — self-contained vector content inlined into `tokenURI`. For a **tiny 1/1** (under ~0.5 KB — a one-line SVG) that's `abx deploy … --onchain-uri`; **at a few KB and up, reach for `--onchain-image --compress fastlz` instead** (cheaper to write past ~0.5 KB — see *Inline vs reader* below, and note the 2.4 KB example SVG lands on that side). Same flag for a **Series** of tiny SVGs: `abx deploy-series … --onchain-image --compress fastlz` (bare `--onchain-uri` on a folder does NOT inline the images — it's the image-custody flag `--onchain-image` that puts SVG bytes on-chain per token).
|
|
21
|
+
|
|
22
|
+
**All three deploy commands can now do pattern 2.** `deploy` (1/1), `deploy-series` and `deploy-code`
|
|
23
|
+
each upload to the chosen backend under `--onchain-uri` and bake the resulting public URL into the
|
|
24
|
+
on-chain JSON. (Until 2026-08-03 the 1/1 silently could not — it committed a hash and rendered a
|
|
25
|
+
placeholder. If you remember that limitation, it is gone.)
|
|
26
|
+
|
|
27
|
+
## `--copies` (ERC-1155 editions) — custody is NOT orthogonal to the shape
|
|
28
|
+
|
|
29
|
+
Adding `--copies <n|open>` does not leave the four patterns above intact. Three combinations are
|
|
30
|
+
**refused** today, and each one is something you would reach for first. Check this BEFORE you promise a
|
|
31
|
+
creator a lane — the refusal lands at deploy time, and two of these have already pushed agents into the
|
|
32
|
+
wrong architecture:
|
|
33
|
+
|
|
34
|
+
| Want | `deploy --copies` (1/1-edition) | `deploy-series --copies` (EditionImage) | `deploy-code --copies` (EditionCode) |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| **Pattern 1** — bytes fully on-chain (`--onchain-image`) | ✅ hot + `--sign` | ✅ hot + `--sign` | n/a |
|
|
37
|
+
| **Pattern 2** — image off-chain, JSON on-chain, **no server** (`--onchain-uri --backend arweave\|ipfs`) | ✅ | ✅ (uniform extensions → ONE `url-template` collection field, O(1)) | n/a |
|
|
38
|
+
| **Pattern 4** — inline SVG on-chain (`--onchain-uri`, SVG content) | ✅ | ✅ (every id must be SVG) | n/a |
|
|
39
|
+
| **Pattern 3** — resolver (`--public-base-url`) | ✅ | ✅ | ✅ |
|
|
40
|
+
| on-chain library dep (`--dep p5@…`) | n/a | n/a | ✅ **Sepolia only** (no registry on Base Sepolia) |
|
|
41
|
+
| build directory (`--code-dir`) · field renderers | n/a | n/a | ❌ refused — `--script` only |
|
|
42
|
+
|
|
43
|
+
**One lane genuinely can't be signed offline:** `--onchain-image` is refused on the **cold**
|
|
44
|
+
(`--unsigned`) lane everywhere, 721 and edition alike — staging is a sequence where each chunk tx's
|
|
45
|
+
receipt feeds the next, so use the hot lane or `--sign`.
|
|
46
|
+
|
|
47
|
+
Otherwise custody is now fully symmetric with the 721 side, including the case that used to have no
|
|
48
|
+
answer: **"N works × M copies each" with raster images and no server** is
|
|
49
|
+
`abx deploy-series --dir <folder> --copies <n> --onchain-uri --backend arweave` — a uniform file
|
|
50
|
+
extension uploads the folder as ONE directory and bakes a single `{id}`-substituting collection field,
|
|
51
|
+
so the whole collection costs one field rather than N.
|
|
52
|
+
|
|
53
|
+
**Picking IPFS (or Arweave) does NOT mean running a server.** The `--onchain-uri --backend ipfs|arweave` path (pattern 2) bakes the image's public **gateway** URL into on-chain JSON — a pinning service's read endpoint (a *dedicated* Pinata gateway for IPFS), not a resolver you host. So when a creator chooses IPFS, **default to this no-server path** — image on IPFS, JSON on-chain, nothing to keep running (just keep the pin alive). You only need a **resolver** (pattern 3 — managed or self-hosted) if they want *freely editable* metadata. Never present IPFS as blocked on "a public URL" or "a server always online": the gateway belongs to the pinning service and the JSON lives on-chain. (The one real input IPFS needs is `PINATA_JWT` in `.env` for pinning — that's an API upload, not a host.)
|
|
54
|
+
|
|
55
|
+
**No-server tradeoff (patterns 1, 2, 4):** with the on-chain renderer only the *image* is off-chain — any **description / traits / animation_url live on-chain** (gas to write, permanent, lockable), vs a hosted resolver where they're free to edit. **PostParams are on-chain either way** — they live in the contract's own store and enumerate from it (`abx state` / `abx tokens`), so a chain-only reader sees the whole configuration with no server on *any* pattern; they just aren't copied into the metadata JSON. Cheap (a shared value is **one collection-scope field**, not one per token — the renderer falls back token→collection), but the creator should choose "no server" knowing their text metadata is on-chain.
|
|
56
|
+
|
|
57
|
+
Get decisions 1–2 right before deploy (image commitment + resolver URL are written then; re-pointable, but):
|
|
58
|
+
|
|
59
|
+
**1. Storage permanence** — where bytes live. Not irreversible: bytes are content-addressed by their on-chain keccak, so start on one backend and move later (`abx verify` confirms the hash). Don't let it block a first deploy.
|
|
60
|
+
- `arweave` = pay-once permanent, no recurring fee. `cloud` (S3/R2) = durable, you maintain it. `ipfs` = decentralized, you pin it. `fs` = zero-config start, dies with the disk → move before it matters.
|
|
61
|
+
- **`arweave` is nearly as easy as `fs` for small content** — Turbo default: **under 100 KB free, no setup** (a managed `.abx-self-host/arweave-key.json` minted the first time a command needs it — an upload, or a `storage balance`/`topup`; back it up with `abx storage backup-key`). Choose per command with `--backend` (stateless, no config file). **A backend missing its secret does NOT fall back to `fs`** — it fails: `cloud` refuses up front naming the missing values, and `ipfs` without `PINATA_JWT` resolves to **kubo mode against a local node** (`http://127.0.0.1:5001`), which fails at upload time if you aren't running one. Check with `abx storage show` (it prints the resolved backend *and* mode) before a real upload.
|
|
62
|
+
- **Who pays is a lane (`--storage-signer`)** — Turbo credits attach to an identity (managed key · `.env` key · browser wallet). **Before any top-up, check BOTH balances** (`abx storage balance --backend arweave` shows the managed key AND the wallet — spend the wallet's credits if present). On an upload error surface it verbatim — `…already been uploaded…` is *success* (dedup); don't reflexively top-up or switch to IPFS. Full lanes + failure playbook → [hosting.md](reference/hosting.md#arweave-via-turbo--the-easy-permanent-path-read-before-quoting-setup).
|
|
63
|
+
|
|
64
|
+
**2. Public host URL — and who runs the resolver** (**off-chain custody only**). Baked into `tokenURI` at deploy, so the CLI **refuses an off-chain deploy without a public URL** (`ABX_PUBLIC_BASE_URL` or `--public-base-url https://…`) and **never bakes localhost** (that token resolves for no one). No exceptions.
|
|
65
|
+
- **First ask whether you need a host at all** — tiny content is cheaper and more durable on-chain (no host). For bigger content, Arweave (no host to run) beats a resolver unless you need mutability or serve many files.
|
|
66
|
+
- **A named remote is already configured (`ABX_REMOTE_<NAME>_URL` in `.env`)? Use it.** The creator already chose a provider — don't stand up new infrastructure beside it. **Run `abx remote <name>` FIRST, before registering anything**: it prints the provider's chain coverage + whether rendering is managed, and it *validates the key* (`401` = the token in `ABX_REMOTE_<NAME>_TOKEN` is stale/wrong → they replace the value in `.env`; `403` = the key is fine but not authorized for this contract/chain → provider-side scoping, don't touch the key). Then register: `abx add <addr> --remote <name>`. Testing a replacement key without editing `.env` first: `abx remote <name> --remote-token <new-key>`.
|
|
67
|
+
- **Otherwise, two equal ways to have a resolver, one config change apart.** A **managed provider** — one base URL + one API key, no cloud account, nothing to keep alive; often **managed rendering** too, so a code drop needs no effects runner (**lead with this when the creator doesn't already run infrastructure or doesn't want to** — [hosting.md → Managed providers](reference/hosting.md#managed-providers--a-resolver-someone-else-runs---remote-name)). Or **self-host** (`abx deploy-resolver`, [hosting.md](reference/hosting.md)) — the creator owns the node and the cloud account. Same interface, same commands; a project moves between them with one re-point + re-register. **No provider key in hand and none to get? Self-host is the fully-supported path today** — the provider market is only starting to form; never invent or recommend a provider that isn't in front of you.
|
|
68
|
+
- **Tunnels (ngrok/cloudflared) are preview-only — never bake one on-chain** (dies on sleep, rotates on restart). A real launch puts the resolver on a host you control under your own domain (move = a DNS re-point), or behind a provider.
|
|
69
|
+
|
|
70
|
+
**3. Identity** — `--name`, `--symbol`, `--royalty-bps` (default 500 = 5%), `--royalty-cap` (the permanent, **reduce-only** royalty ceiling — default 10%, or your royalty rate if higher; shown in the deploy readout, lowered later with `abx set-royalty-cap`), `--burnable` (opt-in: holders may burn their own token, default off), `--description "…"`, `--external-url <url>` (both served in the metadata — set them or the description is boilerplate). Owner + royalty receiver = the deploying wallet. These default to off-chain operator metadata (editable via `abx add <addr> --description "…"`). For a description that should outlast any node, add `--description-onchain` (or later `abx set-field <addr> --field description --text "…"`) → on-chain, freezable via `lock-field`; the resolver prefers the on-chain value. This is the per-field on-chain model — any field on-chain or off, one active `representation` (inline · reader · keccak256 · arweave · ipfs · url). Background: [metadata model](https://abx.docs.artblocks.io/protocol/metadata/).
|
|
71
|
+
- **Credit + license** — deploy flags `--creator "…"` · `--license "…"` (also `--display-notes`, `--creator-links`) bake authorship + rights ON-CHAIN in the deploy tx (all three deploy commands); or set/change them later with `abx set-field <addr> --collection --field creator|license --text "…"`. Reserved collection fields served in `contractURI`, on any type (1/1 · Series · code). Detail: [operating.md → Authorship + rights](reference/operating.md#authorship--rights-credit--license).
|
|
72
|
+
- **Propose a real name/symbol and confirm — never silently bake a generic folder-name guess.** A folder called `series`/`images`/`photos` infers junk ("Series" / "SRS"), and on all three deploy commands the CLI *refuses* a real send that would bake its own placeholder identity (`--name`/`--symbol` missing) — because on-chain identity is effectively permanent. **In `--dry-run` the same check only warns** (so a preview still runs before you have the creator's title); don't read that warning as "the CLI allows it" — the real deploy stops. Suggest a specific title + a short ticker-style symbol drawn from the actual work, and get an explicit yes before deploying. Inference is a suggestion to confirm, not a default to ship — if the folder name is generic, say so and ask rather than proposing it.
|
|
73
|
+
|
|
74
|
+
**4. Image placement** — `--image <path>` (png · jpg · gif · svg · webp). The on-chain keccak256 (`image` field) anchors integrity; size is bounded by the backend, not the chain.
|
|
75
|
+
- *Off-chain:* the served `image` is an **HTTPS gateway URL** (`https://<gateway>/ipfs/<cid>`), not raw `ipfs://` (wallets/marketplaces can't render that). So off-chain needs a pinning service + a **public** gateway — with Pinata use a **dedicated** gateway (`--gateway https://<you>.mypinata.cloud`); a local kubo gateway is preview-only. Which prefix gets used depends on the field's representation, and the two cases differ: a **keccak-custody** field is served from whatever gateway the resolver is configured with (no tx to change it), while an **`ipfs`/`arweave`** field holds the bare CID/txid and takes its prefix from the collection's on-chain preference — floors `https://ipfs.io/ipfs/` and `https://arweave.net/`, changed with **one tx** (`abx set-gateway`), never a re-upload.
|
|
76
|
+
- *Fully on-chain:* `abx set-field <addr> --field image --file <path> [--compress fastlz]` splits into SSTORE2 chunks behind the shared reader; or bake it in with `abx deploy --image <path> --onchain-image [--compress fastlz]`.
|
|
77
|
+
|
|
78
|
+
**Inline vs reader — default to the reader for real work.** `--onchain-uri` alone inlines the SVG (1 tx, ~700 gas/byte); `--onchain-image --compress fastlz` stages via SSTORE2 + a small `reader` pointer (~200 gas/byte, +1 tx) — **cheaper above ~0.5 KB** and widening with size. So: tiny (<~0.5 KB, a one-line SVG/short text) → `--onchain-uri` inline; real work (a few KB+) → `--onchain-image --compress fastlz`. **Never `--compress gzip` for an on-chain-rendered token** — gzip decodes off-chain only, breaking `--onchain-uri`; use fastlz (it decodes *in* the reader).
|
|
79
|
+
|
|
80
|
+
### The READ ceiling — the number that actually decides "can this go on-chain?"
|
|
81
|
+
|
|
82
|
+
Storage cost is not the constraint people think it is. **`tokenURI` reassembles the whole document on
|
|
83
|
+
every call, measured at ~360,000–405,000 gas per KB across the 10–100 KB range that matters — and the
|
|
84
|
+
rate climbs with size** (~460,000/KB at 187 KB, ~510,000/KB at 256 KB), because EVM memory expansion is
|
|
85
|
+
quadratic. Quote it as a range, never as one flat per-KB number. `inline` and `reader` measure within
|
|
86
|
+
~1% of each other up to 75 KB (the cost is the renderer's string building, not the storage mechanism),
|
|
87
|
+
diverging to ~5% at 187 KB and ~10% at 256 KB as the chunk store's read loop dominates. So
|
|
88
|
+
`--compress fastlz` makes it cheaper to *write* and not one gas cheaper to *read*.
|
|
89
|
+
|
|
90
|
+
| On-chain content per token | `tokenURI` read (measured) | What it means |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| ≲ **40 KB** | ~15M gas or less | fine — ordinary RPC tooling reads it |
|
|
93
|
+
| **40–100 KB** | ~15–40M gas | **CLI warns.** Needs a deliberately high-gas RPC; past ~90 KB (~36M) no contract can read it inside a tx |
|
|
94
|
+
| **> 100 KB** | ~40M+ | **CLI refuses** — a margin, not a wall: geth's own 50M `eth_call` default is not exhausted until ~120 KB, but hosted providers cap lower, so past 100 KB many marketplaces and indexers see a *revert* rather than the token |
|
|
95
|
+
|
|
96
|
+
The refusal has an override, `--allow-unreadable-onchain`, and it is a real choice rather than a
|
|
97
|
+
formality: maximum durability, nothing to run, ever — in exchange for a token most marketplaces and
|
|
98
|
+
indexers cannot display. **Do not reach for it to get past a warning.** Name the trade to the creator in
|
|
99
|
+
their words ("almost nothing will show your work unless someone runs a beefy node") and get an explicit
|
|
100
|
+
yes; if they hesitate, the answer is `--backend arweave` (image off-chain, JSON still on-chain, no
|
|
101
|
+
server) — same no-server promise, no read problem.
|
|
102
|
+
|
|
103
|
+
**The gate is PER TOKEN, not per project.** Each token's `tokenURI` assembles only its own content, so a
|
|
104
|
+
300-piece collection of 5 KB SVGs reads perfectly. The old "≲ 256 KB total per project" figure is a
|
|
105
|
+
*write*-cost note only — the CLI still flags it, and it says nothing about readability.
|
|
106
|
+
|
|
107
|
+
For a **code** project the same arithmetic applies to the assembled document, so a registry-hosted
|
|
108
|
+
library (`--dep p5@1.0.0`, ~200 KB in the document) is inherently past the readable range. That lane is
|
|
109
|
+
not refused — a big document is legitimate there and clients assemble it from the generator's piecewise
|
|
110
|
+
getters (`document`, `tokenDataJson`, `dependencyTag`, `abxJs`, `gunzipScript`, `registryScriptChunk`) —
|
|
111
|
+
but `deploy-code --dry-run` prints the gas figure, and you should relay it before the creator commits.
|
|
112
|
+
|
|
113
|
+
**5. On-chain vs off-chain resolution** — by default `tokenURI`/`contractURI` point at your resolver. `--onchain-uri` = JSON assembled *on-chain* by the shared `AbxMetadataRenderer`, self-resolving forever — so it pairs with on-chain content, cost-effective only for tiny content (thresholds above).
|
|
114
|
+
- **Fully on-chain = no server.** Don't stand one up; never cite a localhost URL. **Prove it with `abx tokenuri <addr>`** (reads `tokenURI(0)` over RPC, no `serve`). `abx serve` is only for off-chain-resolving tokens.
|
|
115
|
+
- **The off-chain `tokenURI` is a base, not a per-token URL** — the contract stores a base and derives `{base}/{chainId}/{address}/{tokenId}`. Set via `--public-base-url` or `set-token-uri --uri <base>` later.
|
|
116
|
+
- **Don't default to a frozen `ipfs://` tokenURI** — every edit then = re-pin + on-chain re-point, and the event spine stops driving the token (exiting the spec). Right only for true immutability, then lock it (`set-token-uri --override ipfs://<cid>` then `lock-uri`).
|
|
117
|
+
- **Store ≠ lock; lock last.** On-chain ≠ frozen. Deploy unlocked, confirm it resolves in production, *then* freeze. For a static image/1-of-1, two locks freeze the metadata: `lock-field <addr> --field <name>` (a value) + `lock-uri <addr>` (how it resolves). **For a generative/code drop these are NOT enough** — the work is the on-chain program, which stays rewritable until you also run `lock-script <addr>` (and `lock-dependencies <addr>` for the library set). The full set for a code project = `lock-script` + `lock-dependencies` + `lock-field`/`lock-uri`. A deliberate follow-up, not the first deploy.
|
|
118
|
+
|
|
119
|
+
**Say "locked metadata", never "immutable work".** Every lock freezes what *this contract stores*, and
|
|
120
|
+
two things sit downstream of that: **an *ungoverned* PostParam has no lock** (the metadata locks cover
|
|
121
|
+
fields and the URI config, not the param store — so the owner's raw setter keeps writing it and the
|
|
122
|
+
renderer keeps serving it into `tokenURI`; a *schema'd* param can be welded permanently with
|
|
123
|
+
`set-schema … :lock=now`, value **and** schema, so never say "params can't be locked"), and a **`Registry` dependency
|
|
124
|
+
is re-fetched from the registry on every read** — `lock-dependencies` pins which library the ref means,
|
|
125
|
+
not the bytes the registry hands back. `abx verify` reports `chain-complete` for a registry dep either
|
|
126
|
+
way, because that flag is about *where* bytes come from, not whether they change. So the honest line to a
|
|
127
|
+
creator (and to their buyers) is "the metadata is frozen"; only a project whose whole graph is on-chain
|
|
128
|
+
`--dep 0x…` refs plus locked fields is a frozen *output*. Read the other way this is a feature — a piece
|
|
129
|
+
that adapts to live chain state is a real thing to build — but it has to be said out loud either way.
|
|
130
|
+
Background: https://abx.docs.artblocks.io/protocol/owner-powers/
|
|
131
|
+
|
|
132
|
+
**6. When to mint** → [Deploy strategy](../SKILL.md#deploy-strategy--when-to-mint).
|
|
133
|
+
|
|
134
|
+
## Locks — what actually freezes, and what a buyer is really asking
|
|
135
|
+
|
|
136
|
+
Every lock in this toolkit freezes a **pointer or a stored value**, never *behavior*. Say "locked
|
|
137
|
+
metadata", never "immutable work" — the difference is the whole of this section, and a creator who
|
|
138
|
+
promises the second while shipping the first has mis-sold their collection.
|
|
139
|
+
|
|
140
|
+
**An *ungoverned* PostParam has no lock at all.** The owner's raw setter keeps writing it past every
|
|
141
|
+
metadata lock, and the generator keeps injecting the new value as `tokenData`. A *schema'd* param can
|
|
142
|
+
be welded permanently (`set-schema … :lock=now`, `abx retire-param`), which freezes that key's
|
|
143
|
+
**token-scope value** and its **schema** forever. So "no lock" is a statement about the ungoverned
|
|
144
|
+
store, never about a governed key.
|
|
145
|
+
|
|
146
|
+
**The documented exception to a weld: a contract-scope DEFAULT can still be deleted.**
|
|
147
|
+
`clearContractParam` deliberately sits outside the schema guard — it is the recovery path out of a
|
|
148
|
+
value poisoned before the schema existed. So an owner can set a collection-wide default, attach and
|
|
149
|
+
expire a schema on that key, sell tokens that inherit the default, and *then* clear it; every token
|
|
150
|
+
with no override of its own changes. Clearing can only *remove* a fallback — never forge a value,
|
|
151
|
+
never bypass an auth rule, never touch a token-scope value already written. **If a creator is
|
|
152
|
+
promising a frozen collection-wide value, write it per token** (governed, welded) rather than leaving
|
|
153
|
+
it as an inherited default.
|
|
154
|
+
|
|
155
|
+
**A `Registry` dependency's bytes are re-fetched on every read.** `lock-dependencies` pins which
|
|
156
|
+
library a ref *means*, not what it returns. `abx verify`'s `chain-complete` is a claim about where
|
|
157
|
+
bytes come from, not that they are frozen.
|
|
158
|
+
|
|
159
|
+
**The one lock a BUYER asks about is `lock-param-hooks`.** A code project's `--transfer` hook is a
|
|
160
|
+
**veto** — its revert fails a transfer, and mints too — so until the hooks are frozen the owner holds
|
|
161
|
+
a standing power over whether a collector can ever sell. Freezing an empty set is how a creator proves
|
|
162
|
+
they never will.
|
|
163
|
+
|
|
164
|
+
**Every lock freezes a POINTER, not behavior.** A locked hook, renderer or reader is still a
|
|
165
|
+
*contract*, and a contract can be a proxy whose code is upgraded later: `paramHooksLocked()` keeps
|
|
166
|
+
reading `true` while a re-pointed proxy starts reverting every transfer. The protocol does not try to
|
|
167
|
+
detect proxies on chain (complex, incomplete) — it defines locks as pointer locks and discloses it. So
|
|
168
|
+
**for any permanence claim, the hook/renderer must itself be an immutable deployment**: say "these
|
|
169
|
+
exact three addresses can never change" and check what is behind them, never "the behavior is frozen".
|
|
170
|
+
|
|
171
|
+
A token that live-adapts to chain state is a legitimate thing to build. Just don't sell a frozen one as
|
|
172
|
+
frozen unless it is. See also
|
|
173
|
+
[code-projects.md → hooks](code-projects.md#live-data-the-augment-hook--the-hook-is-the-setting) and
|
|
174
|
+
https://abx.docs.artblocks.io/protocol/owner-powers/
|
|
@@ -13,7 +13,18 @@ Storage is **stateless** — there's no config file. Choose a backend **per comm
|
|
|
13
13
|
| **`ipfs`** | Decentralized (CID) | kubo: `--backend ipfs --mode kubo --api-url http://127.0.0.1:5001 --gateway http://127.0.0.1:8080` · pinata: `--backend ipfs --mode pinata --gateway https://<you>.mypinata.cloud` | `PINATA_JWT` (pinata mode) |
|
|
14
14
|
| **`arweave`** | Pay-once permanent | `--backend arweave` (Turbo default; or `ABX_STORAGE_BACKEND=arweave`) | the Turbo key (auto-managed) |
|
|
15
15
|
|
|
16
|
-
All backends verify via the on-chain keccak commitment wherever the bytes live. **
|
|
16
|
+
All backends verify via the on-chain keccak commitment wherever the bytes live. **What a marketplace fetches is always an HTTPS gateway URL** → IPFS/Arweave custody needs a *public* gateway. The two public **floors**, used whenever a project states no preference, are:
|
|
17
|
+
|
|
18
|
+
| Scheme | Floor (used when the project sets nothing) |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `ipfs` | `https://ipfs.io/ipfs/` |
|
|
21
|
+
| `arweave` | `https://arweave.net/` |
|
|
22
|
+
|
|
23
|
+
Do not guess these and do not substitute the *upload* gateway for them — a project that uploaded through Pinata still **serves** from `ipfs.io` unless it said otherwise, and telling a creator otherwise gives them a URL their token does not use. A `127.0.0.1` gateway is preview-only (the CLI warns); the shared `gateway.pinata.cloud` is rate-limited — use a **dedicated** gateway for anything real, and set it with `--ipfs-gateway` (deploy) or `abx set-gateway` (after).
|
|
24
|
+
|
|
25
|
+
**A resolver adds nothing marketplace-facing to a project that already resolves on-chain — say so before you set one up.** The docs below explain when a resolver *helps* (mutable metadata, code drops needing rendering, off-chain traits); the inverse is just as load-bearing and was missing. Check first: `abx state <addr>` printing `renderer on-chain (0x…)` means `tokenURI` answers from the chain, so wallets and marketplaces read **that** document and never touch a resolver. Registering with one is still worth doing for **indexing, the live view, and managed rendering** — but it will not change a single thing a collector sees, and a creator told to "put it on a hosted service so it shows up properly" needs to hear that plainly rather than be walked through a setup that changes nothing. (`abx add --remote` now says this too.) Conversely, a project whose renderer is **off-chain** genuinely depends on a resolver, and that is the case the rest of this page is about.
|
|
26
|
+
|
|
27
|
+
**Which gateway, and who can change it.** For a `keccak256`-custody field the resolver builds that URL from the backend's own gateway. For an `ipfs`/`arweave` field — what `--onchain-uri --backend ipfs|arweave` writes — the chain holds only the CID/txid, and the prefix comes from the collection's `abx_gateway_ipfs` / `abx_gateway_arweave` setting (public defaults when unset). Set it at deploy with `--ipfs-gateway` / `--arweave-gateway` (plain `--gateway` seeds the one matching your backend), and change it any time with **`abx set-gateway <addr> --ipfs <prefix> --arweave <prefix>`** — one tx, every token, no re-upload, and it works on fields that are already locked. `--gateway` remains the UPLOAD/probe gateway; these are the SERVING one, and they are the same value in almost every project.
|
|
17
28
|
|
|
18
29
|
**Code projects (directory mode) need a gateway that serves HTML.** A `deploy-code --code-dir` build's live view 302-redirects to `<gateway>/ipfs/<cid>/index.html` — and the **shared Pinata public gateway blocks HTML** (`ERR_ID:00023`, "HTML content cannot be served through the pinata public gateway"). So a code directory drop requires a **dedicated** Pinata gateway (paid) or **Arweave** (`https://arweave.net` serves HTML). Static image directories (`deploy-series`) are unaffected — only the executable HTML build hits this.
|
|
19
30
|
|
|
@@ -25,11 +36,11 @@ All backends verify via the on-chain keccak commitment wherever the bytes live.
|
|
|
25
36
|
|
|
26
37
|
### Arweave via Turbo — the easy permanent path (read before quoting setup)
|
|
27
38
|
|
|
28
|
-
`arweave` defaults to the **Turbo** provider (ArDrive). For small
|
|
39
|
+
`arweave` defaults to the **Turbo** provider (ArDrive). For small content it needs **no setup and costs nothing** — no wallet dashboard, no JWT (don't send anyone to `app.arweave.org`). How it works:
|
|
29
40
|
|
|
30
41
|
- **Identity is a lane (`--storage-signer`) — Turbo is multi-chain, so credits attach to whichever identity signs the upload.** Three choices, mirroring the tx signing lanes:
|
|
31
|
-
- **`arweave`** *(default)* — a CLI-managed Arweave key auto-generated at `.abx-self-host/arweave-key.json`
|
|
32
|
-
- **`--storage-signer eth`** — reuse the **`.env` EVM signing key** as the Turbo identity
|
|
42
|
+
- **`arweave`** *(default)* — a CLI-managed Arweave key auto-generated at `.abx-self-host/arweave-key.json` the first time a command actually needs it — an upload, or a `storage balance` / `storage topup` (so a creator can fund the identity *before* uploading). Reads that never need a signer (`doctor`, `--dry-run`, `storage show`) do not create one. The address is printed with a back-it-up notice on creation. Reused across every upload/project; it signs *and* holds credits. Zero-config. Bring-your-own via `ARWEAVE_JWK` (inline) / `ABX_ARWEAVE_KEY_FILE` (path). **Back it up:** `abx storage backup-key --out <path>` (copies the file — the key is never printed to the terminal/log).
|
|
43
|
+
- **`--storage-signer eth`** — reuse the **`.env` EVM signing key** (`ABX_DEPLOYER_PK`) as the Turbo identity. Its ETH address holds the credits, so credits funded on that wallet's Turbo balance are spendable. Hot lane (key in `.env`).
|
|
33
44
|
- **`--storage-signer eth` + `--sign`** — the **browser wallet** signs each upload data-item via `personal_sign` (no gas, no funds move; the upload is paid from the wallet's Turbo credits), in the same sign session as the deploy tx. The key never leaves the wallet. *(New; the mechanism matches arbundles' `InjectedEthereumSigner` and is unit-tested at the CLI↔page contract, but the live MetaMask↔Turbo path wants a manual smoke test. If a live upload misbehaves, fall back to `--storage-signer eth` with a key, or fund the managed key.)*
|
|
34
45
|
- **Under 100 KB → free**, permanent, zero setup. The CLI prints this from the file size (in `--dry-run` too).
|
|
35
46
|
- **Over 100 KB → prepaid credits, one-time.** `abx storage balance` shows the funded address + credits; `abx storage topup --usd <n>` returns a Stripe checkout link. A **pre-upload balance guard** stops *before* the deploy if credits are short — printing the address + fund options — so a shortfall never fails mid-deploy after txs already landed. One-time payment, no recurring fee, nothing to re-pin (contrast `cloud`/`ipfs`). Credits are non-refundable.
|
|
@@ -45,7 +56,7 @@ A failed upload is the #1 place an agent goes off the rails: it invents a cause
|
|
|
45
56
|
- **Check BOTH balances before concluding "needs funding."** `abx storage balance --backend arweave` prints the managed key's balance **and** the deployer wallet's Turbo balance (Turbo exposes balance-by-address publicly — no key needed). The managed key showing 0 does NOT mean "top up": if the **wallet** has credits, use them (`--storage-signer eth`, or `+ --sign` for a browser wallet). The pre-upload funds guard does this check for you and recommends the wallet lane when it applies — checking only the managed key's 0 and pushing a top-up is the classic wrong turn.
|
|
46
57
|
- **Don't reflexively push a $5 top-up or "switch to IPFS."** Both throw away what the creator chose (their funded wallet credits; permanence). Only top up if *neither* the managed key nor the wallet can cover the bytes — and **back up the managed key first** (`abx storage backup-key --out <path>`), since it will hold the credits you buy. Only suggest IPFS if they ask or Arweave is truly unavailable.
|
|
47
58
|
- **A blind retry re-signs the same bytes → same dedup reply.** Retry only after you've identified and fixed the actual cause.
|
|
48
|
-
- **Gateway swappable, integrity independent.** The on-chain keccak256 is the anchor; `arweave.net` is the default gateway (`--gateway`
|
|
59
|
+
- **Gateway swappable, integrity independent.** The on-chain keccak256 (or the CID itself) is the anchor; `arweave.net` is the default gateway (`--gateway` at upload, `abx set-gateway` for serving). A gateway issue is a re-point, never a lost asset.
|
|
49
60
|
- **Provider swappable.** `--provider http-bundler --upload-url <bundler>` uses a dep-free token-authed endpoint you run (secret `ARWEAVE_UPLOAD_TOKEN`); funding then means "fund the wallet directly" (only Turbo tracks credits). Default to Turbo unless asked.
|
|
50
61
|
|
|
51
62
|
### Directory-base — image off-chain, JSON on-chain, no server (the medium/large static default)
|
|
@@ -54,19 +65,30 @@ A failed upload is the #1 place an agent goes off the rails: it invents a cause
|
|
|
54
65
|
|
|
55
66
|
- **Uniform extension → O(1) directory.** Files are renamed to the token-id convention (`0.png`, `1.png`, …), uploaded as one IPFS directory pin (Pinata) / Arweave path manifest (Turbo), and a single collection-scope `image` field gets the **`url-template`** representation — `<gatewayBase>/{id}.png`. The renderer substitutes each id → one field renders the whole collection, any size. No per-token on-chain data.
|
|
56
67
|
- **Mixed extensions → per-token `url`** fields (O(N)) — still no server. A uniform extension unlocks the single-template path; flag it if a folder is mixed.
|
|
57
|
-
- **`cloud` (S3/R2/CDN)** works the same but
|
|
68
|
+
- **`cloud` (S3/R2/CDN)** works the same, but the signed-API host (`--endpoint`/`ABX_S3_ENDPOINT`, where uploads go) and the public read host (`--public-base`/`ABX_S3_PUBLIC_BASE`, also what `deploy-code --image-base` takes) are **different hosts by design** — one auth-only, one public (R2's own API endpoint is never public; its public URL is a separate `pub-<hash>.r2.dev` or custom domain you enable). **`abx storage show --check` proves the two agree** — a real PUT through the API + an unsigned GET through the public base (`abx doctor` runs the same probe). Files upload under a content-derived prefix → `<publicBase>/<prefix>/{id}.png`. **Caveat:** S3 is centralized, mutable, not content-addressed (no CID/txid root). Good if the creator already runs a bucket/CDN; IPFS/Arweave are the trustless/permanent options.
|
|
58
69
|
- **Integrity** (IPFS/Arweave) comes from the content-addressed root (CID / manifest txid), not a per-token keccak. The gateway host is baked on-chain → moving gateways is a `set-field` (bytes stay put). Prefer a **dedicated** gateway.
|
|
59
70
|
- **`url-template`** is a first-class representation ([spec](../../../../specs/protocol/onchain-metadata.md)); set by hand with `abx set-field <addr> --collection --field image --representation url-template --text "<gateway>/ipfs/<cid>/{id}.png"` then `abx set-renderer <addr>`.
|
|
60
71
|
|
|
61
|
-
##
|
|
72
|
+
## Managed providers — a resolver someone else runs (`--remote <name>`)
|
|
62
73
|
|
|
63
|
-
|
|
74
|
+
The other way to have a resolver: be a **customer** of a hosted provider instead of running a node — one base URL + one API key, no cloud account, no Dockerfile, nothing to keep alive. Same interface, same commands as self-hosting ([spec](../../../../specs/self-host-toolkit/remote-services.md)); a project moves between a provider and your own node with one re-point + re-register ([operating.md → Moving your hosting](operating.md#moving-your-hosting--two-cases-dont-conflate)).
|
|
75
|
+
|
|
76
|
+
- **Named remotes.** Put the provider in `.env`: `ABX_REMOTE_<NAME>_URL=<base>` + `ABX_REMOTE_<NAME>_TOKEN=<api key>`. **`<NAME>` is YOUR label for the provider, not their legal name** — you pick it, you pass the same word to `--remote`. Normalization matches `ABX_RPC_URLS_<CHAIN>`: uppercase, runs of non-alphanumerics → `_`. So `--remote my-provider` reads `ABX_REMOTE_MY_PROVIDER_*`. For a multi-word brand, pick ONE short word and use it consistently — "Meridian Metadata" → `--remote meridian` + `ABX_REMOTE_MERIDIAN_URL`/`_TOKEN` (using `meridian metadata` would work too, as `ABX_REMOTE_MERIDIAN_METADATA_*`, but short is kinder to type). Then every remote command takes the name: `abx add <addr> --remote <name>`, ditto `index` / `forget` / `render` / `verify`, `abx migrate --from/--to <name>`, and owner-op nudges. Ad-hoc: `--remote <url> --remote-token <key>`. **Bare `--remote` stays the self-host default** (`ABX_PUBLIC_BASE_URL` + `ABX_REMOTE_SELF_TOKEN` — "self" is just another named remote) — nothing changes for a node you deployed. A named remote never falls back to `ABX_REMOTE_SELF_TOKEN` (or the resolver's own server-side `ABX_RESOLVER_ADMIN_TOKEN`), so neither secret can leak to a provider.
|
|
77
|
+
- **Ask up front whether they serve YOUR domain.** Whatever base URL is baked on-chain is what a marketplace fetches. If that's a domain the creator controls (pointed at the provider), leaving later is a DNS change — no tx, no gas. If it's the provider's own hostname, leaving costs an **owner-signed `set-token-uri`/`set-contract-uri` tx**. Same exit either way, different price; worth knowing before you bake it, not after.
|
|
78
|
+
- **Read the descriptor BEFORE registering — match the project to the provider.** `abx remote <name>` (or `curl <base>/.well-known/abx-service`, public, no key) prints what the service supports: **`chains`** must cover the project's chain (else registration is refused with `unsupported_chain`); **`render.attached`** means thumbnails/traits for code drops are rendered behind the provider — **skip `abx deploy-effects` entirely** (without it, renders are still yours: `abx render <addr> --remote <name>` or your own runner); **`auth.signupUrl`** is where a human gets a key.
|
|
79
|
+
- **The API key is the human's step — a membrane ask, never a chat paste.** Say: *"This provider covers your chain and manages rendering. You'll need an API key from `<signupUrl>` — put it in `.env` as `ABX_REMOTE_<NAME>_TOKEN` (never paste it in chat) and I'll do the rest."* Then verify with `abx remote <name>`: it lists the projects visible to the key. **401** = the key is missing/wrong (check the var, rotate at the provider). **403** = the key is valid but not authorized for this contract/chain — provider-side scoping, not a typo; don't retry-loop, check the provider dashboard.
|
|
80
|
+
- **The exit is guaranteed by the interface**, not by the provider's goodwill: registration is never load-bearing for resolution, and `abx migrate --from <provider> --to <anywhere>` reads only the provider's *public* endpoints — leaving is one config change plus (for a provider-hostname base) one on-chain re-point.
|
|
81
|
+
- **Honesty:** no default provider is baked into the CLI, the SDK, or this skill, and the provider market is only starting to form. A creator with a provider key (or a named remote already in `.env`) should use it; otherwise **self-hosting (next section) is the fully-supported path today** and what the rest of this file assumes.
|
|
82
|
+
|
|
83
|
+
## Self-hosted resolver — `abx deploy-resolver`
|
|
84
|
+
|
|
85
|
+
For the large/mutable default, when the creator runs the node themselves (the alternative to a managed provider above — same interface, they own the cloud account). `abx deploy-resolver --provider <fly|render|vps> --domain <meta.you.xyz>` scaffolds the artifact and prints the exact next steps, the DNS record, and the bake reminder. Providers: **fly.io** / **render** (Docker PaaS, custom domain) and a **VPS** (compose + Caddy auto-TLS). **Do not quote their plans or promise a free tier** — those move, and a stale claim in here becomes a stale claim to a creator; point at the provider's own pricing page and let them read it. The host is **read-only** — serves + accepts admin index-control, no signing key on it (writes are signed locally), so a compromised host can at worst serve wrong bytes (the keccak catches it). Prefer **a domain you control** (move = DNS, not a tx) — but without one the scaffold now bakes the **real platform hostname** (`<app>.fly.dev` / `<app>.onrender.com`) so the resolver works out of the box (add a custom domain later). It NO LONGER bakes a dead `<app>.example` placeholder, and the resolver **refuses to serve** an `.example`/placeholder base (or a localhost base in a hosted image, `ABX_HOSTED=1`) — a loud fail beats silently serving dead image/animation links. The one sub-decision is *which provider* (ask + recommend); you scaffold, the human owns the cloud account + domain.
|
|
64
86
|
|
|
65
87
|
**The artifact is fully self-contained.** `deploy-resolver` writes `deploy/<provider>/` with its OWN `Dockerfile` + `.dockerignore` + config (a production image installs the published CLI: `npm i -g --no-optional @artblocks/abx-cli`). **Run every next step from that dir.** You never supply, copy, or hand-edit a Dockerfile. **If a step seems to need a file from elsewhere (`../Dockerfile`, a `packages/` dir, the repo), STOP — that's a scaffold bug, not something to work around.** Report it; don't MacGyver it. (Local from-source/contributor dev sets `ABX_RESOLVER_SOURCE=1` → a build-from-source artifact instead, still self-contained; you don't set this.)
|
|
66
88
|
|
|
67
89
|
**Never inline a secret into a command.** The generated steps **source from `.env`** (`set -a; . ../../.env; set +a` then `fly secrets set ABX_RPC_URLS="$ABX_RPC_URLS"`) — keep that shape. Never substitute a literal secret into command text; it must never appear in output or shell history.
|
|
68
90
|
|
|
69
|
-
`deploy-resolver` also **generates `
|
|
91
|
+
`deploy-resolver` also **generates `ABX_REMOTE_SELF_TOKEN`** → your local `.env` (a secret — never printed, env-only; "self" is just the named remote for a node you run); the printed steps set that same value on the host under **its own** var name, `ABX_RESOLVER_ADMIN_TOKEN` (server-side config, unrelated to how any client authenticates). That token is what later lets `abx add <clone> --remote` tell the hosted node which contracts to index — the only new secret the hosted path adds, and it authorizes indexing control, not signing.
|
|
70
92
|
|
|
71
93
|
**Gather the decisions in one pass.** The hosted path has a few small choices — provider, custom domain (or the provider-hostname tradeoff), app name, plus identity. Collect them together, then show the full plan; don't drip one question per turn. Once the human says "you run it," run the steps — don't flip between "here are commands" and "I'll do it."
|
|
72
94
|
|
|
@@ -74,18 +96,37 @@ For the large/mutable default. `abx deploy-resolver --provider <fly|render|vps>
|
|
|
74
96
|
|
|
75
97
|
## Render runner — `abx deploy-effects`
|
|
76
98
|
|
|
77
|
-
A code project's marketplace still is rendered off-chain, so a **runner** (Playwright + Chromium — the resolver image stays browserless) must render each token's live view and hand the resolver the result. `abx deploy-effects --resolver-url <resolver>` scaffolds it for Fly (a self-contained `Dockerfile.effects` + `fly.toml` + vendored source), wired to the resolver and your storage home. **Decide where renders live up front** (`ABX_STORAGE_BACKEND`): **
|
|
99
|
+
A code project's marketplace still is rendered off-chain, so a **runner** (Playwright + Chromium — the resolver image stays browserless) must render each token's live view and hand the resolver the result. **A managed provider with `render.attached` in its descriptor does all of this for you — skip this section.** Self-hosting it: `abx deploy-effects --resolver-url <resolver>` scaffolds it for Fly (a self-contained `Dockerfile.effects` + `fly.toml` + vendored source), wired to the resolver and your storage home. **Decide where renders live up front** (`ABX_STORAGE_BACKEND`): **cloud (S3/R2 + a public base) · ipfs · arweave** — equal options, pick on cost/ops. **NOT** the default `fs` for a hosted setup, and this is now refused rather than warned: the runner HOLDS its render bytes and hands the resolver a URL, so a backend that can't name one has no lane (`abx deploy-effects` errors out, and the runner refuses to start). The runner **registers** each render with the resolver's control plane (`POST /v1/effect-artifacts`, gated by the same token — your local `ABX_REMOTE_SELF_TOKEN`, the resolver's own `ABX_RESOLVER_ADMIN_TOKEN` — index-control only, never signing), so it does NOT need to share the resolver's disk. What travels: a **locator** for the still (the resolver 302-redirects; it never proxies or fetches it), and the **content** for `traits` (≤64KB — traits stitch into the token JSON, so a pointer there could never work). It does not matter which scheme the URL uses: an `https://` gateway or bucket URL is a peer of `ipfs://`/`ar://`, since a lost render is a re-render. It only has to be fetchable by someone other than you — a loopback gateway or a presigned expiring URL is refused. Two topologies:
|
|
78
100
|
|
|
79
|
-
- **Locator bridge (default for a hosted drop):** runner on its own host + a public storage home; it
|
|
80
|
-
- **Co-located:** runner beside the resolver sharing one backend/volume (no admin token needed — both read/write the same store).
|
|
101
|
+
- **Locator bridge (default for a hosted drop):** runner on its own host + a public storage home; it registers locators, the resolver redirects. `deploy-effects` sets this up.
|
|
102
|
+
- **Co-located:** runner beside the resolver sharing one backend/volume (no admin token needed — both read/write the same store). This is the escape hatch when there's no public storage home: `fs` is fine here.
|
|
81
103
|
|
|
82
|
-
One-shot without a service: **`abx render <addr> --remote <resolver>`** renders locally and
|
|
104
|
+
One-shot without a service: **`abx render <addr> --remote <resolver>`** renders locally and registers with the hosted resolver — same requirement, so pass `--backend cloud|ipfs|arweave` (the default `fs` is refused before Chromium launches, naming the fix). Re-run any time: registration is idempotent and repairs a resolver that lost its rows without re-rendering. Optional immediacy: set `ABX_EFFECTS_URL` on the resolver so it pings the runner on re-index; the periodic sweep is the eventual floor regardless.
|
|
83
105
|
|
|
84
|
-
## Local vs remote — two SEPARATE projection stores (read before deploying to a
|
|
106
|
+
## Local vs remote — two SEPARATE projection stores (read before deploying to a remote resolver)
|
|
85
107
|
|
|
86
|
-
A project is served only if it's in **the resolver's own** store. `abx deploy`/`abx add` (no `--remote`) index into **this machine's** store; a
|
|
108
|
+
A project is served only if it's in **the resolver's own** store. `abx deploy`/`abx add` (no `--remote`) index into **this machine's** store; a remote resolver — self-hosted or a managed provider — is a **different store** and doesn't learn about a contract just because you deployed it. A local deploy pointing its `tokenURI` at a remote resolver returns `{"error":"unknown project"}` for everyone — "works on my `serve`, broken for the world." **Bridge it:**
|
|
87
109
|
- **Local resolver** (`abx serve` here): a local `deploy`/`add` already indexed it — done.
|
|
88
|
-
- **Remote resolver** (baked URL is
|
|
110
|
+
- **Remote resolver** (baked URL is remote): after deploy run **`abx add <clone> --remote [name|url]`** to register + index it on the node (bare `--remote` defaults to `ABX_PUBLIC_BASE_URL` + `ABX_REMOTE_SELF_TOKEN`; a named remote uses its own `ABX_REMOTE_<NAME>_TOKEN`). Also **bridges** what the node can't derive: the durable `ipfs://`/`ar://` locator and off-chain traits. Post-deploy nudge (after a deferred mint) is **`abx index <clone> --remote`** (idempotent). Remove with **`abx forget <clone> --remote`**. Every remote command prints `REMOTE → <url>`.
|
|
111
|
+
|
|
112
|
+
### Indexing status — `backfilling` is not a failure
|
|
113
|
+
|
|
114
|
+
A service may accept a registration and catch up **asynchronously** (a cold replay on a rate-limited RPC takes minutes). `abx add --remote` handles it: prints `registered — backfilling…`, polls, then prints the normal summary once the project is **`live`**. So a slow add is *slow*, not broken — don't re-run `add` to "retry" it (a re-POST never starts a second replay, but you'll just wait again). `--no-wait` returns immediately when you don't want to block.
|
|
115
|
+
|
|
116
|
+
**Check anytime — same five words locally and remotely:** `abx status <address>` (this node) · `abx status <address> --remote <name> [--watch]` (a service) · `abx remote <name>` (roll-up: *"3 live, 1 backfilling, 1 failed (rpc_rate_limited)"*).
|
|
117
|
+
|
|
118
|
+
**On a node YOU run**, two knobs shape this (both optional): `ABX_REGISTER_DEADLINE_MS` (default 8000) is how long a register may hold the HTTP request before answering "accepted, still catching up" and finishing in the background — the registration is durable either way, so this only decides who waits; and `ABX_STALE_LAG_BLOCKS` (default 5000) is how far behind chain head the watcher may fall before it reports its projects `stale`. A backfill interrupted by a restart is re-queued automatically on the next `abx serve` — you don't have to re-add it.
|
|
119
|
+
|
|
120
|
+
| status | what it means | what to do |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `queued` / `backfilling` | accepted, catching up (registration is already durable) | **wait** — poll, don't re-add |
|
|
123
|
+
| `live` | caught up, tracking head | done |
|
|
124
|
+
| `stale` | was live, now lagging — **still serving** old state | usually the service's RPC; investigate if it sticks |
|
|
125
|
+
| `failed` | catch-up errored; retried with backoff | read the **class** (below); the registration survived |
|
|
126
|
+
|
|
127
|
+
`failed` names a cause: **`rpc_rate_limited`/`rpc_unavailable`** = the *service's* RPC, not your key and not your address — it retries, so wait or tell the operator; **`not_abx_contract`** = wrong address/chain (or a provider that validates clones refusing a non-clone); **`internal`** = the operator's logs have it. Report the class verbatim to the user rather than guessing a cause.
|
|
128
|
+
|
|
129
|
+
**A `live` project with 0 events is a red flag, not a success** — a real ABX clone always emits a spine, so 0 events means the service scanned the wrong chain or its RPC didn't serve the logs (check `abx remote <name>` covers your chain, and that the deploy block was forwarded). The CLI warns instead of printing ✓ here.
|
|
89
130
|
|
|
90
131
|
### Deploying to a hosted resolver — pre-warm flow (preferred)
|
|
91
132
|
|
|
@@ -117,12 +158,15 @@ Drop `--no-mint` (steps 5–6 collapse) for the express path. Add `--sign` to st
|
|
|
117
158
|
|
|
118
159
|
## Token API (the resolver)
|
|
119
160
|
|
|
161
|
+
⚠ **Read this list; never infer a route from another one — and prefer not needing it at all.** The contract commits its own metadata URL on-chain, so **`abx tokenuri <addr>`** and **`abx contracturi <addr>`** (ERC-7572 collection metadata: reads `contractURI()`, follows it, decodes the JSON) answer "where does this resolve, and what does it say" without you assembling a URL. Guessing has a specific trap: **`/t/<chainId>/<address>` is NOT collection metadata** — collection metadata is `/c/<chainId>/<address>`. A miss now names which of three things happened, in a machine `code`: **400 `invalid_request`** (your path shape — the body names the right template and may carry `didYouMean`) · **404 `unknown_route`** (this node serves no such path; body lists what it does serve) · **404 `not_registered`** (path fine, this node doesn't index that contract → `abx add <addr> --remote`) · **400 `unsupported_chain`** (body carries the `chains` it serves) · **410 `burned`** (that token existed and was destroyed — permanent; stop asking, and drop it from any listing. An *edition* id with zero live copies is NOT this: it still serves, and can mint again). None of those mean the service is down — don't report an outage off a URL you built.
|
|
162
|
+
|
|
120
163
|
Default `http://localhost:8787` (or `ABX_PUBLIC_BASE_URL`). Routes carry the **chainId** (the protocol path grammar) so one host can serve many chains:
|
|
121
|
-
- `GET /` — read-only index of served contracts · `GET /t/<chainId>/<address>/0` — ERC-721
|
|
164
|
+
- `GET /` — read-only index of served contracts · `GET /t/<chainId>/<address>/0` — token metadata (ERC-721 `tokenURI` on a 1/1/Series/code project, ERC-1155 `uri` on an edition — auto-detected, same route) · `…/0/image` — the image
|
|
122
165
|
- `GET /c/<chainId>/<address>` — ERC-7572 collection metadata · `GET /api/project/<address>` — full reconstructed state · `GET /d/<chainId>/<address>` — per-contract read-only dashboard (namespaced so one host serves many contracts). No public action buttons anywhere.
|
|
123
166
|
- For IPFS/Arweave the served `image` is the **gateway HTTPS URL** (`https://<gateway>/ipfs/<cid>` / `<gateway>/<txid>`), the form wallets/marketplaces render (raw `ipfs://` doesn't). The on-chain commitment is the **keccak256** (backend-neutral anchor, survives a gateway migration); the CID/txid is just the locator. The locator lives in the **deployer's** local index, so a **remote** resolver emits the gateway URL only once it's bridged (`abx add <clone> --remote`), else `image` falls back to the resolver's own `/…/image` route.
|
|
124
|
-
- `
|
|
125
|
-
- `POST /
|
|
167
|
+
- `GET /.well-known/abx-service` — the **service descriptor** (public): what the node supports — `interfaces`, `chains`, `auth` (incl. `signupUrl` for humans), `render.attached` (managed rendering). Read it before registering with any remote service.
|
|
168
|
+
- `POST /v1/projects` `{chainId, address, fromBlock?, factory?, description?, externalUrl?, attributes?, contentLocators?}` · `GET /v1/projects` (the projects visible to the token, each with `status`) · `DELETE /v1/projects/<chainId>/<address>` · `POST /v1/projects/<chainId>/<address>/reindex` · `GET /v1/projects/<chainId>/<address>/status` (`status` + `fromBlock`/`toBlock`/`headBlock` + `error.class`) — the **control plane** ([spec](../../../../specs/self-host-toolkit/remote-services.md)): register/forget which contracts the node indexes, bridge off-chain traits + locators; a remote `add`/`forget`. Bearer-gated (`Authorization: Bearer <token>`), 404 code `disabled` when no token is configured. Errors carry a machine `code` (`unauthorized` 401 · `forbidden` 403 · `unsupported_chain` · `not_registered`). Register answers **200** (caught up, with counts) or **202** (accepted, still catching up — poll status); the registration is durable either way. Indexing control only — never signing.
|
|
169
|
+
- `GET /api/project/<address>/verify` — bearer-gated too (it triggers outbound fetches). Run from the CLI: `abx verify <addr>` / `abx index <addr> --remote`.
|
|
126
170
|
|
|
127
171
|
`/api/project/<address>` key fields: `isCanonical` (factory-verified — note: `isCanonical`, **not** `canonical`), `owner`, `royalty`, `collectionFields[]`/`lockedCollectionFields[]`, `extensions[]`, `tokens[]` each `{minted, owner, tokenURI, fields[] ({field, representation, value}), lockedFields[]}`. The JSON also carries **`abx_provenance`** — per-field `source` + `status` (`on-chain` · `verified` · `mismatch` · `anchored` · `off-chain` · `n/a`), with an `anchor` for off-chain bytes that carry an on-chain hash (`off-chain` = plain operator value, benign; `anchored` = verifiable via `abx verify`). Confirm a deploy with **`abx verify <addr>`**, not curl.
|
|
128
172
|
|