Skip to content

Asset-rights NFT stack: mock + EVM mint registry for sealed .mass releases - #391

Draft
ibuilder wants to merge 4 commits into
mainfrom
cursor/asset-chain-provider-f622
Draft

Asset-rights NFT stack: mock + EVM mint registry for sealed .mass releases#391
ibuilder wants to merge 4 commits into
mainfrom
cursor/asset-chain-provider-f622

Conversation

@ibuilder

@ibuilder ibuilder commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

Completes the asset-rights / NFT stack for sealed .mass releases — steps 4 and 5.

Each sealed release (asset_rights.json + content_hash) can now be registered as a mock or on-chain ERC-721 token, with optional IPFS metadata pinning.

Step 4 (previous commit)

  • ChainProvider protocol + MockChainProvider
  • release_tokens Postgres table (keyed on content_hash)
  • POST /asset-rights/mint, lookup, project list routes

Step 5 (this commit)

Smart contract (services/chain/)

  • MassRelease721 — OpenZeppelin ERC-721, one token per content_hash, owner-only mint
  • Foundry tests (5 passing, including fuzz)
  • Deploy script: MASS_RELEASE721_OWNER=0x… forge script script/DeployMassRelease721.s.sol --broadcast
  • CI job runs forge install + forge test

Python providers

Module Role
evm_provider.py Real minting via web3.py + JSON-RPC
ipfs_storage.py Mock (deterministic CID) or Pinata pinning
release_nft_metadata.py ERC-721 JSON from manifest

Hardening

  • Strict sha256:<64 hex> validation on all mint paths
  • Auto-pin metadata when pin_metadata=true (default)
  • Idempotent mint + IntegrityError race handling
  • EVM gas estimation + EIP-1559 fee fields
  • GET /asset-rights/mints/verify — registry vs on-chain cross-check
  • /asset-rights/status now reports chain.ipfs and chain.evm readiness
  • 42 checks in test_asset_chain.py

Configuration

Mock (default — no network):

export AEC_CHAIN_ENABLED=true
export AEC_CHAIN_PROVIDER=mock   # default

EVM (testnet/mainnet):

export AEC_CHAIN_ENABLED=true
export AEC_CHAIN_PROVIDER=evm
export AEC_CHAIN_RPC_URL=https://…
export AEC_CHAIN_ID=11155111
export AEC_CHAIN_CONTRACT=0x…
export AEC_CHAIN_MINT_KEY=0x…          # issuer key — never in .mass
export AEC_CHAIN_DEFAULT_RECIPIENT=0x… # optional

IPFS (optional — auto-pin on mint):

export AEC_IPFS_PROVIDER=pinata
export AEC_IPFS_PINATA_JWT=…

Test plan

  • python3 test_asset_chain.py (42 checks)
  • forge test in services/chain/
  • test_alembic_single_head.py
  • ruff on changed modules
Open in Web Open in Cursor 

…tep 4)

Introduce a ChainProvider protocol with a deterministic mock implementation,
a release_tokens Postgres table keyed on content_hash, and API routes to mint,
look up, and list release tokens. Chain minting is off by default
(AEC_CHAIN_ENABLED) and validates sealed manifests before recording a mock mint.
No RPC, wallet, or Solidity toolchain yet — step 5 can swap in a real provider.

Co-authored-by: Matthew M. Emma <ibuilder@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Add MassRelease721 (Foundry + OpenZeppelin ERC-721) binding each sealed release
to a bytes32 content_hash, an EvmChainProvider via web3.py, mock/Pinata IPFS
storage for ERC-721 metadata, auto-pin on mint, and registry/on-chain verify.

Hardening: strict content_hash validation, idempotent mint with IntegrityError
race handling, gas estimation for EVM txs, expanded test_asset_chain (42 checks),
forge test CI job, and web3 added to requirements.lock.

Co-authored-by: Matthew M. Emma <ibuilder@users.noreply.github.com>
@cursor cursor Bot changed the title Asset-rights step 4: chain provider abstraction + mock mint registry Asset-rights NFT stack: mock + EVM mint registry for sealed .mass releases Sep 1, 2026
MERGED origin/main (33 commits, v0.3.1133 -> v0.3.1143, zero conflicts). That alone fixed two of
the four API-gate failures the old head had: 656/660 -> 658/660 locally.

1. LOCKFILE — provenance, not dependencies. `pip-compile --generate-hashes` failed with "stale".
   The committed lock was generated as `--output-file=requirements.lock.new requirements.in` from
   inside services/api/, so its header recorded the wrong command and all 11 annotations read
   `# via -r requirements.in` instead of `# via -r services/api/requirements.in`. Regenerated with
   CI's exact command on Python 3.12 (the prod base image's version; on 3.11 numpy==2.5.2 will not
   even resolve). 84 lines changed and ZERO of them a version or a hash — measured, not assumed:
   `diff | grep -E '^[<>] [a-z0-9_.-]+=='` is empty. The dependency set was always right; only its
   record of where it came from was wrong.

2. ALEMBIC — a real model/schema divergence, and the gate was right to fail.
   `models.py` declares `content_hash: mapped_column(String, unique=True, index=True)`, which
   SQLAlchemy renders as ONE unique index `ix_release_tokens_content_hash`. The migration instead
   wrote a named `UniqueConstraint("content_hash", name="uq_release_tokens_content_hash")` AND a
   NON-unique index — so the database carried a constraint the model does not declare and lacked
   the uniqueness the model does. `alembic check` reported three pending operations. Migration now
   matches the model. The guarantee is identical (Postgres enforces a unique index exactly as a
   unique constraint); what changes is that the schema stops having drifted from its mapping.

3. ROUTE REACHABILITY — two new unreachable routes, frozen with an expiry condition.
   `/asset-rights/mints` and `/projects/{pid}/asset-rights/mints`. This PR touches NO apps/web file,
   so "no client caller" is the accurate state of a staged feature, not an oversight. Recorded with
   the part that matters: FOUR routes were added and the rule can only see TWO. `POST
   /asset-rights/mint` (leaf `mint`) and `GET /asset-rights/mints/verify` (leaf `verify` — the
   ASSET-VERIFY blind spot, met a second time) are equally uncalled and simply invisible to it.
   Freezing two does not make the other two reachable, and a reader assuming otherwise would be
   wrong.

STILL RED, and I cannot close it from here: `test_actions_pinned`. #391 adds
`foundry-rs/foundry-toolchain@v1` to ci.yml, and that gate requires third-party actions to be
pinned to a 40-hex commit SHA because a tag is mutable. Resolving the real SHA needs read access to
foundry-rs/foundry-toolchain, which this session is not scoped for. A fabricated SHA would satisfy
the regex while pointing at nothing, so it is left failing and raised instead.

Verified: full backend suite 658/660 (the two above; the reachability one now fixed, so 659/660 on
this tree), ruff clean, test_lock_satisfies_requirements / test_declared_imports /
test_alembic_single_head / test_alembic_migrations all green.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA

ibuilder commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Picked this up with the owner's approval of web3>=7.0 and the services/chain stack. Merged main (33 commits, v0.3.1133 → v0.3.1143, zero conflicts) and fixed three of the four failures in 117ef6f2. That merge alone fixed two of them — 656/660 → 658/660 — so part of the red was staleness, not this diff.

was failing cause state
pip-compile --generate-hashes provenance, not dependencies fixed
alembic upgrade + check a real model/schema divergence fixed
API test gate (route reachability) two new unreachable routes fixed
API test gate (test_actions_pinned) unpinned third-party action needs you

The lockfile was never wrong about dependencies. It was generated as --output-file=requirements.lock.new requirements.in from inside services/api/, so the header recorded the wrong command and all eleven annotations read # via -r requirements.in rather than # via -r services/api/requirements.in. Regenerated with CI's exact command on Python 3.12 — 84 lines changed and zero of them a version or a hash, measured rather than asserted (diff | grep -E '^[<>] [a-z0-9_.-]+==' is empty).

The alembic failure was a genuine defect and the gate was right. models.py declares content_hash: mapped_column(String, unique=True, index=True) — one unique index. The migration wrote a named UniqueConstraint and a non-unique index, so the database carried a constraint the model doesn't declare and lacked the uniqueness it does. alembic check reported three pending operations. The migration now matches the model; the guarantee is identical, but the schema stops having drifted from its mapping.

On the two frozen routes — the part worth reading. /asset-rights/mints and /projects/{pid}/asset-rights/mints are frozen with an expiry condition, because this PR touches no apps/web file at all, so "no client caller" is the honest state of a staged feature. But four routes were added and the rule can only see two: POST /asset-rights/mint (leaf mint) and GET /asset-rights/mints/verify (leaf verify — the ASSET-VERIFY blind spot, met a second time) are equally uncalled and simply invisible to the matcher. Freezing two does not make the other two reachable, and that is written into the entry so the next reader isn't misled.

⛔ What I can't do from here

test_actions_pinned fails on foundry-rs/foundry-toolchain@v1 in ci.yml. That gate requires third-party actions to be pinned to a 40-hex commit SHA, because a tag is mutable and a workflow step runs upstream code against this repo. actions/* and github/codeql-action are exempt as first-party; foundry-rs is not.

Resolving the real SHA needs read access to foundry-rs/foundry-toolchain, which this session isn't scoped for. I will not invent one — a fabricated 40-hex value satisfies the regex while pointing at nothing, which is the exact "a check that cannot fail" failure this repo's gates exist to catch. Raised with the owner rather than worked around.

Everything else verified: backend suite 659/660 on this tree (the remaining one being the pin above), ruff clean, and test_lock_satisfies_requirements, test_declared_imports, test_alembic_single_head, test_alembic_migrations all green.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants