Skip to content

feat(protected-content): 0.7.1 UI/UX, cleanup and crypto-review follow-up - #62

Closed
irzhywau wants to merge 6 commits into
feat/protected-content-atomic-cutoverfrom
feat/protected-content-0.7.1-followup
Closed

irzhywau wants to merge 6 commits into
feat/protected-content-atomic-cutoverfrom
feat/protected-content-0.7.1-followup

Conversation

@irzhywau

@irzhywau irzhywau commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Scope

Single follow-up PR on top of feat/protected-content-atomic-cutover (stack #51 → #60 → #59 → this PR) closing three issues in risk order:

Chain, rights, elastos-protected-content-contracts, custody-provider and chain-provider are not modified.

Plan

This first commit records the implementation plan as a reviewable document: docs/audits/2026-09-08-protected-content-0.7.1-followup-plan.md (18 tasks, TDD steps, exact file/line anchors, interfaces between tasks, and the resolved decisions D1–D7). Implementation commits follow task by task.

Resolved decisions

  • Non-media sealing reuses payload.rs (EPC1); the only crypto-module change exposes its existing chunk loop as a streaming sealer/decrypter, pinned byte-for-byte against the one-shot functions.
  • Viewer egress is decrypted bytes with client-side rendering (same posture as Elacity Player). Server render-IR is a recorded follow-up.
  • Creator owns its file input and reuses the Library upload transport; it is granted the four upload handlers plus roots, stat, publish.
  • Audio is media: transcoded to audio/mp4 / mp4a.40.2 alongside the pinned video/mp4 / avc1.640028.
  • External / PQ cryptographic review of the protected-content contracts and custody crates #48 is docs plus golden vectors; no proptest/fuzz.
  • Mint journal record codec bumps epc-mj05 → epc-mj06 (kind byte for the content identity); v5 records stay readable and media mint ids stay byte-stable.

Verification (per task, see plan)

  • git diff --check, node scripts/home-entropy-check.mjs, cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings
  • Narrow tests named in each task; just verify at three checkpoints; just verify-ci before marking ready.
  • Installed proof for Creator → Library → Player (video), Creator → Marketplace buy → Reader (PDF, PNG, CBZ, EPUB), and audio playback, with artifact SHA-256s and restart evidence recorded in state.md.

Status

Closes #42, closes #48, closes #49 once all tasks land.

@irzhywau irzhywau changed the title feat(protected-content): 0.7.1 UI/UX, cleanup and crypto-review follow-up (#42, #48, #49) feat(protected-content): 0.7.1 UI/UX, cleanup and crypto-review follow-up Sep 9, 2026
@irzhywau irzhywau self-assigned this Sep 9, 2026
@irzhywau
irzhywau force-pushed the feat/protected-content-0.7.1-followup branch from decab1f to 0617957 Compare September 10, 2026 22:32
@irzhywau
irzhywau marked this pull request as ready for review September 14, 2026 12:56
@irzhywau

Copy link
Copy Markdown
Contributor Author

Where this branch stands — 2026-09-15

One unpushed commit (0edd56d5) on top of 58686dd4. Everything below is
source-verified only; no installed journey has been run for this slice.

Gates: cargo fmt --all --check, clippy --workspace --all-targets -D warnings, cargo test --workspace (3178 passed, 0 failed), all 21
own-workspace capsule suites, the CI source-gate scripts and the three
capsule browser suites all pass locally.

What changed

The starting condition was that a mint could not be completed from the Creator
at all, and that when it failed it said almost nothing — 23 call sites shared
one "Runtime custody creator mint is unavailable", five of them logging
nothing.

  • External wallets are accepted. signing_available was always the real
    precondition; the proof-type half of the gate shut out connector-linked
    wallets that could in fact sign. An unlinked external account still fails
    closed.
  • Exactly one wallet effect per mint. The second setApprovalForAll, and
    the isApprovedForAll probe that read any undecodable answer as "not
    approved" and re-raised it every mint, are gone. Scoped to minting — an
    operator approval remains the mechanism for secondary trading, and a resale
    surface will need its own.
  • A mint keeps the account it started with, and says so. The publish
    request is identified by principal, object and storage, never by wallet. With
    terms recorded and nothing raised, Runtime refuses and names both accounts;
    once an effect is raised the mint finishes on the account that raised it
    (refusing would strand a transaction in flight); a settled mint is never
    refused.
  • A replay is no longer reported as fresh work, naming when it settled, the
    transaction, and the account that minted it. This is the defect that let a
    mint settled 90 minutes earlier, on a since-abandoned wallet, read as work
    just done.
  • Custody roles and kubo peering. Nodes take --role <storage|custody>, so
    key-share custody no longer conscripts a node as a ciphertext replica.
    Peering comes from the operator peer store; kubo starts eagerly on both node
    and client gateway. Proven by block transfer, not inspection: a block
    published on the client was fetched by all three custody nodes, one in 0.18s
    without ever being explicitly dialed.
  • Typed failures and waits in place of one shared sentence: which
    wallet-default precondition failed, wallet drift, whether an outstanding
    approval needs a person or only the chain, an approval that can never
    complete, and publish progress projected from the mint journal.
  • A recursive-directory fix in the mint journal: the store lives two levels
    down and the lock created only one, so the first journal operation on a fresh
    data dir failed, surfacing as "mint intent is unavailable". This was the CI
    failure.

Landed but deliberately not wired

The Elacity listing folder builder (metadata.json, content.json,
contract.json, two token-type files) plus the listing wire contract and a
runtime-custody protections[0] descriptor. Shapes are settled and tested;
nothing calls it, so a mint still lists incorrectly.

It is blocked on one decision: the Elacity metadata.json must occupy the
directory root, because that is where the on-chain token URI resolves — and
that is the filename the runtime's own document uses today. The runtime
document therefore has to move and verify_runtime_portable_metadata follow
it. Whether the verifier accepts both names through a transition or takes a
clean break decides whether already-published listings keep verifying.

Known still wrong

  • A mint completes but lists incorrectly: folder unwired, royalties still the
    hardcoded 950, and the opRawData metadata URI names metadata.json rather
    than its directory (so metadataURI() resolves to
    …/metadata.json/contract.json).
  • A declined wallet approval leaves the mint at EffectRaised, which
    discard_creator_state refuses, so the object cannot be minted again. The
    failure is reported accurately; recovery is not implemented. Allowing the
    discard means asserting the raised effect is provably dead, which moves a
    safety guarantee from the journal to its caller.
  • "Transcode & fragment" is still untracked; deriving it needs the media
    preparation record by request id, which the creator tail does not hold.
  • custody-harness-smoke has not been run. Its path filter matches these
    changes so CI will run it; locally it stops any live custody-host compose
    project. Both failure modes were audited statically.

Configuration note

The local chain configuration moved off the Anvil Base fork to real Base
mainnet, because the RPC in use could not serve eth_getTransactionReceipt for
mined transactions and stalled every mint. Anders has since approved Base
mainnet for the bounded J5 acceptance journey; protected-content-installed-e2e-proof.sh
and its receipt labels still assume the fork.

state.md and TASKS.md in this commit carry the full verified state and the
open items.

@irzhywau
irzhywau force-pushed the feat/protected-content-0.7.1-followup branch from 0edd56d to b2ad4c0 Compare September 16, 2026 08:36
@irzhywau

Copy link
Copy Markdown
Contributor Author

Progress report — 16 September 2026

Status checked on 17 September. The mint audit and first recovery/progress fixes are complete at source level. Receipt handling, Creator polling, and Library integration are currently in progress. Installed frontend acceptance remains open.

Revision and verification status

Scope Current state
Published PR b2ad4c0b9dca55ce2a6670fbeafcf0e34b123c60
Local committed follow-up f217bf05884fea5ba9cab43a8aef05b93d239423, dated 16 September; one commit ahead of the published branch
Current staged work 15 files, +1,492 / −162 lines; additional unstaged edits in chain-provider tests
Published CI Eight successful checks in run 35074673782; release publishing skipped

Those CI results cover the published revision. The local commit and staged changes require their own verification. This report does not establish that the installed application matches either revision.

Work completed yesterday

  • Mint and custody audit: traced the generic Runtime custody mint failed and Runtime custody mint requires cleanup reconciliation failures through the provider boundary, journal, recovery model, and Creator UI. Identified stale custody statement timestamps, loss of useful provider error details, uncertain external effects, and recovery states that left users without a clear next action. The original failed mint's precise cause still requires runtime evidence; node availability alone was not established as the explanation.
  • Media and progress audit: identified video preparation dropping audio through -an, deletion of the generated MPD, and progress labels that did not reflect the operation state. Existing protected fMP4/CENC handling and PSSH support do not establish a complete MPEG-DASH delivery workflow or preserved audio/video playback.
  • Test coverage audit: identified why green tests can coexist with a broken frontend journey: some fixtures begin with completed custody records, helper tests bypass the DOM/provider boundary, and mocked media checks cannot establish playback or installed-artifact correctness.
  • Execution and documentation planning: prepared separate mint/recovery/media and documentation-refresh plans. The plans are local, Git-ignored files under docs/superpowers/plans; the documentation audit itself changed the update plan, rather than refreshing canonical documentation.

The local commit f217bf05 implements the first remediation slice:

  • Creates custody statements using dispatch-time clocks, so preparation and earlier node calls do not consume the next request's freshness window.
  • Preserves provider failure categories and distinguishes a pre-write refusal from an uncertain dispatched effect in the journal. A node's no-effect refusal remains an assertion, rather than cryptographic proof of absence.
  • Keeps failed custody attempts terminal and retains their history and cleanup obligations while allowing a separate subsequent attempt. General reconciliation of uncertain effects remains a separate requirement.
  • Guards Creator state changes against concurrent discard or terms changes.
  • Projects pending/active/done/failed stages, removes the misleading “not tracked” labels, and presents an error summary with details and recovery controls tied to available operations. Transcode completion is inferred from operation state; granular preparation telemetry still needs further work and acceptance.
  • Adds source regressions for the affected behavior.

Work currently staged / in progress

  • Mint receipt and listing resolution: decode and bind ItemListed evidence, carry quantity/price/payment-token data, handle evidence arriving at different times, and support receipt validation for delegated transaction paths while retaining event bindings.
  • Settlement policy: the staged resolver moves from the RPC finalized block to a configured confirmation depth. This changes settlement guarantees and needs explicit review of confirmation policy, reorg behavior, and negative tests before acceptance.
  • Creator waiting and retry UX: start each waiting budget at the relevant phase, distinguish human approval from chain waiting, preserve progress before timeout, and provide “Check again” for the same intent.
  • Library integration: represent the minted capsule separately on the appropriate shelf, preserve the source item, and return the capsule URI from publication.
  • Provider diagnostics and tests: reduce routine polling log noise, add elapsed-time diagnostics, and extend chain-provider, gateway, Creator, and Library coverage. These files are still WIP, including unstaged test edits.

What remains

  1. Finish and independently review the current implementation. Reconcile the staged and unstaged work, verify the receipt/settlement invariants, and run the relevant source checks on the final revision. Refresh the PR description: its earlier statement that chain-provider is untouched will no longer describe this follow-up.
  2. Close recovery and UX acceptance. Exercise refusal, unavailable nodes, timeout after a possible write, partial provisioning, restart, and repeated attempts. For each blocked state, show the impact, responsible actor, safe next action, and recovery status. Preserve uncertain effects until evidence supports reconciliation.
  3. Complete media packaging. Preserve audio where present, define the delivered DASH/manifest contract against the actual encrypted fragments, and validate video-with-audio, audio-only, seeking, and playback. Include reliable preparation and storage progress for media and non-media paths.
  4. Prove the real user journeys. Verify installed Runtime/provider/capsule artifact parity, then exercise Creator → custody → storage → chain → Library → buyer → Player/Reader on the final artifacts. Cover video/audio and supported document/image formats, plus failure and recovery paths. Record browser/manual evidence and update the journey register. Any funded proof needs an agreed spending bound.
  5. Refresh documentation after the implementation handoff. Follow the planned P0–P5 sequence: establish the baseline; update state/evidence; align contracts and workflows; document media, user guidance, and operator recovery; repair supporting pages/navigation; validate every readiness claim against its evidence. Include Reader and current chain operations, and keep planned behavior separate from implemented and installed-proven behavior.
  6. Complete the external cryptographic review gate. Record its owner, scope, schedule, findings, and disposition before claiming cryptographic readiness.

The acceptance criterion is a reproducible working frontend journey on identified installed artifacts, supported by failure-path evidence. Passing unit and integration tests is one part of that evidence.

@irzhywau

Copy link
Copy Markdown
Contributor Author

Session report — 2026-09-17

Four commits pushed (b2ad4c0b..9d623977). All suites green: 1780 (elastos-server) + 97 (wallet-provider) + 94 (chain-provider), fmt and clippy -D warnings clean on every workspace touched.

Done

Ownership is one thing. A creator could not open what they had minted. Access rights live on chain in the access token, and hasAccess already answered for a buyer — nobody ever asked on the creator's behalf. A mint now records an owned copy in the same store a purchase uses; below the entitlement check nothing distinguishes the two.

A capsule describes itself. A .ddrm now carries the asset's own metadata.json with the facts only the chain can supply raised beside it — authority, kid, tokenId, ledger, contentType, and the content CID. That makes it readable with nothing fetched, and self-describing enough that writing the file is the repair for assets minted before any of this existed.

Two permanent wedges removed. A mint attempt that could never finish used to strand the object it was minted from, for good, because the intent's identity derives from the object path. Both the unfinishable record and the unmintable intent are now discarded rather than kept. Found by live minting, not by tests.

A lapsed wallet approval no longer strands a mint. A ten-minute TTL expired, and the mint behind it became unfinishable — content published, custody provisioned, only the chain transaction missing, and no sequence of user actions could produce another prompt. An expired approval may now be replaced under its own request id. This deliberately narrows replay protection for that one case and the narrowing is documented at the guard; a rejected approval is never re-raised, and a test holds that line.

One openable file per asset. Capsule filing suffixed on name collision, which is right for two different assets sharing a name and wrong for the same asset owned again — repair left a stale twin beside the current file, with the ownership record pointing at whichever was written last. A same-mint write now rewrites in place, identified by the mint id inside the document rather than by filename.

Findings

The rights call was being read at the wrong block. hasAccess is readable in the very block that carries the mint — verified on Base: the call reverts at block 51438373 and returns true at 51438374, the mint's own block. The chain provider evaluated it at finalized, which trails the head by ~16 minutes, so the creator tail asked about a state older than the transaction it had just watched confirm. The two readers of that call now state which block they need: evidence authorizing a content-key release stays on finalized state; the upfront "is this copy theirs" check reads the head.

The publish path is now instrumented end to end, because every defect this session was found by live minting and none by a test. availability_publish reports four timed legs; a content publish separates writing bytes locally from replicating them; the availability fan-out times each target in turn; a pin reports elapsed beside its own timeout; and a probe samples kubo's wantlist, block counters and peers while a pin is stalled.

The 46-second publish has a cause, and it is not what it looked like. Measured on a 689 KB file: local write 62 ms, the pins themselves 48–100 ms each, and 44 seconds between the two in which nothing happens. The gap sits in the carrier's gossip topic join (carrier.rs:2978), which takes the DHT branch because state.bootstrap_peers is empty — it is only populated by the "connect" op (carrier.rs:6170), and nothing in a mint calls it. The BitTorrent Mainline DHT then bootstraps against public routers. Topics are derived per CID, so the cost recurs for every new piece of content and can never be amortised; that is also why the same size took 46 s, 12 s and 1 s on three different publishes.

The progress UI misattributes that time, and the correct answer already exists. The backend emits escrow/publish/listing, the Creator maps them correctly, and a "Publish to storage" row is already on the page. But the client sets "Encrypt & escrow" active as a guess before the request (creator.js:1057), the whole pipeline runs inside one blocking POST, and the first server-authored progress arrives only on the pending response — by which time it reads publish: done. The journal meanwhile records publish: active at t≈+345 ms and holds it, accurate and unread, for the next 46 seconds.

Next — immediate

  1. Measure the exact blocking await in the gossip join. By static reading the _no_wait join should return immediately, yet 21.8 s elapses. Time the three awaits in join_gossip_topic and raise RUST_LOG for distributed_topic_tracker and mainline. One publish names the line. Not claiming an exact cause before this.
  2. Take the DHT off the publish critical path. The gossip topic is a discovery side-channel; the pins are the replication. Either spawn the announce, or use the direct join that already exists (join_gossip_topic_direct) with custody peers seeded at startup. Sequencing depends on (1).
  3. Surface the progress that already exists. A read-only publish_progress op reading the mint journal, polled by a client timer replacing the guess. Existing schema, existing consumer, no restructuring.
  4. Verify the capsule override end to end. Capsules resolve from ~/Library/Application Support/elastos/bin/, and just build-capsule does not install there — so the rights fix, pin probe and availability-provider timings have not yet run live. ELASTOS_CAPSULE_BIN_DIR points the runtime at the repo builds; a mint under it should record its owned copy and open in the reader.

0.7.1 completion scope

  • Buy path proven live end to end, following the same read workflow as a minted copy.
  • A Library sync that pulls every owned asset, so ownership recorded elsewhere lands on this machine.
  • Publish latency inside a target a creator would accept — on the evidence above, seconds rather than tens of seconds for a file of this size.
  • Integration coverage for the mint through settlement with a chain mock at head. Every defect this session lived in that seam and every one was found by hand.

Known issues, filed not fixed

  • Orphaned custody shares after a discarded mint. They seal against an envelope that no longer exists and no listing will ever name them, so they open nothing; reclaiming the space is housekeeping and would need the same authorization as provisioning.
  • Unbounded pin timeout and sequential replica fan-out.

@irzhywau
irzhywau force-pushed the feat/protected-content-0.7.1-followup branch from 120a6f7 to 093b67d Compare September 21, 2026 16:37
… readable object

Squashes the first half of the 0.7.1 follow-up (#42, #48, #49) into one slice.

A creator uploads a file through the Library transport and protects and lists
it in one flow, through the Creator app. The source mime decides the kind:
video and audio take the rendition path, everything else is an object and takes
the chunked-payload path, and the kind then decides the viewer, so the creator
never picks one. Non-media objects work end to end, and Elacity Reader renders
pictures, documents, text, 3D models, books and comics.

Audio becomes a first-class rendition rather than a video with the picture
missing: media-provider probes the source, keeps exactly one track, and skips
cover art rather than taking it for a video track. Protected media carries a
CENC Protection System Specific Header declaring the Elacity post-quantum
hybrid-threshold scheme, written, parsed and admitted by one module so the
producer, the read path and the layout validator cannot drift apart.

The crypto-review package documents every primitive with its crate version and
parameters, what each construction binds, and a threat model naming the test
that pins each claim.

Original commits: 9404e8d, afabc11, d10b199, 0617957, ca7fa26, 2596100,
9709934.
…that completes

Squashes the reliability half of the 0.7.1 follow-up.

Custody membership and ciphertext replication become separate roles, so a node
runs with --role custody and is never counted toward the replica requirement,
and a kubo peering mesh is proven by real block transfer rather than assumed.

Minting is repaired where it could not finish. External wallets are accepted
wherever signability rather than proof type is the precondition; the creator
tail raises exactly one wallet effect, the mint itself; a mint keeps the account
it started with across retries, and Runtime refuses before raising an effect
when the wallet's default has moved on, naming both accounts and the two ways
out. Settlement waits on confirmations instead of refusing, a completed mint is
owned as a Library item, and an approval that lapses is asked again rather than
stranding what it stood for. Publish progress is read from the mint journal, so
a stage cannot report a phase the server has not reached.

Failures and waits answer as typed data rather than one shared sentence.

Original commits: 58686dd, b2ad4c0, f217bf0, 9989aa3, 8657a95, 9d62397,
7a6e955, 093b67d.
… replicas that settle

Squashes three parallel workstreams that complete the buy-and-open journey.

MARKETPLACE AND BUY. The protected shelf reads what Runtime actually publishes:
the listing parser had refused every row carrying an availability
receipt_digest since 2026-08-28, so protected items were invisible for
twenty-four days, and one bad row no longer takes the whole answer down. A
listing states its own kind, derived from the same content identity the open
path enforces, so an item opens in the viewer its kind needs instead of the
player for everything. A listing published on another Home can be added to this
one, a purchase shows its terms before it spends anything, and a purchase says
which wait it is in while Marketplace waits with it.

THE OPEN CEREMONY. Every first rights-signature request answers
requires_approval, for managed and external accounts alike, so the first open of
every protected item returned a pending answer that both viewers rendered as
"unavailable" -- the journey was a dead end at its first step. Open progress is
now typed data carried beside the stable message, naming the stage, whether
asking again can change the answer, whether a person must act, and where. Both
viewers show the ceremony and re-issue the identical open until the person
answers or the window closes, disposing timers, object URLs, MediaSource and
renderer resources on every path. The approval window moves from sixty seconds
to the largest that stays inside the recipient-authorization and rights-request
contracts, so a human approval is no longer lost by default.

AVAILABILITY. A real local pin counts as the replica it already is,
replication sends the bytes rather than asking a peer to find them, and a
delivered block graph settles on the peer's own pin. Metadata publishes had
been finishing at one replica because every remote proof failed on a
manifest-identity defect.

Also: the locked-renderer boundary design, with the decisions that a decoder
sits inside the decrypt provider under a pure-Rust rule and that AVIF keeps its
current path; and a node_modules symlink carrying an operator's own absolute
path is untracked, with the ignore rule widened from a directory pattern that
never matched a symlink.

Original commits: 80ca4467, d31c43a6, cafb624a, 7f29f1a2, 723711d9, 996eae09,
e7ef1109, 55cd077c, ddde6665, d861fcd7, f2e1f9e4, 988211f3, 788a3b37, and the
two integration merges 5dbb4199 and 7ab64780.
…rchase stops hiding after a reload

Two things a person could not do, both about a copy that is theirs and not in
front of them.

DOWNLOAD. A `.ddrm` file is made of public material: the metadata document the
token URI resolves to, and the content the listing names. What makes the copy
theirs is the access token on the chain, not the file. So the file can always
be built again — and until now it was only ever built as a side effect of
buying or minting, with no way to ask. Someone whose write failed, who deleted
the file, or who holds the token on a Home that never had it, owned something
they could not see and had nothing to press.

`download_owned_copy` runs the rebuild that already existed. The door is a live
`hasAccess` read at the head block for the account this principal transacts
with: a purchase record on this Home is neither necessary nor sufficient, which
is the point — someone who bought on one Home owns the same token on another,
and a local record is not a grant. Asking twice is safe, which is the whole
point of a control a person presses when something is missing.

A COPY IN FLIGHT. A purchase lives in Runtime, not in the page that started it,
so closing Marketplace or reloading left the row looking untouched. Pressing
Buy resumed the same attempt against the same effect — safe, and invisible.
The row now says a purchase is already under way and offers Continue, which
resumes rather than starting again, and does not re-ask for terms the record
has already fixed.

That is a new field, `purchase_in_flight`, rather than a new `access_state`
value. A value is something consumers switch on, and a new one falls silently
into whatever their default does; an unknown field is refused loudly by the
parser on the other side of this contract. Marketplace's own `accessStateLabel`
and `mediaActionButton` are two such switches, and both would have offered a
person a purchase that was already running.

Verified in this worktree:

    cargo test -p elastos-server --lib -- runtime_custody
    node --test capsules/marketplace/browser/src/listing.test.mjs   # 21 pass
    node scripts/marketplace-product-behavior-smoke.mjs             # OK
    node scripts/marketplace-product-layout-smoke.mjs               # OK
    node scripts/home-entropy-check.mjs                             # PASS

This also untracks `elastos/tools/browser-playwright-engine/node_modules`,
which my own buy-progress commit swept into the tree: a symlink I made locally
to avoid a second Playwright install while the disk was full. The ignore rule
reads `node_modules/` with a trailing slash, which is a directory pattern and
never matched a symlink of that name, so a broad `git add` took it. It resolved
in the worktree that made it and became a loop anywhere else, and its content
was a local path and an operator username, which this repository asks never to
commit. No committed value is right here; the only correct content is whatever
a local `npm ci` produces.

The browser gate walks both: Download appears on a copy someone holds and not
on an item still on offer, and it asks Runtime to rebuild exactly the copy its
row names; a row whose purchase is still under way says so and offers Continue.
@irzhywau
irzhywau force-pushed the feat/protected-content-0.7.1-followup branch from 093b67d to 70b66bd Compare September 21, 2026 18:54
@irzhywau

Copy link
Copy Markdown
Contributor Author

Progress report — 21–23 September 2026

Three days on the feat/protected-content-0.7.1-followup branch. Completion below is measured against the 0.7.1 target line (TASKS.md → Protected-content 0.7.1 follow-up and Integrated UIUX and protected-content proof), not against this PR's original 18-task list, which closed at Task 18.

Nothing is pushed. Everything described here is local.

Revision status

Branch tip 6094c99c (2026-09-22)
Commits in this window 5 — cd396e00, 0448896c, bff51fde, 70b66bdb, 6094c99c
Uncommitted on top 15 files, +1,682 / −75, plus one untracked script
Published none of the above

What landed, 21–23 September

The mint completes, and what it produces is readable

0448896c, 6094c99c. A mint could not be driven to completion from the Creator; now it can, and the asset it produces is one a marketplace can actually index.

  • Elacity listing metadata is published at mint — metadata.json, content.json, contract.json and the per-token-type documents, carrying the creator's title, description, cover, category and content flags, with a creator-named royalty split reaching the chain in ERC-1155 ROYALTY_SHARE units.
  • The token URI names the metadata directory, not a file inside it. The Operative appends its own suffixes, so the previous URI resolved to …/metadata.json/0000…0001.json — nothing. Mints completed and were unlistable. The Runtime's own document moved to manifest.json; listings published before the rename still verify.
  • Approving an open shows a message a person can read. The wallet was handed the rights request's canonical bytes hex-encoded; a wallet strips 0x before display, so ~2 KB of a nested identity graph arrived as replacement characters. The signature was sound — a consent defect, not a correctness one. The wallet now signs four checkable lines (action, account, expiry, one committing hash). RightsRequestV1::signing_message / ::signing_hash is the single definition reached by the handoff, the managed signing path, the wallet's completion check and verify_unclaimed.
  • External wallets are accepted where signability — not proof type — is the precondition. Custody nodes carry --role <storage|custody>; the kubo peering mesh is proven by block transfer.

Purchases and owned copies stop disappearing

bff51fde, 70b66bdb. A purchase that survives a reload; one button that rebuilds a copy you already own; replicas that settle instead of hanging; a viewer that waits rather than failing.

Marketplace → Media: a real storefront

6094c99c plus all current uncommitted work. This is the bulk of 22–23 September.

  • Two surfaces under one Media title — Explore and Shops, plus My listings.
  • Explore reads the public on-chain index (fetchNFTItems), so items minted by anyone appear, not only what this Home holds. Grouped by content type, newest first, 3 video / 3 images / 4 documents per row with a More → link.
  • Shops lists channels from fetchChannels, each card offering Subscribe unless this Home administers the channel or already holds a subscription — and that state is read from chain (hasActiveSubscription, hasRole) and corroborated across sources, never taken from the index's label.
  • Consent machinery written once. gateway_onchain_directory.rs is descriptor-driven: source decision, policy file, approval request, audit streams, GraphQL post. Each surface keeps its own request id, approval, policy file and audit prefix — one endpoint is not one consent.
  • Partial GraphQL answers are used, and their complaints are logged. The live index routinely returns rows and errors together (60 assets, 18 complaints). One reader (OnchainDirectory::rows) handles it, guarded by a source-scanning test so a second reader cannot reintroduce the old "errors ⇒ empty list" behaviour.
  • Prices are human-readable — pay-token decimals applied with integer-only scaling, $ for USDC, Ξ for ETH, crypto-standard subscript-zero notation for tiny values.
  • The design is ported, not re-derived. media-studio.css is the supplied stylesheet carried over verbatim, every selector scoped under .media-studio. Card states, badges, overflow menus and skeleton placeholders (content and covers independently) follow the supplied design.
  • A Refresh control that re-reads the market, and same-origin cover bytes served by the gateway out of the already-replicated metadata directory, with magic-number content-type sniffing that deliberately never answers text/html or image/svg+xml for an unnamed blob.

Build and tooling

scripts/setup-dev-home.sh --mode isolated — a dev-profile installer that shares one Cargo build cache with the VS Code launch configuration, so an isolated setup no longer discards the cache the debugger depends on.


Where 0.7.1 stands

Landed in source: the #42 product surface, the #49 cleanups and the #48 crypto-review package (all closed at Task 18); the mint-path repairs; the buy/own/open journey; and now the Marketplace storefront.

Open on the 0.7.1 line — 11 items in TASKS.md, of which these decide the release:

Blocker Why it holds
Three installed journeys never run — Creator→Library→Player, Creator→Marketplace→Reader, audio Every deferred proof from the object, reader and audio work collapses into this one item. Everything above is source-verified only.
Base mainnet proof driver Mainnet is approved for the bounded J5 journey, but protected-content-installed-e2e-proof.sh and its receipt labels still name the old network. Chain id, contracts, RPC and a spending limit must be agreed before any funded run.
A real 2-of-3 custody composition across three genuinely distinct operators and failure domains The ceremony and its offline verify path are proven on the single-host simulation harness only.
Readable-signature cutover Designed and implemented; needs a custody image rebuild and a coordinated cutover, plus security review, because verify_unclaimed runs on every custody node.
Mint recovery from a declined approval A rejected approval is now reported accurately but leaves the mint at EffectRaised, which discard_creator_state refuses. Allowing the discard moves a safety guarantee from the journal to its caller — an owner decision, not a repair to make in passing.
External cryptographic review The #48 package is the input to that review, not a substitute for it. Required before any public dKMS or production confidentiality claim.

Non-blocking and recorded: transcode-stage progress tracking, custody-harness-smoke on this branch, the locked renderer for pictures, an audio thumbnail source, and the CENC media-integrity constraint for any frontend-only client.


Current stage, and what blocks it right now

Stage: the Explore storefront renders the global catalog correctly. The remaining step is making a globally-discovered item buyable.

The blocker, measured today. A listing package cannot be reconstructed for an asset this Runtime did not mint. Five fields are required and supplied by neither chain nor published metadata:

Field What it actually is Substitutable?
mint_id sha256(content identity ‖ access id ‖ key envelope ‖ policy ‖ commitment ‖ threshold ‖ **selected custody node bindings**) — a local custody digest, not a chain fact. Serves as the on-disk key and the buy API's identifier. Yes, as a key: sha256(chain_namespace ‖ ledger ‖ token_id ‖ operative) works for every mint from every entrypoint.
content_id "content:" + hex(EncryptedContentIdentityV1::canonical_hash()). The buy path already re-derives it and refuses on mismatch. Yes — a computation, not a fact. Drop it from the required set.
publisher_profile_did Which publisher's content-plane replication receipt is acceptable. No honest substitute — the chain states an address, not a DID. But for content this Home did not publish there is no receipt to bind, so the requirement does not apply.
mint_transaction_hash Feeds resolve_protected_content_mint_receipt. Yes — retire it. Every field the receipt contributes (schema, network, chain_id, token_id, operative) is already asserted by resolve_protected_content_verified_listing, read at head rather than from a historic transaction.
published_at One use: display in the listing summary. Yes — the index returns createdAt.

But the fields are the symptom, not the cause. The buy pre-flight (gateway_provider_proxy.rs:4115 → verify_fresh_runtime_custody_availability → verify_runtime_portable_content) refuses a third-party asset before reading any of them, because it:

  1. refreshes a runtime content-plane availability receipt signed by this Home's device DID, bound to content_id + publisher DID, under a 3-replica policy;
  2. fetches an elastos.content.object.manifest/v1 at the content CID and verifies every file against the content identity;
  3. loads the local custody composition and rebuilds the mint draft.

A base.ela.city mint — or any mint through the published SDK — has IPFS bytes, no runtime content object, no availability receipt and no custody composition. It fails at step 1.

So: there is exactly one listing trust path, and it only accepts assets this Runtime minted. Proposed resolution is a second listing origin (Indexed) with a chain-anchored, content-addressed verification instead of a custody-anchored one:

tokenURI(tokenId)   → metadata directory CID          (on chain)
metadata.kid        == operative.contentId            (on chain)
metadata.media.uri  → content CID, fetched and hashed (content-addressed)
verified listing    → seller / price / payToken / qty (on chain, at head)

Nothing taken from the index on trust; terms still re-read from chain before signing. What it does not provide is a replication guarantee, so an Indexed listing must state plainly that availability is the seller's responsibility. Awaiting a decision before implementation.

Related caveat, for the product owner

A listing is published as an immutable IPFS object and handed out as elastos://<cid>, but a listing is mutable state — a seller changes the price, a buyer re-lists the copy they bought, an item sells out. A content address cannot express any of that. It has not bitten because the chain is re-read before anything is signed, so a stale package causes a refused purchase rather than a wrong one. What the package genuinely provides that the chain does not is the protection material, which really is immutable. The seam is that the package carries two things with opposite lifetimes. Splitting it is a protocol change touching publish, import, buy and every record on disk — recorded, not acted on.


Gates

git diff --check, home-entropy-check, marketplace-product-behavior-smoke, marketplace-product-layout-smoke, listing.test.mjs and cargo fmt --all --check all pass on the working tree; clippy was last clean at -D warnings.

Stated precisely: 148 of 156 targeted elastos-server tests pass under a bare cargo test. The other 8 — the publish/buy/open journey tests — abort on a missing ELASTOS_TEST_PROTECT_PROVIDER_BIN because they need the release provider binaries that just test-elastos builds through prepare-providers and that CI supplies. They are environment-gated, not regressions, and they were not re-run under the full harness in this window.

No installed journey has been run for any of this work.

Design reference

The Explore and card-state surfaces are ported from the supplied media-studio.html, carried into the repo as capsules/marketplace/browser/media-studio.css (scoped under .media-studio, app chrome removed). The two reference boards are A · Studio — Media window@2x.png and A · Studio — Card states@2x.png; they are not in the repository, so they are named here rather than embedded.

…pletes, and a market anyone can see

A protected file opened and read on an installed Home for the first time, and
then everything the creator had been unable to choose became the next thing in
the way. Both halves are here.

An external wallet approval passes through three states, not two: `pending`
while it waits for the person, `approved` once they press Confirm, and
`completed` once the connector posts the signature back. The release read
`requires_approval`, which is `status == Pending` alone, so the middle state
looked like a finished approval carrying no signature and the open failed with
"wallet result carries no rights signature". That gap is a second or two wide
and every external open crosses it. Live evidence: the approval was raised and
confirmed, and eleven seconds later the release reported no signature. The
release now waits on `approved` too, replaying the identical request, and
answers a rejected or expired approval by its own type.

Approving an open meant confirming two kilobytes of replacement characters.
The signature was sound, so this was a consent defect: an approval screen
exists so a person can decide, and nobody can decide about a screen of binary.
The handoff sent canonical bytes 0x-prefixed, and a wallet decodes 0x input
before displaying it. The wallet is now shown four readable lines whose hash
covers them, `RightsRequestV1::signing_message` is the single definition, and
every site reaches the signature through it.

Every chunk read fetched the whole framed object and re-hashed all of it, kept
a megabyte and dropped the rest -- a thirteen-chunk read spent 95% of its
27.45 s between chunks rather than producing them. The open already fetches and
verifies that object, so it stages the verified bytes and a read seeks to its
own range. What is staged is published ciphertext, verified before writing and
length-checked on every read; per-chunk integrity rests on AES-256-GCM, as it
always did.

The wallet also opened two connector windows per click, because a click
bubbled through two listeners that both recognised it. Two windows offering
Review for one request let the same approval be answered twice.

And the listing metadata did not match the schema it claims, so items appeared
on the shelf without a title, type or price. Checked against the schema itself
rather than the generator, which mattered twice: `media.protectionType` is an
array, and `category` is a declared field.

A mint carries the metadata CID twice and they are not the same string. The
operative's base URI stays the folder, because `OperativePrimitive.uri(id)`
appends `/{id}.json` to whatever it is given. The media token's URI has to name
the document, because a marketplace dereferences it rather than appending -- a
folder there is why published listings arrived empty.

`AssetCreated._uri` echoes the media token URI, so the receipt corroboration
compares against the derived form. Adding the suffix at the encode site alone
would have left every mint settling on chain and then failing to verify; the
receipt tests caught exactly that.

**The access method.** All three publish now. `AssetFactory.registerNewAsset`
gates operative creation, listing and royalty split on the same `opType > 0`
branch, so free creates none of them: its `opRawData` is the content id alone,
which the factory still reads to bind the id, and its receipt may not be
expected to prove a listing that was never made. Buy and resell carries the
trailing `uint16 resellerCut` that only it may carry, defaulting to 90%.

**The channel.** A mint settles on the channel its creator chose, and a mint
that names none is refused. The configured `ledger` was being read as *the*
channel; it no longer is. The choice is recorded in the mint's own terms so a
retry publishes into the same place, and the source digest drops the channel
and the pay token, which now legitimately vary -- its domain moves to /v2 and
mints recorded under /v1 will be refused.

Channels come from a GraphQL index of on-chain state, asked
`access: "mint:0x…"` -- the set an account may publish into. Not `creator`,
which returns nothing for an account publishing into a public channel it did
not create, and not `user:`, which is the wider set including channels that
would refuse the mint. The directory is discovery, never authority: it may be
absent, stale or unapproved, and a typed address always works.

External HTTP follows the path the Wallet's price source already sets: off
until approved through the Inbox, the approval recorded with who gave it, every
fetch audited, and a failure degrading to a note rather than a refusal. Its own
policy and actions, because approving prices must not approve this.

**The currency.** Selecting USDC scaled a price by six decimals and then listed
it against the native token, because the currency was deliberately never sent:
0.11 USDC settled as 110000 wei, about a ten-trillionth of the intended price.
Not a mislabelled listing -- one priced in a currency nobody agreed to.

The pay token is now a term with an allow-list stating each token's decimals,
and the price's scale travels with it. A scale that does not match the chosen
token is refused before anything is encoded. An allow-list rather than any
address a page sends, because a token of unknown decimals is a price of unknown
meaning.

A read failing with "release approval is unavailable" told a person nothing,
while all three custody nodes had said "content access id is not bound on
chain" -- a mint too recent for the finalized block the release evidence is
required to read. The reason was dropped twice: the carrier replaced the peer's
message with a generic string, and the coordinator discarded each node's error
entirely.

The coordinator's classification stays text-free on purpose -- it decides
whether a node can have acted, which must never widen on a remote peer's
say-so. The node's own sentence is collected separately, bounded and
diagnostic, and reported with the refusal.

ONE WIRE, DEFINED ONCE

Four field mismatches between the Runtime and its chain provider reached a
person while every test passed: `op_type_code`, the channel and pay-token
reshape, `pay_token_decimals`, and a free mint's null listing. Each was found
by building and watching it fail.

One cause. The gateway declared each answer as a `deny_unknown_fields` struct,
the capsule built the same answer as a JSON literal, and the only thing
reconciling them was a hand-written mock that the tests answered with INSTEAD
of the capsule. A field added on one side left the other refusing at runtime
while the suite stayed green, so green was never evidence that the boundary
worked.

The shapes now live in the contracts crate both sides already depend on, and
the mock is built from those same structs rather than written out again. A
field that exists on one side only stops compiling.

The fourth was never reached. A free mint's receipt reports no quantity, price
or pay token, because it lists nothing, and the gateway declared all three
non-optional: the first free mint anyone tried would have failed at its
receipt. Shared types turned that into a type error, and the absence is
recorded as what it is -- a sale of nothing, at no price, in the chain's own
coin.

A SALE PRICED IN A TOKEN NO LONGER WAITS FOR FINALITY

A mint priced in anything but the chain's own coin re-read its entire listing
at a FINALIZED block, re-proving terms the receipt had already proved: the
`ItemListed` is emitted by the very transaction that settled. The one fact a
receipt cannot carry is where an ERC-20 sale pays through, which lives on the
operative rather than in the event.

So a USDC mint settled on chain and then failed. Sixteen minutes before Base
finalizes, the two evidence sources disagreed about a block neither had
finalized, and the listing went unassembled while the asset sat minted. Only
the payment processor is read now, at the head and corroborated across
sources, and a buyer's own purchase re-reads and re-checks it before any money
moves.

NOT ADDRESSED

The finality bar itself, which is correct: a key release may only rest on state
no reorg can take back, and that costs roughly sixteen minutes after a mint.
The viewer should render it as a wait rather than a dead end, and does not yet.

The custody ceremony's 16.9-36.6 s, which is three nodes each requiring two
agreeing finalized results from Base -- distributed work, not waste.

A CHANNEL DIRECTORY THE MARKETPLACE MAY READ, AND CONTENT A CAPSULE MAY SHOW

Reading an on-chain index had one surface and now has two, so the terms that
let it happen are written once instead of copied: source decision, policy
file, approval request, audit streams, the GraphQL post, and the
errors-before-data read that stops a refused query from looking like an empty
answer. The Creator's picker is re-pointed at it and its four tests pass
unchanged.

Each surface keeps its OWN request id, approve action, policy file and audit
stream. One endpoint is not one consent, and approving a picker in one app
must not quietly enable another.

The Marketplace asks for the channels that exist, with no `access` filter:
Shops is a window onto what others publish, not a list of this account's own
permissions. What it shows next to each channel -- whether this Home
administers it, already subscribes, or neither -- is read from the chain, not
from the index, because a directory naming a channel's creator is a label and
a label must not decide what a person is offered.

An unreadable channel answers `unknown` rather than `none`. `none` is what
draws a Subscribe button, so it has to rest on an answer rather than on a
silence -- otherwise an RPC hiccup invites someone to pay for a subscription
they already hold.

TWO REFUSALS THAT LOOK LIKE ANSWERS

Forty calls sent one at a time took 46 seconds with every card showing
"checking", so they were batched. Both configured sources refuse a batch that
size, and they refuse it differently:

  mainnet.base.org   a single object   "maximum 10 calls in 1 batch"
  base.drpc.org      an ARRAY of 40 errors, every element the same refusal

The second is the dangerous one. A well-formed array whose every answer is an
error is indistinguishable from a source that answered and knew nothing, and
read that way it made a source contribute silence while looking healthy --
which is why most cards said "access unknown". A whole batch failing is now
treated as a refusal, so the caller asks again one call at a time; batches are
three, the strictest source's limit, and the chunks run together rather than
one after another.

CONTENT A CAPSULE CAN ACTUALLY SHOW

Published CID content was served correct and then thrown away by the browser.
A capsule runs under `require-corp` in an opaque origin, so a subresource
without `Cross-Origin-Resource-Policy` is fetched and discarded: a 200 that
renders nothing. Reading one with `fetch` needs CORS as well, and neither
header was set -- so an image was blocked and a document read failed before
any status could be seen, which is not a 404 a caller can act on.

A bare CID also answered `application/octet-stream` whatever its bytes were,
which a browser treats as a file to save rather than a picture to show. It is
typed from its own magic number now, and deliberately never as `text/html` or
`image/svg+xml` from an unnamed blob: both are documents that can carry
script, and this origin serves the Home's own pages.

EXPLORE IS THE MARKET, NOT THIS HOME'S SHELF

Explore showed what this Home held. A Home that has minted nothing therefore
opened a market with nothing in it, and there was nothing anywhere to buy --
which is not a thin shelf, it is the wrong shelf. It now shows what anyone has
minted, with this Home's own items among them rather than instead of them.

The index answers in its own dialect and every value is converted once, in
Runtime, rather than in each surface that shows one:

  price        0.1                 a float in TOKEN units   -> base units, as a uint256 string
  createdAt    1790138251000       milliseconds             -> seconds
  kid          "27df70c5…"         no 0x                    -> 0x-prefixed
  tokenURI     ipfs://Qm…/metadata.json                     -> the directory CID
  media.uri    ipfs://Qm…                                   -> the content CID
  contentType  "video"             a category, not a MIME   -> carried as a category

Each item is then joined against this Home's own listings on the names the
chain uses -- channel and token -- because the channel an item lives on is
deliberately not published to a surface, so a surface could not make that join.
What comes back is the mint this Home holds for that asset, if any, and what
this person may do with it. An item with no mint here is one to buy; the access
state is per principal, so two people on one Home get their own answer about
the same asset.

A row missing the ledger, the token or the content id is dropped rather than
drawn as a card whose buttons cannot do what they say, and one the index has
marked unpublished is on nobody's shelf. None of it is authority: a price shown
here is the index's claim, and the terms of a sale are still read from chain
before anything is signed.

A PRICE ARRIVES IN TWO SCALES AND ONLY ONE OF THEM IS OBVIOUS

A listing's price is already in base units -- 200000 for 0.2 USDC -- while the
item's headline price is in token units -- 0.2. Scaling both multiplied every
listed item by a million and put $200000 on a twenty-cent card.

The conversion moves digits through text, never through a float: 0.1 at six
decimals is exactly 100000, and arriving there by multiplication is how a price
becomes 99999.99999999999. Exponent form is written out rather than evaluated,
because 0.000001 serialises as 1e-6. A token whose decimals this Home does not
know is refused rather than guessed -- a price of unknown scale is not a price
-- and so is more precision than the token has, because rounding a price is
changing it.

ROWS AND COMPLAINTS ARRIVE TOGETHER, AND THE ROWS ARE STILL ROWS

GraphQL reports a field it could not resolve for one row beside the rows it did
resolve. The live index does exactly this: sixty assets came back with eighteen
complaints about a non-nullable `image` that was null. Reading `errors` first
threw every good row away with the bad ones, so Explore was empty while the
index was answering perfectly well -- which cost an afternoon to find, because
an empty shelf and an unreachable market look the same.

Rows now win when there are rows, and the complaints are kept: shown to nobody,
because they are about the index's schema rather than about anything a person
did, and said in the log, bounded, because a shelf quietly missing rows is what
took the afternoon. An answer with no rows at all is still a refusal and still
carries its reason. This lives in the one place every surface reads a GraphQL
answer through, and a test pins it there, so a surface cannot get it wrong
again on its own.

WHAT A BUYER NEEDS IN ORDER TO OPEN WHAT THEY BOUGHT

The document the token URI names carried the rights policy identity and
nothing else. Someone who finds an asset in an index can buy it from what the
chain says -- and could then hold something they cannot open, because the three
facts needed to open it were reachable only from a link the creator hands out.

It now carries them: which envelope holds the key, that the key belongs to this
content, and what the content is. All three were already public -- the listing
package carries them and sits on IPFS, fetched by CID with no authentication --
so what changed is their reachability, not the judgement about key material.
None of them is any: each is an identity, naming the custody pool, epoch and
committee authorization. The shares stay with the custody nodes, and the chain
still decides whether this account may open it. Four fields became seven, and
the test that forbids material travelling here grew `key` to its list rather
than losing a name from it.

THE SHELF ITSELF

An item this Home holds answers to its mint; one known only to the index has no
mint here at all and is named by its asset instead. Keying both on the mint
alone gave every catalogue item the same empty key, so one item's title and
cover appeared on all of them.

The line under a title must not repeat it. For an item this Home holds it names
the file, which says WHICH copy this is -- two mints of one title are one title
and two files. An item from the index has no file name, so that line would echo
the title and say nothing; it says how many are left and who is selling instead.

Asking again is now a control rather than a reason to reopen the app, and it
says what it found: a market that cannot be read must not look like an empty
one. The same distinction is drawn on the shelf itself, as a note above the
items rather than an error in place of them, because the items are real and
what is missing is beside them.

While the market is on its way the shelf keeps its shape with the design's own
skeletons, and a cover holds a shimmer until it lands -- the fetch goes to this
Home's node and then, for anything it does not already hold, out to the
network. A cover that never arrives settles to the placeholder rather than
shimmering for ever.

A person searches for what the card shows them, so a price is matched in the
form they read as well as in full, through the same formatter the card uses.
Pressing the tab you are already on takes you back to the whole of it, which is
how anyone would expect to leave a "See all". And Explore asks for its items
when Media is opened rather than only when a tab is switched -- it is where a
person lands, so a load that needed a switch never happened. Asking is what
raises an approval, so a surface nobody opens still asks for nothing.

One CSS rule said `margin-top: 24` with no unit, which is not a length, so the
spacing it describes had never once been applied.

A HOME BUILT FOR ITERATING ON

`scripts/setup-dev-home.sh` installs the same Home with the dev profile. It is
a wrapper rather than a second installer -- what "install a Home" means stays
in one script, and a copy of nineteen hundred lines would drift from it within
a week -- and the profile is now a parameter of the installer, including the
`debug/` directory cargo actually writes it to.

It passes the VS Code launch's own debug-info overrides, because those change
the fingerprint and without them cargo would keep a third copy of the gateway
rather than reuse the one the editor already built. The free-space gate is
lowered rather than removed: this build reuses those artifacts and needs a
fraction of sixteen GiB, and a gate that stops an iteration loop over headroom
it never uses is a gate that gets bypassed instead of heeded -- while running
out of disk mid-install still leaves a half-written Home. It says on the way in
that it is slow where it hurts: custody and the crypto paths are the longest
work this Home does, and unoptimised they are painful rather than merely
slower.

Verification:

  cargo test --workspace (just test-elastos)              0 failed
  cargo test -p elastos-server runtime_custody          120 passed
  cargo test -p elastos-server market_directory          17 passed
  chain-provider cargo test                             103 passed
  creator page node --test                               71 passed
  marketplace listing node --test                        21 passed
  marketplace behavior smoke, layout smoke                   pass
  cargo clippy --workspace --all-targets -- -D warnings     clean
  cargo fmt --all -- --check                                clean
  home entropy, public-copy entropy, browser entropy,
  vendor-ui, diff --check                                    pass

Driven on an installed Home as far as a settled mint: the asset reached chain
with a token id and an operative, and the listing step after it was what
failed -- which is what the finality change above addresses. That change has
not itself been watched complete on an installed Home, and is not claimed
here. Nor has a free mint been tried.

The market shelf rests on its own tests and the product smoke, and on one live
observation: the rows-and-complaints answer above is what the configured index
actually sends, and the conversion test's row is one of its answers unedited.
Buying another person's item has not been driven end to end, so what a buyer
can do with the three identities now published is proven in the source and not
yet on an installed Home.
Removed the unused `ethereum_signed_message_hash` import from the custody provider tests. Updated the run creation logic in the model provider tests to utilize a more robust request handling mechanism, ensuring timely responses and improved error handling during stalled operations.
@irzhywau

irzhywau commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by the new stack on develop: the Creator, Reader, audio and crypto-review half is #204, and the mint and open half is #205 (stack #203 → #204 → #205 → #206).

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