diff --git a/docs/known-blockers.md b/docs/known-blockers.md index 2cb2ba59..a709c689 100644 --- a/docs/known-blockers.md +++ b/docs/known-blockers.md @@ -60,13 +60,15 @@ contributors who start from the README. - Dependency script retrieval now has segment-safe typed chunk and content hashes, so the former packed/dynamic chunk-boundary Slither finding is fixed. Current metadata golden fixtures now lock the pre-beta off-chain pending/final - URI behavior and current on-chain pending/final JSON output. ERC-4906 support - and current `MetadataUpdate` / `BatchMetadataUpdate` semantics now cover - `StreamCore` metadata mutations; dependency-registry reverse signaling remains - part of dependency versioning/freeze work. Remaining - metadata blockers include the final schema-versioned public-beta metadata - implementation, collection freeze manifests, immutable dependency version - records, registry identity pinning, burn semantics, escaping, and size limits. + URI behavior and schema-v1 on-chain pending/final base64 JSON output with + explicit `metadata_schema_version` and `metadata_state` fields. Pending + on-chain metadata no longer runs final generative HTML with a zero token hash. + ERC-4906 support and current `MetadataUpdate` / `BatchMetadataUpdate` + semantics now cover `StreamCore` metadata mutations; dependency-registry + reverse signaling remains part of dependency versioning/freeze work. + Remaining metadata blockers include collection freeze manifests, immutable + dependency version records, registry identity pinning, burn semantics, + escaping, and size limits. - Dead public/allowlist mint-count mappings and retrieval APIs were removed from `StreamCore`; the retained airdrop counter now has explicit regression tests for zero initial state, authorized increments, and failed-mint rollback. diff --git a/docs/metadata.md b/docs/metadata.md index d798d453..59bf2576 100644 --- a/docs/metadata.md +++ b/docs/metadata.md @@ -25,21 +25,30 @@ would produce `https://example.com/collections/abcpending` and Current on-chain metadata is returned as: ```text -data:application/json;utf8, +data:application/json;base64, ``` The current JSON includes: +- `metadata_schema_version` with value `6529stream-v1` +- `metadata_state` with value `pending` or `final` - `name` - `description` - `image` - `attributes` -- `animation_url` +- `animation_url` for final on-chain metadata only -The current on-chain pending output still embeds the zero token hash in the -generated HTML. ADR 0006 rejects that as the final public-beta behavior; future -metadata work must replace it with an explicit pending state and update the -golden fixtures intentionally. +Pending on-chain metadata no longer runs the final generative HTML path with a +zero token hash. It returns schema-versioned JSON with +`metadata_state: "pending"` and omits `animation_url`. Final on-chain metadata +returns schema-versioned JSON with `metadata_state: "final"` and the existing +base64-encoded HTML animation URL. + +`StreamCore.metadataSchemaVersion()` exposes the active schema version and +`StreamCore.tokenMetadataState(tokenId)` exposes the current `pending` or +`final` state for minted tokens. The current schema version does not yet solve +JSON escaping, raw attribute validation, metadata size limits, freeze manifests, +dependency immutability, stale randomness display, or burn metadata semantics. ## Golden Fixtures @@ -47,12 +56,11 @@ golden fixtures intentionally. - `test/fixtures/metadata/offchain-pending-token-uri.txt` - `test/fixtures/metadata/offchain-final-token-uri.txt` -- `test/fixtures/metadata/current-onchain-pending-token-uri.txt` -- `test/fixtures/metadata/current-onchain-final-token-uri.txt` +- `test/fixtures/metadata/onchain-pending-schema-v1-token-uri.txt` +- `test/fixtures/metadata/onchain-final-schema-v1-token-uri.txt` -The fixture names use `current-onchain-*` because the current on-chain JSON is -not yet the accepted public-beta schema. These fixtures are meant to make -metadata changes reviewable and deliberate. +The on-chain fixture names include the schema version so later schema migrations +are reviewable and deliberate. ## ERC-4906 Events @@ -93,9 +101,7 @@ P1-META-003. ADR 0006 requires future metadata work to add: -- schema version fields -- explicit pending, final, stale, and burned-state policy -- base64 JSON data URIs for on-chain metadata +- stale and burned-state policy - JSON escaping and raw-attribute validation - freeze manifests and immutable dependency version pins - burn semantics and callback-after-burn tests diff --git a/docs/status.md b/docs/status.md index 8d2d1597..b88e831d 100644 --- a/docs/status.md +++ b/docs/status.md @@ -35,10 +35,13 @@ The current Gate A smoke baseline proves: - Metadata tests now prove dependency chunk boundaries are included in typed content hashes while preserving the existing rendered generative script output, and `StreamMetadataGolden.t.sol` locks current off-chain pending/final - URIs plus current on-chain pending/final JSON data URIs against fixtures. - `StreamMetadataEvents.t.sol` proves ERC-4906 interface support and current - metadata update event semantics for token-level updates, collection-range - updates, randomness fulfillment, mint-only paths, and burn. + URIs plus schema-v1 on-chain pending/final base64 JSON data URIs against + fixtures. The on-chain schema exposes `metadata_schema_version` and + `metadata_state`, and pending on-chain metadata no longer runs final + generative HTML with a zero token hash. `StreamMetadataEvents.t.sol` proves + ERC-4906 interface support and current metadata update event semantics for + token-level updates, collection-range updates, randomness fulfillment, + mint-only paths, and burn. - CI can run the same build/test smoke commands and publish logs. The current tests are regression tripwires, not a correctness proof. Known @@ -47,8 +50,8 @@ pull-payment ledger abstraction or protocol-wide aggregation layer, fuller randomizer reserve lifecycle accounting, callback-after-burn policy, canonical randomizer lifecycle ownership, lower-impact static-analysis cleanup beyond the now-triaged -high/medium baseline, signer/deployment ceremony runbooks, final metadata -schema/escaping, dependency version/freeze manifest work, deployment +high/medium baseline, signer/deployment ceremony runbooks, metadata escaping, +dependency version/freeze manifest work, burn metadata policy, deployment discipline, and the broader P0/P1 test suite. Contributor and security intake files exist so future work can be packaged and diff --git a/ops/AUTONOMOUS_RUN.md b/ops/AUTONOMOUS_RUN.md index a61c4797..42b56e09 100644 --- a/ops/AUTONOMOUS_RUN.md +++ b/ops/AUTONOMOUS_RUN.md @@ -33,11 +33,11 @@ tests, security hardening, deployment discipline, and release/audit readiness. | Field | Value | | --- | --- | | Remote | `https://github.com/6529-Collections/6529Stream.git` | -| Active PR branch | `codex/metadata-erc4906-events` | -| Last merged PR | `https://github.com/6529-Collections/6529Stream/pull/81` | +| Active PR branch | `codex/metadata-schema-state` | +| Last merged PR | `https://github.com/6529-Collections/6529Stream/pull/82` | | Roadmap file | `ops/ROADMAP.md` | | State file | `ops/AUTONOMOUS_RUN.md` | -| Last updated | `2026-06-10 23:15 UTC` | +| Last updated | `2026-06-10 23:55 UTC` | ## Packaging Notes @@ -93,7 +93,8 @@ The queue will evolve as PRs merge and bot feedback arrives. | 36 | Add payment ledger view aliases | Gate C/Gate D | Expose missing ADR 0003 local-ledger view names such as `totalReserved()` and `surplus()`, add category aliases where useful, assert them in payment invariants, and reconcile P0-PAY-002 roadmap state | Merged in PR #78 | | 37 | Add signer lifecycle manager | Gate B1/Gate C | Implement P0-ADMIN-003 by separating drop-signing identity from signer-management authority, adding signer-manager role tests, proving rotation invalidates stale payloads, and updating ADR/roadmap state | Merged in PR #80 | | 38 | Add metadata schema and golden-file tests | Gate D | Implement the first P1-META-001 test/docs slice: lock current off-chain pending/final tokenURI behavior, add on-chain JSON golden fixtures where feasible, document schema fields, and update roadmap/test traceability | Merged in PR #81 | -| 39 | Add ERC-4906 metadata update signaling | Gate D | Implement P1-META-004 for `StreamCore`: interface support, token-level and collection-range metadata update events, no misleading mint/burn-only events, docs, and roadmap/test traceability | PR #82 open; CodeRabbit inline review fix ready to push | +| 39 | Add ERC-4906 metadata update signaling | Gate D | Implement P1-META-004 for `StreamCore`: interface support, token-level and collection-range metadata update events, no misleading mint/burn-only events, docs, and roadmap/test traceability | Merged in PR #82 | +| 40 | Add schema-v1 metadata state outputs | Gate D | Continue P1-META-001 by adding schema-versioned on-chain base64 JSON, explicit pending/final metadata state views, golden fixtures, docs, and roadmap/test traceability | PR #83 open on `codex/metadata-schema-state` | ## Current PR Worklog @@ -3341,7 +3342,7 @@ Outcome: ### PR #82: Add ERC-4906 metadata update signaling (Queue Item 39) -Status: PR open; CodeRabbit inline review fix applied locally and ready to push. +Status: Merged in PR #82. Branch: `codex/metadata-erc4906-events`. Pull request: `https://github.com/6529-Collections/6529Stream/pull/82`. Related issue: @@ -3435,7 +3436,108 @@ Review requests: and implemented locally. - CodeRabbit inline review thread `PRRT_kwDOM7REis6IpTxK` / review `4472309839` identified the raw-topic negative-test gap; the local follow-up - is ready to push for rereview. + was pushed in commit `87e9634bb93681db7ffe3e9dec5bfcfd657614c5`. +- CodeRabbit latest-head comment `4675594572` confirmed all review items were + addressed, the thread was resolved, and no concerns remained. +- GitHub Actions CI passed in run `27312549432`. +- The aggregate CodeRabbit commit status stayed pending after the clean + latest-head review, so PR comment `4675606561` documented the autonomous + maintainer merge decision using green CI, resolved threads, and explicit + CodeRabbit approval evidence. +- Claude remains intentionally skipped per current user instruction; use + CodeRabbit unless risk or future user instruction changes. + +Merge: + +- Squash merge commit: `944f614688ea15ec6cd7317a940978dfa9aaeeb3`. +- Merged at `2026-06-10 23:20 UTC`. +- Issue `#49` closed by PR merge. + +### PR #83: Add schema-v1 metadata state outputs (Queue Item 40) + +Status: PR open; CodeRabbit assertion-label nitpick accepted and ready to push. +Branch: `codex/metadata-schema-state`. +Pull request: `https://github.com/6529-Collections/6529Stream/pull/83`. +Related issue: + +- `https://github.com/6529-Collections/6529Stream/issues/46` + +Goal: + +- Continue P1-META-001 after the current-output golden baseline. +- Add an explicit `metadata_schema_version` field to on-chain JSON. +- Add an explicit `metadata_state` field for pending and final on-chain JSON. +- Base64-encode on-chain JSON data URIs. +- Stop pending on-chain metadata from running final generative HTML with a zero + token hash. +- Expose public schema/state views for tests and integrators. +- Update golden fixtures, docs, roadmap, and test traceability. + +Candidate files: + +- `smart-contracts/StreamCore.sol` +- `test/StreamMetadataGolden.t.sol` +- `test/fixtures/metadata/onchain-pending-schema-v1-token-uri.txt` +- `test/fixtures/metadata/onchain-final-schema-v1-token-uri.txt` +- `docs/metadata.md` +- `docs/status.md` +- `docs/known-blockers.md` +- `test/README.md` +- `ops/ROADMAP.md` +- `ops/AUTONOMOUS_RUN.md` + +Implementation notes: + +- Added `METADATA_SCHEMA_VERSION = "6529stream-v1"` plus public + `metadataSchemaVersion()` and `tokenMetadataState(tokenId)` views. +- Off-chain token URI behavior remains compatibility-preserving: + `baseURI + "pending"` before final randomness and `baseURI + tokenId` after + final randomness. +- On-chain token URI output now uses `data:application/json;base64,`. +- Pending on-chain JSON includes `metadata_state: "pending"` and omits + `animation_url`. +- Final on-chain JSON includes `metadata_state: "final"` and preserves the + existing base64 HTML animation URL. +- CodeRabbit correctly noted that `bytes32(0)` is now the pending sentinel, so + `setTokenHash` must reject zero hashes at the randomizer write boundary. +- Added the nonzero-hash guard plus a regression proving a configured + randomizer cannot finalize a token with `bytes32(0)` and the metadata state + remains pending after the rejected write. +- This PR intentionally leaves JSON escaping, raw-attribute validation, stale + state display, freeze manifests, dependency immutability, and burn semantics + to the remaining P1-META issues. + +Validation so far: + +- Focused metadata golden tests passed: + `forge test --match-contract StreamMetadataGoldenTest -vvv` with 6 tests, + 0 failed, after the CodeRabbit zero-hash guard fix. +- Full canonical local gate passed: `make check` with 212 tests, 0 failed. +- Windows wrapper passed: + `powershell -ExecutionPolicy Bypass -File scripts\check.ps1` with 212 tests, + 0 failed. +- Touched-file formatting passed: + `forge fmt --check smart-contracts\StreamCore.sol test\StreamMetadataGolden.t.sol`. +- Diff whitespace check passed: `git diff --check`. +- Markdown heading scan passed for `docs\metadata.md`, `docs\status.md`, + `docs\known-blockers.md`, `test\README.md`, `ops\ROADMAP.md`, and + `ops\AUTONOMOUS_RUN.md`. +- Traceability grep passed for `P1-META-001`, `metadata_schema_version`, + `metadata_state`, `metadataSchemaVersion`, `tokenMetadataState`, + `onchain-pending-schema-v1`, `onchain-final-schema-v1`, + `codex/metadata-schema-state`, and `Queue Item 40`. +- Slither baseline comparison remains non-blocking and high/medium unchanged: + `717` total findings, `4` High, `19` Medium, `92` Low, `591` + Informational, `11` Optimization. The only metadata-schema-related row is an + existing informational `too-many-digits` style finding. + +Review requests: + +- CodeRabbit was requested in issue comment `4675688299`. +- CodeRabbit review thread `PRRT_kwDOM7REis6IpsTo` requested the nonzero hash + guard; CodeRabbit marked it addressed in commit `3dc56d6`. +- CodeRabbit comment `4675802795` confirmed the guard and regression are solid + and suggested a clearer assertion label; the label nitpick was accepted. - Claude remains intentionally skipped per current user instruction; use CodeRabbit unless risk or future user instruction changes. @@ -3443,6 +3545,12 @@ Review requests: | Time UTC | Decision | Rationale | | --- | --- | --- | +| 2026-06-10 23:55 | Accept CodeRabbit PR #83 assertion-label nit | Clarified the zero-hash regression failure message after CodeRabbit confirmed the guard and coverage were solid; focused metadata tests, `make check`, Windows wrapper, formatting, and whitespace checks all pass | +| 2026-06-10 23:48 | Address CodeRabbit PR #83 zero-hash finding | Added `setTokenHash` guard for `bytes32(0)`, added pending-sentinel regression coverage, and reran focused metadata tests, `make check`, Windows wrapper, formatting, whitespace, and Slither baseline comparison | +| 2026-06-10 23:30 | Open PR #83 | Schema-v1 metadata state outputs are published with local validation evidence; next step is state follow-up push and CodeRabbit request | +| 2026-06-10 23:28 | Validate Queue Item 40 locally | Focused metadata tests, full `make check`, Windows wrapper, formatting, whitespace, heading scan, traceability grep, and Slither comparison all pass; Slither high/medium remain unchanged | +| 2026-06-10 23:26 | Start Queue Item 40 | ADR 0006 sequencing calls for schema versioning and explicit metadata state after current-output golden files and before freeze/dependency/burn hardening | +| 2026-06-10 23:20 | Merge PR #82 | CI passed, CodeRabbit resolved the inline thread and approved the latest head, and the stale aggregate CodeRabbit status was documented before merge | | 2026-06-10 23:15 | Address CodeRabbit PR #82 inline test gap | Hardened negative-path ERC-4906 tests to assert no raw metadata event topics, added post-burn hash-storage no-event coverage, and refreshed focused/full/Windows/Slither validation with 210 total tests | | 2026-06-09 22:34 | Use `ops/ROADMAP.md` as canonical roadmap | Existing roadmap already contains detailed gates, P0 issues, ADRs, Slither appendix, and test matrix | | 2026-06-09 22:34 | Add `ops/AUTONOMOUS_RUN.md` as durable state | Long-running execution needs repo-persisted state across compaction and PR cycles | diff --git a/ops/ROADMAP.md b/ops/ROADMAP.md index f2c2ab51..d94d464d 100644 --- a/ops/ROADMAP.md +++ b/ops/ROADMAP.md @@ -1547,10 +1547,11 @@ Acceptance criteria: and frozen collection pinning remain with [`P1-META-003`](https://github.com/6529-Collections/6529Stream/issues/48). - Implement [`P1-META-001`](https://github.com/6529-Collections/6529Stream/issues/46): - metadata schema and golden-file tests. Initial characterization fixtures now - pin current off-chain pending/final URIs and current on-chain pending/final - JSON data URIs; the final schema-versioned public-beta metadata model remains - open. + metadata schema and golden-file tests. Initial characterization fixtures pin + current off-chain pending/final URIs, and schema-v1 on-chain fixtures now pin + base64 JSON output with explicit `metadata_schema_version` and + `metadata_state` fields. Remaining P1-META-001 work is tied to freeze/burn + state coverage and any future schema migration. - Implement [`P1-META-002`](https://github.com/6529-Collections/6529Stream/issues/47): collection freeze boundaries and immutable metadata state. - Implement [`P1-META-003`](https://github.com/6529-Collections/6529Stream/issues/48): @@ -1571,7 +1572,7 @@ Acceptance criteria: `attributes`, and `animation_url`. - Escape quotes, backslashes, brackets, control characters, and untrusted token data. -- Decide raw UTF-8 JSON vs base64 JSON for on-chain metadata. +- On-chain metadata now uses base64 JSON data URIs for schema-v1 output. - Set size limits for collection scripts, dependency scripts, `tokenData`, image data, attributes, and `tokenURI`. - Define dependency creation, update, versioning, deprecation, and immutability @@ -2116,8 +2117,8 @@ Status values: `Missing`, `Planned`, `In Progress`, `Passing`, `Blocked`. | Randomness retry | Manual retry reprocesses the same provider output and cannot redraw randomness | `test/StreamRandomizerRetry.t.sol` | Passing for bounded deterministic retry: VRF and arRNG adapters expose admin-gated `retryRandomnessPostProcessing`, retry only `FailedPostProcessing` requests, reuse the stored derived seed, emit retry success/failure and fulfillment events without duplicating the initial failure event on retry failure, refresh fulfillment timing on retry success, preserve token/collection/provider/epoch binding validation, reject unauthorized callers and terminal fulfilled requests, and cap repeated failed attempts with `MAX_RANDOMNESS_POST_PROCESSING_RETRIES` | [`P0-RAND-006`](https://github.com/6529-Collections/6529Stream/issues/42) | Gate C | TBD | | Randomness seed storage | Derived seed/hash includes `RANDOMNESS_SEED_TYPEHASH`, provider, request ID, collection, token, randomizer epoch, and raw-output hash | `test/StreamRandomizerLifecycle.t.sol`, `test/StreamRandomizerRetry.t.sol` | Passing: VRF and arRNG adapters store `rawOutputHash = keccak256(abi.encode(randomWords))`, derive the token seed from `RANDOMNESS_SEED_TYPEHASH`, provider, request ID, collection, token, randomizer epoch, and raw-output hash, expose both values in request/token views and lifecycle interface views, emit both values in fulfillment/failure/retry events, emit provider-specific raw-word fulfillment events for off-chain auditability, avoid storing full provider word arrays, and prove post-request token-data mutation cannot bias the seed | [`P0-RAND-007`](https://github.com/6529-Collections/6529Stream/issues/43) | Gate C | TBD | | Weak helper randomness | `RandomizerNXT` and `XRandoms` are removed, test/demo-scoped, or impossible to configure for production drops | `test/StreamRandomizerLifecycle.t.sol` | Passing: `RandomizerNXT.isRandomizerContract()` returns false, `StreamCore.addRandomizer` rejects it for production collections, and the concrete `XRandoms` helper contract was removed from production source; Slither now reports `weak-prng=0` | [`P0-RAND-ADR`](https://github.com/6529-Collections/6529Stream/issues/14), [`P0-RAND-008`](https://github.com/6529-Collections/6529Stream/issues/73) | Gate C/Gate F | TBD | -| Pending randomness metadata | Off-chain and on-chain `tokenURI` pending/final behavior is deterministic and never treats zero hash as finalized randomness | `test/StreamMetadataGolden.t.sol`, later `test/StreamMetadata.t.sol` | Characterization passing for current behavior: off-chain pending/final URIs match fixtures, current on-chain pending output with zero hash is fixture-locked as pre-beta behavior, and current on-chain final output matches fixtures. ADR 0006 target work remains open because public-beta on-chain pending metadata must not treat a zero hash as final generative input. | [`P1-META-ADR`](https://github.com/6529-Collections/6529Stream/issues/45), [`P1-META-001`](https://github.com/6529-Collections/6529Stream/issues/46), [`P0-RAND-004`](https://github.com/6529-Collections/6529Stream/issues/40) | Gate C/Gate D | TBD | -| Metadata schema golden files | Off-chain URI rules, on-chain pending JSON, on-chain final JSON, and generated HTML remain deterministic under the accepted schema | `test/StreamMetadataGolden.t.sol` | Initial characterization passing: `offchain-pending-token-uri.txt`, `offchain-final-token-uri.txt`, `current-onchain-pending-token-uri.txt`, and `current-onchain-final-token-uri.txt` lock current output. Final schema-versioned fixtures, base64 JSON policy, escaping, freeze, and burn remain future P1-META work. | [`P1-META-001`](https://github.com/6529-Collections/6529Stream/issues/46) | Gate D | TBD | +| Pending randomness metadata | Off-chain and on-chain `tokenURI` pending/final behavior is deterministic and never treats zero hash as finalized randomness | `test/StreamMetadataGolden.t.sol`, later `test/StreamMetadata.t.sol` | Passing for schema-v1 coverage: off-chain pending/final URIs match fixtures, on-chain pending output returns base64 JSON with `metadata_state: "pending"` and no final animation HTML, and on-chain final output returns base64 JSON with `metadata_state: "final"` and the animation URL. Stale-state display remains future metadata/randomness work. | [`P1-META-ADR`](https://github.com/6529-Collections/6529Stream/issues/45), [`P1-META-001`](https://github.com/6529-Collections/6529Stream/issues/46), [`P0-RAND-004`](https://github.com/6529-Collections/6529Stream/issues/40) | Gate C/Gate D | TBD | +| Metadata schema golden files | Off-chain URI rules, on-chain pending JSON, on-chain final JSON, and generated HTML remain deterministic under the accepted schema | `test/StreamMetadataGolden.t.sol` | Passing for current schema-v1 slice: `offchain-pending-token-uri.txt`, `offchain-final-token-uri.txt`, `onchain-pending-schema-v1-token-uri.txt`, and `onchain-final-schema-v1-token-uri.txt` lock output, and `metadataSchemaVersion()` plus `tokenMetadataState(tokenId)` expose the active schema and state. Escaping, freeze, and burn remain future P1-META work. | [`P1-META-001`](https://github.com/6529-Collections/6529Stream/issues/46) | Gate D | TBD | | Metadata escaping and render safety | JSON, HTML, JavaScript, raw attributes, URI, and size-limit inputs are escaped, validated, or rejected | `test/StreamMetadataEscaping.t.sol` | Missing | [`P1-META-006`](https://github.com/6529-Collections/6529Stream/issues/51) | Gate D | TBD | | Collection freeze boundary | Frozen collections cannot mutate collection fields, base URI, metadata mode, scripts, dependency references, token data, image, attributes, final supply, or live-token metadata state | `test/StreamMetadataFreeze.t.sol` | Missing | [`P1-META-002`](https://github.com/6529-Collections/6529Stream/issues/47) | Gate D | TBD | | Dependency registry immutability | Dependency versions are immutable, pinned by key/version/content hash, and cannot change frozen collection output | `test/StreamDependencyRegistry.t.sol` | Missing | [`P1-META-003`](https://github.com/6529-Collections/6529Stream/issues/48) | Gate D | TBD | diff --git a/smart-contracts/StreamCore.sol b/smart-contracts/StreamCore.sol index 4babba13..cc76a9f5 100644 --- a/smart-contracts/StreamCore.sol +++ b/smart-contracts/StreamCore.sol @@ -27,6 +27,9 @@ contract StreamCore is ERC721Enumerable, ERC2981, Ownable, IERC4906 { using Strings for uint256; bytes4 private constant _INTERFACE_ID_ERC4906 = 0x49064906; + string public constant METADATA_SCHEMA_VERSION = "6529stream-v1"; + string private constant _METADATA_STATE_PENDING = "pending"; + string private constant _METADATA_STATE_FINAL = "final"; error PendingRandomnessRequests( uint256 collectionId, address randomizer, uint256 pendingRequests @@ -402,6 +405,7 @@ contract StreamCore is ERC721Enumerable, ERC2981, Ownable, IERC4906 { // function to set the tokenHash (this function is called only from randomizer contracts) function setTokenHash(uint256 _collectionID, uint256 _mintIndex, bytes32 _hash) external { require(msg.sender == collectionAdditionalData[_collectionID].randomizerContract); + require(_hash != bytes32(0), "Zero token hash"); require( tokenToHash[_mintIndex] == 0x0000000000000000000000000000000000000000000000000000000000000000 @@ -506,52 +510,104 @@ contract StreamCore is ERC721Enumerable, ERC2981, Ownable, IERC4906 { // function that return the tokenURI function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { _requireMinted(tokenId); - if ( - onchainMetadata[tokenIdsToCollectionIds[tokenId]] == false - && tokenToHash[tokenId] - != 0x0000000000000000000000000000000000000000000000000000000000000000 - ) { - string memory - baseURI = collectionInfo[tokenIdsToCollectionIds[tokenId]].collectionBaseURI; - return - bytes(baseURI).length > 0 - ? string(abi.encodePacked(baseURI, tokenId.toString())) - : ""; - } else if ( - onchainMetadata[tokenIdsToCollectionIds[tokenId]] == false - && tokenToHash[tokenId] - == 0x0000000000000000000000000000000000000000000000000000000000000000 - ) { - string memory - baseURI = collectionInfo[tokenIdsToCollectionIds[tokenId]].collectionBaseURI; - return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, "pending")) : ""; - } else { - string memory b64 = Base64.encode( - abi.encodePacked( - "" - ) - ); - string memory _uri = string( + uint256 collectionId = tokenIdsToCollectionIds[tokenId]; + bool finalMetadata = _isTokenMetadataFinal(tokenId); + + if (!onchainMetadata[collectionId]) { + string memory baseURI = collectionInfo[collectionId].collectionBaseURI; + if (bytes(baseURI).length == 0) { + return ""; + } + return finalMetadata + ? string(abi.encodePacked(baseURI, tokenId.toString())) + : string(abi.encodePacked(baseURI, _METADATA_STATE_PENDING)); + } + + return _onchainTokenURI(tokenId, collectionId, finalMetadata); + } + + /// @notice Returns the active on-chain metadata schema version. + function metadataSchemaVersion() public pure returns (string memory) { + return METADATA_SCHEMA_VERSION; + } + + /// @notice Returns the token's public metadata state under the active schema. + function tokenMetadataState(uint256 tokenId) public view returns (string memory) { + _requireMinted(tokenId); + return _isTokenMetadataFinal(tokenId) ? _METADATA_STATE_FINAL : _METADATA_STATE_PENDING; + } + + function _isTokenMetadataFinal(uint256 tokenId) private view returns (bool) { + return tokenToHash[tokenId] != bytes32(0); + } + + function _onchainTokenURI(uint256 tokenId, uint256 collectionId, bool finalMetadata) + private + view + returns (string memory) + { + return string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode(bytes(_onchainMetadataJson(tokenId, collectionId, finalMetadata))) + ) + ); + } + + function _onchainMetadataJson(uint256 tokenId, uint256 collectionId, bool finalMetadata) + private + view + returns (string memory) + { + string memory animationField = ""; + if (finalMetadata) { + animationField = string( abi.encodePacked( - "data:application/json;utf8,{\"name\":\"", - getTokenName(tokenId), - "\",\"description\":\"", - collectionInfo[tokenIdsToCollectionIds[tokenId]].collectionDescription, - "\",\"image\":\"", - tokenImageAndAttributes[tokenId][0], - "\",\"attributes\":[", - tokenImageAndAttributes[tokenId][1], - "],\"animation_url\":\"data:text/html;base64,", - b64, - "\"}" + ",\"animation_url\":\"", _onchainAnimationURI(tokenId, collectionId), "\"" ) ); - return _uri; } + + return string( + abi.encodePacked( + "{\"metadata_schema_version\":\"", + METADATA_SCHEMA_VERSION, + "\",\"metadata_state\":\"", + finalMetadata ? _METADATA_STATE_FINAL : _METADATA_STATE_PENDING, + "\",\"name\":\"", + getTokenName(tokenId), + "\",\"description\":\"", + collectionInfo[collectionId].collectionDescription, + "\",\"image\":\"", + tokenImageAndAttributes[tokenId][0], + "\",\"attributes\":[", + tokenImageAndAttributes[tokenId][1], + "]", + animationField, + "}" + ) + ); + } + + function _onchainAnimationURI(uint256 tokenId, uint256 collectionId) + private + view + returns (string memory) + { + return string( + abi.encodePacked( + "data:text/html;base64,", + Base64.encode( + abi.encodePacked( + "" + ) + ) + ) + ); } // function to retrieve the name attribute diff --git a/test/README.md b/test/README.md index e7e529f8..2355e0f6 100644 --- a/test/README.md +++ b/test/README.md @@ -148,10 +148,11 @@ compatibility-preserving. Metadata golden fixtures now have P1-META-001 characterization coverage in `StreamMetadataGolden.t.sol`: current off-chain pending and final token URI -rules, current on-chain pending JSON output, and current on-chain final JSON -output are compared byte-for-byte against `test/fixtures/metadata/`. The -on-chain fixtures are labeled as current pre-beta behavior because ADR 0006 -still requires an explicit public-beta metadata state model. +rules, schema-v1 on-chain pending base64 JSON, and schema-v1 on-chain final +base64 JSON are compared byte-for-byte against `test/fixtures/metadata/`. The +suite also asserts `metadataSchemaVersion()` and the token-level +`pending`/`final` metadata state view, and pending on-chain metadata no longer +executes the final generative HTML path with a zero token hash. ERC-4906 metadata signaling now has P1-META-004 target-state coverage in `StreamMetadataEvents.t.sol`: `supportsInterface(0x49064906)` succeeds, diff --git a/test/StreamMetadataGolden.t.sol b/test/StreamMetadataGolden.t.sol index 1414d5b6..043bb3fa 100644 --- a/test/StreamMetadataGolden.t.sol +++ b/test/StreamMetadataGolden.t.sol @@ -20,6 +20,39 @@ contract StreamMetadataGoldenTest is CharacterizationTestBase, StreamFixture { string private constant TOKEN_DATA = "1,2,3"; uint256 private constant TOKEN_SALT = 7; + function testMetadataSchemaVersionAndTokenStateViews() public { + DeployedStream memory pendingDeployment = deployStream(address(0xBEEF), address(0xCAFE)); + NoopRandomizer noopRandomizer = new NoopRandomizer(); + pendingDeployment.core.addRandomizer(COLLECTION_ID, address(noopRandomizer)); + _mintGoldenToken(pendingDeployment); + + pendingDeployment.core.metadataSchemaVersion() + .assertEq("6529stream-v1", "schema version changed"); + pendingDeployment.core.tokenMetadataState(TOKEN_ID) + .assertEq("pending", "pending state changed"); + + DeployedStream memory finalDeployment = deployStream(address(0xBEEF), address(0xCAFE)); + _mintGoldenToken(finalDeployment); + + finalDeployment.core.tokenMetadataState(TOKEN_ID).assertEq("final", "final state changed"); + } + + function testSetTokenHashRejectsZeroHashReservedForPendingState() public { + DeployedStream memory deployed = deployStream(address(0xBEEF), address(0xCAFE)); + NoopRandomizer noopRandomizer = new NoopRandomizer(); + deployed.core.addRandomizer(COLLECTION_ID, address(noopRandomizer)); + _mintGoldenToken(deployed); + + vm.prank(address(noopRandomizer)); + vm.expectRevert("Zero token hash"); + deployed.core.setTokenHash(COLLECTION_ID, TOKEN_ID, bytes32(0)); + + deployed.core.retrieveTokenHash(TOKEN_ID) + .assertEq(bytes32(0), "token hash should remain unset after zero-hash rejection"); + deployed.core.tokenMetadataState(TOKEN_ID) + .assertEq("pending", "zero hash changed metadata state"); + } + function testOffchainPendingTokenUriMatchesGoldenFile() public { DeployedStream memory deployed = deployStream(address(0xBEEF), address(0xCAFE)); NoopRandomizer noopRandomizer = new NoopRandomizer(); @@ -48,7 +81,7 @@ contract StreamMetadataGoldenTest is CharacterizationTestBase, StreamFixture { ); } - function testCurrentOnchainPendingTokenUriMatchesGoldenFile() public { + function testOnchainPendingSchemaV1TokenUriMatchesGoldenFile() public { DeployedStream memory deployed = deployStream(address(0xBEEF), address(0xCAFE)); NoopRandomizer noopRandomizer = new NoopRandomizer(); deployed.core.addRandomizer(COLLECTION_ID, address(noopRandomizer)); @@ -60,12 +93,12 @@ contract StreamMetadataGoldenTest is CharacterizationTestBase, StreamFixture { deployed.core.retrieveTokenHash(TOKEN_ID).assertEq(bytes32(0), "pending hash changed"); _assertMatchesFixture( deployed.core.tokenURI(TOKEN_ID), - "test/fixtures/metadata/current-onchain-pending-token-uri.txt", - "current on-chain pending tokenURI" + "test/fixtures/metadata/onchain-pending-schema-v1-token-uri.txt", + "schema-v1 on-chain pending tokenURI" ); } - function testCurrentOnchainFinalTokenUriMatchesGoldenFile() public { + function testOnchainFinalSchemaV1TokenUriMatchesGoldenFile() public { DeployedStream memory deployed = deployStream(address(0xBEEF), address(0xCAFE)); _mintGoldenToken(deployed); @@ -75,8 +108,8 @@ contract StreamMetadataGoldenTest is CharacterizationTestBase, StreamFixture { (deployed.core.retrieveTokenHash(TOKEN_ID) != bytes32(0)).assertTrue("expected final hash"); _assertMatchesFixture( deployed.core.tokenURI(TOKEN_ID), - "test/fixtures/metadata/current-onchain-final-token-uri.txt", - "current on-chain final tokenURI" + "test/fixtures/metadata/onchain-final-schema-v1-token-uri.txt", + "schema-v1 on-chain final tokenURI" ); } diff --git a/test/fixtures/metadata/current-onchain-final-token-uri.txt b/test/fixtures/metadata/current-onchain-final-token-uri.txt deleted file mode 100644 index f8a702ba..00000000 --- a/test/fixtures/metadata/current-onchain-final-token-uri.txt +++ /dev/null @@ -1 +0,0 @@ -data:application/json;utf8,{"name":"Genesis #0","description":"Description","image":"ipfs://image/10000000000.png","attributes":[{"trait_type":"Mood","value":"Calm"}],"animation_url":"data:text/html;base64,PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PjxzY3JpcHQgc3JjPSJodHRwczovL2Nkbi5leGFtcGxlL3NjcmlwdC5qcyI+PC9zY3JpcHQ+PHNjcmlwdD5sZXQgaGFzaD0nMHgwMTBmOTU4YzQzZjU5YTE1ZDJhNTA0OWY1ZTBjNjRkMDQyMTA0ODM4NzJmYTg1ZjgxNmYxNzc5MDM2MDM4MTE0JztsZXQgdG9rZW5JZD0xMDAwMDAwMDAwMDtsZXQgdG9rZW5EYXRhPVsxLDIsM107bGV0IGRlcGVuZGVuY3lTY3JpcHQ9Jyc7ZnVuY3Rpb24gZHJhdygpe308L3NjcmlwdD48L2JvZHk+PC9odG1sPg=="} diff --git a/test/fixtures/metadata/current-onchain-pending-token-uri.txt b/test/fixtures/metadata/current-onchain-pending-token-uri.txt deleted file mode 100644 index 0a87912d..00000000 --- a/test/fixtures/metadata/current-onchain-pending-token-uri.txt +++ /dev/null @@ -1 +0,0 @@ -data:application/json;utf8,{"name":"Genesis #0","description":"Description","image":"ipfs://image/10000000000.png","attributes":[{"trait_type":"Mood","value":"Calm"}],"animation_url":"data:text/html;base64,PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PjxzY3JpcHQgc3JjPSJodHRwczovL2Nkbi5leGFtcGxlL3NjcmlwdC5qcyI+PC9zY3JpcHQ+PHNjcmlwdD5sZXQgaGFzaD0nMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwJztsZXQgdG9rZW5JZD0xMDAwMDAwMDAwMDtsZXQgdG9rZW5EYXRhPVsxLDIsM107bGV0IGRlcGVuZGVuY3lTY3JpcHQ9Jyc7ZnVuY3Rpb24gZHJhdygpe308L3NjcmlwdD48L2JvZHk+PC9odG1sPg=="} diff --git a/test/fixtures/metadata/onchain-final-schema-v1-token-uri.txt b/test/fixtures/metadata/onchain-final-schema-v1-token-uri.txt new file mode 100644 index 00000000..f66ea1fb --- /dev/null +++ b/test/fixtures/metadata/onchain-final-schema-v1-token-uri.txt @@ -0,0 +1 @@ +data:application/json;base64,eyJtZXRhZGF0YV9zY2hlbWFfdmVyc2lvbiI6IjY1MjlzdHJlYW0tdjEiLCJtZXRhZGF0YV9zdGF0ZSI6ImZpbmFsIiwibmFtZSI6IkdlbmVzaXMgIzAiLCJkZXNjcmlwdGlvbiI6IkRlc2NyaXB0aW9uIiwiaW1hZ2UiOiJpcGZzOi8vaW1hZ2UvMTAwMDAwMDAwMDAucG5nIiwiYXR0cmlidXRlcyI6W3sidHJhaXRfdHlwZSI6Ik1vb2QiLCJ2YWx1ZSI6IkNhbG0ifV0sImFuaW1hdGlvbl91cmwiOiJkYXRhOnRleHQvaHRtbDtiYXNlNjQsUEdoMGJXdytQR2hsWVdRK1BDOW9aV0ZrUGp4aWIyUjVQanh6WTNKcGNIUWdjM0pqUFNKb2RIUndjem92TDJOa2JpNWxlR0Z0Y0d4bEwzTmpjbWx3ZEM1cWN5SStQQzl6WTNKcGNIUStQSE5qY21sd2RENXNaWFFnYUdGemFEMG5NSGd3TVRCbU9UVTRZelF6WmpVNVlURTFaREpoTlRBME9XWTFaVEJqTmpSa01EUXlNVEEwT0RNNE56Sm1ZVGcxWmpneE5tWXhOemM1TURNMk1ETTRNVEUwSnp0c1pYUWdkRzlyWlc1SlpEMHhNREF3TURBd01EQXdNRHRzWlhRZ2RHOXJaVzVFWVhSaFBWc3hMRElzTTEwN2JHVjBJR1JsY0dWdVpHVnVZM2xUWTNKcGNIUTlKeWM3Wm5WdVkzUnBiMjRnWkhKaGR5Z3BlMzA4TDNOamNtbHdkRDQ4TDJKdlpIaytQQzlvZEcxc1BnPT0ifQ== diff --git a/test/fixtures/metadata/onchain-pending-schema-v1-token-uri.txt b/test/fixtures/metadata/onchain-pending-schema-v1-token-uri.txt new file mode 100644 index 00000000..a89699b2 --- /dev/null +++ b/test/fixtures/metadata/onchain-pending-schema-v1-token-uri.txt @@ -0,0 +1 @@ +data:application/json;base64,eyJtZXRhZGF0YV9zY2hlbWFfdmVyc2lvbiI6IjY1MjlzdHJlYW0tdjEiLCJtZXRhZGF0YV9zdGF0ZSI6InBlbmRpbmciLCJuYW1lIjoiR2VuZXNpcyAjMCIsImRlc2NyaXB0aW9uIjoiRGVzY3JpcHRpb24iLCJpbWFnZSI6ImlwZnM6Ly9pbWFnZS8xMDAwMDAwMDAwMC5wbmciLCJhdHRyaWJ1dGVzIjpbeyJ0cmFpdF90eXBlIjoiTW9vZCIsInZhbHVlIjoiQ2FsbSJ9XX0=