Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ jobs:
- name: Run test gate
run: python services/api/run_tests.py

chain-tests:
name: MassRelease721 contract tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: foundry-rs/foundry-toolchain@v1

- name: Install contract dependencies
run: |
cd services/chain
forge install foundry-rs/forge-std --no-commit
forge install OpenZeppelin/openzeppelin-contracts@v5.0.2 --no-commit

- name: forge test
run: cd services/chain && forge test

web-build:
name: Web typecheck + test + build
runs-on: ubuntu-latest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""release_tokens: off-chain mint registry for asset-rights step 4

Records mock (or future on-chain) mints keyed by `content_hash`. Provenance binds to `asset_id`,
not `project_id`, which `import_bundle` regenerates on every `.mass` import.

Revision ID: a1b2c3d4e5f6
Revises: f4b8c2d51e93
"""
import sqlalchemy as sa
from alembic import op

revision = "a1b2c3d4e5f6"
down_revision = "f4b8c2d51e93"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"release_tokens",
sa.Column("id", sa.String(), nullable=False),
sa.Column("asset_id", sa.String(), nullable=False),
sa.Column("release_id", sa.String(), nullable=False, server_default=""),
sa.Column("content_hash", sa.String(), nullable=False),
sa.Column("manifest_hash", sa.String(), nullable=False, server_default=""),
sa.Column("provider", sa.String(), nullable=False, server_default="mock"),
sa.Column("chain_id", sa.Integer(), nullable=False, server_default="0"),
sa.Column("contract_address", sa.String(), nullable=False, server_default=""),
sa.Column("token_id", sa.String(), nullable=False, server_default=""),
sa.Column("tx_hash", sa.String(), nullable=True),
sa.Column("metadata_uri", sa.String(), nullable=True),
sa.Column("recipient", sa.String(), nullable=True),
sa.Column("minted_by", sa.String(), nullable=True),
sa.Column("project_id", sa.String(), nullable=True),
sa.Column("status", sa.String(), nullable=False, server_default="minted"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_release_tokens_asset_id", "release_tokens", ["asset_id"])
#: UNIQUE ON THE INDEX, NOT A SEPARATE CONSTRAINT — the model is the spec.
#:
#: `models.py` declares `content_hash: Mapped[str] = mapped_column(String, unique=True,
#: index=True)`, which SQLAlchemy renders as ONE unique index named
#: `ix_release_tokens_content_hash`. The first draft of this migration wrote a named
#: `UniqueConstraint("content_hash", name="uq_release_tokens_content_hash")` AND a non-unique
#: index, so the database had a constraint the model does not declare and lacked the uniqueness
#: the model does. `alembic check` caught it as three pending operations — remove the
#: constraint, drop the index, re-add it unique — which is CI failing on a real divergence, not
#: a style preference.
#:
#: The guarantee is identical either way (Postgres enforces a unique index exactly as it
#: enforces a unique constraint); what differs is whether the schema matches the mapping, and a
#: schema that has drifted from its model is how the next autogenerate produces a migration
#: nobody meant to write.
op.create_index("ix_release_tokens_content_hash", "release_tokens", ["content_hash"],
unique=True)
op.create_index("ix_release_tokens_project_id", "release_tokens", ["project_id"])
op.create_index("ix_release_tokens_asset_created", "release_tokens", ["asset_id", "created_at"])


def downgrade() -> None:
op.drop_index("ix_release_tokens_asset_created", table_name="release_tokens")
op.drop_index("ix_release_tokens_project_id", table_name="release_tokens")
op.drop_index("ix_release_tokens_content_hash", table_name="release_tokens")
op.drop_index("ix_release_tokens_asset_id", table_name="release_tokens")
op.drop_table("release_tokens")
4 changes: 4 additions & 0 deletions services/api/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ pillow>=12.3.0
# dependency, and the failure would land on a subcontractor rather than on us.
python-docx>=1.2

# --- ASSET-RIGHTS step 5: optional EVM minting (evm_provider.py) ---
# MIT. Lazy-imported — only loaded when AEC_CHAIN_PROVIDER=evm. Mock minting needs no chain deps.
web3>=7.0

# --- security floors on TRANSITIVE packages -------------------------------------------------------
# Nothing here is imported by our code. Each line exists only to raise a floor on a package we get
# through something else, because a transitive dependency has no other place to be pinned. Delete a
Expand Down
Loading
Loading