@artblocks/abx-cli 0.1.0-alpha.21 → 0.1.0-alpha.23
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 +906 -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 +2 -2
- package/assets/renderer-scaffold/test/MyRenderer.t.sol +1 -1
- package/dist/commands/deploy.d.ts +30 -17
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +234 -97
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/project.d.ts +16 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +150 -10
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/reads.d.ts.map +1 -1
- package/dist/commands/reads.js +8 -4
- package/dist/commands/reads.js.map +1 -1
- package/dist/commands/scaffold.d.ts +3 -1
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +64 -20
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/service.js +1 -1
- package/dist/commands/service.js.map +1 -1
- 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 +1 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/flag-allowlists.d.ts.map +1 -1
- package/dist/flag-allowlists.js +5 -1
- package/dist/flag-allowlists.js.map +1 -1
- package/dist/flags.d.ts +4 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +23 -0
- package/dist/flags.js.map +1 -1
- package/dist/main.js +125 -37
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +29 -4
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +32 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +74 -9
- package/dist/output.js.map +1 -1
- package/dist/ownerops.d.ts +101 -13
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +357 -89
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +1 -1
- package/dist/schema.d.ts +18 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +37 -2
- package/dist/schema.js.map +1 -1
- package/dist/served.js +1 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +9 -3
- package/dist/update-check.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +62 -32
- package/skill/reference/code-projects.md +78 -24
- package/skill/reference/creator-token.md +16 -3
- package/skill/reference/decisions.md +59 -13
- package/skill/reference/hosting.md +1 -1
- package/skill/reference/operating.md +33 -13
- package/skill/reference/setup.md +1 -1
- package/skill/reference/troubleshooting.md +16 -6
|
@@ -25,7 +25,7 @@ When a creator arrives with an *idea* and you write the program, it must read it
|
|
|
25
25
|
var seed = td.seed || '0x1'; // mint-time seed (hex)
|
|
26
26
|
var palette = td.palette || '#3355ff'; // a PostParam: --schema palette:HexColor:TokenOwner
|
|
27
27
|
|
|
28
|
-
var z = 0; // seed a PRNG deterministically (same seed → same
|
|
28
|
+
var z = 0; // seed a PRNG deterministically (same seed → same output)
|
|
29
29
|
for (var i = 2; i < seed.length; i++) z = (z * 16 + (parseInt(seed[i], 16) || 0)) % 4294967296;
|
|
30
30
|
function rnd() { z = (1664525 * z + 1013904223) % 4294967296; return z / 4294967296; }
|
|
31
31
|
|
|
@@ -75,7 +75,7 @@ Everything else — seeds, traits, PostParams, the studio loop — is identical
|
|
|
75
75
|
`--shoot`'s per-seed traits table still works: encode musical invariants (key, tempo, section count)
|
|
76
76
|
as traits and it becomes your property check.
|
|
77
77
|
|
|
78
|
-
## Studio loop — iterate on the
|
|
78
|
+
## Studio loop — iterate on the work before you deploy anything
|
|
79
79
|
|
|
80
80
|
[← Phase 0 in SKILL.md](../SKILL.md#phase-0--make-the-work-first-skip-every-gate-below-until-its-good). When the creator is still designing, your job is to make the work **visible, interactive, and fast to change**. One command does it:
|
|
81
81
|
|
|
@@ -83,7 +83,7 @@ as traits and it becomes your property check.
|
|
|
83
83
|
abx preview --script art.js --schema "palette:HexColor:TokenOwner" # → http://localhost:8788
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
**Give the creator the URL and let them drive.** This is the one place in the toolkit where handing over a link is right — the
|
|
86
|
+
**Give the creator the URL and let them drive.** This is the one place in the toolkit where handing over a link is right — the work is theirs to judge, and a browser they control is the only honest way to judge it. The studio gives them a seed shuffle, real inputs for every PostParam they declared, a live traits readout, and `/grid` for N seeds at once. `/view` is the bare document.
|
|
87
87
|
|
|
88
88
|
**Why a server and not a screenshot sweep:** a still flattens every time-based piece. Plenty of generative work animates, and `abx.done()` exists *because* stills need a settle point — so a proof sheet of an animated piece is a set of arbitrary frozen frames presented as the work. The server also makes PostParams tangible (a color picker that re-renders beats any explanation of governed params), and it costs no Chromium download.
|
|
89
89
|
|
|
@@ -97,7 +97,7 @@ abx preview --script art.js --schema "palette:HexColor:TokenOwner" # → http
|
|
|
97
97
|
abx preview --script art.js --shoot ./frames --count 9 # PNGs + traits.json, then exits
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Same server, same document, headless. Needs Playwright + Chromium (`npm i -D playwright && npx playwright install chromium`); the interactive lane needs neither. **Read the PNGs** — don't report on
|
|
100
|
+
Same server, same document, headless. Needs Playwright + Chromium (`npm i -D playwright && npx playwright install chromium`); the interactive lane needs neither. **Read the PNGs** — don't report on work you haven't looked at. `--shoot` also flags the two silent killers for you: no frame reporting traits (⇒ no marketplace `attributes` on any lane), and identical traits across every seed (⇒ the sketch isn't reading `abx.tokenData.seed`, so the drop mints N identical tokens).
|
|
101
101
|
|
|
102
102
|
Use both: `--shoot` to check your own work between rounds, the live URL as what the creator actually looks at.
|
|
103
103
|
|
|
@@ -108,11 +108,11 @@ abx inspect art.js # the wiring check: are traits + PostParams actually r
|
|
|
108
108
|
abx deploy-code --script art.js --onchain-uri --dry-run # the lane + surfaces check
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
And a **testnet deploy remains the faithful end-to-end** (the real generator, the real assembled document, the real seed from the chain). Both come after the
|
|
111
|
+
And a **testnet deploy remains the faithful end-to-end** (the real generator, the real assembled document, the real seed from the chain). Both come after the work is settled.
|
|
112
112
|
|
|
113
113
|
## What a code project requires you to run — and keep running (say this up front)
|
|
114
114
|
|
|
115
|
-
A code project's
|
|
115
|
+
A code project's output depends on live on-chain state (the per-token `seed`, mutable PostParams), and *something* must read that state and inject it at view time. That something is a **resolver you run** — **unless** you take the fully-on-chain lanes (`--onchain-uri` for the tokenURI+animation, `--image-base` for a deterministic off-chain thumbnail, `--attributes-renderer` for on-chain traits), which can eliminate the metadata resolver entirely. When a resolver *is* in play, it's three pieces of ongoing infrastructure — lay them out plainly before they commit:
|
|
116
116
|
|
|
117
117
|
1. **A hosted resolver (required unless the deploy takes the `--onchain-uri` lane).** Serves `tokenURI` (`/t`) and the **live view** (`/a`, where it injects the seed + current PostParams). Its **public URL is baked on-chain at deploy**, so a domain/host must be ready first — `deploy-code` **refuses a localhost/missing URL**. Stand it up with `abx deploy-resolver` ([hosting.md](hosting.md)); keep it up or the token stops resolving. **The resolver is also the protocol's one chain-watcher**: `abx serve` runs an incremental getLogs poll (~12s, `ABX_WATCH_INTERVAL_MS`, 0=off) over every registered project, so **any** on-chain change — an external mint through a minter, a param change from a foreign tool or another wallet — auto-indexes and fans out ONE coarse notification per changed project to the effects layer (`ABX_EFFECTS_URL` + `ABX_EFFECTS_TOKEN`). Effects never watch the chain themselves. (No reorg lookback — post-PoS reorgs are rare; the repair is the deterministic `abx index <addr> --full`.)
|
|
118
118
|
2. **An effect runner + a storage home — decide the thumbnail mode AND where renders live, up front.** A code project's marketplace still is **rendered off-chain** (there's no image file to point at); on-chain-stitched `attributes` come from the same render. Two coupled decisions to make *before* deploying: **(i) the mode** — **(a) continuous runner** that auto-renders every new mint + param change (the default for a live / for-sale drop): `abx deploy-effects --resolver-url <resolver>` HOSTS it (fly/docker) beside a hosted resolver, or `abx effects` runs it **LOCALLY, in-process** (co-located with `abx serve` — great for testing/iteration; blocks, so background it), **(b) one-shot** (`abx render <addr> --remote <resolver>` after mint, for a fixed supply — re-run for later mints / param changes), or **(c) none** (the live view still animates, but the marketplace thumbnail stays a placeholder SVG); and **(ii) the storage home** (`ABX_STORAGE_BACKEND`) — **ipfs / arweave** (durable; the runner publishes a locator the resolver redirects to) or **s3**, **NOT** the default `fs` for a hosted resolver (a laptop-local store a hosted node can't read → the placeholder never clears). The runner uploads each render to that home and **publishes** it to the resolver (the locator bridge). Verify with `abx verify <addr>` after minting token 0 (below).
|
|
@@ -135,35 +135,58 @@ abx verify <addr> --remote http://localhost:<port> # confirm: renders up
|
|
|
135
135
|
|
|
136
136
|
Two kinds of inputs feed a piece: **settled state** (explicit PostParams, the seed — event-derived, indexed) and optional **live data** (an on-chain augment hook read fresh per view: block data, an oracle, anything a `view` returns). No hook — the overwhelming default — means the resolver makes **zero** live reads and serves pure indexed params: nothing to configure, maximum efficiency. With a hook set, the **live view** reads it per view (always current), while the **still is a snapshot of settled state only** — live data never re-addresses the render, so a volatile hook (a timestamp) animates the live view without re-rendering the thumbnail every block. Re-render triggers are settled-state changes only. `abx verify` prints the project's live-data posture; `ABX_DISABLE_AUGMENT=1` is a resolver-operator kill-switch (degrades to settled params).
|
|
137
137
|
|
|
138
|
-
**Wiring the hooks — `abx set-param-hooks <addr>` (SeriesCode only, owner-only).** A
|
|
138
|
+
**Wiring the hooks — `abx set-param-hooks <addr>` (SeriesCode/EditionCode only, owner-only).** A code project has three optional param-lifecycle hook addresses, each a contract the creator deploys: **`--augment`** (the live-data hook above — read-time derivation folded into tokenData), **`--configure`** (a write-time veto/validator — a governed `configure-param` reverts if this hook reverts), and **`--transfer`** (an ownership-change call that is **also a veto**: see below). The contract has **no per-hook setter** — it writes all three at once — so the command reads the current trio and re-sends it with your change applied: **omit a role to keep it**, pass an address to set it, `none` to clear it (`--clear` clears all three). Run it bare to print the current hooks. Any signing lane; guards `--dry-run`. A 1/1 or plain Series has no configurable params, so it has no hooks (the command refuses it).
|
|
139
|
+
|
|
140
|
+
**The transfer hook is a VETO — tell the creator before they arm one, and tell a buyer it exists.** The token calls it plainly, so **its revert fails the transfer**, and because a mint is a transfer from `0x0` a reverting hook also **stops minting** for that project, including through the shared minter (the same is true of a burn — `to == 0x0` — though no ABX token exposes one). This is not a bug to design around: it's how a piece can react to ownership at all, and the hook is always the creator's own contract. (It differs from the 721C/1155C **transfer validator**, which never sees mint/burn precisely so a third party's policy contract cannot brick issuance.) An earlier version of the protocol swallowed the hook's revert and promised the lifecycle could never block a transfer; that promise was withdrawn rather than restated, because the receiver acceptance check runs *after* the hook, so on `safeTransferFrom` — what a marketplace fill wraps — a hook cheap at gas-estimation time and expensive at execution starved it regardless of any cap.
|
|
141
|
+
|
|
142
|
+
**Writing a hook? The interface, and the one trap on an edition.** `IAbxTransferHook.onTokenTransfer(uint256 tokenId, address from, address to, address operator, uint256 amount)` — `operator` is whoever initiated the move (the holder, an approved operator, or a minter; on ERC-1155 an approved marketplace moves a holder's copies, so it is NOT redundant with `from`), and `amount` is copies moved (always 1 on ERC-721). The token refuses to call the hook at all on a **zero-amount** entry or a **self-transfer** — that closes a spoof an independent audit reproduced, where a stranger holding no copy could fire the lifecycle for any id via `safeTransferFrom(from, to, id, 0, "")`. **Think hard before a hook stores per-id state on a multi-copy edition:** params are per id and therefore SHARED, so a hook writing "the current owner" is really writing "whoever moved most recently" and it changes the work for every co-holder. Aggregate or monotonic state (transfer counts, "has ever been held by") is coherent there; a single-owner notion is not, unless the edition size is 1.
|
|
143
|
+
|
|
144
|
+
**`abx lock-param-hooks <addr>` freezes all three addresses forever** (owner-only, one-way, `--dry-run`-guarded; `set-param-hooks` reverts `ParamHooksLocked` after it). It prints the exact trio it will freeze before sending. Two uses, both about what a buyer can verify rather than trust:
|
|
145
|
+
- **No hooks set → freezing PROVES the project can never arm a transfer veto.** The strongest thing a code project can say about its own transferability. Offer it before a sale, not after.
|
|
146
|
+
- **Hooks set → freezing pins WHICH contracts can ever run.** A hook already wired keeps its veto — freezing the set is not disarming what's in it. Say that to the creator so they don't hear "safe now".
|
|
147
|
+
- **It freezes ADDRESSES, not behavior.** A hook is a contract, and a contract can be a proxy: a locked proxy hook can be upgraded later — to revert on every transfer, say — while `paramHooksLocked()` still reads `true`. The protocol defines locks as pointer locks and discloses it rather than attempting on-chain proxy detection. So when a creator wants the permanence claim, the hook has to be an **immutable** deployment, and the honest line to a buyer is *"these exact three addresses can never change"* — plus, if the trio is non-empty, check what's behind them.
|
|
148
|
+
|
|
149
|
+
`abx state <addr>` prints the three hooks and whether they're frozen (`ParamHooksFrozen` is the on-chain proof); `abx verify` flags an armed-but-unfrozen transfer hook. Background: https://abx.docs.artblocks.io/protocol/owner-powers/
|
|
150
|
+
|
|
151
|
+
**There is no release valve, and don't imply one: `setParamHooks` is owner-only forever.** No permissionless path exists in any state, including a renounced (`owner() == 0x0`) project, and a freeze holds against everyone. Do NOT carry the 721C/1155C validator's dead-man release over to hooks — a **validator** is usually a third party's purely-restrictive policy contract, so disarming it can only ever permit more, while a **hook** is the creator's own contract and is often the work itself. So the honest line for a creator planning to renounce: *"renounce with a live transfer hook that reverts and every collector's token, plus all remaining issuance, is frozen permanently — nobody can undo it, including us."* Not a reason to avoid hooks; a reason to `lock-param-hooks` deliberately and to test a hook before arming it. `paramHooks()` / `paramHooksLocked()` (both in `abx state`) are what a buyer reads.
|
|
139
152
|
|
|
140
153
|
## `--onchain-uri` — the chain-complete lane (internals)
|
|
141
154
|
|
|
142
155
|
`deploy-code --onchain-uri` makes `tokenURI` resolve **on-chain** via the canonical metadata renderer, with `animation_url` **computed on-chain** by the canonical **`AbxGenerator`** (a `renderer`-representation collection field). No `--public-base-url`, no resolver base baked.
|
|
143
156
|
|
|
144
|
-
**When to choose it (owner guidance from real-world experience): NOT the default for a generative drop meant to sell — lean off-chain resolver there.** A resolver keeps you maneuverable (metadata/serving can evolve without on-chain re-points) and lets marketplaces fetch a **small** `tokenURI`; a fully-on-chain code `tokenURI` carries the whole ~200KB+ document per call, and large-`tokenURI` marketplace/indexer compatibility is a real-world risk that grows with the
|
|
157
|
+
**When to choose it (owner guidance from real-world experience): NOT the default for a generative drop meant to sell — lean off-chain resolver there.** A resolver keeps you maneuverable (metadata/serving can evolve without on-chain re-points) and lets marketplaces fetch a **small** `tokenURI`; a fully-on-chain code `tokenURI` carries the whole ~200KB+ document per call, and large-`tokenURI` marketplace/indexer compatibility is a real-world risk that grows with the size of the work. Reach for `--onchain-uri` **deliberately** when maximal durability / "resolves from any RPC forever" / zero always-on infra outweighs those — a legitimate, proven lane, just not the marketplace default.
|
|
145
158
|
|
|
146
|
-
**What "fully on-chain" (chain-complete) means, and the one silent trap:** the `tokenURI` **and** its `animation_url` document come back entirely from on-chain bytes — no server, gateway, or CDN in the graph. It does **NOT** include the marketplace thumbnail (`image`) — that is *always* rendered off-chain. ⚠ **The silent breaker:** a `--dep` that resolves to a CDN instead of on-chain bytes **deploys fine and renders fine**, but you are no longer fully on-chain — a URL is back in the graph, with no error. The deploy's dependency report ("ON-CHAIN bytes available" vs "served from CDN") and `abx verify` (`chain-complete: yes/no`) both call this out **before and after** you spend — read them.
|
|
159
|
+
**What "fully on-chain" (chain-complete) means, and the one silent trap:** the `tokenURI` **and** its `animation_url` document come back entirely from on-chain bytes — no server, gateway, or CDN in the graph. It says nothing about **immutability** (a registry dep's bytes can still change — see freezing, below) and it does **NOT** include the marketplace thumbnail (`image`) — that is *always* rendered off-chain. ⚠ **The silent breaker:** a `--dep` that resolves to a CDN instead of on-chain bytes **deploys fine and renders fine**, but you are no longer fully on-chain — a URL is back in the graph, with no error. The deploy's dependency report ("ON-CHAIN bytes available" vs "served from CDN") and `abx verify` (`chain-complete: yes/no`) both call this out **before and after** you spend — read them.
|
|
147
160
|
|
|
148
161
|
- **Template mode (`--script`) can be CHAIN-COMPLETE** — the generator assembles the full HTML document (`data:text/html;base64`) from the on-chain chunks — **iff every `--dep` resolves to proven on-chain bytes** on the registry (`p5@1.0.0` qualifies on Sepolia). A CDN-served dep still *serves fine* but breaks chain-completeness. Zero-dep vanilla JS is trivially chain-complete.
|
|
149
162
|
- **Directory mode (`--code-dir`) is no-server, not chain-complete**: the generator emits `{gateway}/{code root}/index.html?abx=<tokenData>` — liveness rides the gateway (default `ipfs.io`/`arweave.net`; repoint with `abx configure-param <addr> - display.gateway <prefix>`), permanence rides the pin/endowment, params ride the URL (**8KB budget** — `abx verify` reports `urlOverBudget`; big params ⇒ prefer template mode).
|
|
150
163
|
- **Key enumeration lives in the contract — nothing to maintain**: the params store lists its own keys on-chain, so the generator's `tokenData` always carries the full param surface, byte-aligned with the resolver's. Add a param key any way you like and it appears; there is no key list to sync and nothing that can drift. (Projects deployed before this shipped point at the older generator, which read a `params.keys` CSV — they keep working, untouched, and nothing writes one any more.)
|
|
151
|
-
- **Verify it**: `abx verify <addr>` eth_calls the generator's `onChainStatus` (branch — template/directory · chain-complete · unresolved refs · URL budget) AND decodes `tokenURI` straight from the contract, reporting the `animation_url` form. `abx tokenuri <addr>` is the quick raw read
|
|
164
|
+
- **Verify it**: `abx verify <addr>` eth_calls the generator's `onChainStatus` (branch — template/directory · chain-complete · unresolved refs · URL budget) AND decodes `tokenURI` straight from the contract, reporting the `animation_url` form. It also reports the **script + dependency lock state** (see freezing, below). `abx tokenuri <addr>` is the quick raw read of the document. **The cheapest proof a param really landed is `abx tokens <addr>`** — it reads the param store itself (`tokenParamKeys` → `tokenParam`), no resolver and no indexer anywhere. Don't go looking for params inside `tokenURI`: they aren't projected into it (they'd be a duplicate of chain state nobody parsed back). The other place they show up is inside the `animation_url` document, as the `tokenData` the program receives.
|
|
165
|
+
- **Freezing a code project — the program, not just the metadata.** `lock-field`/`lock-uri` freeze *metadata*; they do **not** touch the on-chain program. The work is the script chunks, and the owner can keep rewriting them (`setScriptChunk`/`removeLastScriptChunk`) until you run **`abx lock-script <addr>`**. The full set is **`abx lock-script`** (the program) + **`abx lock-dependencies`** (the library set) + `lock-field`/`lock-uri` (the metadata) + `set-schema … lock=now` for any param whose value should also freeze. Lock last — deploy unlocked, confirm it resolves, *then* freeze; `abx verify` shows what's still mutable. (SeriesCode/EditionCode expose `scriptLocked()`/`dependenciesLocked()` — the locks are plain owner-only function calls.)
|
|
166
|
+
- **With ALL of those engaged, the output can still change — say so instead of promising a freeze.** Four routes, and none of them is a bug: **(1) an *ungoverned* param has no lock**, so the owner keeps writing it and the generator keeps injecting it as `tokenData`, which is exactly how a piece stays responsive to its holder — and even a *welded* param (`:lock=now`, below) can still be overridden at read time by an `--augment` hook until `lock-param-hooks` freezes the hook set; **(2) a `name@version` (Registry) `--dep` is re-fetched from the registry contract on every read** — `lock-dependencies` freezes the ref and the registry pointer, not the bytes the registry returns, so the library can change under a locked project. Only `--dep 0x…` (an immutable SSTORE2 data contract) is frozen by being resolved; **(3) a welded param's *contract-scope default* can still be cleared.** The weld closes token-scope writes and further `set-schema` on that key, but `clearContractParam` sits outside the schema guard on purpose (it is the only exit from a value poisoned before the schema existed), so an owner who left a collection-wide default in place can delete it after the lock and every token that merely inherited it changes. Writing the value **per token** under the governed path is what actually welds it; **(4) every lock freezes a POINTER, not the code behind it** — a locked hook, renderer or reader is a contract address, and if that contract is an upgradeable proxy its behavior can be replaced while `paramHooksLocked()` still reads `true`. The protocol defines locks as pointer locks and discloses the limit rather than attempting on-chain proxy detection, so a permanence claim requires **immutable** hook/renderer deployments. You cannot verify that from the address alone and there is no tool that does it for you — so the honest move is not to investigate, it is to scope the claim: say "these exact addresses can never change", which is true and verifiable, rather than "the output can never change", which you do not know. If the creator deployed the hook themselves and it has no upgrade path, they can say the stronger thing. `abx verify` says `chain-complete` in all of these cases, because that flag is about *where* bytes come from. **The line to give a creator: "your metadata is locked, and these exact addresses can never change" — and, if they want the stronger claim, the route is on-chain `0x…` deps, immutable hook/renderer contracts, per-token governed values, and locked fields.** A token that deliberately live-adapts is a good thing to build; just don't describe one as immutable. Background: https://abx.docs.artblocks.io/protocol/owner-powers/
|
|
152
167
|
|
|
153
168
|
### PostParam schema — the Type + Auth catalog
|
|
154
169
|
|
|
155
|
-
A schema is `key:Type:Auth` (repeat comma-separated: `--schema palette:HexColor:TokenOwner,speed:Uint256Range[0..100]:
|
|
170
|
+
A schema is `key:Type:Auth` (repeat comma-separated: `--schema palette:HexColor:TokenOwner,speed:Uint256Range[0..100]:Creator`). The Type token carries an optional **bracket suffix**: a **`Select` MUST list its options** (`Select[Spring|Summer|Autumn|Winter]`, pipe-delimited — a Select with no options is rejected, because the on-chain schema requires them), and a **Range MAY carry bounds** (`Uint256Range[0..100]`, `Int256Range[-50..50]`, `DecimalRange[0..1]`, `Timestamp[2026-01-01..2026-12-31]` — omit for unbounded). The delimiters never collide (params `,` · fields `:` · options `|` · bounds `..`, all inside `[…]`). Declared at **deploy** (`--schema`); set later per token with `abx configure-param <addr> <id> <key> <value>` (value canonically encoded per Type — you pass the human form; a Select takes a label or its index).
|
|
156
171
|
|
|
157
172
|
**See what a live project already has: `abx state <addr>`** lists every governed PostParam — type, auth, bounds/options, an upcoming lock date, and a `retired` marker. Read it BEFORE `set-schema` on an existing key: the write is a full-row upsert, so you need the current shape to avoid clobbering a field you didn't mean to touch.
|
|
158
173
|
|
|
159
|
-
**Both halves are plain chain reads.** Declared schemas enumerate on-chain (`paramSchemaKeys()` — every governed key, including one nobody has written yet), and every *set* value
|
|
174
|
+
**Both halves are plain chain reads.** Declared schemas enumerate on-chain (`paramSchemaKeys()` — every governed key, including one nobody has written yet), and every *set* value enumerates too (`contractParamKeys()` / `tokenParamKeys(id)`, then `contractParam`/`tokenParam` for the value; `paramSchema(key)` for its type). So a project's configure UI can be built from the chain alone, and a collector's write is publicly readable the moment it lands — no resolver, no indexer, no key list to keep in sync. `abx state <addr>` reads the schema half; `abx tokens <addr>` reads the value half. **Params are chain state, not a metadata projection** — they are deliberately not copied into `tokenURI` (a second serialization of something already enumerable, which nothing parsed back, on a document marketplaces need to stay cheap). Where they *do* appear is `tokenData`, injected into the program at render time; anything meant for a marketplace's trait display belongs in `attributes` (`abx.traits()` or an `--attributes-renderer`).
|
|
175
|
+
|
|
176
|
+
**⚠ On an edition (`--copies`), a holder-writable param is SHARED — resolve this with the creator before the schema is committed, because it is on-chain from deploy.** A param belongs to the **id**, and an id's copies are all the same id, so:
|
|
177
|
+
|
|
178
|
+
- **One value, many holders, last writer wins.** `TokenOwner` on an edition means *any* holder of that id. A `name:String:TokenOwner` schema does not let each collector name their copy — it lets whoever wrote most recently name the work for everyone. If per-collector configuration is the point, it needs **one id per copy** (a 721 `deploy-code`, or single-copy ids). Aggregate or monotonic state — a transfer counter, a communal mood, "has ever been held by" — is exactly what shared params are *good* at, and reads as intended there.
|
|
179
|
+
- **A holder-writable `String`/`Bytes` key has no on-chain size budget.** The contract accepts any non-empty valid value (one SSTORE2 blob per key), so a single holder can fill every declared data key and push that id's `tokenURI`/generator document past what common RPCs will serve — for every co-holder at once, and **permanently** if a `:lock=` deadline then bites. This is a deliberate protocol choice: on-chain byte accounting would tax every project to police a configuration almost nobody should use, so the mitigation is the schema you choose. Keep holder-writable keys to **scalar** types (`HexColor`, `Select`, `Bool`, a bounded Range) unless a large holder-authored payload is genuinely the work; if it is, reach for `Address(0x…)` auth and a controller contract that applies your own size policy before forwarding the write.
|
|
180
|
+
- The CLI says the same thing at the moment of decision — `deploy-code --copies` with a holder-writable `--schema` prints an advisory naming the keys. **Relay it to the creator; don't step past it.**
|
|
181
|
+
|
|
182
|
+
Both are fine when *intended*: a 1-copy edition behaves like a 721, and the shared-value shape is the whole appeal of a communal piece. What must not happen is a creator discovering it after the mint.
|
|
160
183
|
|
|
161
184
|
**The param surface is NOT frozen at deploy.** `abx set-schema <addr> --schema key:Type:Auth` attaches or replaces one key's schema on a live contract, so a piece that turns out to need another dial does **not** need a redeploy (which would cost the address, the mints, and the collectors). Two things to hold onto when you use it: it is a **full-row upsert**, so replacing a schema rewrites every field — restate anything you want to keep, including an existing `lock=`; and the chain does **not** re-validate values already stored under the key, so narrowing a bound, dropping a `Select` option, or changing the Type strands them (the CLI refuses that unless you pass `--force`). Tell the creator plainly before forcing one.
|
|
162
185
|
|
|
163
186
|
- **Types:** `Bool` (`true`/`false`) · `Select[A|B|C]` (**options required in brackets**; set by a label from the list, or its index) · `Uint256Range[min..max]` (non-negative integer; bounds optional) · `Int256Range[min..max]` (signed integer) · `DecimalRange[min..max]` (decimal, ≤10 places) · `HexColor` (`#rrggbb`) · `Timestamp[min..max]` (Unix seconds **or** an ISO date like `2026-07-16`) · `String` · `Bytes` (`--file <path>` for the payload).
|
|
164
|
-
- **Auth — who may set the param:** `
|
|
165
|
-
- **`:lock=<when>` — an optional 4th field** that freezes the
|
|
166
|
-
- Examples: a collector-tunable color → `palette:HexColor:TokenOwner`; a collector-chosen mood → `mood:Select[Calm|Wild|Chaotic]:TokenOwner`;
|
|
187
|
+
- **Auth — who may set the param:** `Creator` (the contract owner) · `TokenOwner` (the token's current holder; **delegate.xyz honored**) · `Address(0x…)` (a specific named writer — **name it inline**, e.g. `board:Bytes:Address(0xabc…)`) · and the `Or` combinations `CreatorOrTokenOwner` · `CreatorOrAddress` · `TokenOwnerOrAddress` · `CreatorOrTokenOwnerOrAddress`. The chain enforces it — a wrong signer reverts. There is **no "anyone" leg**, but the `Address` leg is a plain `msg.sender` check with no EOA restriction, so **a contract may hold it** — that is how open / multi-party participation is built (a controller contract applies its own rules and forwards the write). If a creator wants a communal canvas or open entry, that is the shape to describe, not a missing feature.
|
|
188
|
+
- **`:lock=<when>` — an optional 4th field** that freezes the param after a time (`palette:HexColor:TokenOwner:lock=2026-12-31`; ISO date, unix seconds, or `now`). A lock already in the past is permanent, which is the supported way to **retire** a param: `abx retire-param <addr> <key>`. Past the deadline the chain welds **both halves**: every `configure-param` reverts `ParamLockExpired`, and so does any further `set-schema` on that key — so the Type, Auth, bounds and `Select` options are frozen too (otherwise a locked Select's options could be swapped and a collector's chosen "Ember" would re-render as "Frost"). The deadline is **monotonic** — a later `set-schema` may only move it *earlier*, never later and never back to open (`ParamLockNotExtendable`), so a weld can't be undone. It does **not** remove the key (a governed key stays governed) and does **not** erase a value already stored — that value keeps serving. Never describe retiring as deleting. **The one thing the weld does not cover: a contract-scope DEFAULT on that key can still be cleared.** `clearContractParam` is deliberately outside the schema guard (it is the only recovery path from a value poisoned before the schema existed), so an owner can leave a collection-wide default in place, weld the key, sell, and later delete the default — changing every token that never wrote its own value. Clearing can only *remove* a fallback: it cannot forge a value, bypass an auth rule, or touch a token-scope value already written. If a collection-wide value must be frozen, write it **per token** through the governed path rather than relying on the inherited default.
|
|
189
|
+
- Examples: a collector-tunable color → `palette:HexColor:TokenOwner`; a collector-chosen mood → `mood:Select[Calm|Wild|Chaotic]:TokenOwner`; a creator-only bounded dial → `speed:Uint256Range[1..10]:Creator`; an on/off toggle → `invert:Bool:TokenOwner`.
|
|
167
190
|
|
|
168
191
|
## `--image-base` — deterministic S3/CDN thumbnail URLs (no metadata resolver)
|
|
169
192
|
|
|
@@ -177,17 +200,17 @@ A schema is `key:Type:Auth` (repeat comma-separated: `--schema palette:HexColor:
|
|
|
177
200
|
|
|
178
201
|
## In-chain Solidity SVG — the zero-dependency lane
|
|
179
202
|
|
|
180
|
-
The purest form: the
|
|
203
|
+
The purest form: the work itself is a **Solidity `IAbxFieldRenderer`** that returns an SVG from the token's `seed` + params — no JS program, no browser, no bucket, no resolver, no effect runner. Image AND traits are computed on-chain and the `tokenURI` is assembled on-chain, so the token depends on **nothing outside the EVM**.
|
|
181
204
|
|
|
182
205
|
```bash
|
|
183
206
|
abx deploy-code --image-renderer 0x<svgRenderer> [--attributes-renderer 0x<traitsRenderer>] \
|
|
184
207
|
--onchain-uri --schema palette:HexColor:TokenOwner --name "…" --symbol … [--dep none]
|
|
185
208
|
```
|
|
186
209
|
|
|
187
|
-
- **No `--script`/`--code-dir` is ALLOWED, not required.** `deploy-code` accepts a renderer-only project (neither program mode) as long as `--image-renderer` (and/or `--attributes-renderer`) is set. Renderer-only has no `animation_url` — the SVG `image` *is* the
|
|
210
|
+
- **No `--script`/`--code-dir` is ALLOWED, not required.** `deploy-code` accepts a renderer-only project (neither program mode) as long as `--image-renderer` (and/or `--attributes-renderer`) is set. Renderer-only has no `animation_url` — the SVG `image` *is* the work; the metadata renderer omits an unset animation field, and the CLI does **not** wire the generator (wiring an animation leg at a zero generator would revert every `tokenURI`).
|
|
188
211
|
- **But a script AND renderers together is the both-worlds shape — verified, and the most-missed option.** `--script f.js --image-renderer 0x… --attributes-renderer 0x… --onchain-uri` gives `animation_url` assembled on-chain from the script chunks *and* `image`/`attributes` computed by Solidity: every marketplace surface has an on-chain home, and there is **nothing to render, host, or refresh** (no runner, no bucket, no resolver). The fields are independent in the metadata renderer, so they compose. Prefer this over renderer-only whenever the piece is actually a program.
|
|
189
212
|
- **On-chain `tokenURI` is the CLEAR default here** — unlike the JS/p5 lanes. A Solidity SVG reads *small* (a few hundred bytes–few KB), so the large-`tokenURI` marketplace-read caveat does not apply. Recommend it enthusiastically; there is no maneuverability/infra tradeoff to weigh because there is no infra.
|
|
190
|
-
- **The renderer is a contract the creator deploys separately** (the CLI doesn't compile Solidity). **`abx scaffold-renderer <dir>`** writes a ready-to-build Foundry project — a worked `MyRenderer.sol` (image → `image/svg+xml` from `seed` + a `palette` PostParam), a coherent `MyTraits.sol` reading the SAME seed math, the `IAbxFieldRenderer`/`IAbxParams` interfaces (with the invariants documented), a `forge test` proving `render()` never reverts (incl. the collection surface + a fuzz), a `Deploy.s.sol`, and a README. The creator forks the
|
|
213
|
+
- **The renderer is a contract the creator deploys separately** (the CLI doesn't compile Solidity). **`abx scaffold-renderer <dir>`** writes a ready-to-build Foundry project — a worked `MyRenderer.sol` (image → `image/svg+xml` from `seed` + a `palette` PostParam), a coherent `MyTraits.sol` reading the SAME seed math, the `IAbxFieldRenderer`/`IAbxParams` interfaces (with the invariants documented), a `forge test` proving `render()` never reverts (incl. the collection surface + a fuzz), a `Deploy.s.sol`, and a README. The creator forks the sample, then `forge soldeer install && forge test`, deploys with forge, and passes the address to `deploy-code --image-renderer 0x…`, which **verifies the address has code** (real deploy refuses a codeless address; dry-run probes best-effort) — same guard as `--attributes-renderer`. A renderer reads the token's seed/params directly (`IAbxParams(token).tokenParam(tokenId, "seed"|"palette")`), computes bytes, and returns `(contentType, data)`. Interface + invariants also at https://abx.docs.artblocks.io/protocol/renderers/.
|
|
191
214
|
- **PostParams still apply — and you MUST declare them; the CLI can't.** Unlike the JS lane (where `abx inspect` statically detects the params a script reads), a Solidity renderer is opaque to the CLI — it cannot know your renderer reads a `palette`. **Read the renderer, and declare every PostParam it reads with `--schema key:Type:Auth`** (the example reads `palette` → `--schema palette:HexColor:TokenOwner`). Skip it and the param is **fixed at the renderer's default forever** — collectors can't set it (the exact miss from a real session: a palette-tinted renderer shipped with `schemas []`, stuck on the default). With the schema declared, a collector's `configure-param` re-addresses the on-chain image automatically (the renderer reads the live param — no re-render, there's no off-chain still). `deploy-code --dry-run` nudges when renderers are set with no `--schema`.
|
|
192
215
|
- **`abx render`/`abx effects` are irrelevant** (there's no off-chain still to produce), and `isCodeProject` is false for a renderer-only project — both are expected, not errors.
|
|
193
216
|
- **Verify:** a raw `abx tokenuri <addr>` (or `cast call tokenURI(0)`) decodes to `name` + `image` = `data:image/svg+xml;base64,…` + on-chain `attributes`, with **zero `http(s)` URLs** — the from-chain proof it's fully in-chain.
|
|
@@ -232,14 +255,45 @@ abx deploy-code (--script <file> | --code-dir <dir>) --name "…" --symbol … \
|
|
|
232
255
|
- **Dependencies (template mode)** — declare libraries with `--dep <ref>` (repeatable or comma-separated; **ordered — the first ref is index 0 = the runtime**, e.g. `--dep p5@1.0.0`). A ref auto-detects: `name@version` (AB registry naming) resolves through the collection's registry pointer — `deploy-code` defaults it to the chain's **AB Dependency Registry** (`--dep-registry 0x…` overrides; a chain with none known warns + skips, never blocks) — while `0x…` declares an on-chain data contract, read directly. Registry deps are existence-checked before deploy (best-effort; a miss is a warning): **a CDN-served record is the normal production path, not a degradation** — on-chain bytes are the durability floor. Post-deploy: `abx set-dependency <addr> <index> <ref>` · `abx remove-last-dependency <addr>` · `abx set-dependency-registry <addr> <0x…|none>` · `abx lock-dependencies <addr>` (freezes list + pointer).
|
|
233
256
|
- **`--code-dir <dir>`** — directory mode: a build folder (must contain `index.html` **and its own `abx.js` copy** — the build must read `abx.tokenData` + call `abx.traits({…})`, see [Authoring the program](#authoring-the-program--the-abxjs-runtime-contract-get-this-right-first)) uploaded via `putDirectory` (ipfs/arweave); its root becomes the on-chain `code` field. The live view 302s to the gateway with `?abx=<canonical tokenData>` — **so the gateway must serve HTML** (dedicated Pinata gateway or Arweave, never the shared public one).
|
|
234
257
|
- **`--description "…"` / `--external-url <url>`** — collection identity, written as **on-chain collection fields in the deploy tx** (a code project has no operator-metadata table of its own, so these ride on-chain; the metadata renderer stitches them into `tokenURI` under `--onchain-uri`, a resolver reads the same fields). Set them or the metadata is bare. (Any other unsupported flag warns "unrecognized flag, ignored" — a typo can't quietly drop a value.)
|
|
235
|
-
- **Seeds** — drawn at mint from the canonical `AbxSeedSource` (settled once assigned; `--no-seed` opts out; curated pre-set seeds win).
|
|
258
|
+
- **Seeds** — drawn at mint from the canonical `AbxSeedSource` (settled once assigned; `--no-seed` opts out; curated pre-set seeds win). **Pseudorandom, NOT lottery-grade** — see [Seeds](#seeds--pseudorandom-not-lottery-grade) below before a creator prices scarcity off it.
|
|
236
259
|
- **`--schema key:Type:Auth`** — governed PostParams (e.g. `palette:HexColor:TokenOwner`). Reconfigure any lane with `abx configure-param <addr> <tokenId> <key> <value>` — it reads the on-chain schema and canonically encodes the input (`#rrggbb`, decimals, Select by label). **Payload types are explicit: `String` takes literal text, `Bytes` takes `0x`-prefixed hex or `--file <path>` — a bare string on a `Bytes` key is REFUSED**, because storing those characters as bytes can't be told apart from meaning them literally; delegate.xyz honored on the TokenOwner leg. A param change **re-addresses** renders (self-invalidating stills). **The re-render is automatic wherever the resolver's chain watcher is on (the default):** the watcher sees the change on its next poll (~12s) — no matter WHO sent it or WITH WHAT tool — re-indexes, and notifies the runner. `abx configure-param … --remote <resolver>` remains an **immediate nudge** (skips the poll wait) and the fallback for a watcher-disabled resolver. Expect thumbnail refresh ≈ poll cadence + render time (~15–30s), not instant.
|
|
237
260
|
- **How it resolves (live, uncached; not on-chain):** the resolver rebuilds `tokenData` per view. Directory mode 302-redirects to the gateway with params in the **query string** (so **very large PostParams favor template mode**, which inlines them with no URL ceiling); template mode assembles the HTML inline from the chunks.
|
|
238
|
-
- **Lanes**: all three. `--unsigned` needs `--for <signer>` and prints the whole pre-computed sequence (deterministic deploy → the clone address is known up front). On a chain with no canonical factory, all deploy commands **stop with guidance**; deploying your OWN trust anchor is an explicit `--bootstrap-factory` opt-in (private chains/sandboxes only).
|
|
261
|
+
- **Lanes**: all three. `--unsigned` needs `--for <signer>` and prints the whole pre-computed sequence (deterministic deploy → the clone address is known up front). On a chain with no canonical factory, all deploy commands **stop with guidance**; deploying your OWN trust anchor is an explicit `--bootstrap-factory` opt-in (private chains/sandboxes only). For a code project that anchor is a short sequence — the write-path libraries (`AbxParamsLib`, `AbxCodeLib`, plus `AbxEditionLib` with `--copies`), then the factory linked against them — and note any `--copies` deploy, code project or not, bootstraps `AbxParamsLib` + `AbxEditionLib`, since all three ERC-1155 token types link that library — all CREATE2 at canonical salts, so it lands at the same addresses a `forge` bootstrap would and **re-running finishes a partial bootstrap** (an already-deployed library is reused, never duplicated) rather than failing.
|
|
262
|
+
|
|
263
|
+
### Seeds — pseudorandom, not lottery-grade
|
|
264
|
+
|
|
265
|
+
Each mint calls the project's seed source and stores the result as the token's `seed` param, which is what `abx.tokenData.seed` delivers to the program. The canonical `AbxSeedSource` hashes the project address, the token id, and block values (`prevrandao`, the previous block hash, the timestamp) — **not** the recipient, deliberately: a buyer names the recipient (`purchaseTo`), so hashing it turned a buyer-chosen field into a search space (grind candidates in a view loop, buy once at the winner).
|
|
266
|
+
|
|
267
|
+
**Say this plainly to any creator whose pitch leans on rarity.** The seed is pseudorandom, generated from on-chain params, and is **not strong enough to run lottery-like logic**. Both of these are true at once:
|
|
268
|
+
- **Deterministic after the fact** — anyone replays it from the block. That is the property it exists for: it's what makes the generative output verifiable.
|
|
269
|
+
- **Not secret before the fact** — every input is readable *inside* the minting transaction, so a contract minting there can compute the seed it would receive and revert unless it likes the result (a buyer declines outcomes for the price of gas), and a block builder can reorder or omit transactions. Dropping the recipient removed cheap *targeting*, not accept-or-decline — **on the 721 lane.**
|
|
270
|
+
|
|
271
|
+
**On an edition (`--copies`) the buyer CHOOSES, and that's the product.** Sales are keyed `(token, id)`, so an edition buyer names the `id` — which work they want — and an `EditionCode` id's `seed` is drawn at that id's **FIRST** mint (the seed belongs to the work, not to a copy). So: buying an already-minted id is **choosing among settled, publicly readable works**, exactly as on secondary; buying an unminted id **draws its seed with the buyer's chosen `id` in the preimage**, which is a real selection surface (grind ids in a view loop, buy the best) bounded only by which ids have a configured sale and by `maxInvocations`. Never describe the edition lane as "decline, not choose" — that's the 721 story. `id` can't be dropped from the preimage the way the recipient was: it *is* the work. A creator who doesn't want selection has two clean answers: **mint the ids themselves first, then list on secondary** (a first-class way to run an edition — the owner may always mint, paused or not, and every seed settles under their own transactions), or use a custom `IAbxSeedSource`.
|
|
272
|
+
|
|
273
|
+
So it fits work where the seed diversifies output and the **distribution** is the product. It does **not** fit a prize draw, a raffle, or any mint where one rare outcome is worth materially more than the mint price and ordering is contestable.
|
|
274
|
+
|
|
275
|
+
**If seed generation must be fully random, the creator supplies their own `IAbxSeedSource`** — commit-reveal (commit at mint, resolve from a later block), an off-chain VRF oracle, a curated queue. `seedSource` is a **per-project address**, so that is a swap, not a fork of ABX, and both ends of it are first-class CLI surface:
|
|
276
|
+
|
|
277
|
+
| | |
|
|
278
|
+
|---|---|
|
|
279
|
+
| at deploy | `abx deploy-code --seed-source 0xTheirSource …` (omit it, or `--seed-source canonical`, for the shared `AbxSeedSource`; `--no-seed` for none at all — passing both `--no-seed` and `--seed-source` is refused) |
|
|
280
|
+
| after deploy | `abx set-seed-source <addr> <0x…\|canonical\|none>` — owner-only, **future mints only** |
|
|
281
|
+
|
|
282
|
+
Both **probe the address before using it**: an `eth_call` to `seed(uint256,address)` that must return 32 bytes, else the command refuses. That check is not ceremony. A seed source is the one setting whose misconfiguration is completely silent — the write succeeds, `seedSource()` reads back what you set, `abx state` prints it, and then *every mint of the collection reverts* in the token's `bytes32` decode. `code.length > 0` does not catch it: a Safe, an uninitialised proxy, and a 7702-delegated EOA all have code and all answer an unknown selector with empty success. Pasting the creator's own wallet or Safe here is the common way in, so expect the refusal and read it.
|
|
283
|
+
|
|
284
|
+
Two things the CLI still does **not** do: it does not write or audit the source contract (its randomness properties are the creator's to state to buyers — ABX makes no claim about them), and it cannot verify a source that isn't answering yet. A commit-reveal source that reverts until it's armed will be refused — arm it first, then `abx set-seed-source`. A custom source is free to hash `to`, and inherits the grinding surface above if it does.
|
|
285
|
+
|
|
286
|
+
**Re-pointing mid-sale is legitimate but visible-only-if-you-look.** Assigned seeds are settled, so a change applies to future mints and a part-sold collection ends up spanning two sources. `SeedSourceSet` puts it on the event spine and `abx state <addr>` prints the current source (flagged CUSTOM when it isn't the canonical one), but an early buyer is not notified. If a drop is live, pause and say so.
|
|
287
|
+
|
|
288
|
+
**A collector-chosen seed is a pre-sale commitment — and it is not a "re-roll".** A settled seed is terminal by every route — nothing rewrites or clears it — *unless* the project declared a `seed` PostParam schema, typically with a `TokenOwner` auth leg so a collector can set the seed of their own token.
|
|
289
|
+
|
|
290
|
+
Be exact about what that gives a collector, because the intuitive word for it is wrong. The governed path is `configureTokenParam(tokenId, "seed", value)`: **the caller supplies the value.** Nothing is re-randomized and the seed source is never called again — with an unbounded `Uint256Range` the authorized party may set literally any 32-byte value, and set it again until they like the output. So describe it as "pick your seed" / "set your own seed", never as a re-roll: a re-roll implies a fresh random draw a collector could reasonably expect to be fair, and there is no draw.
|
|
291
|
+
|
|
292
|
+
The schema can only be declared **before the collection's first seed exists**; afterwards the contract reverts `SeedSettled`. So decide it with the creator before minting anything, and never promise it as something you'd add later. Treat it as advanced and prove it on testnet first: the type has to be a **literal**/scalar one (a `String`/`Bytes` schema routes writes to the data path, which a seed refuses outright). A buyer reads the answer on-chain with `paramSchema("seed")` — or `abx state <addr>`, whose **PostParams** block lists every declared schema: a `seed` row there means the value can be set (and says by whom), no row means final.
|
|
239
293
|
|
|
240
294
|
## `--copies` — a generative drop sold as an EDITION (EditionCode), and what v1 gives up
|
|
241
295
|
|
|
242
|
-
`abx deploy-code --copies <n|open>` swaps the 721 **SeriesCode** for its ERC-1155 twin **EditionCode**: N ids, each a distinct
|
|
296
|
+
`abx deploy-code --copies <n|open>` swaps the 721 **SeriesCode** for its ERC-1155 twin **EditionCode**: N ids, each a distinct work/seed, **× `--copies` copies of each**. `--mint-count`/`--mint-all` keep their meaning (which ids premint) and `--mint-amount <n>` sets how many copies of each. Per-id sales, per-id supply caps → [operating.md](operating.md#edition-owner-ops).
|
|
243
297
|
|
|
244
298
|
**Source is `--script` only; on-chain dependencies DO work. The CLI refuses the rest rather than silently dropping them:**
|
|
245
299
|
|
|
@@ -250,7 +304,7 @@ abx deploy-code (--script <file> | --code-dir <dir>) --name "…" --symbol … \
|
|
|
250
304
|
| `--code-dir <dir>` | ❌ refused — *"use `--script` (on-chain chunks) instead"* |
|
|
251
305
|
| `--image-renderer` / `--attributes-renderer` / `--image-base` | ❌ all three refused together |
|
|
252
306
|
|
|
253
|
-
So **"an edition of my p5 sketch, with p5 coming from the chain" is a real lane**: `abx deploy-code --script art.js --copies 100 --dep p5@1.0.0 --onchain-uri`, on **Sepolia** (`ABX_CHAIN=sepolia` — the AB dependency registry does not exist on Base Sepolia, where the pointer leg is skipped with a warning and the drop is NOT chain-complete). Confirm it with `abx verify <addr>`, which should say *"chain-complete — every dependency resolves to
|
|
307
|
+
So **"an edition of my p5 sketch, with p5 coming from the chain" is a real lane**: `abx deploy-code --script art.js --copies 100 --dep p5@1.0.0 --onchain-uri`, on **Sepolia** (`ABX_CHAIN=sepolia` — the AB dependency registry does not exist on Base Sepolia, where the pointer leg is skipped with a warning and the drop is NOT chain-complete). Confirm it with `abx verify <addr>`, which should say *"chain-complete — every dependency resolves to on-chain bytes; no server, gateway, or CDN in the graph"* (a claim about provenance, not immutability — see freezing, above).
|
|
254
308
|
|
|
255
309
|
Two things still to watch on this lane:
|
|
256
310
|
|
|
@@ -263,7 +317,7 @@ Everything else about a code project is unchanged by `--copies`.
|
|
|
263
317
|
|
|
264
318
|
The render effect is the **ONLY source of a real thumbnail** — skip it and `image` stays a placeholder. Three ways to run it: the **hosted** runner (`abx deploy-effects --resolver-url <resolver>`), a **local continuous** runner (`abx effects` — in-process beside `abx serve`, auto-renders every mint + param change), or the **one-shot repair lane** `abx render <addr> [id…]` (add `--remote <resolver>` to publish to a HOSTED resolver; idempotent; local captures need `npx playwright install chromium`).
|
|
265
319
|
|
|
266
|
-
- **`abx render <addr> <id> --force`** re-renders a still that already exists — the fix for a **bad / blank / timed-out capture**. The
|
|
320
|
+
- **`abx render <addr> <id> --force`** re-renders a still that already exists — the fix for a **bad / blank / timed-out capture**. The output is deterministic (fixed seed + params), so a plain `render` idempotent-skips an existing still and `--force` re-captures the *same* pixels; it is **not** a way to change how correct output looks (to change appearance, change an input — the `palette`/etc. PostParam via `abx configure-param`, which re-addresses the render).
|
|
267
321
|
- A render that hits an unavailable/erroring live view **fails loudly and stores nothing** (no garbage thumbnail).
|
|
268
322
|
- The runner uploads each render to the storage home (`ABX_STORAGE_BACKEND`) and **publishes** it to the resolver — a durable `ipfs://`/`ar://` locator the resolver 302-redirects to, or bytes for small must-inline outputs (traits). A local `abx render` with the default `fs` backend does NOT reach a hosted resolver — use `--remote` (publishes) or a public backend / co-located runner.
|
|
269
323
|
- Script-reported traits (`abx.traits({...})`) stitch into `attributes` (on-chain wins > render > operator).
|
|
@@ -282,7 +336,7 @@ The render effect is the **ONLY source of a real thumbnail** — skip it and `im
|
|
|
282
336
|
|
|
283
337
|
## Mint order, timing, pause, supply
|
|
284
338
|
|
|
285
|
-
**Metadata is the token id.** Tokens mint **in order** (`0,1,2,…`); token 3 shows
|
|
339
|
+
**Metadata is the token id.** Tokens mint **in order** (`0,1,2,…`); token 3 shows work 3. No token-id ↔ metadata-id decoupling — to shuffle or sell specific tokens, pre-mint then trade.
|
|
286
340
|
|
|
287
341
|
**Mint timing** — same three paths as the 1/1:
|
|
288
342
|
| Path | Flag / command |
|
|
@@ -72,15 +72,28 @@ abx state <addr> # shows a "721C validator" row ONLY when enrolled ("suspended
|
|
|
72
72
|
Enrolling prints one plain statement of what enforcement means — relay it, don't embellish. An
|
|
73
73
|
un-enrolled deploy prints nothing about 721C/1155C.
|
|
74
74
|
|
|
75
|
+
**One exception to "owner-only", worth knowing before you tell a collector they're stuck:** once a
|
|
76
|
+
collection's `owner()` is the zero address (renounced), **anyone** may send the `none` (suspend) form,
|
|
77
|
+
and **nobody** may ever arm a validator again. It's the release valve for an abandoned collection whose
|
|
78
|
+
validator reverts every transfer — the only thing a stranger can do is make the tokens transferable, never
|
|
79
|
+
the reverse. So a creator who plans to renounce should hear the trade: "renounced **and** still enforcing
|
|
80
|
+
royalties forever" is not a state this protocol offers.
|
|
81
|
+
|
|
75
82
|
## Refusals the CLI enforces (don't fight them; relay the reason)
|
|
76
83
|
|
|
77
84
|
- **`--721c none`/zero at deploy** — refused: a zero validator never enrolls, and "enrolled but
|
|
78
85
|
suspended" is not a deploy-time state. Plain (721 or 1155) is already the default; drop the flag.
|
|
79
86
|
- **`--721c recommended` on a chain with no known validator** — refused, naming the chains that have
|
|
80
87
|
one. Pass an explicit deployed validator address instead; never guess one.
|
|
81
|
-
- **A
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
- **A validator address that isn't a validator** — refused before any gas at deploy AND at re-point
|
|
89
|
+
(the contract reverts `InvalidTransferValidator()`); a mixed-case address must pass its EIP-55
|
|
90
|
+
checksum. **Two shapes are refused, not one:** no code at all, and an address that answers *any*
|
|
91
|
+
call successfully — a **Safe**, an uninitialised proxy, or a 7702-delegated EOA. `validateTransfer`
|
|
92
|
+
returns nothing, so there's no decode to fail and such an address would enforce nothing while
|
|
93
|
+
`getTransferValidator()` and ERC-165 both reported enforcement as ON. If a creator offers their Safe
|
|
94
|
+
address, that's the refusal you'll get — relay it as "that's a wallet, not a validator", and point at
|
|
95
|
+
`recommended`. A real deploy also refuses to enroll blind when the RPC can't answer (a dry-run
|
|
96
|
+
defers it).
|
|
84
97
|
- **`set-transfer-validator` on a plain, unenrolled collection** — refused up front (721 or 1155):
|
|
85
98
|
enrollment is a deploy-time decision and this collection didn't enroll. Do not retry with different
|
|
86
99
|
flags; if the creator truly needs enforcement, that's a redeploy conversation.
|
|
@@ -10,14 +10,14 @@ Master call is **custody × mutability**:
|
|
|
10
10
|
|
|
11
11
|
| | **Mutable** (name/desc/traits may change) | **Immutable** (never changes) |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
| **
|
|
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
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
15
|
|
|
16
16
|
**Four patterns, by where bytes live × how `tokenURI` resolves:**
|
|
17
|
-
1. **Fully on-chain** (`--onchain-image`) — bytes + JSON on-chain.
|
|
18
|
-
2. **Image off-chain, JSON on-chain, no server** (`--onchain-uri --backend arweave|ipfs|cloud`) — the sweet spot for static
|
|
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
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
|
|
20
|
+
4. **Inline SVG on-chain** — self-contained vector content inlined into `tokenURI`. For a **1/1** that's `abx deploy … --onchain-uri`; for a **Series** of tiny SVGs use `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
21
|
|
|
22
22
|
**All three deploy commands can now do pattern 2.** `deploy` (1/1), `deploy-series` and `deploy-code`
|
|
23
23
|
each upload to the chosen backend under `--onchain-uri` and bake the resulting public URL into the
|
|
@@ -35,7 +35,7 @@ wrong architecture:
|
|
|
35
35
|
|---|---|---|---|
|
|
36
36
|
| **Pattern 1** — bytes fully on-chain (`--onchain-image`) | ✅ hot + `--sign` | ✅ hot + `--sign` | n/a |
|
|
37
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
|
|
38
|
+
| **Pattern 4** — inline SVG on-chain (`--onchain-uri`, SVG content) | ✅ | ✅ (every id must be SVG) | n/a |
|
|
39
39
|
| **Pattern 3** — resolver (`--public-base-url`) | ✅ | ✅ | ✅ |
|
|
40
40
|
| on-chain library dep (`--dep p5@…`) | n/a | n/a | ✅ **Sepolia only** (no registry on Base Sepolia) |
|
|
41
41
|
| build directory (`--code-dir`) · field renderers | n/a | n/a | ❌ refused — `--script` only |
|
|
@@ -45,43 +45,89 @@ wrong architecture:
|
|
|
45
45
|
receipt feeds the next, so use the hot lane or `--sign`.
|
|
46
46
|
|
|
47
47
|
Otherwise custody is now fully symmetric with the 721 side, including the case that used to have no
|
|
48
|
-
answer: **"N
|
|
48
|
+
answer: **"N works × M copies each" with raster images and no server** is
|
|
49
49
|
`abx deploy-series --dir <folder> --copies <n> --onchain-uri --backend arweave` — a uniform file
|
|
50
50
|
extension uploads the folder as ONE directory and bakes a single `{id}`-substituting collection field,
|
|
51
51
|
so the whole collection costs one field rather than N.
|
|
52
52
|
|
|
53
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
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
|
|
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
56
|
|
|
57
57
|
Get decisions 1–2 right before deploy (image commitment + resolver URL are written then; re-pointable, but):
|
|
58
58
|
|
|
59
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
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
|
|
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 on first upload; 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
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
63
|
|
|
64
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
|
|
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
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
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
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
69
|
|
|
70
70
|
**3. Identity** — `--name`, `--symbol`, `--royalty-bps` (default 500 = 5%), `--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 `--
|
|
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
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
73
|
|
|
74
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
75
|
- *Off-chain:* the served `image` is the backend's **gateway HTTPS 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. The keccak stays the anchor → move gateways without a tx.
|
|
76
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
77
|
|
|
78
|
-
**Inline vs reader — default to the reader for real
|
|
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
79
|
|
|
80
|
-
|
|
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).
|
|
81
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.
|
|
82
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.
|
|
83
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`).
|
|
84
|
-
- **Store ≠ lock; lock last.** On-chain ≠ frozen. Deploy unlocked, confirm it resolves in production, *then* freeze.
|
|
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/
|
|
85
131
|
|
|
86
132
|
**6. When to mint** → [Deploy strategy](#deploy-strategy--when-to-mint).
|
|
87
133
|
|
|
@@ -25,7 +25,7 @@ All backends verify via the on-chain keccak commitment wherever the bytes live.
|
|
|
25
25
|
|
|
26
26
|
### Arweave via Turbo — the easy permanent path (read before quoting setup)
|
|
27
27
|
|
|
28
|
-
`arweave` defaults to the **Turbo** provider (ArDrive). For small
|
|
28
|
+
`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
29
|
|
|
30
30
|
- **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
31
|
- **`arweave`** *(default)* — a CLI-managed Arweave key auto-generated at `.abx-self-host/arweave-key.json` on first upload (address printed). 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).
|