@artblocks/abx-cli 0.1.0-alpha.0
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/LICENSE +21 -0
- package/assets/renderer-scaffold/README.md +59 -0
- package/assets/renderer-scaffold/foundry.toml +15 -0
- package/assets/renderer-scaffold/remappings.txt +3 -0
- package/assets/renderer-scaffold/script/Deploy.s.sol +23 -0
- package/assets/renderer-scaffold/src/MyRenderer.sol +123 -0
- package/assets/renderer-scaffold/src/MyTraits.sol +75 -0
- package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +32 -0
- package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +26 -0
- package/assets/renderer-scaffold/test/MyRenderer.t.sol +110 -0
- package/dist/config.d.ts +69 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +184 -0
- package/dist/config.js.map +1 -0
- package/dist/deps.d.ts +46 -0
- package/dist/deps.d.ts.map +1 -0
- package/dist/deps.js +90 -0
- package/dist/deps.js.map +1 -0
- package/dist/flags.d.ts +25 -0
- package/dist/flags.d.ts.map +1 -0
- package/dist/flags.js +34 -0
- package/dist/flags.js.map +1 -0
- package/dist/inspect.d.ts +48 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +184 -0
- package/dist/inspect.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +5102 -0
- package/dist/main.js.map +1 -0
- package/dist/migrate.d.ts +65 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +180 -0
- package/dist/migrate.js.map +1 -0
- package/dist/mintpage.d.ts +46 -0
- package/dist/mintpage.d.ts.map +1 -0
- package/dist/mintpage.js +461 -0
- package/dist/mintpage.js.map +1 -0
- package/dist/onchain-uri.d.ts +97 -0
- package/dist/onchain-uri.d.ts.map +1 -0
- package/dist/onchain-uri.js +243 -0
- package/dist/onchain-uri.js.map +1 -0
- package/dist/ownerops.d.ts +195 -0
- package/dist/ownerops.d.ts.map +1 -0
- package/dist/ownerops.js +1270 -0
- package/dist/ownerops.js.map +1 -0
- package/dist/provision.d.ts +86 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +372 -0
- package/dist/provision.js.map +1 -0
- package/dist/remote.d.ts +58 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +54 -0
- package/dist/remote.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +125 -0
- package/dist/schema.js.map +1 -0
- package/dist/series-traits.d.ts +30 -0
- package/dist/series-traits.d.ts.map +1 -0
- package/dist/series-traits.js +103 -0
- package/dist/series-traits.js.map +1 -0
- package/dist/signer.d.ts +80 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signer.js +520 -0
- package/dist/signer.js.map +1 -0
- package/dist/upload.d.ts +28 -0
- package/dist/upload.d.ts.map +1 -0
- package/dist/upload.js +41 -0
- package/dist/upload.js.map +1 -0
- package/package.json +55 -0
- package/skill/SKILL.md +304 -0
- package/skill/reference/code-projects.md +211 -0
- package/skill/reference/hosting.md +138 -0
- package/skill/reference/operating.md +116 -0
- package/skill/reference/setup.md +36 -0
- package/skill/reference/troubleshooting.md +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Art Blocks, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ABX in-chain renderer scaffold
|
|
2
|
+
|
|
3
|
+
A ready-to-build Foundry project for the **fully on-chain Solidity art lane** — the artwork is a
|
|
4
|
+
smart contract that computes an SVG (and traits) from the token's `seed` and collector-set
|
|
5
|
+
PostParams. No JavaScript, no browser, no bucket, no resolver, no effect runner: once the renderer
|
|
6
|
+
is deployed and wired, `tokenURI` — name, image, traits — resolves from chain forever.
|
|
7
|
+
|
|
8
|
+
`abx` does **not** compile or deploy Solidity for you; you build, test, and deploy this project with
|
|
9
|
+
Foundry, then hand the deployed address(es) to `abx deploy-code`, which verifies they have code.
|
|
10
|
+
|
|
11
|
+
## What's here
|
|
12
|
+
|
|
13
|
+
- `src/MyRenderer.sol` — the **image** renderer: seed → geometry, a `palette` HexColor PostParam →
|
|
14
|
+
tint. **Fork the geometry/palette math for your own art.**
|
|
15
|
+
- `src/MyTraits.sol` — the **attributes** renderer: reads the *same* seed math so traits agree with
|
|
16
|
+
the image by construction.
|
|
17
|
+
- `src/interfaces/` — the two ABX interfaces you implement/read (`IAbxFieldRenderer`, `IAbxParams`).
|
|
18
|
+
Read `IAbxFieldRenderer.sol` first: it documents **the five invariants** (above all, `render()`
|
|
19
|
+
must NEVER revert — a revert bricks the whole `tokenURI`, including the collection surface
|
|
20
|
+
`tokenId == type(uint256).max`).
|
|
21
|
+
- `test/MyRenderer.t.sol` — proves `render()` never reverts (incl. no-seed, no-palette, the
|
|
22
|
+
collection surface, and a fuzz over every seed/tokenId) and that image ↔ traits stay coherent.
|
|
23
|
+
- `script/Deploy.s.sol` — deploys both renderers and prints the addresses + the next command.
|
|
24
|
+
|
|
25
|
+
## Build, test, deploy
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
forge soldeer install # fetch solady + forge-std (pinned in foundry.toml)
|
|
29
|
+
forge build
|
|
30
|
+
forge test # MUST pass — especially the never-revert cases
|
|
31
|
+
|
|
32
|
+
# deploy to your testnet, then copy the printed addresses:
|
|
33
|
+
forge script script/Deploy.s.sol --rpc-url <your-rpc-url> --private-key <key> --broadcast
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Wire it into a drop
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
abx deploy-code \
|
|
40
|
+
--image-renderer <MyRenderer address> \
|
|
41
|
+
--attributes-renderer <MyTraits address> \
|
|
42
|
+
--onchain-uri \
|
|
43
|
+
--schema palette:HexColor:TokenOwner \
|
|
44
|
+
--name "Your Collection" --symbol SYM [--max N] --sign --for <your wallet>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `--onchain-uri` (no `--script`/`--code-dir`) is the in-chain lane — a small Solidity SVG is a
|
|
48
|
+
great fit for an on-chain `tokenURI` (unlike a 200KB JS bundle).
|
|
49
|
+
- `--schema palette:HexColor:TokenOwner` declares the collector param your renderer reads. **Omit it
|
|
50
|
+
and the palette is fixed at the default** — collectors can't set it. After launch, a collector
|
|
51
|
+
runs `abx configure-param <addr> <id> palette #ff3366` and the on-chain image re-tints instantly
|
|
52
|
+
(the renderer reads the live param — nothing to re-render).
|
|
53
|
+
- Verify from chain with `abx tokenuri <addr>` — no server involved.
|
|
54
|
+
|
|
55
|
+
## The renderer contract, in one breath
|
|
56
|
+
|
|
57
|
+
Implement `render(address token, uint256 tokenId, bytes32 field) view → (string contentType, bytes data)`:
|
|
58
|
+
read live state via `IAbxParams(token).tokenParam(...)` / `.contractParam(...)`, compute the field's
|
|
59
|
+
bytes, return them with the right MIME. Never revert. Keep the output small. `forge test` before you wire.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[profile.default]
|
|
2
|
+
src = "src"
|
|
3
|
+
out = "out"
|
|
4
|
+
test = "test"
|
|
5
|
+
script = "script"
|
|
6
|
+
libs = ["dependencies"]
|
|
7
|
+
solc = "0.8.28"
|
|
8
|
+
evm_version = "paris" # broad multi-chain compatibility (no PUSH0/cancun assumptions)
|
|
9
|
+
optimizer = true
|
|
10
|
+
optimizer_runs = 1_000_000 # a renderer is deployed once and called forever → optimize runtime
|
|
11
|
+
|
|
12
|
+
# Dependencies are fetched with soldeer (`forge soldeer install`), pinned here.
|
|
13
|
+
[dependencies]
|
|
14
|
+
solady = "0.1.26"
|
|
15
|
+
forge-std = "1.16.1"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {Script, console2} from "forge-std/Script.sol";
|
|
5
|
+
import {MyRenderer} from "../src/MyRenderer.sol";
|
|
6
|
+
import {MyTraits} from "../src/MyTraits.sol";
|
|
7
|
+
|
|
8
|
+
/// @notice Deploys the image + traits renderers and prints their addresses. Run with:
|
|
9
|
+
/// forge script script/Deploy.s.sol --rpc-url <your-rpc> --private-key <key> --broadcast
|
|
10
|
+
/// then wire them into your drop:
|
|
11
|
+
/// abx deploy-code --image-renderer <MyRenderer addr> --attributes-renderer <MyTraits addr> \
|
|
12
|
+
/// --onchain-uri --schema palette:HexColor:TokenOwner --name "..." --symbol ...
|
|
13
|
+
contract Deploy is Script {
|
|
14
|
+
function run() external {
|
|
15
|
+
vm.startBroadcast();
|
|
16
|
+
MyRenderer image = new MyRenderer();
|
|
17
|
+
MyTraits traits = new MyTraits();
|
|
18
|
+
vm.stopBroadcast();
|
|
19
|
+
console2.log("MyRenderer (image) :", address(image));
|
|
20
|
+
console2.log("MyTraits (attributes) :", address(traits));
|
|
21
|
+
console2.log("Next: abx deploy-code --image-renderer <image> --attributes-renderer <traits> --onchain-uri --schema palette:HexColor:TokenOwner ...");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {LibString} from "solady/utils/LibString.sol";
|
|
5
|
+
import {IAbxFieldRenderer} from "abx/IAbxFieldRenderer.sol";
|
|
6
|
+
import {IAbxParams} from "abx/IAbxParams.sol";
|
|
7
|
+
|
|
8
|
+
/// @title MyRenderer — a fully on-chain generative image, tinted by a collector PostParam
|
|
9
|
+
/// @notice FORK THIS. It renders the `image` field as an `image/svg+xml` document computed
|
|
10
|
+
/// entirely on-chain from:
|
|
11
|
+
/// • the per-token `seed` (mint-time randomness) → the geometry (ring count, weight,
|
|
12
|
+
/// rotation, hues), so every token is a distinct composition; and
|
|
13
|
+
/// • a `palette` (HexColor) PostParam a collector can set → the background tint, so
|
|
14
|
+
/// owners reshape their piece after minting (`abx configure-param <addr> <id> palette #ff3366`).
|
|
15
|
+
///
|
|
16
|
+
/// Wire it: deploy this contract, then
|
|
17
|
+
/// `abx deploy-code --image-renderer <this address> --attributes-renderer <MyTraits> \
|
|
18
|
+
/// --onchain-uri --schema palette:HexColor:TokenOwner --name "..." --symbol ...`
|
|
19
|
+
/// (declare the `palette` schema, or the param is fixed at the default and collectors
|
|
20
|
+
/// can't set it.) Pair with MyTraits.sol so image AND traits read the SAME seed math.
|
|
21
|
+
///
|
|
22
|
+
/// Swap the geometry/palette math below for your own art. Keep the FIVE INVARIANTS in
|
|
23
|
+
/// IAbxFieldRenderer — above all, NEVER revert (a revert bricks the whole tokenURI).
|
|
24
|
+
contract MyRenderer is IAbxFieldRenderer {
|
|
25
|
+
using LibString for uint256;
|
|
26
|
+
|
|
27
|
+
/// @dev Wiring this renderer to a field it doesn't compute is a deploy-time miswiring — fail
|
|
28
|
+
/// loudly on THAT, but never on the field you DO render (invariant #3).
|
|
29
|
+
error UnsupportedField();
|
|
30
|
+
|
|
31
|
+
string private constant CONTENT_TYPE = "image/svg+xml";
|
|
32
|
+
bytes32 private constant F_IMAGE = "image";
|
|
33
|
+
bytes32 private constant SEED_KEY = "seed";
|
|
34
|
+
bytes32 private constant PALETTE_KEY = "palette";
|
|
35
|
+
string private constant DEFAULT_PALETTE = "#0e1a40";
|
|
36
|
+
|
|
37
|
+
/// @inheritdoc IAbxFieldRenderer
|
|
38
|
+
function render(address token, uint256 tokenId, bytes32 field)
|
|
39
|
+
external
|
|
40
|
+
view
|
|
41
|
+
returns (string memory contentType, bytes memory data)
|
|
42
|
+
{
|
|
43
|
+
if (field != F_IMAGE) revert UnsupportedField();
|
|
44
|
+
|
|
45
|
+
// Invariant #1: the collection surface (contractURI) has no token — return a neutral
|
|
46
|
+
// card, never a revert.
|
|
47
|
+
if (tokenId == type(uint256).max) {
|
|
48
|
+
return (CONTENT_TYPE, bytes(_svg(keccak256("collection"), DEFAULT_PALETTE)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bytes32 seed = _seed(token, tokenId); // always renderable (falls back if unset)
|
|
52
|
+
string memory palette = _palette(token, tokenId); // "#rrggbb", default if unset
|
|
53
|
+
return (CONTENT_TYPE, bytes(_svg(seed, palette)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── the art: seed → geometry, palette → tint (replace with your own) ──────────
|
|
57
|
+
|
|
58
|
+
function _svg(bytes32 seed, string memory palette) private pure returns (string memory) {
|
|
59
|
+
uint256 rings = 3 + (uint8(seed[0]) % 6); // 3..8 rings (MUST match MyTraits' math)
|
|
60
|
+
uint256 weight = 1 + (uint8(seed[1]) % 3); // stroke 1..3
|
|
61
|
+
uint256 rot = uint8(seed[2]) % 360; // whole-piece rotation
|
|
62
|
+
uint256 baseHue = (uint256(uint8(seed[3])) * 360) / 256; // this token's colorway
|
|
63
|
+
|
|
64
|
+
string memory circles;
|
|
65
|
+
for (uint256 i = 1; i <= rings; ++i) {
|
|
66
|
+
uint256 r = (46 * i) / rings;
|
|
67
|
+
uint256 hue = (baseHue + i * 40) % 360;
|
|
68
|
+
circles = string.concat(
|
|
69
|
+
circles,
|
|
70
|
+
'<circle cx="50" cy="50" r="',
|
|
71
|
+
r.toString(),
|
|
72
|
+
'" fill="none" stroke="hsl(',
|
|
73
|
+
hue.toString(),
|
|
74
|
+
',72%,58%)" stroke-width="',
|
|
75
|
+
weight.toString(),
|
|
76
|
+
'" stroke-opacity="0.85"/>'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return string.concat(
|
|
81
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">',
|
|
82
|
+
'<rect width="100" height="100" fill="',
|
|
83
|
+
palette,
|
|
84
|
+
'"/><g transform="rotate(',
|
|
85
|
+
rot.toString(),
|
|
86
|
+
' 50 50)">',
|
|
87
|
+
circles,
|
|
88
|
+
'</g></svg>'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── param reads (mirror these in your traits renderer for coherence) ──────────
|
|
93
|
+
|
|
94
|
+
/// @dev The token's `seed`, or a deterministic fallback so the image is ALWAYS renderable
|
|
95
|
+
/// (pre-mint preview, or a project with no seed source). Never reverts.
|
|
96
|
+
function _seed(address token, uint256 tokenId) private view returns (bytes32) {
|
|
97
|
+
(bytes32 value, bool isHash) = _param(token, tokenId, SEED_KEY);
|
|
98
|
+
if (uint256(value) != 0 && !isHash) return value;
|
|
99
|
+
return keccak256(abi.encodePacked(token, tokenId));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// @dev `palette` (HexColor) → "#rrggbb". Token scope wins over contract scope; a default
|
|
103
|
+
/// when unset. HexColor stores the RGB in the low 3 bytes of the param value.
|
|
104
|
+
function _palette(address token, uint256 tokenId) private view returns (string memory) {
|
|
105
|
+
(bytes32 value, bool isHash) = _param(token, tokenId, PALETTE_KEY);
|
|
106
|
+
if (uint256(value) == 0 || isHash) return DEFAULT_PALETTE;
|
|
107
|
+
return string.concat("#", LibString.toHexStringNoPrefix(uint256(value) & 0xffffff, 3));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// @dev token-scope value, falling back to contract-scope (the tokenData merge rule). Returns
|
|
111
|
+
/// the value + whether it's a keccak commitment (a literal scalar param is NOT a hash).
|
|
112
|
+
function _param(address token, uint256 tokenId, bytes32 key)
|
|
113
|
+
private
|
|
114
|
+
view
|
|
115
|
+
returns (bytes32 value, bool valueIsHash)
|
|
116
|
+
{
|
|
117
|
+
bool isSet;
|
|
118
|
+
(value, valueIsHash, isSet) = IAbxParams(token).tokenParam(tokenId, key);
|
|
119
|
+
if (isSet) return (value, valueIsHash);
|
|
120
|
+
(value, valueIsHash,) = IAbxParams(token).contractParam(key);
|
|
121
|
+
return (value, valueIsHash);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {LibString} from "solady/utils/LibString.sol";
|
|
5
|
+
import {IAbxFieldRenderer} from "abx/IAbxFieldRenderer.sol";
|
|
6
|
+
import {IAbxParams} from "abx/IAbxParams.sol";
|
|
7
|
+
|
|
8
|
+
/// @title MyTraits — on-chain marketplace traits, coherent with MyRenderer
|
|
9
|
+
/// @notice FORK THIS. Renders the `attributes` field as an `application/json` array a marketplace
|
|
10
|
+
/// reads. Wire with `abx deploy-code --attributes-renderer <this address> --onchain-uri`.
|
|
11
|
+
///
|
|
12
|
+
/// COHERENCE RULE: read the SAME seed bytes the SAME way as your image renderer, so a
|
|
13
|
+
/// trait can never disagree with what's drawn (here `Rings` = `3 + seed[0] % 6`, byte-
|
|
14
|
+
/// identical to MyRenderer). Derive traits from the seed with integer/threshold math so
|
|
15
|
+
/// they're exactly reproducible on-chain (a raw float or `Math.random` would not be).
|
|
16
|
+
contract MyTraits is IAbxFieldRenderer {
|
|
17
|
+
using LibString for uint256;
|
|
18
|
+
|
|
19
|
+
error UnsupportedField();
|
|
20
|
+
|
|
21
|
+
string private constant CONTENT_TYPE = "application/json";
|
|
22
|
+
bytes32 private constant F_ATTRIBUTES = "attributes";
|
|
23
|
+
bytes32 private constant SEED_KEY = "seed";
|
|
24
|
+
bytes32 private constant PALETTE_KEY = "palette";
|
|
25
|
+
|
|
26
|
+
/// @inheritdoc IAbxFieldRenderer
|
|
27
|
+
function render(address token, uint256 tokenId, bytes32 field)
|
|
28
|
+
external
|
|
29
|
+
view
|
|
30
|
+
returns (string memory contentType, bytes memory data)
|
|
31
|
+
{
|
|
32
|
+
if (field != F_ATTRIBUTES) revert UnsupportedField();
|
|
33
|
+
|
|
34
|
+
// Invariant #1: the collection surface has no per-token traits — an empty array, not a revert.
|
|
35
|
+
if (tokenId == type(uint256).max) return (CONTENT_TYPE, bytes("[]"));
|
|
36
|
+
|
|
37
|
+
bytes32 seed = _seed(token, tokenId);
|
|
38
|
+
uint256 rings = 3 + (uint8(seed[0]) % 6); // SAME math as MyRenderer._svg
|
|
39
|
+
(bytes32 pal, bool palIsHash) = _param(token, tokenId, PALETTE_KEY);
|
|
40
|
+
bool paletteSet = uint256(pal) != 0 && !palIsHash;
|
|
41
|
+
|
|
42
|
+
string memory json = string.concat(
|
|
43
|
+
"[",
|
|
44
|
+
_trait("Rings", rings.toString()),
|
|
45
|
+
",",
|
|
46
|
+
_trait("Palette", paletteSet ? "Custom" : "Default"),
|
|
47
|
+
"]"
|
|
48
|
+
);
|
|
49
|
+
return (CONTENT_TYPE, bytes(json));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _trait(string memory k, string memory v) private pure returns (string memory) {
|
|
53
|
+
return string.concat(
|
|
54
|
+
'{"trait_type":"', LibString.escapeJSON(k), '","value":"', LibString.escapeJSON(v), '"}'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _seed(address token, uint256 tokenId) private view returns (bytes32) {
|
|
59
|
+
(bytes32 value, bool isHash) = _param(token, tokenId, SEED_KEY);
|
|
60
|
+
if (uint256(value) != 0 && !isHash) return value;
|
|
61
|
+
return keccak256(abi.encodePacked(token, tokenId));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _param(address token, uint256 tokenId, bytes32 key)
|
|
65
|
+
private
|
|
66
|
+
view
|
|
67
|
+
returns (bytes32 value, bool valueIsHash)
|
|
68
|
+
{
|
|
69
|
+
bool isSet;
|
|
70
|
+
(value, valueIsHash, isSet) = IAbxParams(token).tokenParam(tokenId, key);
|
|
71
|
+
if (isSet) return (value, valueIsHash);
|
|
72
|
+
(value, valueIsHash,) = IAbxParams(token).contractParam(key);
|
|
73
|
+
return (value, valueIsHash);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
/// @title IAbxFieldRenderer — the contract your in-chain art implements
|
|
5
|
+
/// @notice A field renderer returns ONE metadata field's value, COMPUTED from chain state
|
|
6
|
+
/// (the token's seed + PostParams, its owner, anything readable on-chain). The ABX
|
|
7
|
+
/// metadata renderer staticcalls it to build `tokenURI` — e.g. an `image` renderer's
|
|
8
|
+
/// SVG becomes `data:image/svg+xml;base64,…`. Deploy your renderer, then wire it with
|
|
9
|
+
/// `abx deploy-code --image-renderer <address> [--attributes-renderer <address>] --onchain-uri`.
|
|
10
|
+
///
|
|
11
|
+
/// @dev THE FIVE INVARIANTS (a reverting or malformed renderer bricks the WHOLE tokenURI —
|
|
12
|
+
/// the metadata renderer staticcalls you with NO try/catch):
|
|
13
|
+
/// 1. NEVER revert for ANY (token, tokenId, field) — including the collection surface
|
|
14
|
+
/// `tokenId == type(uint256).max` (used by `contractURI`). Return a neutral value.
|
|
15
|
+
/// 2. Return the correct `contentType` (`image/svg+xml` for an image; `application/json`
|
|
16
|
+
/// for an `attributes` array).
|
|
17
|
+
/// 3. Guard `field`: revert only a truly unsupported field (a deploy-time miswiring), but
|
|
18
|
+
/// still never revert on the fields you DO render.
|
|
19
|
+
/// 4. `view` + deterministic: same chain state → same bytes.
|
|
20
|
+
/// 5. Bounded output: keep it small (a few hundred bytes of SVG) so `tokenURI` stays a
|
|
21
|
+
/// cheap single eth_call. `forge test` the never-revert cases before you wire it.
|
|
22
|
+
interface IAbxFieldRenderer {
|
|
23
|
+
/// @param token the ABX collection contract (read its params via {IAbxParams}).
|
|
24
|
+
/// @param tokenId the token, or `type(uint256).max` for the collection surface.
|
|
25
|
+
/// @param field which field to compute (e.g. `"image"`, `"attributes"`).
|
|
26
|
+
/// @return contentType MIME of `data` (e.g. `image/svg+xml`, `application/json`).
|
|
27
|
+
/// @return data the field's finished bytes.
|
|
28
|
+
function render(address token, uint256 tokenId, bytes32 field)
|
|
29
|
+
external
|
|
30
|
+
view
|
|
31
|
+
returns (string memory contentType, bytes memory data);
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
/// @title IAbxParams — read the token's on-chain params (seed + collector PostParams)
|
|
5
|
+
/// @notice Your renderer reads live state through this. Each value is a `bytes32`; `isSet` tells
|
|
6
|
+
/// you whether it exists (fall back to a default if not — see invariant #1). `valueIsHash`
|
|
7
|
+
/// is true when the on-chain value is a keccak commitment to off-chain bytes (a literal
|
|
8
|
+
/// scalar param like a `seed` or a `HexColor` palette is NOT a hash — check `!valueIsHash`).
|
|
9
|
+
///
|
|
10
|
+
/// The tokenData merge rule: a token-scope value overrides the contract-scope one. The
|
|
11
|
+
/// common pattern is `tokenParam` first, then `contractParam` as a fallback (see the
|
|
12
|
+
/// `_param` helper in MyRenderer.sol).
|
|
13
|
+
interface IAbxParams {
|
|
14
|
+
/// Returns the token-scope param: its bytes32 `value`, `valueIsHash` (true if it commits to
|
|
15
|
+
/// off-chain bytes — a literal scalar like a seed/HexColor is false), and `isSet` (does it exist).
|
|
16
|
+
function tokenParam(uint256 tokenId, bytes32 key)
|
|
17
|
+
external
|
|
18
|
+
view
|
|
19
|
+
returns (bytes32 value, bool valueIsHash, bool isSet);
|
|
20
|
+
|
|
21
|
+
/// @notice The contract-scope (collection-wide) value — the fallback when a token has none.
|
|
22
|
+
function contractParam(bytes32 key)
|
|
23
|
+
external
|
|
24
|
+
view
|
|
25
|
+
returns (bytes32 value, bool valueIsHash, bool isSet);
|
|
26
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {MyRenderer} from "../src/MyRenderer.sol";
|
|
6
|
+
import {MyTraits} from "../src/MyTraits.sol";
|
|
7
|
+
import {IAbxParams} from "abx/IAbxParams.sol";
|
|
8
|
+
|
|
9
|
+
/// A tiny stand-in for the ABX contract's param surface, so the renderers can be tested in
|
|
10
|
+
/// isolation — set a seed / palette (or don't) and assert the renderer behaves.
|
|
11
|
+
contract MockParams is IAbxParams {
|
|
12
|
+
mapping(uint256 => mapping(bytes32 => bytes32)) private tv;
|
|
13
|
+
mapping(uint256 => mapping(bytes32 => bool)) private ts;
|
|
14
|
+
mapping(bytes32 => bytes32) private cv;
|
|
15
|
+
mapping(bytes32 => bool) private cs;
|
|
16
|
+
|
|
17
|
+
function setToken(uint256 id, bytes32 key, bytes32 val) external { tv[id][key] = val; ts[id][key] = true; }
|
|
18
|
+
function setContract(bytes32 key, bytes32 val) external { cv[key] = val; cs[key] = true; }
|
|
19
|
+
|
|
20
|
+
function tokenParam(uint256 id, bytes32 key) external view returns (bytes32, bool, bool) {
|
|
21
|
+
return (tv[id][key], false, ts[id][key]);
|
|
22
|
+
}
|
|
23
|
+
function contractParam(bytes32 key) external view returns (bytes32, bool, bool) {
|
|
24
|
+
return (cv[key], false, cs[key]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
contract MyRendererTest is Test {
|
|
29
|
+
MyRenderer img;
|
|
30
|
+
MyTraits traits;
|
|
31
|
+
MockParams params;
|
|
32
|
+
|
|
33
|
+
bytes32 constant IMAGE = "image";
|
|
34
|
+
bytes32 constant ATTRIBUTES = "attributes";
|
|
35
|
+
|
|
36
|
+
function setUp() public {
|
|
37
|
+
img = new MyRenderer();
|
|
38
|
+
traits = new MyTraits();
|
|
39
|
+
params = new MockParams();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _has(bytes memory hay, string memory needle) internal pure returns (bool) {
|
|
43
|
+
bytes memory n = bytes(needle);
|
|
44
|
+
if (n.length == 0 || hay.length < n.length) return n.length == 0;
|
|
45
|
+
for (uint256 i = 0; i <= hay.length - n.length; ++i) {
|
|
46
|
+
bool ok = true;
|
|
47
|
+
for (uint256 j = 0; j < n.length; ++j) if (hay[i + j] != n[j]) { ok = false; break; }
|
|
48
|
+
if (ok) return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function test_image_contentTypeAndSvg() public {
|
|
54
|
+
params.setToken(0, "seed", bytes32(uint256(0x1234)));
|
|
55
|
+
(string memory ct, bytes memory data) = img.render(address(params), 0, IMAGE);
|
|
56
|
+
assertEq(ct, "image/svg+xml");
|
|
57
|
+
assertTrue(_has(data, "<svg"));
|
|
58
|
+
assertTrue(_has(data, "</svg>"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function test_palette_tintsTheImage() public {
|
|
62
|
+
params.setToken(0, "seed", bytes32(uint256(0x1234)));
|
|
63
|
+
params.setToken(0, "palette", bytes32(uint256(0xff3366))); // RGB in low 3 bytes
|
|
64
|
+
(, bytes memory data) = img.render(address(params), 0, IMAGE);
|
|
65
|
+
assertTrue(_has(data, "#ff3366"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function test_neverReverts_noSeedNoPalette() public view {
|
|
69
|
+
// unset seed + palette → deterministic fallback, still a valid SVG, no revert
|
|
70
|
+
(string memory ct, bytes memory data) = img.render(address(params), 7, IMAGE);
|
|
71
|
+
assertEq(ct, "image/svg+xml");
|
|
72
|
+
assertTrue(_has(data, "<svg"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function test_neverReverts_collectionSurface() public view {
|
|
76
|
+
// tokenId == type(uint256).max is the collection surface (contractURI) — MUST NOT revert
|
|
77
|
+
(string memory ct, bytes memory data) = img.render(address(params), type(uint256).max, IMAGE);
|
|
78
|
+
assertEq(ct, "image/svg+xml");
|
|
79
|
+
assertTrue(_has(data, "<svg"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function test_unsupportedField_reverts() public {
|
|
83
|
+
vm.expectRevert(MyRenderer.UnsupportedField.selector);
|
|
84
|
+
img.render(address(params), 0, ATTRIBUTES);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function test_traits_coherentWithImage() public {
|
|
88
|
+
bytes32 seed = bytes32(uint256(0xABCDEF));
|
|
89
|
+
params.setToken(0, "seed", seed);
|
|
90
|
+
(string memory ct, bytes memory data) = traits.render(address(params), 0, ATTRIBUTES);
|
|
91
|
+
assertEq(ct, "application/json");
|
|
92
|
+
// Rings trait must equal the image's ring count: 3 + seed[0] % 6
|
|
93
|
+
uint256 rings = 3 + (uint8(seed[0]) % 6);
|
|
94
|
+
assertTrue(_has(data, string.concat('"value":"', vm.toString(rings), '"')));
|
|
95
|
+
assertTrue(_has(data, "Rings"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function test_traits_collectionSurface_emptyArray() public view {
|
|
99
|
+
(, bytes memory data) = traits.render(address(params), type(uint256).max, ATTRIBUTES);
|
|
100
|
+
assertEq(string(data), "[]");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The whole point: render() must NOT revert for ANY seed / tokenId. A revert here bricks tokenURI.
|
|
104
|
+
function testFuzz_imageNeverReverts(uint256 tokenId, bytes32 seed, uint24 rgb) public {
|
|
105
|
+
params.setToken(tokenId, "seed", seed);
|
|
106
|
+
params.setToken(tokenId, "palette", bytes32(uint256(rgb)));
|
|
107
|
+
(, bytes memory data) = img.render(address(params), tokenId, IMAGE);
|
|
108
|
+
assertTrue(data.length > 0);
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ArweaveJwk, type ResolveStorageOptions } from '@artblocks/abx-storage';
|
|
2
|
+
import { type Address } from '@artblocks/abx-sdk';
|
|
3
|
+
export declare function dataDir(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Whether `baseUrl` is a localhost/loopback address — a URL that, once baked into an on-chain
|
|
6
|
+
* `tokenURI`/`contractURI`, resolves for NO ONE off this machine. The single source of truth for
|
|
7
|
+
* the off-chain-deploy refusal shared by `deploy`, `deploy-series`, and `deploy-code` (previously
|
|
8
|
+
* duplicated inline — that drift is exactly how `deploy-code` shipped without the guard).
|
|
9
|
+
*/
|
|
10
|
+
export declare function loopbackBaseUrl(baseUrl: string): boolean;
|
|
11
|
+
/** Canonical clone factory for the active chain — flag override → `ABX_FACTORY` → manifest. */
|
|
12
|
+
export declare function factoryAddress(override?: string): Address | null;
|
|
13
|
+
/** Canonical Series clone factory — flag override → `ABX_SERIES_FACTORY` → manifest. */
|
|
14
|
+
export declare function seriesFactoryAddress(override?: string): Address | null;
|
|
15
|
+
/** Canonical on-chain metadata renderer — flag override → `ABX_RENDERER` → manifest. */
|
|
16
|
+
export declare function rendererAddress(override?: string): Address | null;
|
|
17
|
+
/** Canonical multi-chunk on-chain content store — flag override → `ABX_CHUNK_STORE` → manifest. */
|
|
18
|
+
export declare function chunkStoreAddress(override?: string): Address | null;
|
|
19
|
+
/** Canonical shared fixed-price minter — flag override → `ABX_FIXED_PRICE_MINTER` → manifest. */
|
|
20
|
+
export declare function fixedPriceMinterAddress(override?: string): Address | null;
|
|
21
|
+
/** Per-invocation storage overrides parsed from flags (e.g. `abx deploy --backend cloud --bucket …`). */
|
|
22
|
+
export interface StorageOverrides {
|
|
23
|
+
backend?: string;
|
|
24
|
+
endpoint?: string;
|
|
25
|
+
bucket?: string;
|
|
26
|
+
region?: string;
|
|
27
|
+
prefix?: string;
|
|
28
|
+
publicBase?: string;
|
|
29
|
+
gateway?: string;
|
|
30
|
+
mode?: 'kubo' | 'pinata';
|
|
31
|
+
apiUrl?: string;
|
|
32
|
+
uploadUrl?: string;
|
|
33
|
+
provider?: 'turbo' | 'http-bundler';
|
|
34
|
+
/** Which identity signs Turbo (arweave) uploads + holds the credits: the CLI-managed Arweave key
|
|
35
|
+
* (default) or the `.env` EVM signing key (`eth`). The browser-wallet identity is injected by
|
|
36
|
+
* the deploy path, not chosen here. */
|
|
37
|
+
storageSigner?: 'arweave' | 'eth';
|
|
38
|
+
}
|
|
39
|
+
/** Resolve the storage-signer lane: flag → `ABX_STORAGE_SIGNER` → default `arweave`. */
|
|
40
|
+
export declare function storageSignerChoice(ov?: StorageOverrides): 'arweave' | 'eth';
|
|
41
|
+
/**
|
|
42
|
+
* Build the storage backend options the SDK needs, resolving **explicit override → env →
|
|
43
|
+
* default** for non-secret values; secrets come from env only. No persisted config layer — a
|
|
44
|
+
* flag or a declared env var is the only way to choose a backend, so nothing is hidden.
|
|
45
|
+
*/
|
|
46
|
+
export declare function storageOptions(ov?: StorageOverrides): ResolveStorageOptions;
|
|
47
|
+
/** The active backend id, for display (no secrets, no construction). */
|
|
48
|
+
export declare function activeBackendId(): string;
|
|
49
|
+
/** Resolve the active backend id + where it came from — for transparent readouts. */
|
|
50
|
+
export declare function backendResolution(ov?: StorageOverrides): {
|
|
51
|
+
backend: string;
|
|
52
|
+
source: 'flag' | 'env' | 'default';
|
|
53
|
+
};
|
|
54
|
+
/** Where the managed Turbo identity lives — for display ("back this up"). */
|
|
55
|
+
export declare function arweaveKeyFilePath(): string;
|
|
56
|
+
/**
|
|
57
|
+
* Load the Turbo identity if one exists — env inline secret, an explicit key-file path, or the
|
|
58
|
+
* managed location. **Never creates** (read-only paths like `serve`/`status` must not mint a key)
|
|
59
|
+
* and **never silently replaces** a corrupt key (that would strand its credits) — a bad key throws.
|
|
60
|
+
*/
|
|
61
|
+
export declare function loadArweaveJwk(): ArweaveJwk | undefined;
|
|
62
|
+
/** Load or CREATE the managed Turbo identity, persisting a fresh key (0600) to the data dir. */
|
|
63
|
+
export declare function ensureArweaveJwk(): {
|
|
64
|
+
jwk: ArweaveJwk;
|
|
65
|
+
address: string;
|
|
66
|
+
created: boolean;
|
|
67
|
+
path: string;
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EASL,KAAK,OAAO,EACb,MAAM,oBAAoB,CAAC;AAoB5B,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAOD,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAEhE;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAEtE;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAEjE;AAED,mGAAmG;AACnG,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAEnE;AAED,iGAAiG;AACjG,wBAAgB,uBAAuB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAEzE;AAID,yGAAyG;AACzG,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACpC;;4CAEwC;IACxC,aAAa,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC;CACnC;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,EAAE,GAAE,gBAAqB,GAAG,SAAS,GAAG,KAAK,CAGhF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAE,gBAAqB,GAAG,qBAAqB,CAgD/E;AAED,wEAAwE;AACxE,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,EAAE,GAAE,gBAAqB,GAAG;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAAA;CAAC,CAIlH;AAaD,6EAA6E;AAC7E,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAOD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,UAAU,GAAG,SAAS,CAKvD;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,IAAI;IAAC,GAAG,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAQrG"}
|