Skip to content

feat(protected-content): buy from any live offer, found by link or in Explore - #70

Closed
irzhywau wants to merge 3 commits into
feat/protected-content-0.7.1-followupfrom
feat/protected-content-listing-buy
Closed

irzhywau wants to merge 3 commits into
feat/protected-content-0.7.1-followupfrom
feat/protected-content-listing-buy

Conversation

@irzhywau

Copy link
Copy Markdown
Contributor

Stacked on #62 (feat/protected-content-0.7.1-followup). It merges only after, or together with, #62.

What this does

A buyer can buy one copy of any protected item that has a live offer on chain, whoever minted it: this Runtime, base.ela.city or the SDK. The item can come from a pasted link or from Explore. The purchase rests on chain terms alone. Reading (open, playback) is a separate workflow with its own dataset, the shared metadata.json, and never blocks a purchase.

  • Design and all rulings (D1–D16, R9–R51): docs/audits/2026-09-24-protected-content-market-buy-design.md
  • Implementation report and installed results: docs/audits/2026-09-25-protected-content-market-buy-implementation-report.md
  • Operator flow: docs/PROTECTED_CONTENT.md (steps 7–8, "Buying from an offer")
  • Open work: TASKS.md, "Buy from any live offer"

Changes

  • chain-provider reads corroborated by two sources pinned to a common finalized block:

    • item;
    • KID binding;
    • every live offer, capped at 32 sellers;
    • access by item (hasAccess(address,address,uint256));
    • verified listing.

    A source that returns HTTP 429 gets a short retry, then a 60 s cooldown while the read goes on without it. A read stops once two sources agree.

  • Runtime:

    • POST /api/apps/marketplace/listing returns a ListingObject built from a token URI, a KID or (ledger, tokenId).
    • buy_offer buys on the agreed terms and answers terms_changed, attempt_in_progress or own_offer where they apply.
    • The market purchase record is created only once, under a per-principal lock.
    • A buyer holds at most one open purchase per item, across buy and buy_offer.
    • A declined or reverted attempt is retired.
    • Adoption onto the existing open path uses the shared metadata.json.
  • Marketplace capsule:

    • The offer sheet lists every live seller.
    • An item can be bought from a link or from Explore.
    • A recorded attempt can be continued.
    • An item whose adoption is pending gets Download copy.
    • A bought item gets a Details view.
    • Owned labels follow what this Home actually holds.
    • Explore opens on the market, with one automatic retry.
  • Providers:

    • An idled Kubo is restarted instead of every fetch failing (a defect that predates this PR).
    • The market index gets time for a cold cache (12.7 s measured).
    • The purchase's verified-listing read now pins its sources to a common block (a defect that predates this PR).

Verification

Suite Result
Server broad filter set 351 passed, 0 failed, 1 ignored
Server fmt / clippy -D warnings clean
chain-provider 139 passed, fmt and clippy clean
Marketplace node tests 109 passed
Layout and behavior smoke OK
home-entropy-check, git diff --check pass

Money and security behaviour went through four independent reviews (security/money, protocol/spec, capsule UI, tests/quality) and a scoped re-review after each fix round. No Critical or Important finding is open.

Installed, Base mainnet, 25 September: a buyer Home bought an ERC-20 (USDC) offer from Explore. The item was minted on base.ela.city with Lit custody.

  • USDC approve, then buyAccess.
  • Transaction 0x4d8ba231db31d81239af3257832335b3d951578bd150eb4ba2a2aba4b971bc12.
  • Chain access proven at block 51786085.
  • Record complete; adoption foreign, which is correct for Lit custody.

Not covered yet

… Explore

Buying and reading become two workflows with two datasets. A purchase
names an item, a seller and the terms the buyer agreed to, and rests on
chain terms alone; opening keeps the existing custody read path.

- Chain-provider: resolve an item's operative and token URI, a content
  id's binding, every live offer (32-seller cap), an item-keyed access
  read (hasAccess(address,address,uint256)) and the verified listing.
  Every corroborated read pins its sources to the lowest finalized block
  they agree on. A rate-limited source is retried briefly, then read
  around for 60 s, and a read stops once two sources agree.
- Listing object: POST /api/apps/marketplace/listing rebuilds an item's
  listing from a token URI, a content id or (ledger, tokenId), with every
  live seller's terms. The shared metadata.json and the KID binding are
  readability evidence only (verified, unverified, foreign or unknown),
  never a purchase gate. A proven KID contradiction is refused.
- buy_offer: one copy on exactly the agreed terms; terms_changed,
  attempt_in_progress and own_offer answers. Ownership is checked on
  chain by item before paying. A buyer holds at most one open purchase
  per item across buy and buy_offer, created under a per-principal lock
  and updated or retired only by its own attempt; a declined or reverted
  attempt is retired. Wallet addresses are redacted from error detail.
- Adoption: a completed purchase of an ElastOS-protected item becomes
  the records the open path reads, built from the shared metadata.json
  once its KID is proven; other items stay foreign.
- Marketplace capsule: offer sheet with every live seller, buy from a
  link or from Explore, resumable recorded attempts, Download copy for a
  pending adoption, a Details view of a bought item's properties, owned
  labels that follow what this Home holds, and Explore opening on the
  market with one automatic retry. Modals open on Close.
- Providers: an idled Kubo is restarted instead of failing every fetch;
  the market index gets time for a cold cache and one retry.

Design: docs/audits/2026-09-24-protected-content-market-buy-design.md
Report: docs/audits/2026-09-25-protected-content-market-buy-implementation-report.md
@irzhywau
irzhywau added this pull request to stack #61 September 25, 2026 19:08
@irzhywau
irzhywau requested a review from andersalm September 25, 2026 19:08
@andersalm

Copy link
Copy Markdown
Contributor

Thanks, Irzhy. We are keeping J5 listing and Buy work for later in 0.7.2 while we finish the focused release path. I have not reviewed this PR yet. Please keep it open for a separate review.

…d of failing

Custody releases a key share only on rights evidence read at the finalized
block. On Base that trails a confirmed mint or purchase by 15-20 minutes, so
an open attempted right after one spent a Wallet approval and three custody
calls, then failed closed as "unavailable". The finalized-only rule stays;
the open now recognises the wait and says so.

- Chain-provider: resolve_protected_content_purchase_access takes an
  optional block (latest by default, so existing callers are unchanged).
  At finalized it asks exactly the question custody will ask.
- Runtime open: for a copy confirmed less than an hour ago, before any
  Wallet request or custody call, access is read at finalized and then at
  the head. Granted only at the head answers the new typed `finalizing`
  stage (resumable, awaits nobody, with an estimated ready_at). Every other
  answer, including a failed read, leaves the open unchanged.
- Reader and Player: wait through `finalizing`, asking every 15 s for up to
  40 minutes, and show when the copy is expected to open.
- Docs: why key release rests on finalized state rather than safe, what the
  wait costs, and how the open handles it.
… terminal cleanup proof is not adopted

The assertion printed only the state, so a cleanup_pending answer could not
say which fallback produced it. It now prints the whole response, reason
included.
@irzhywau

irzhywau commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #206, the last PR of the new stack #203 → #204 → #205 → #206 on develop.

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.

2 participants