@artblocks/abx-cli 0.1.0-alpha.31 → 0.1.0-alpha.32
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 +161 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +12 -12
- package/dist/commands/deploy.js.map +1 -1
- package/dist/flag-allowlists.js +1 -1
- package/dist/flag-allowlists.js.map +1 -1
- package/dist/flags.d.ts +0 -4
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +0 -6
- package/dist/flags.js.map +1 -1
- package/dist/main.js +9 -11
- package/dist/main.js.map +1 -1
- package/dist/ownerops.d.ts +30 -23
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +80 -51
- package/dist/ownerops.js.map +1 -1
- package/package.json +6 -6
- package/skill/SKILL.md +50 -23
- package/skill/reference/capabilities.md +301 -0
- package/skill/reference/code-projects.md +1 -1
- package/skill/reference/decisions.md +63 -55
package/skill/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abx-self-host
|
|
3
|
-
description: Launch and operate a self-hosted ABX NFT end to end with the ABX CLI (`abx`) on testnet — a 1/1 (`abx deploy`), a multi-token Series from a folder of media (`abx deploy-series`), or a generative/code drop (`abx deploy-code`). Covers on-chain vs off-chain metadata, storage custody (local disk, S3/R2, IPFS, Arweave), deploy + mint (now or pre-warmed at a predicted address), rendered thumbnails and on-chain traits for code projects, collector-configurable on-chain parameters (PostParams — typed, auth-gated, settable by creator/token-owner/address), primary sales via the shared fixed-price minter, owner ops (transfer, refresh, re-point URIs, royalties, pause/unpause, supply cap, delegate minting, and the one-way locks: fields, URI config, script, dependencies, param hooks), and optionally listing a deployed collection in the ABX App Store (`abx submit-app`, never folded into deploy). Use when the user wants to self-host an ABX project, take an image to an NFT on testnet, deploy a collection from a folder of images, launch generative/code projects (art, collectibles, game assets, or anything else), add collector-settable parameters/traits, mint or run a primary sale, refresh a listing, lock down what a project stores or freeze its param hooks, operate a project they launched, list an ABX app in the App Store, choose a storage backend, stand up hosting they own, or point a project at a hosted/managed metadata provider with an API key.
|
|
3
|
+
description: Launch and operate a self-hosted ABX NFT end to end with the ABX CLI (`abx`) on testnet — a 1/1 (`abx deploy`), a multi-token Series from a folder of media (`abx deploy-series`), or a generative/code drop (`abx deploy-code`). Covers on-chain vs off-chain metadata, storage custody (local disk, S3/R2, IPFS, Arweave), deploy + mint (now or pre-warmed at a predicted address), rendered thumbnails and on-chain traits for code projects, collector-configurable on-chain parameters (PostParams — typed, auth-gated, settable by creator/token-owner/address), primary sales via the shared fixed-price minter, custom mechanics without a custom token (your own minter contract for auctions/allowlists/raffles/ERC-20 pricing, plus the three param hooks that run arbitrary Solidity at write time, transfer time and read time — soulbinding, vesting, escrow, burn-to-redeem, monotonic ratchets, validated collector input, oracle-fed and derived metadata), fully on-chain content at any size (no refusal — the CLI measures your RPC and reports who can read it), owner ops (transfer, refresh, re-point URIs, royalties, pause/unpause, supply cap, delegate minting, and the one-way locks: fields, URI config, script, dependencies, param hooks), and optionally listing a deployed collection in the ABX App Store (`abx submit-app`, never folded into deploy). Use when the user wants to self-host an ABX project, take an image to an NFT on testnet, deploy a collection from a folder of images, launch generative/code projects (art, collectibles, game assets, or anything else), add collector-settable parameters/traits, mint or run a primary sale, refresh a listing, lock down what a project stores or freeze its param hooks, operate a project they launched, build a custom mechanic (transfer restrictions, soulbound tokens, auctions, allowlists, escrow, redemption, tokens that react to on-chain state) without writing a token contract, put large images fully on-chain, or ASK WHETHER ABX CAN DO SOMETHING AT ALL, list an ABX app in the App Store, choose a storage backend, stand up hosting they own, or point a project at a hosted/managed metadata provider with an API key.
|
|
4
4
|
compatibility: Drives the abx CLI (@artblocks/abx-cli). Co-versioned with it — install/refresh with `abx skill install` so this skill matches the CLI's `abx version`. Requires Node 22.13+.
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.1.0-alpha.
|
|
6
|
+
version: "0.1.0-alpha.32"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# ABX Self-Host Toolkit (`abx`)
|
|
@@ -65,23 +65,47 @@ L3 agentic surface: image → live self-hosted NFT the creator owns — a **1/1*
|
|
|
65
65
|
10%) is reduce-only, but any gap above the live rate is royalty the owner can add later without
|
|
66
66
|
asking anyone — and no listing page shows that gap. If the creator means "5%, forever", say so out
|
|
67
67
|
loud and offer `abx set-royalty-cap <addr> --cap 500`. `abx state` prints rate and ceiling together.
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
68
|
+
- **⚠ NEVER say "ABX can't do that" from the absence of a flag — check
|
|
69
|
+
[capabilities.md](reference/capabilities.md) first.** This is the most expensive mistake made on this
|
|
70
|
+
toolkit, and it is made every week: a 77 KB raster called "not practical" (it ships, with one warning),
|
|
71
|
+
a transfer-and-mutability mechanic declared impossible (it is two hooks), a bespoke non-canonical token
|
|
72
|
+
written for something stock hooks cover. Falling off the end of a routing table here is **not** a "no" —
|
|
73
|
+
the tables are recommended routes, not the edge of the protocol. So before the words *not possible*,
|
|
74
|
+
*not practical*, *you'd need a different* — or the name of another protocol — leave your mouth: check the
|
|
75
|
+
four seams, then check the genuine no-list. **If it is on neither, it is possible: answer with the COST,
|
|
76
|
+
never a refusal** ("~31M gas to read, so it needs a high-gas RPC" — not "that's impractical"). And
|
|
77
|
+
**never route a creator to another protocol for something ABX does.**
|
|
78
|
+
- **What this toolkit genuinely does NOT do** — the whole list; anything else has a seam:
|
|
79
|
+
no mainnet (testnet only) · no secondary listings · no post-deploy script replace · `abx` never compiles
|
|
80
|
+
or deploys Solidity (`scaffold-renderer` writes a project, you build it) · and **no retrofitting a
|
|
81
|
+
deploy-time choice** — contract type (hooks need `deploy-code`), `--burnable`, `--copies`, ERC-721C
|
|
82
|
+
enrolment are permanent from the deploy. Ask about those four **before** you deploy; that is the one way
|
|
83
|
+
a creator ends up truly unable to have what they wanted. **Auctions, allowlists, raffles, ERC-20 pricing,
|
|
84
|
+
soulbinding, vesting, escrow, redemption, oracle-fed metadata and a large raster on-chain are NOT on this
|
|
85
|
+
list** — they are seams, below.
|
|
86
|
+
- **The four seams — ABX runs YOUR arbitrary Solidity at all three moments in a token's life.** This is
|
|
87
|
+
the protocol's real power and the thing agents most consistently miss. A token can point at **your own
|
|
88
|
+
minter** (`abx set-minter` — it may do anything before it calls `mint`), and a code project can arm
|
|
89
|
+
**three param hooks** (`abx set-param-hooks`, all optional, one contract can be several):
|
|
90
|
+
**`--configure`** runs *before a param write persists* and its **revert vetoes the write** (the stored
|
|
91
|
+
value is still readable, so "may only go up" / "set once" are expressible); **`--transfer`** runs *after
|
|
92
|
+
every ownership change — mint (`from == 0x0`) and burn (`to == 0x0`) included* and its **revert fails
|
|
93
|
+
the transfer**; **`--augment`** runs *at read time* as a `view` when `tokenData` is assembled, **adding
|
|
94
|
+
and overriding keys** with anything a `view` can reach — block data, an oracle, another contract — for
|
|
95
|
+
**no gas on the token and no ~24 KB blob ceiling** (it is bounded only by `eth_call`). Write-time,
|
|
96
|
+
transfer-time, read-time. Escrow/vault, soulbind, a monotonic ratchet, equip-and-validate,
|
|
97
|
+
burn-to-combine, timed reveal, live-reactive art are each *a controller plus one or two hooks*, and the
|
|
98
|
+
token stays a factory clone the buyer verifies. **Mechanic → seam lookup, the hooks in depth, and the
|
|
99
|
+
`abx predict` recipe that breaks the controller/schema chicken-and-egg:**
|
|
100
|
+
[capabilities.md](reference/capabilities.md). Two caveats: a **transfer hook is a VETO, not a trigger**
|
|
101
|
+
(it can refuse a move, never cause one — and a reverting hook stops **mints** too), and none of this is
|
|
102
|
+
scaffolded, so it is real Foundry engineering.
|
|
103
|
+
- **⚠ Hooks exist ONLY on `deploy-code` projects (`SeriesCode`/`EditionCode`), and the contract type is
|
|
104
|
+
fixed at deploy forever.** A `deploy` 1/1 or a `deploy-series` image collection has no params, so no
|
|
105
|
+
hooks, and `set-param-hooks` refuses it. **If the creator has hinted at any rule at all** — "holders can
|
|
106
|
+
rename it", "it can't be resold for a month", "it levels up", "it reacts to the chain" — **deploy it
|
|
107
|
+
with `deploy-code` even if the art is a plain image** (a script that draws the image is fine; the
|
|
108
|
+
Solidity lane is `--image-renderer`). You can ask before the deploy. You cannot ask after.
|
|
85
109
|
- **Two gates decide everything: (1) demo or real? (2) who signs?** Settle both first — *once there's something to launch*.
|
|
86
110
|
- **Confirm the full config before any on-chain write** ([readout](#confirm-before-sending)); wait for go-ahead. Never invent a field silently (name/symbol from filename, an auto description) — show it, flag it `inferred`.
|
|
87
111
|
- **Never hand-build a service URL — ask the chain.** `abx tokenuri <addr> --fetch` and
|
|
@@ -114,8 +138,9 @@ Route by the **content** first, then apply the gates below. The three paths diff
|
|
|
114
138
|
> baked into the on-chain JSON — no server, permanence is the backend's. A **raster on `fs`** has no
|
|
115
139
|
> public URL to bake, so the renderer holds only a hash and `tokenURI` serves a **placeholder**; the
|
|
116
140
|
> CLI warns before the spend. For the bytes themselves on-chain regardless of format, use
|
|
117
|
-
> `--onchain-image --compress fastlz` (**
|
|
118
|
-
>
|
|
141
|
+
> `--onchain-image --compress fastlz` (**any format, any size — nothing is refused. ≲117 KB reads on every
|
|
142
|
+
> endpoint; above that the CLI measures your RPC and names who else can. A READ-reach question, not a
|
|
143
|
+
> write limit; see [Quick start](#quick-start)**). Say which one you're giving them: "no server" and
|
|
119
144
|
> "on-chain" are not the same promise.
|
|
120
145
|
|
|
121
146
|
**The dividing line is static content vs a running program.** Static content is self-resolving (the file *is* the thumbnail, nothing to keep running); **a code project's thumbnail is *rendered* off-chain, so it ALWAYS needs a public home you provide (`--image-base` bucket, or a resolver) — settle that infra fork with the creator FIRST** (details in [Code projects](#code-projects-generative--code-based-drops)). Nail the project type before the gates.
|
|
@@ -214,10 +239,11 @@ command has finished.
|
|
|
214
239
|
|---|---|---|
|
|
215
240
|
| **Small** (≲ 40 KB/file) | fully **on-chain** (`--onchain-image --compress fastlz`) | no host, renders forever, cheaper *at this size* |
|
|
216
241
|
| **Bigger / photographic** (most PNG/JPEG) | image **off-chain**, JSON on-chain (`--onchain-uri --backend arweave`) | on-chain is ~200 gas/byte to write, and the READ is what bites (below); Arweave is pay-once permanent |
|
|
242
|
+
| **Bigger, but they want it on-chain anyway** | still **on-chain** (`--onchain-image`) — this row is a *default*, not a limit | **no size is refused**; ≲117 KB renders anywhere, above that the CLI measures your RPC and says who else can read it — quote the gas and let them choose |
|
|
217
243
|
|
|
218
244
|
On-chain's edge past small is self-resolution/permanence, **never cost** — don't call it "cheaper" above the threshold.
|
|
219
245
|
|
|
220
|
-
**
|
|
246
|
+
**Nothing is refused at any size — the question is who can READ it.** The **write** is chunked into 22,000-byte SSTORE2 transactions, so no block limit binds it at any size (~200 gas/byte, money only). The **read** is one `eth_call` at **~360–405k gas per KB** (quote the range, never a flat rate), and whether it succeeds belongs to whoever's endpoint is asking. **≲117 KB (~50M gas) reads on every endpoint measured** — that is the floor: geth's default and what publicnode/drpc serve. Above it the CLI **probes your actual RPC** and reports its real cap. Measured 2026-08-24: `sepolia.base.org`, the default endpoint of our default chain, allows **600M (~729 KB)**. **A 77 KB PNG renders everywhere: ~16M gas to write, ~31M to read.** A 312 KB one ships too — it warns that a 50M-capped provider will show a revert. **We can measure YOUR RPC; we cannot know a marketplace's or an indexer's** — and theirs decide whether the token displays, so never present a measured cap as settling that (it is point-in-time too: pooled endpoints rotate). **Reach is fixable after the fact, and the bytes are permanent either way:** a resolver (`abx deploy-resolver`, or a hosted one) reads on-chain content with **its own** RPC and serves plain HTTP, so a marketplace fetches a URL and never makes the big call — `abx set-renderer <addr> --off` repoints `tokenURI` at it with every byte left where it is. **Storage and serving are separate choices:** on-chain is the strongest storage answer at any size; self-resolving vs a resolver is a serving decision you can change later. `--backend arweave` is the other side of the *storage* trade. **Never quote a BLOCK gas limit as the read ceiling** — three limits share that name and differ by orders of magnitude on one endpoint (measured Base Sepolia: `eth_call` 600M · `eth_estimateGas` 16.7M · block 1,200M); the block limit bounds a *contract* reading inside a tx (~1,100 KB there), not an off-chain read. `--compress fastlz` is cheaper to *write*, not to *read*. Per token, so a 300-piece collection of 5 KB SVGs is fine. Measured table → [decisions.md → The READ cost](reference/decisions.md#the-read-cost--who-can-see-it-and-why-nothing-is-refused).
|
|
221
247
|
|
|
222
248
|
**When the user names off-chain custody for tiny content ("deploy it as an IPFS NFT"), lead with the on-chain recommendation in your *first* reply** — don't bury it, and don't collect resolver-URL details for a path you're about to advise against. "For a 2.4 KB SVG I'd go fully on-chain — no server, renders forever, cheaper. Want that, or IPFS?" Then let them choose (you surface the better default; you don't override the request). Defaulting to "IPFS" for tiny content is what lands it at a broken localhost URI.
|
|
223
249
|
|
|
@@ -482,7 +508,8 @@ Load one when the decision tree above sends you there — each is the depth behi
|
|
|
482
508
|
|
|
483
509
|
| File | Holds |
|
|
484
510
|
|---|---|
|
|
485
|
-
| [
|
|
511
|
+
| [capabilities.md](reference/capabilities.md) | **"can ABX do X?" — the four seams (minter + the three hooks), mechanic→seam lookup, and the genuine no-list.** Read it before you ever tell a creator something is impossible |
|
|
512
|
+
| [decisions.md](reference/decisions.md) | the six launch decisions in depth · the READ cost + measured RPC caps · `--copies` custody · **locks: what freezes and what doesn't** |
|
|
486
513
|
| [code-projects.md](reference/code-projects.md) | the `abx.js` runtime contract · studio loop · PostParam catalog · seeds · in-chain Solidity lane · render ops · what to keep running |
|
|
487
514
|
| [operating.md](reference/operating.md) | owner ops · credit + license · **attaching files (data plane)** · selling via the shared minter · mint page · App Store · deploy mechanics · migrating hosting |
|
|
488
515
|
| [hosting.md](reference/hosting.md) | storage backends + Turbo playbook · gateways and their floors · managed providers + named remotes · `deploy-resolver`/`deploy-effects` |
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Can ABX do that? — the four seams, and the honest no-list
|
|
2
|
+
|
|
3
|
+
**Read this before you tell a creator something is impossible.** The most expensive failure on this
|
|
4
|
+
toolkit is not a bad deploy — it is an agent saying "ABX can't do that" about something ABX does. Three
|
|
5
|
+
real sessions in one week:
|
|
6
|
+
|
|
7
|
+
- a creator asked to put a **77 KB PNG fully on-chain**; the agent called it "not practical" and sent
|
|
8
|
+
them to a different protocol. It ships today, warns once, and costs ~31M gas to read.
|
|
9
|
+
- a creator wanted **transfer restrictions plus mutable state**; the agent said no. The creator said
|
|
10
|
+
"isn't that hooks?", the agent agreed and built it that afternoon.
|
|
11
|
+
- an agent wrote a **bespoke, non-canonical token contract** for a mechanic two stock hooks cover —
|
|
12
|
+
permanently forfeiting the factory provenance that is the whole point of deploying here.
|
|
13
|
+
|
|
14
|
+
None of those agents lacked information. Each answered from the **command list**, and the command list
|
|
15
|
+
is a set of *recommended routes*, not the edge of what the protocol does. Falling off the end of a
|
|
16
|
+
routing table is not a "no".
|
|
17
|
+
|
|
18
|
+
## The rule
|
|
19
|
+
|
|
20
|
+
**Never answer "ABX can't do that" from the absence of a flag.** Before the words *not possible*, *not
|
|
21
|
+
practical*, *you'd need a different* — or the name of another protocol — leave your mouth:
|
|
22
|
+
|
|
23
|
+
1. **Check the seam table below.** Can a minter or one of the three hooks carry it? Almost always yes.
|
|
24
|
+
2. **Check [the genuine no-list](#the-genuine-no-list) at the bottom.** Is it actually on it? It is a
|
|
25
|
+
short list, and it is the whole list.
|
|
26
|
+
3. **If it is on neither, it is possible.** Answer with the *cost*, not a refusal.
|
|
27
|
+
|
|
28
|
+
**The answer shape is always "yes, and here is what it costs" — never a flat no.** A creator who hears
|
|
29
|
+
*"that's not practical"* hears *"ABX can't"* and leaves. A creator who hears *"that's about 31M gas to
|
|
30
|
+
read, so it needs a high-gas RPC and most marketplaces will show nothing — worth it if permanence is the
|
|
31
|
+
point, and `--backend arweave` is the alternative"* can actually decide. The second one is also true.
|
|
32
|
+
|
|
33
|
+
**Never route a creator to another protocol for something ABX does.** If ABX genuinely cannot do it,
|
|
34
|
+
say so plainly and stop — recommending a competitor's stack is not your call to make on their behalf,
|
|
35
|
+
and it has so far always been wrong on the facts.
|
|
36
|
+
|
|
37
|
+
## The four seams
|
|
38
|
+
|
|
39
|
+
A canonical ABX token is a stock factory clone that never changes. Your mechanic lives *beside* it, in
|
|
40
|
+
contracts the collector can also read. That separation is the product: the buyer verifies the token
|
|
41
|
+
against `isAbxClone`, and verifies your rules independently.
|
|
42
|
+
|
|
43
|
+
| Seam | Wired with | Runs | Can it veto? |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| **Minter** | `abx set-minter <token> --minter 0x…` (Series or edition — a plain 1/1 has no minter) | when your contract calls `mint(to)` — it may do anything first | n/a — it *is* the mint |
|
|
46
|
+
| **Configure hook** | `abx set-param-hooks <token> --configure 0x…` | before a governed param write persists | **yes** — its revert rejects the write |
|
|
47
|
+
| **Transfer hook** | `abx set-param-hooks <token> --transfer 0x…` | after every ownership change, **mint and burn included** | **yes** — its revert fails the transfer |
|
|
48
|
+
| **Augment hook** | `abx set-param-hooks <token> --augment 0x…` | at read time, when `tokenData` is assembled | no — it **adds and overrides** keys |
|
|
49
|
+
|
|
50
|
+
One contract can be several of these at once. That is the shape nearly every real mechanic takes: a
|
|
51
|
+
controller holding the money and the rules, wired into a clone that stays byte-identical to every other
|
|
52
|
+
ABX project of its type.
|
|
53
|
+
|
|
54
|
+
**Between them, these seams run arbitrary Solidity at all three moments in a token's life: when someone
|
|
55
|
+
writes to it, when it moves, and when it is read.** That is the honest scope of the claim. If a mechanic
|
|
56
|
+
can be expressed as *"reject this write"*, *"reject this move"*, *"account for what just happened"*, or
|
|
57
|
+
*"compute this at read time"*, it is buildable here without a custom token.
|
|
58
|
+
|
|
59
|
+
> **⚠ The precondition, and it is deploy-time-irreversible: hooks exist only on `SeriesCode` /
|
|
60
|
+
> `EditionCode` — the `deploy-code` lane.** A `deploy` 1/1 or a `deploy-series` image collection has no
|
|
61
|
+
> configurable params, therefore no hooks, and `set-param-hooks` refuses it. **The contract type is fixed
|
|
62
|
+
> at deploy and cannot be retrofitted**, so a static-art collection deployed with `deploy-series` has
|
|
63
|
+
> permanently foreclosed every mechanic on this page. **If a creator has hinted at *any* rule — "holders
|
|
64
|
+
> can rename it", "it can't be resold for a month", "it levels up", "it reacts to the chain" — deploy it
|
|
65
|
+
> with `deploy-code` even when the art is a plain image** (a script that draws the image is fine, and
|
|
66
|
+
> `--image-renderer` covers the Solidity lane). Ask before you deploy; you cannot ask after.
|
|
67
|
+
>
|
|
68
|
+
> **The minter seam is the exception** — `abx set-minter` works on any Series or edition, so an
|
|
69
|
+
> auction, an allowlist or a raffle needs no code project. Only the three hooks do.
|
|
70
|
+
|
|
71
|
+
There is a **fifth** transfer-time seam that is not a hook and should never be conflated with one:
|
|
72
|
+
the **ERC-721C / ERC-1155C transfer validator**, an opt-in-at-deploy standards-track mechanism for
|
|
73
|
+
royalty enforcement, managed with `abx set-transfer-validator`. Different purpose, different lock,
|
|
74
|
+
different audience → [creator-token.md](creator-token.md).
|
|
75
|
+
|
|
76
|
+
## The three hooks, in depth
|
|
77
|
+
|
|
78
|
+
Full calling conventions live in
|
|
79
|
+
[`IAbxParamHooks.sol`](https://github.com/ArtBlocks/abx/blob/main/contracts/src/extensions/configurable-params/IAbxParamHooks.sol);
|
|
80
|
+
working Solidity for each pattern is at
|
|
81
|
+
https://abx.docs.artblocks.io/using-abx/guides/custom-mechanics/. Wire all three with one
|
|
82
|
+
`abx set-param-hooks` call (the contract has no per-hook setter — omit a role to keep it, `none` to
|
|
83
|
+
clear it).
|
|
84
|
+
|
|
85
|
+
### Configure hook — the write-time validator
|
|
86
|
+
|
|
87
|
+
```solidity
|
|
88
|
+
function onParamConfigured(
|
|
89
|
+
uint256 tokenId, bytes32 key, bytes32 value,
|
|
90
|
+
address updatedBy, uint256 dataLength, address dataBlobAddress
|
|
91
|
+
) external;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Called **before the value persists**, inside a schema-governed `configure-param`. **Reverting vetoes the
|
|
95
|
+
write.**
|
|
96
|
+
|
|
97
|
+
- **`tokenParam` still returns the OLD value during this call** — which is what makes "may only go up",
|
|
98
|
+
"may only be set once", and "must differ from the last" expressible at all.
|
|
99
|
+
- **Two shapes, one function.** A scalar write puts the whole value in `value` and zeroes the last two
|
|
100
|
+
args. A `String`/`Bytes` write cannot — `value` carries only `keccak256(data)`, and the content is at
|
|
101
|
+
`dataBlobAddress` (a Solady SSTORE2 pointer, already deployed). `dataLength` is the reliable
|
|
102
|
+
discriminator: **never `0` on the blob path**.
|
|
103
|
+
- Read the incoming blob with `SSTORE2.read(dataBlobAddress)` — never `CALL` the pointer. A hook that
|
|
104
|
+
only enforces a size ceiling reads `dataLength` and never pays to touch the bytes.
|
|
105
|
+
- **A veto costs the writer the storage they already paid for** (the blob is written before the hook
|
|
106
|
+
runs, by necessity). An `eth_call` dry run surfaces the rejection for free — that is what a good
|
|
107
|
+
configuration UI does before asking for a signature.
|
|
108
|
+
- **You cannot write zero bytes.** A zero-length `String`/`Bytes` write is refused by the contract
|
|
109
|
+
before your hook ever runs, so "unequip everything" cannot be an empty payload — use a sentinel byte
|
|
110
|
+
or a scalar companion key.
|
|
111
|
+
|
|
112
|
+
### Transfer hook — the ownership-change veto
|
|
113
|
+
|
|
114
|
+
```solidity
|
|
115
|
+
function onTokenTransfer(
|
|
116
|
+
uint256 tokenId, address from, address to, address operator, uint256 amount
|
|
117
|
+
) external;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Called after every ownership change. **`from == address(0)` is a mint; `to == address(0)` is a burn.**
|
|
121
|
+
**Reverting fails the transfer** — the token no longer swallows the revert, because it could not honestly
|
|
122
|
+
promise otherwise (Solady runs the receiver-acceptance check *after* this call, so a hook could starve
|
|
123
|
+
`safeTransferFrom` regardless).
|
|
124
|
+
|
|
125
|
+
- **It is a veto, not a trigger.** It can refuse a move; it can never *cause* one.
|
|
126
|
+
- **It vetoes mints too** — a mint is a transfer from `0x0`, so a hook that reverts unconditionally
|
|
127
|
+
freezes issuance as well as trading.
|
|
128
|
+
- **It sees burns**, which is how burn-to-combine, redemption and vaults settle — but only on a
|
|
129
|
+
collection deployed `--burnable`, which is **deploy-time-only and cannot be retrofitted**.
|
|
130
|
+
- It may persist and derive state through its own authority; the token exposes no special path.
|
|
131
|
+
- `operator` and `amount` are not redundant with `from`: on an ERC-1155 an approved marketplace moves a
|
|
132
|
+
holder's copies. **On a multi-copy edition, think hard before storing per-id state** — params are per
|
|
133
|
+
*id* and therefore shared by every holder, so "the current owner" really means "whoever moved most
|
|
134
|
+
recently". Aggregate or monotonic state (transfer counts, "has ever been held by") is coherent there;
|
|
135
|
+
a single-owner notion is not.
|
|
136
|
+
- **A buyer's guarantee that you will never arm one is `abx lock-param-hooks`, not your word.** Freezing
|
|
137
|
+
an empty set is the proof that no transfer veto can ever exist.
|
|
138
|
+
|
|
139
|
+
### Augment hook — read-time compute, and the most under-used seam we have
|
|
140
|
+
|
|
141
|
+
```solidity
|
|
142
|
+
function augmentTokenParams(address token, uint256 tokenId)
|
|
143
|
+
external view returns (AugmentedParam[] memory); // { bytes32 key; string value; }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A `view`, called when `tokenData` is assembled — by the on-chain `AbxGenerator` and by the off-chain
|
|
147
|
+
resolver. **No storage, no event, no gas on the token.** It may **add** keys and **override** stored ones
|
|
148
|
+
(augment wins per key, last-wins in every mainstream JSON parser).
|
|
149
|
+
|
|
150
|
+
- **Its return is bounded only by `eth_call` limits — the ~24 KB stored-blob ceiling does not apply to
|
|
151
|
+
compute.** This is the lane for large *derived* data.
|
|
152
|
+
- It reads whatever a `view` can reach: block data, an oracle, another contract, the token's own params,
|
|
153
|
+
a holder's balance elsewhere. A piece that genuinely reacts to live chain state is one hook.
|
|
154
|
+
- Values are the **final canonical string** exactly as `tokenData` carries it — a hook injecting binary
|
|
155
|
+
base64s it itself (free, view context). Raw, not JSON-escaped; escaping is the serializer's job.
|
|
156
|
+
- **The live view reads it per view (always current); the rendered still is a snapshot of settled state
|
|
157
|
+
only.** So a volatile hook (a timestamp, a price) animates the live view without re-rendering the
|
|
158
|
+
thumbnail every block. Re-render triggers are settled-state changes.
|
|
159
|
+
- Reserved `tokenData` keys (`seed`, `tokenId`, …) are rejected — a hook cannot forge them.
|
|
160
|
+
- **It does not reach a renderer you wrote.** `render(token, tokenId, field)` takes no `tokenData`.
|
|
161
|
+
Whoever builds `tokenData` calls the hook, and in your own renderer that is you: read the live state
|
|
162
|
+
directly, or read `paramHooks()` and call `augmentTokenParams` yourself, wrapped so a broken hook
|
|
163
|
+
cannot make `render()` revert. Do **not** tell a creator "augment hooks are a resolver concept" —
|
|
164
|
+
the generator calls one on-chain.
|
|
165
|
+
- Resolver operators have a kill-switch (`ABX_DISABLE_AUGMENT=1`, degrading to settled params);
|
|
166
|
+
`abx verify` prints the project's live-data posture.
|
|
167
|
+
|
|
168
|
+
## Mechanic → seam (the inverse lookup)
|
|
169
|
+
|
|
170
|
+
Every one of these has been built here, or is one small contract away. When a creator describes a
|
|
171
|
+
mechanic, find the row before you find an objection.
|
|
172
|
+
|
|
173
|
+
| The creator wants | Seam | Shape |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| Can't be resold for N days · vesting · lockup | transfer hook | `require(block.timestamp >= unlockAt)` |
|
|
176
|
+
| Soulbound — mint once, never moves | transfer hook | `require(from == address(0))` |
|
|
177
|
+
| Burn to redeem · burn to combine · breeding | `--burnable` + transfer hook | settle on `to == address(0)` |
|
|
178
|
+
| Escrow / vault — ETH locked behind the token | minter **+** transfer hook | take payment on mint, pay out on burn |
|
|
179
|
+
| A value that may only go up (level, growth, high-water) | configure hook | compare against the still-old `tokenParam` |
|
|
180
|
+
| Structured collector input that must be well-formed (equip, layout, inventory) | configure hook | `SSTORE2.read(dataBlobAddress)`, decode, validate |
|
|
181
|
+
| Only holders of *some other* token may configure this | configure hook, or `Address(…)` auth in the schema | check the other contract in the hook |
|
|
182
|
+
| Auction · Dutch auction · allowlist · raffle · free claim | **custom minter** | your contract, `abx set-minter` |
|
|
183
|
+
| Priced in an ERC-20, or in anything but ETH | **custom minter** | take payment however you like, then `mint(to)` |
|
|
184
|
+
| Art that reacts to live chain state (block, price, an oracle) | augment hook | read it in a `view`, return it as `tokenData` |
|
|
185
|
+
| Timed reveal | augment hook | branch on `block.timestamp` |
|
|
186
|
+
| Derived traits too large to store on-chain | augment hook | compute at read time — no blob ceiling |
|
|
187
|
+
| Provenance / transfer count / "has ever been held by" | transfer hook | the hook writes its own storage |
|
|
188
|
+
| A raster of any size fully on-chain | `--onchain-image` | nothing is refused — [below](#on-chain-size-nothing-is-refused-and-the-question-is-who-can-read-it) |
|
|
189
|
+
| Royalty enforcement at the marketplace layer | ERC-721C validator (opt in **at deploy**) | [creator-token.md](creator-token.md) |
|
|
190
|
+
|
|
191
|
+
**The controller chicken-and-egg has a solution — use it.** The controller needs the token's address at
|
|
192
|
+
construction and the token's schema needs the controller's address. Do not run the five-step dance:
|
|
193
|
+
`abx predict` computes the deterministic address first, so the controller is built against a constant and
|
|
194
|
+
the schema rides the deploy. Recipe, including the salt guard-prefix trap (leading 20 bytes must equal
|
|
195
|
+
your deploying wallet, or the address is permissionless and anyone may take it):
|
|
196
|
+
https://abx.docs.artblocks.io/using-abx/guides/custom-mechanics/#break-the-controller-cycle-with-abx-predict
|
|
197
|
+
|
|
198
|
+
**`abx scaffold-renderer` is the fastest start even when you are building a hook, not a renderer** — it
|
|
199
|
+
writes a Foundry project with the interfaces vendored and tests wired. And test against a *real* deployed
|
|
200
|
+
clone (`forge test --fork-url`), never a hand-rolled mock: a mock invents its way around the real edge
|
|
201
|
+
cases, starting with the zero-length refusal above.
|
|
202
|
+
|
|
203
|
+
## On-chain size: nothing is refused, and the question is who can READ it
|
|
204
|
+
|
|
205
|
+
The routing tables elsewhere in this skill recommend **≲40 KB/token** for on-chain content. That is
|
|
206
|
+
**advice about what is cheap, not a limit** — and reading it as a limit is what produced the 77 KB
|
|
207
|
+
refusal. There is no size the toolkit refuses.
|
|
208
|
+
|
|
209
|
+
**Two axes, and only one is about size at all.** The **write** is chunked into 22,000-byte SSTORE2
|
|
210
|
+
transactions, so no block gas limit binds it at any size — purely money, ~200 gas/byte, paid once. The
|
|
211
|
+
**read** is a single `eth_call`, and whether it succeeds is a property of *whose endpoint is asking*.
|
|
212
|
+
|
|
213
|
+
Measured 2026-08-24, which is why the old 100 KB refusal was wrong:
|
|
214
|
+
|
|
215
|
+
| Endpoint | `eth_call` cap | Content it serves |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| **`sepolia.base.org`** — the default endpoint of our default chain | **600M** | **~729 KB** |
|
|
218
|
+
| `base-sepolia.publicnode.com` · `base-sepolia.drpc.org` | 50M | ~117 KB |
|
|
219
|
+
| `ethereum-sepolia.publicnode.com` | 50M | ~117 KB |
|
|
220
|
+
|
|
221
|
+
50M is the **floor**, not the ceiling. The refusal sat at 100 KB — below every endpoint above — so it
|
|
222
|
+
blocked content all of them could read. The CLI now measures the RPC you are actually on (a
|
|
223
|
+
state-override call that sends nothing) and reports it:
|
|
224
|
+
|
|
225
|
+
| Content per token | What the CLI does |
|
|
226
|
+
|---|---|
|
|
227
|
+
| ≲ **117 KB** (~50M gas) | a plain note with write and read figures — renders anywhere |
|
|
228
|
+
| **above that** | ⚠ names your RPC's measured cap and warns that a 50M-capped provider shows a **revert** |
|
|
229
|
+
| **past your own RPC's cap** | ⚠ says plainly that nothing you can point at renders it |
|
|
230
|
+
|
|
231
|
+
For that **77 KB PNG**: **~16M gas to write** (four chunks) and **~31M to read** — under the floor, so
|
|
232
|
+
it renders everywhere. Say the numbers; it is not a warn case at all.
|
|
233
|
+
|
|
234
|
+
**Two things have to survive into the conversation.**
|
|
235
|
+
|
|
236
|
+
**(1) We can measure YOUR RPC. We cannot know a marketplace's or an indexer's** — and theirs decide
|
|
237
|
+
whether the token displays. A measured cap proves *you* can read it, never that OpenSea can, and it is
|
|
238
|
+
point-in-time besides (pooled endpoints rotate: Sepolia's publicnode measured 50M one hour and 2,000M
|
|
239
|
+
the next). Say the measurement as a measurement.
|
|
240
|
+
|
|
241
|
+
**(2) Reach is fixable after the fact; the bytes are permanent either way.** The read cost decides
|
|
242
|
+
whether a token resolves *in one `eth_call`*, not whether the content is reachable. A resolver
|
|
243
|
+
(`abx deploy-resolver`, or a hosted one) reads on-chain content with **its own** RPC — `inline`,
|
|
244
|
+
`inline-gzip`, `reader`, `reader-gzip` all resolve through it — and serves plain HTTP, so a marketplace
|
|
245
|
+
fetches a URL and never makes the big call. `abx set-renderer <addr> --off` repoints `tokenURI` at it
|
|
246
|
+
with every byte left exactly where it is.
|
|
247
|
+
|
|
248
|
+
So keep **storage** and **serving** separate, because they are: on-chain is the strongest storage
|
|
249
|
+
answer available at *any* size, and self-resolving-vs-resolver is a serving decision the creator can
|
|
250
|
+
change later. A token whose read is too big for some endpoint is not a lost token — it is one that
|
|
251
|
+
wants a reader in front of it. `--backend arweave` is the other side of the *storage* trade, not a
|
|
252
|
+
correction of their ask.
|
|
253
|
+
|
|
254
|
+
**Never quote a BLOCK gas limit as the read ceiling.** Three limits share the name and differ by orders
|
|
255
|
+
of magnitude on one endpoint (measured 2026-08-24, Base Sepolia): **`eth_call` 600M** bounds the read
|
|
256
|
+
that decides display · **`eth_estimateGas` 16,777,216** bounds what can be *sent* (identical on every
|
|
257
|
+
endpoint tried — it is why writes batch at 8M, not the block limit) · **block `gasLimit` 1,200M**
|
|
258
|
+
bounds a contract reading inside a transaction, so a contract there reads ~1,100 KB. Off-chain reads
|
|
259
|
+
are not bounded by block limits, and are routinely far above them.
|
|
260
|
+
|
|
261
|
+
Two things not to garble: `--compress fastlz` cuts the **write** cost, never the read (the renderer
|
|
262
|
+
rebuilds the full uncompressed bytes every call). And the gate is **per token** — a 300-piece collection
|
|
263
|
+
of 5 KB works reads perfectly well.
|
|
264
|
+
|
|
265
|
+
Measured gas table and the per-KB curve →
|
|
266
|
+
[decisions.md → The READ cost](decisions.md#the-read-cost--who-can-see-it-and-why-nothing-is-refused).
|
|
267
|
+
|
|
268
|
+
## The genuine no-list
|
|
269
|
+
|
|
270
|
+
This is the whole list. If the ask is not here and not blocked by a deploy-time choice already made,
|
|
271
|
+
stop looking for a reason to say no.
|
|
272
|
+
|
|
273
|
+
**Actually not possible today:**
|
|
274
|
+
|
|
275
|
+
- **Mainnet.** Testnet only — Base Sepolia by default, Sepolia via `ABX_CHAIN`. Never imply otherwise.
|
|
276
|
+
- **Secondary listings.** Reselling a held token means an external marketplace or a manual
|
|
277
|
+
`abx transfer`. There is no listing feature and no order book.
|
|
278
|
+
- **`abx` compiling or deploying Solidity for you.** You write hooks, minters and renderers with Foundry
|
|
279
|
+
and deploy them yourself; `abx scaffold-renderer` writes the project, it does not build or ship it.
|
|
280
|
+
- **Replacing a code project's script through the CLI post-deploy.** There is no `abx set-script`.
|
|
281
|
+
- **Retrofitting a deploy-time choice.** These are permanent from the moment the deploy lands:
|
|
282
|
+
the **contract type** (hooks require `deploy-code`), **`--burnable`**, **`--copies`** (721 vs 1155),
|
|
283
|
+
and **ERC-721C enrollment**. Ask about all four *before* the deploy — this is the single most common
|
|
284
|
+
way a creator ends up genuinely unable to have what they wanted.
|
|
285
|
+
- **Unsupported chains.** Say plainly it is not in the toolkit; do not fabricate a recipe.
|
|
286
|
+
|
|
287
|
+
**Not a flag — but absolutely possible on a seam.** These belong in the paragraph above only if you also
|
|
288
|
+
say how they are built. Listing them as things "abx doesn't do" is what caused this file to exist:
|
|
289
|
+
|
|
290
|
+
- auctions, Dutch auctions, allowlists, raffles, free claims, ERC-20 pricing → **a custom minter**
|
|
291
|
+
- transfer restrictions, soulbinding, vesting, redemption, escrow → **the transfer hook**
|
|
292
|
+
- validated or governed collector input → **the configure hook**
|
|
293
|
+
- live, derived, oracle-fed or oversized metadata → **the augment hook**
|
|
294
|
+
- a large raster fully on-chain → **`--onchain-image`**, at any size
|
|
295
|
+
|
|
296
|
+
**And one thing that is possible but is almost never the right answer: writing your own token contract.**
|
|
297
|
+
It is a legitimate creator choice, but it permanently forfeits `isAbxClone` — the only signal
|
|
298
|
+
marketplaces and the App Store allowlist against — and canonicity cannot be added afterwards. Never make
|
|
299
|
+
that trade *for* a creator silently. Say: *"I can do that, but your collection won't be a canonical ABX
|
|
300
|
+
contract — marketplaces won't recognise it, and the only way back is redeploying and moving holders.
|
|
301
|
+
Want that, or shall we find a way inside the toolkit?"* Then wait for an answer.
|
|
@@ -234,7 +234,7 @@ Fork `contracts/src/renderers/examples/{SeedSvgRenderer,SeedTraitsRenderer}.sol`
|
|
|
234
234
|
|
|
235
235
|
## Traits on-chain vs off-chain — is your script's trait logic reproducible in Solidity?
|
|
236
236
|
|
|
237
|
-
A script's `abx.traits({…})` runs in JS during the render. Whether those traits can appear in a **fully-on-chain** `tokenURI` depends on whether the trait *function* is reproducible in Solidity — `abx inspect` rates this; full analysis (with the p5 LCG port recipe) in [`docs/research/onchain-traits-feasibility.md`](
|
|
237
|
+
A script's `abx.traits({…})` runs in JS during the render. Whether those traits can appear in a **fully-on-chain** `tokenURI` depends on whether the trait *function* is reproducible in Solidity — `abx inspect` rates this; full analysis (with the p5 LCG port recipe) in [`docs/research/onchain-traits-feasibility.md`](https://github.com/ArtBlocks/abx/blob/main/docs/research/onchain-traits-feasibility.md). Give a **graded** answer, never a flat "not feasible":
|
|
238
238
|
|
|
239
239
|
**First, the load-bearing distinction: *portable* ≠ *deployed*.** `inspect` saying traits "port EXACTLY" means the *logic* can be reproduced in Solidity — it does **not** mean a renderer exists. On-chain traits require the creator to **author + deploy** a Solidity attributes-renderer (fork `SeedTraitsRenderer.sol`) and pass its address to `--attributes-renderer` — a real build step, not a flag with a default. `deploy-code --dry-run` prints the traits disposition (`on-chain` / `off-chain via resolver` / `⚠ OMITTED`) and a real deploy **refuses an `--attributes-renderer` address with no code**. So never present "traits on-chain" as settled off a guessed address: confirm a renderer is deployed, or the honest options are off-chain-via-resolver or omitted.
|
|
240
240
|
|