Skip to content

feat(pricing): consume the canonical hub /v1/pricing for dynamic per-capsule $DIG#5

Merged
MichaelTaylor3d merged 2 commits into
mainfrom
feat/125-dynamic-commit-pricing
Jul 6, 2026
Merged

feat(pricing): consume the canonical hub /v1/pricing for dynamic per-capsule $DIG#5
MichaelTaylor3d merged 2 commits into
mainfrom
feat/125-dynamic-commit-pricing

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

TLDR

digstore commit/deploy no longer show or spend a flat 100 DIG. They now use the dynamic, USD-pegged per-capsule price from the ONE canonical source — the hub's GET https://hub.dig.net/v1/pricing (mint_dig, base units) — so digstore can never diverge from what DIGHub charges. Closes #125.

What changed

  • digstore-chain::pricing (new): fetches + parses mint_dig from /v1/pricing; fails LOUD on any unreachable endpoint / undecodable body / missing-or-zero price. Pure parse_capsule_price is unit-tested; conformance test pins the exact amounts ($1 ÷ dig_usd) the hub's own dighub_data::pricing produces.
  • CLI money-path (commit, deploy): an explicit amount (--dig-amount > DIGSTORE_DIG_AMOUNT > dig.toml dig-amount) wins and stays deterministic/offline; otherwise the live price is fetched and both DISPLAYED and built into the on-chain DIG-CAT spend (anchor.update_root(..., dig_amount)). deploy defers the fetch past the --if-changed no-op guard so a no-op deploy never hits the endpoint. doctor/setup show the live figure best-effort (never fail on a flaky feed).
  • COMMIT_DIG (100_000) kept ONLY as a public symbol sibling repos depend on (dig-node dig-wallet default) — no longer a spend fallback.
  • SPEC.md §10 documents the contract (resolution order, fail-loud, additive response). SYSTEM.md (superproject) already maps the cross-repo contract; docs.dig.net (onchain-anchoring.md, dig-cat-payment.md) already documents the dynamic price.

Canonical source reused (no second formula)

The price has exactly one computation: the hub server (packages/dighub-data/src/pricing.rs pure formula capsule_price_baseunits + services/api/src/pricing.rs live DIG→USD oracle: dexie DIG_XCH × CoinGecko chia/usd), served as mint_dig. digstore consumes that number verbatim — it does not reimplement the formula or the oracle. Verified live: GET /v1/pricing returns HTTP 200 with mint_dig.

Blast radius

commit/deploy/doctor/setup cost display + the built spend amount; dig_toml amount resolution (resolve_dig_amountexplicit_dig_amount). The DIG-CAT payment builder + verify_commit_pays_dig_treasury are unchanged (they already took the amount as input). No .dig format change.

Tests (TDD, money-path)

  • chain: 8 unit tests (parse, string coercion, fallback/stale detection, missing/zero fail-loud, live-fetch, unreachable fail-loud, conformance amounts).
  • cli: commit_dry_run_fetches_dynamic_price_not_flat_100, commit_fails_loud_when_price_unavailable_and_no_override, explicit_dig_amount_skips_fetch_even_when_endpoint_down, deploy_dry_run_fetches_dynamic_price, plus updated dry-run cost assertions. Offline suite pins DIGSTORE_DIG_AMOUNT; pricing tests use a local mock endpoint / an unreachable URL. No test broadcasts a mainnet spend.
  • fmt clean; clippy -D warnings clean on digstore-chain/digstore-cli.

Version

0.7.2 → 0.8.0minor (new dynamic-pricing capability; backwards-compatible: an explicit amount still works offline).

…capsule $DIG

commit/deploy no longer spend a flat 100 DIG. The per-capsule price is dynamic
and USD-pegged with ONE canonical source: the hub server computes it (pure
formula in dighub_data::pricing + a live DIG->USD oracle) and serves it at
GET https://hub.dig.net/v1/pricing as `mint_dig` (base units). digstore consumes
that SAME number so it can never diverge from what the hub charges.

- digstore-chain: new `pricing` module fetches + parses `mint_dig`, fails LOUD on
  any unreachable endpoint / bad body / missing-zero price (money-path discipline).
- CLI: `explicit_dig_amount` (--dig-amount > DIGSTORE_DIG_AMOUNT > dig.toml) wins
  and stays deterministic/offline; otherwise the live price is fetched and both
  DISPLAYED and built into the on-chain DIG-CAT spend. deploy defers the fetch past
  the --if-changed no-op guard. doctor/setup show the live figure best-effort.
- COMMIT_DIG (100_000) kept ONLY as a public symbol sibling repos depend on; it is
  no longer a spend fallback.
- SPEC.md §10 documents the contract; SYSTEM.md (superproject) already maps it.

TDD: fetch path, fail-loud path, and explicit-override short-circuit are covered
against a local mock endpoint; the offline suite pins DIGSTORE_DIG_AMOUNT.

Closes #125
@MichaelTaylor3d MichaelTaylor3d merged commit 99e244a into main Jul 6, 2026
10 checks passed
@MichaelTaylor3d MichaelTaylor3d deleted the feat/125-dynamic-commit-pricing branch July 6, 2026 17:45
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…-up (D-PROOF-PATHLEN)

§9.1 carries an odd node up unchanged; a carried-up leaf skips a level so its
inclusion path is strictly shorter than the full spine. §9.5's ceil(log2 n) is
therefore the worst-case/upper bound on sibling count, attained by the
full-spine leaf (index 0), not a per-leaf equality. ProofStep{hash,is_left} has
no identity encoding under §9.3's fold-and-hash, and forcing == would require
either duplicating odd nodes (changing the root, breaking §9.1 and the D5
cross-crate root agreement) or a non-spec sentinel step. The <= bound is
strictly sound: verify() still recomputes the trusted root (§9.4).

- Record approved deviation in 00-DATASECTION-CONTRACT.md (D8) and design doc (#5)
- merkle.rs module doc states the bound
- Property tests: every leaf path.len() <= ceil(log2 n) AND verify();
  full-spine leaf attains the bound; concrete n=3 carry case documented

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…dual #4/#5)

Residual #4 — JWT signature verification (guest gate):
- verify_request_jwt now fetches the store's trusted JWKS over the
  session-gated jwks_fetch import (URL from the embedded AuthInfo section,
  §6.2), parses it, selects the key by kid (or the sole key), and verifies the
  token signature via jwt::verify_signature (RS256 = rsa PKCS#1 v1.5 over
  SHA-256, verify-only; ES256 = p256). Claims are still enforced first.
- The gate is no longer hardcoded off: GateConfig::from_embedded derives
  require_jwt from AuthInfo.requires_jwt, wired into get_content/get_proof.
- Fails closed -> Decoy on tampered/absent signature, wrong/unknown key,
  missing JWKS URL, or any claim failure.
- Tests: valid-sig+claims -> Real; tampered-sig/wrong-key/unknown-kid/absent-
  url/expired -> Decoy; existing session-gate + claim tests kept green. Test
  RSA private key is generated and signs in-test only; guest verifies via (n,e).
- Stays no_std (alloc); wasm32-unknown-unknown release build green.

Residual #5 — re-evaluate accepted advisories:
- deny.toml: removed the stale RUSTSEC-2025-0141 (bincode) ignore — bincode is
  not in the dependency tree. Kept RUSTSEC-2023-0071 (rsa/Marvin) with a
  comment: rsa is a direct verify-only dep (JWT RS256), so the decryption-oracle
  timing attack is N/A; re-evaluate if rsa is ever used to decrypt.
- SECURITY.md: residual #4 marked implemented; residual #5 updated (bincode
  removed, rsa verify-only/Marvin N/A; corrected "transitive" to direct dep).

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
… (Wave-1 #5/#6/#13/#14)

Close the biggest journey gaps before a developer ever spends DIG. Everything in
this slice is FREE — no wallet, no chain, no spend — so a developer can scaffold,
preview through the real read path, and preview cost before the one paid step.

#5 `digstore new <template>` — a free, no-mint scaffolder (today the only on-ramp,
`init`, mints 100 DIG into an empty dir). Five templates embedded in the binary
(include_str!): static-site, vite-react, next-static, nft-drop, dapp-window-chia.
Each writes a runnable local project — a `dig.toml`, a starter app, and (dapp/NFT)
a `window.chia` usage example. Refuses a non-empty dir without --force; creates no
`.dig` and never touches chain.

#6 `digstore dev` — the local inner loop: build-on-save + serve the REAL dig://
read path locally (init_store → add → commit → host runtime → merkle-verify →
AES-GCM-decrypt, the same plumbing as compile/cat), with live reload + an injected
dev `window.chia` shim. So "renders under dev" == "renders after deploy". Poll-based
watcher (no notify dep); multi-thread tokio runtime so a page's asset burst doesn't
stall; SPA fallback for routes but a true 404 for missing assets; shims injected
only into HTML at request time (never baked into the served bytes).

#13 `digstore doctor` + `commit --dry-run` — `doctor` runs pre-publish checks
(seed present/unlocked, funds vs 100 DIG + XCH fee, dighub login, default remote
reachable, content dir exists) as pass/fail, exits non-zero on a hard fail, never
spends. `commit --dry-run` computes + prints the resulting version (root) and the
exact DIG/XCH cost with NO seed unlock, NO wallet scan, NO anchor, NO finalize.

#14 task-first CLI help — one-line `about` strings lead with the task (commit =
"Publish your staged files as a new version"); the commit success line is now a
plain "Published a new version — it's live and permanent." with the capsule id
below it and the module/coin protocol detail behind --verbose.

Shared `dig.toml` reader extracted to `dig_toml::DigToml` (deploy + dev + doctor;
removes deploy's private copy). Templates/assets embedded via include_str!.

TDD: failing tests first throughout. 178 lib tests + new integration suites
(cli_new, cli_dev, cli_doctor, cli_commit_dryrun, cli_help additions) green
against the installed binary; cargo fmt + clippy -D warnings clean. Guest wasm
built and binary reinstalled per digstore discipline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjQgGAisBupqs7vpkmUhuN
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…capsule $DIG (#5)

* feat(pricing): consume the canonical hub /v1/pricing for dynamic per-capsule $DIG

commit/deploy no longer spend a flat 100 DIG. The per-capsule price is dynamic
and USD-pegged with ONE canonical source: the hub server computes it (pure
formula in dighub_data::pricing + a live DIG->USD oracle) and serves it at
GET https://hub.dig.net/v1/pricing as `mint_dig` (base units). digstore consumes
that SAME number so it can never diverge from what the hub charges.

- digstore-chain: new `pricing` module fetches + parses `mint_dig`, fails LOUD on
  any unreachable endpoint / bad body / missing-zero price (money-path discipline).
- CLI: `explicit_dig_amount` (--dig-amount > DIGSTORE_DIG_AMOUNT > dig.toml) wins
  and stays deterministic/offline; otherwise the live price is fetched and both
  DISPLAYED and built into the on-chain DIG-CAT spend. deploy defers the fetch past
  the --if-changed no-op guard. doctor/setup show the live figure best-effort.
- COMMIT_DIG (100_000) kept ONLY as a public symbol sibling repos depend on; it is
  no longer a spend fallback.
- SPEC.md §10 documents the contract; SYSTEM.md (superproject) already maps it.

TDD: fetch path, fail-loud path, and explicit-override short-circuit are covered
against a local mock endpoint; the offline suite pins DIGSTORE_DIG_AMOUNT.

Closes #125

* build: bump workspace version to 0.8.0 (Cargo.lock)

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…-up (D-PROOF-PATHLEN)

§9.1 carries an odd node up unchanged; a carried-up leaf skips a level so its
inclusion path is strictly shorter than the full spine. §9.5's ceil(log2 n) is
therefore the worst-case/upper bound on sibling count, attained by the
full-spine leaf (index 0), not a per-leaf equality. ProofStep{hash,is_left} has
no identity encoding under §9.3's fold-and-hash, and forcing == would require
either duplicating odd nodes (changing the root, breaking §9.1 and the D5
cross-crate root agreement) or a non-spec sentinel step. The <= bound is
strictly sound: verify() still recomputes the trusted root (§9.4).

- Record approved deviation in 00-DATASECTION-CONTRACT.md (D8) and design doc (#5)
- merkle.rs module doc states the bound
- Property tests: every leaf path.len() <= ceil(log2 n) AND verify();
  full-spine leaf attains the bound; concrete n=3 carry case documented

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…dual #4/#5)

Residual #4 — JWT signature verification (guest gate):
- verify_request_jwt now fetches the store's trusted JWKS over the
  session-gated jwks_fetch import (URL from the embedded AuthInfo section,
  §6.2), parses it, selects the key by kid (or the sole key), and verifies the
  token signature via jwt::verify_signature (RS256 = rsa PKCS#1 v1.5 over
  SHA-256, verify-only; ES256 = p256). Claims are still enforced first.
- The gate is no longer hardcoded off: GateConfig::from_embedded derives
  require_jwt from AuthInfo.requires_jwt, wired into get_content/get_proof.
- Fails closed -> Decoy on tampered/absent signature, wrong/unknown key,
  missing JWKS URL, or any claim failure.
- Tests: valid-sig+claims -> Real; tampered-sig/wrong-key/unknown-kid/absent-
  url/expired -> Decoy; existing session-gate + claim tests kept green. Test
  RSA private key is generated and signs in-test only; guest verifies via (n,e).
- Stays no_std (alloc); wasm32-unknown-unknown release build green.

Residual #5 — re-evaluate accepted advisories:
- deny.toml: removed the stale RUSTSEC-2025-0141 (bincode) ignore — bincode is
  not in the dependency tree. Kept RUSTSEC-2023-0071 (rsa/Marvin) with a
  comment: rsa is a direct verify-only dep (JWT RS256), so the decryption-oracle
  timing attack is N/A; re-evaluate if rsa is ever used to decrypt.
- SECURITY.md: residual #4 marked implemented; residual #5 updated (bincode
  removed, rsa verify-only/Marvin N/A; corrected "transitive" to direct dep).

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
… (Wave-1 #5/#6/#13/#14)

Close the biggest journey gaps before a developer ever spends DIG. Everything in
this slice is FREE — no wallet, no chain, no spend — so a developer can scaffold,
preview through the real read path, and preview cost before the one paid step.

#5 `digstore new <template>` — a free, no-mint scaffolder (today the only on-ramp,
`init`, mints 100 DIG into an empty dir). Five templates embedded in the binary
(include_str!): static-site, vite-react, next-static, nft-drop, dapp-window-chia.
Each writes a runnable local project — a `dig.toml`, a starter app, and (dapp/NFT)
a `window.chia` usage example. Refuses a non-empty dir without --force; creates no
`.dig` and never touches chain.

#6 `digstore dev` — the local inner loop: build-on-save + serve the REAL dig://
read path locally (init_store → add → commit → host runtime → merkle-verify →
AES-GCM-decrypt, the same plumbing as compile/cat), with live reload + an injected
dev `window.chia` shim. So "renders under dev" == "renders after deploy". Poll-based
watcher (no notify dep); multi-thread tokio runtime so a page's asset burst doesn't
stall; SPA fallback for routes but a true 404 for missing assets; shims injected
only into HTML at request time (never baked into the served bytes).

#13 `digstore doctor` + `commit --dry-run` — `doctor` runs pre-publish checks
(seed present/unlocked, funds vs 100 DIG + XCH fee, dighub login, default remote
reachable, content dir exists) as pass/fail, exits non-zero on a hard fail, never
spends. `commit --dry-run` computes + prints the resulting version (root) and the
exact DIG/XCH cost with NO seed unlock, NO wallet scan, NO anchor, NO finalize.

#14 task-first CLI help — one-line `about` strings lead with the task (commit =
"Publish your staged files as a new version"); the commit success line is now a
plain "Published a new version — it's live and permanent." with the capsule id
below it and the module/coin protocol detail behind --verbose.

Shared `dig.toml` reader extracted to `dig_toml::DigToml` (deploy + dev + doctor;
removes deploy's private copy). Templates/assets embedded via include_str!.

TDD: failing tests first throughout. 178 lib tests + new integration suites
(cli_new, cli_dev, cli_doctor, cli_commit_dryrun, cli_help additions) green
against the installed binary; cargo fmt + clippy -D warnings clean. Guest wasm
built and binary reinstalled per digstore discipline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjQgGAisBupqs7vpkmUhuN
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…capsule $DIG (#5)

* feat(pricing): consume the canonical hub /v1/pricing for dynamic per-capsule $DIG

commit/deploy no longer spend a flat 100 DIG. The per-capsule price is dynamic
and USD-pegged with ONE canonical source: the hub server computes it (pure
formula in dighub_data::pricing + a live DIG->USD oracle) and serves it at
GET https://hub.dig.net/v1/pricing as `mint_dig` (base units). digstore consumes
that SAME number so it can never diverge from what the hub charges.

- digstore-chain: new `pricing` module fetches + parses `mint_dig`, fails LOUD on
  any unreachable endpoint / bad body / missing-zero price (money-path discipline).
- CLI: `explicit_dig_amount` (--dig-amount > DIGSTORE_DIG_AMOUNT > dig.toml) wins
  and stays deterministic/offline; otherwise the live price is fetched and both
  DISPLAYED and built into the on-chain DIG-CAT spend. deploy defers the fetch past
  the --if-changed no-op guard. doctor/setup show the live figure best-effort.
- COMMIT_DIG (100_000) kept ONLY as a public symbol sibling repos depend on; it is
  no longer a spend fallback.
- SPEC.md §10 documents the contract; SYSTEM.md (superproject) already maps it.

TDD: fetch path, fail-loud path, and explicit-override short-circuit are covered
against a local mock endpoint; the offline suite pins DIGSTORE_DIG_AMOUNT.

Closes #125

* build: bump workspace version to 0.8.0 (Cargo.lock)

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant