Commit graph

414 commits

Author SHA1 Message Date
Local Dev
5465029321 feat(mainnet-infra): oracle client + Katalogos deploy script + design doc
Three artifacts the mainnet launch needs, scoped tightly:

design/potidaea/src/oracleClient.ts
  Replaces the baked chipnet dev signing key for mainnet. fetchOracleAttestation()
  pulls a fresh AnyHedge-format signed BCH/USD message from oracles.cash
  (General Protocols' production BCH oracle network — the same signer AnyHedge
  uses), verifies the Schnorr signature client-side against the pinned pubkey
  before handing it to the covenant. Chipnet keeps its publish.ts baked dev
  key (dev environment, no economic bearing). Mainnet pubkey slot is
  INTENTIONALLY empty — any call hard-throws until deploy, which kills the
  'chipnet-sponsor-key-classifier-block' concern for mainnet builds.

scripts/mainnet-katalogos-deploy.mjs
  One-shot broadcast of KatalogosRegistryV1 on BCH mainnet. Refuses to run
  unless .mainnet-katalogos-config.json supplies real (non-placeholder)
  values for feeCategoryId / oraclePubKeyHex / treasuryLB / deaRateSats.
  Interactive 'MAINNET DEPLOY' confirmation gate (or MAINNET_KATALOGOS_YES=1
  for CI). Writes MainnetTests/.mainnet-katalogos.json with the resulting
  (txid, registryCategoryId, contractAddress) and prints paste-ready config
  for KATALOGOS_VERSIONS.mainnet in shells/app/src/core/katalogos.ts.

wiki/infra/oracle-service.md
  Design doc explaining the Path-A choice (consume oracles.cash) vs
  Path-B (self-hosted oracle, future option), the rotation policy
  (operator-driven; we redeploy on their pubkey change), and the
  "do NOT sign messages locally on mainnet" guardrails. Links from
  publish.ts's dev-key constant and the Katalogos deploy script.

Depends on: feat(network) [5f852b9] — oracleClient.ts imports currentNetwork
from the network module.

Follow-up (requires real BCH + external coordination):
  - set the oracles.cash BCH/USD pubkey in oracleClient.ts
  - mint a mainnet DEA category (feeCategoryId prereq)
  - run scripts/mainnet-katalogos-deploy.mjs with the real config
  - paste the result into KATALOGOS_VERSIONS.mainnet
  - refactor publish.ts to consume fetchOracleAttestation() on mainnet

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-09 00:21:13 +02:00
Local Dev
e66a8d6c8a fix(wc): pairing diagnostics + 45s visible-timeout
Aegis (and anything built on @wizardconnect/dapp 0.2.x) can pair on its
own side and never advance ours — the dapp library's handleWalletReady
silently swallows invalid session payloads at dapp-connection-manager.js:436:

    if (!isHdwalletv1Session(sessionData)) {
      console.error("[wizardconnect/dapp] Invalid hdwalletv1 session data:", sessionData);
      return;   // ← no 'walletready', no 'disconnect' event — just a stuck chip
    }

When this fires, nothing in our chip updates. Previously the user watched
"connecting…" forever with no clue why.

Two additions:

Diagnostic ring buffer (last 40 relay messages):
  manager.on('messagereceived') / ('messagesent') / ('reconnecting') →
  push {t, dir, action, raw} into wc.diag, exposed as window.wcDiag.
  After a failed pairing attempt the user can paste
    JSON.stringify(window.wcDiag, (_,v) => typeof v === 'bigint' ? v.toString() : v, 2)
  from the browser console, which — combined with the console.error the
  dapp lib prints — tells us precisely which of the three silent-failure
  modes hit: session-shape rejected, protocol mismatch, or no inbound at
  all (relay dead).

Visible 45-second timeout:
  If snap.phase is still 'awaiting-scan' 45s after connect(), snap flips
  to 'error' with a message pointing the user at window.wcDiag. Any
  inbound message resets the timer (so a slow-reconnect but alive wallet
  doesn't false-trigger). cleanup() + walletready both clear the timer.

Zero behavior change in the happy path — the diagnostic listeners are
purely observational, and the timeout only fires when the dapp lib's
silent-return bug would have left us hung.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-09 00:20:51 +02:00
Local Dev
a7e501087b fix(potidaea): mock-balance flash + 'Potidaea · ASM' title
Two UX fixes on the same two files:

Title (both landing + app):
  'Potidaea · Swap on Bitcoin Cash' → 'Potidaea · ASM on Bitcoin Cash'
  'Potidaea · Swap'                → 'Potidaea · ASM'
Reflects the product-naming lock: the brand is the ASM (Automated Swap
Mechanism) engine, not a generic "swap" app. Also what the dynamic route-
title map in main.ts was already using.

Mock-balance flash (app.html):
  The inline-JS ASSETS array shipped with non-zero mock bals (BCH 0.8092,
  ASM 5000, ASM2 800, CAP 200, GMX 60, CHE 150) and the chip template +
  swap-leg text carried the same hardcoded 0.8092 figures. On page load,
  the inline JS fired renderBalances() with those mock numbers, then
  ~500 ms later main.ts's refreshBalance() overwrote with real electrum
  data — producing a visible "0.8 → 0" flash for any 0-balance wallet.

  Fix: zero every initial bal in ASSETS, replace the hardcoded chip +
  leg text with '…' placeholders, zero PAIR_BALS for the Anthemus swap
  view. Chip gets id="chipBch" so main.ts's first paintBalance writes
  into it directly.

Verified in-browser at localhost:5185/app.html:
  t=  7.1s  chip="…"           ASSETS.BCH.bal=0        (initial)
  t=  7.9s  chip="0.0101 BCH"  ASSETS.BCH.bal=0.01015  (real balance in)
  No 0.8092 ever appears.

Build size unchanged (~337 KB gzip).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-09 00:20:22 +02:00
Local Dev
9a24a19c94 feat(mba): rename covenants to CoinMBA / ShareMBA / OracleMBA + reserve DynamicMBA
Clean names that match the Mathematically Backed Asset framing (which came
first; the covenant source files were a later exploratory implementation
whose historical names outlived their usefulness). Rename is bytecode-
preserving — the chipnet-validated 52,998-swap proof carries over unchanged
because cashscript's `contract Name { }` is a source-level identifier that
doesn't enter the compiled script.

Source renames (git mv):
  contracts/DeviantOpenMintFT.cash  → contracts/CoinMBA.cash    (templateId 0x00)
  contracts/DeviantShareMBA.cash    → contracts/ShareMBA.cash   (templateId 0x01)
  contracts/DeviantOracleMBA.cash   → contracts/OracleMBA.cash  (templateId 0x02)

Deleted:
  contracts/ArtemisionMintFT.cash   (byte-identical duplicate of DeviantOpenMintFT;
                                     "Artemision" lives on only as the wizard UI
                                     concept, not a distinct template)

New:
  contracts/DynamicMBA.cash         (templateId 0x03 — reserved for the Dynamic
                                     Rate Asset covenant; scaffold only, rejects
                                     every call. DRA is distinct from FIATμ:
                                     FIATμ = dynamic-rate + oracle, DRA = pure
                                     state-driven, no oracle, reflective
                                     against a shared pool)

Compiled artifacts rebuilt via `cashc` and shipped to:
  - design/potidaea/src/artifacts/   (live client consumption)
  - site/deviant/docs/mba/artifacts/ (public download URLs for third-party
                                     wallet integration; also deployed to
                                     silentmode primary + Sia fallback)

Bytecode verification (sha256-of-bytecode):
  CoinMBA     == DeviantOpenMintFT  ✓ (6a9ae89b...)
  ShareMBA    == DeviantShareMBA    ✓
  OracleMBA   == DeviantOracleMBA   ✓

Client code updates (live surface only — historical scripts, snapshots,
knowledgebase etc. keep their original names since they reference specific
chipnet tx hashes that happened under those names):
  - design/potidaea/src/orbital.ts      — artifact imports
  - design/potidaea/src/genesis.ts      — artifact imports + Artemision deprecation note
  - design/potidaea/src/registry.ts     — artifact imports + dispatch doc + 0x03 note
  - design/potidaea/src/publish.ts      — TEMPLATE_* constants renamed + 0x03 added
  - design/potidaea/src/main.ts         — TEMPLATE_* usages renamed

Docs updates:
  - site/deviant/docs/mba/index.html         — templates table expanded to 4 rows,
                                              DRA vs FIATμ note added, dev-section
                                              code-reference lines updated
  - site/deviant/docs/mba/artifacts/index.html — one card per new artifact, DRA
                                                scaffold flagged "not deployable"

Follow-up (not in this commit): the chipnet reseed. Current ASM/ASM2
instances were minted under the DeviantOpenMintFT covenant address and
remain redeemable (bytecode is identical, addresses derive to the same
value). A fresh-start deploy that mints new CoinMBA instances under the
new identity is a separate step requiring real chipnet BCH funding +
Katalogos publish. ASM/ASM2 seed entries stay in orbital.ts SEED_MBAS
until that follow-up ships.

Also deferred: the ~15 experimental MBA-adjacent covenants
(DeviantOpenMintFT_Backdoor, _Escalating, _NoPeg, _Whitelist,
DeviantOracleMBAFloor, _DraFee, DeviantManagedMintFT,
DeviantTieredMintOracleFT, OraclePricedVault, ExpiringMBA,
DeviantPlatformMBA, etc.) stay in contracts/ at their current names —
none are user-facing templates, and their fate (archive, promote,
retire) deserves its own taxonomy session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-09 00:00:15 +02:00
Local Dev
6a58143966 feat(bcmr-integration): emit BCMR OP_RETURN on every MBA genesis + publish
Wires the bcmr.ts + bcmr-upload.ts modules (committed in f7ec956) into
the two surfaces where MBA identity enters the chain:

genesis.ts
  - New helper buildAndUploadBcmr(categoryId, args): builds the canonical
    JSON for an MBA via buildBcmrJson(), POSTs it to the gateway via
    uploadBcmr() (best-effort — on-chain OP_RETURN commits URL + hash
    regardless of upload success), returns the [magic, hash, url] chunks
    the caller passes to tb.addOpReturnOutput()
  - Called from both genesisArtemisionMBA (Static family, templateId 0x00)
    and genesisDeviantShareMBA (Share family, templateId 0x01); emits the
    DVMB template params inside extensions.deviant.dvmb so our stack can
    cross-check the covenant identity through both sidecars at once
  - GENESIS_MINER_FEE bumped 2000n → 3000n to cover the extra OP_RETURN
    output (~180B worst case: 4B BCMR magic + 32B hash + ~90B URL + push
    opcodes). Keeps every genesis above 1 sat/byte

publish.ts
  - Katalogos register() tx now emits a BCMR OP_RETURN alongside the
    existing DVMB OP_RETURN when metadata.ticker is present. Two
    OP_RETURN outputs in one tx is standard-policy-accepted on chipnet
    + mainnet BCHN; combined DVMB + BCMR data sits under the 220-byte
    datacarriersize ceiling.

End-to-end effect: every new MBA minted or registered through Potidaea
now carries both sidecars. Our stack reads DVMB; every general-purpose
BCH wallet (Cauldron, Selene, Zapit, Bitcoin.com, Cashonize, Paytaca)
reads BCMR and autoresolves the ticker/name/icon without any
coordination with us.

Depends on: feat(network) [5f852b9] + feat(broadcast) [b8ac497] +
feat(bcmr) [f7ec956]. The imports and the tb.build() + fanoutBroadcast
pattern pre-exist in both files from those commits; this one adds the
BCMR hunks only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-08 23:36:16 +02:00
Local Dev
b8ac497323 feat(broadcast): multi-source fan-out replacing single-endpoint cashscript send
Introduces design/potidaea/src/broadcast.ts and wires every tx-emitting
site in the Potidaea client through it. Replaces cashscript's
TransactionBuilder.send() — which hits ONE hardcoded electrum endpoint
(wss://chipnet.bch.ninja:50004) — with a parallel fan-out that pushes
the same signed tx to as many independent nodes as the pool allows.

broadcast.ts
  - fanoutBroadcast(rawTxHex) fires in parallel:
      (a) every pre-opened ElectrumClient the caller passed in
      (b) up to N (default 3) fresh ephemeral WSS connections drawn
          from endpointsFor(currentNetwork()), shuffled; closed after
      (c) every HTTPS JSON-RPC proxy in broadcastRpcFor(currentNetwork())
          — our own-node write path, uncoupled from third-party Fulcrums
  - Resolves on first success, aggregates rejections for diagnostics
  - txidOf(rawTxHex) computes txid client-side (double-SHA256 + display
    byte-order) so we trust our own hash, not whatever a server echoes
  - rpcBroadcast handles BCHN "already known" (-27) as success — a tx
    already in mempool is "safely propagated" from the caller's view

Mixed-content handling: ephemerals skip ws:// URLs when the page itself
is served over https:, since Chrome's loopback exception doesn't apply
to in-page requests from a secure origin.

Integration (all 6 tb.send() sites):
  orbital.ts
    - claimViaLocalSigner       (BCH → MBA, Static family)
    - claimViaLocalSignerShare  (BCH → MBA, Share family)
    - redeemViaLocalSigner      (MBA → BCH, any family)
  genesis.ts
    - genesisArtemisionMBA      (Static family genesis)
    - genesisDeviantShareMBA    (Share family genesis)
  publish.ts
    - publishViaLocalSigner     (KatalogosRegistryV1.register tx)

Shape at each site: tb.build() to get raw hex, then
  const { txid } = await fanoutBroadcast(rawTxHex)
Caller gets the exact same ClaimTxResult / GenesisResult / PublishResult
shape as before — swapping cashscript's send for our fan-out is purely
additive at the API layer.

Server-side companion: scratchpad/silentmode-broadcast-proxy/ (chipnet
RPC proxy bundle) + scratchpad/mainnet-broadcast-proxy/ (mainnet twin,
pending BCHN install per MAINNET-BCH-PLAYBOOK.md). The chipnet
broadcastRpcFor() URL (chipnet.silentmode.st/rpc/broadcast) starts
responding once that bundle lands on silentmode; until then the public
Fulcrums carry the broadcast.

Depends on: feat(network) — broadcast.ts imports endpointsFor +
currentNetwork from the network module.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-08 22:45:12 +02:00
Local Dev
5f852b945e feat(network): chipnet|mainnet state module + chain-aware wiring
Introduces design/potidaea/src/network.ts as the single source of truth
for which BCH chain the browser session is pointed at. Everything that
talks to the chain reads from it: endpoint pool, address encoding,
cashscript provider, catalog, WalletConnect chainId, topbar chip.

New module:
  network.ts
    - currentNetwork() / setNetwork() / onNetworkChange()
    - persisted in localStorage so a reload keeps the choice
    - addrPrefix(net), cashscriptNetwork(net), networkLabel(net) helpers

endpoints.ts
  - Split into per-chain CHIPNET_ENDPOINTS / MAINNET_ENDPOINTS plus
    endpointsFor(net) selector (mirror in CHIPNET_BROADCAST_RPC /
    MAINNET_BROADCAST_RPC for the broadcast-RPC pool that the broadcast
    commit will consume).
  - Chipnet pool widens to include the local-dev Fulcrum (slot 0, Vite
    DEV only), silentmode Fulcrum slot, and coinspectrum fallback.
  - Mainnet pool mirrors shells/app/src/core/spv/pool.ts public set.

Chain-aware integration (single-line swaps across many files):
  electrum.ts       — ElectrumClient.connectAny defaults to endpointsFor(currentNetwork())
  orbital.ts        — all four ElectrumNetworkProvider sites use cashscriptNetwork(currentNetwork());
                      KNOWN_MBAS → per-chain SEED_MBAS[Network] with refreshMbasForNetwork(net)
                      wipe-and-reseed, mainnet starts empty
  genesis.ts        — provider swap at both Artemision + Share genesis entry points
  publish.ts        — provider swap + mainnet guard throwing
                      PublishError('wrong-network') (KatalogosRegistryV1 is
                      chipnet-only until deployed; the chipnet dev oracle key
                      must never ship to mainnet)
  registry.ts       — provider swap in discoveredParamsToKnownMba
  signer-store.ts   — signer address encoding follows currentNetwork() so
                      the same WIF produces bchtest: on chipnet and
                      bitcoincash: on mainnet; createUserWallet() uses the
                      matching WIF flavor

shells/app/src/core/spv/pool.ts
  - Owns-slot wiring: dev Fulcrum ws://127.0.0.1:50003 (DEV only) +
    chipnet.silentmode.st:50012 own slot + coinspectrum fallback on
    chipnet; mainnet set unchanged from the existing public pool.

main.ts
  - window.setPotidaeaNetwork bridge invoked by app.html netMenu
  - onNetworkChange listener: close cached client, refresh catalog,
    re-derive signer, repaint chip + mainnet banner
  - Boot-time badge + banner sync (localStorage might hold "mainnet"
    from a prior session; corrects the HTML's static "chipnet" default)
  - WC import-flow prefix check uses addrPrefix() instead of hardcoded
    "bchtest:"

app.html
  - netMenu mainnet item un-disabled, tag now "read-only" with tooltip
  - netMenu click handler calls window.setPotidaeaNetwork(net)
    (no more "mainnet is coming" toast)

Verified in-browser: flipping to mainnet shows the view-only banner,
badge updates to "● mainnet", localStorage persists; flipping back
restores chipnet reads (live BCH balance via local Fulcrum).

Follow-up commits land: feat(broadcast) — fan-out module + wiring,
feat(bcmr-integration) — genesis/publish emission hooks that depend on
both this + the broadcast commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-08 22:42:49 +02:00
Local Dev
f7ec956d6e feat(bcmr): CHIP-2023-04 metadata registry client for MBAs
Two new modules the Potidaea client uses to emit BCMR sidecars alongside
every MBA genesis + Katalogos publish. CHIP-2023-04 is the interop standard
every general-purpose BCH wallet already reads (Cauldron, Selene, Zapit,
Bitcoin.com, Cashonize, Paytaca); emitting it means our MBAs show up with
correct ticker/name/icon in those wallets without any coordination.

bcmr.ts
  - BcmrRegistry JSON schema (v2 subset — tokens, uris.icon, extensions)
  - buildBcmrJson(): canonical identity payload per MBA, with our DVMB
    template params stashed in extensions.deviant.dvmb so our own stack
    can cross-check the covenant identity through both channels
  - canonicalize() + serializeBcmr(): JCS-style ordered serializer so
    the content hash is reproducible across publishers
  - bcmrContentHash(): single SHA-256 of canonical bytes (per CHIP spec)
  - bcmrPublicUrl(): per-network URL at silentmode.st/bcmr/
  - bcmrOpReturnChunks(): the [magic, hash, url] tuple cashscript's
    tb.addOpReturnOutput() takes directly

bcmr-upload.ts
  - uploadBcmr(): POST canonical bytes to navigate.st/api/bcmr-upload/.
    Mirrors the icon-upload contract (content-addressed by categoryId,
    gateway-side shape check, idempotent). Best-effort — the on-chain
    OP_RETURN commits the URL+hash regardless of upload success, so a
    later retry of the same canonical bytes is a no-op.

Wire format verified against the CHIP: OP_RETURN <'BCMR'> <sha256(json)>
<url>. Single SHA-256 (not double). URLs utf-8, multiple pushes allowed.

The genesis.ts / publish.ts integration hooks that CALL these modules are
tangled with the broadcast-fanout + network-awareness work from this
session (same lines, same imports) and will land together in a follow-up
commit. The two modules here are self-contained and build cleanly as-is.

Server-side bundle (bcmr-gateway.mjs sidecar + systemd unit + nginx snippet)
lives in the session scratchpad for deployment to silentmode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-08 22:29:45 +02:00
Local Dev
11e83e2501 docs(mba): publish Mathematically Backed Asset explainer at deviant.x/docs/mba/
Hub at site/deviant/docs/index.html (lists MBA + draft stubs for Potidaea,
Anthemus, FIATμ, correspondent). MBA page covers the sound-money heritage
(PoW BCH as "metal", covenant bytecode as "stamp"), three template families
(Static/Share/Oracle), interaction model, trust guarantees + explicit
non-guarantees, BCMR + DVMB wallet-integration story.

Third-party integration section is Tier-1 / Tier-2 / Tier-3 with the full
claim + redeem tx shapes, so a wallet dev can transact MBAs without any
coordination with us. Covenant artifact JSONs published alongside at
docs/mba/artifacts/ (DeviantOpenMintFT, ArtemisionMintFT, DeviantShareMBA,
DeviantOracleMBA) with constructor args and a verify-covenant example.

Deployed via both legs: silentmode:/opt/silent-mode/site/deviant/docs/ and
Sia s3://bns/deviant.x/docs/. Reachable at navigate.st/deviant/docs/mba/,
navigate.st/bns/deviant.x/docs/mba/, and deviant.x/docs/mba/ (BNS canonical).

README adds the docs/ inventory and the two-leg deploy block for future
docs/<topic>/ pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-08 22:27:17 +02:00
Local Dev
a014f6636f feat(asm-sequencer): settings-UI toggle in both surfaces (wallet committed; Potidaea drafted)
Beta wallet (shells/app/src/screens/SettingsScreen.tsx)
  New "ASM sequencer relay" group between Network and Token definitions.
  URL input + live reachability probe (useEffect fires on endpoint change,
  shows checking → Reachable / Unreachable). Persists via localStorage key
  asm-sequencer.endpoint through the sequencerRelay module. Default empty =
  trustless tier-4 retry stays in charge.

Server (scripts/asm-sequencer/server.mjs)
  Permissive CORS (Access-Control-Allow-Origin: *, methods GET/POST/OPTIONS,
  content-type header, 10 min max-age) + explicit OPTIONS preflight handler.
  Production deploys tighten this at the reverse proxy; the dev daemon needs
  CORS so any wallet/dApp origin can probe /health and POST /claim.

Potidaea (design/potidaea/{app.html, src/main.ts})
  Drafted locally but NOT committed — those files carry the DEA asset-side
  session's uncommitted network-toggle work on the same lines. My wiring
  (wireSequencerSettings + Section 4 render in paintWalletMenu) will rebase
  cleanly once that session commits. Browser-verified against the live
  chipnet daemon: ✓ reachable chip, saved to LS, menu stays open while
  typing, ⚠ unreachable chip for 127.0.0.1:9999.

Snapshot 0280 documents the arc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-02 20:40:59 +02:00
Local Dev
07fd344299 feat(asm-sequencer): systemd + user-funded /tip+/submit + wallet client
Three-part completion of the tier-2 arc from 0278:

1. systemd — scripts/asm-sequencer/deploy/{asm-sequencer.service,README.md}
   Sandboxed unit (NoNewPrivileges, ProtectSystem=strict, PrivateTmp,
   MemoryDenyWriteExecute), EnvironmentFile at /etc/asm-sequencer/env,
   SIGINT graceful shutdown, auto-restart, nginx reverse-proxy snippet.

2. User-funded endpoints — server.mjs adds:
   GET  /tip      current reservoir + token metadata for local tx build
   POST /submit   body {signedTxHex, tipTxid, tipVout, amountUnits}
                  under mutex: verify tip, provider.sendRawTransaction,
                  advance local chain. 409 stale with currentTip on race.

   Chipnet proof via scripts/chipnet-asm-sequencer-userfunded-test.mjs:
     5/5 accepted, 15 total /submit attempts (mean 3.0), 6.6 s wall time,
     daemon chain depth 0 → 5. Users signed their own BCH inputs locally;
     daemon received signed hex only, never saw a user key.
     Txids: 32ba4055… → 10945cf9… → 71ef9a7e… → ed8fedaa… → 05e7f66d…

   Funded A/C/E/F/G from H via new scripts/chipnet-fund-from-h.mjs
   (one tx dc725fa5f3252b40…, 5×150k sats outputs, 32.6M-sat change
   back to H).

3. Wallet-side client — sequencerRelay.ts in both:
   - shells/app/src/asm-core/
   - design/potidaea/src/

   Same API: getSequencerEndpoint/setSequencerEndpoint (localStorage
   key asm-sequencer.endpoint), sequencerReachable, sequencerClaim,
   claimViaRelayOrFallback. Probe-first-then-commit dispatch — the
   relay and local path both advance the SAME reservoir UTXO so parallel
   execution would create a self-double-spend risk; the probe cost is
   worth paying for correctness. Default disabled; opts in via LS key.

Snapshot 0279 + wiki/asm/zero-contention-protocol.md updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-02 00:21:51 +02:00
Local Dev
1180b2665d chore(asm-sequencer): default COORD to wallet H (consolidated reserve)
Wallet A-G were drained by the retry proof + first sequencer run.
User consolidated their chipnet BCH into wallet H (~1.86 BCH across
39 UTXOs, top 33.4M sats) on 2026-09-29 as the single funder for
future chipnet demos. GMX still has 0.386 BCH but is dust-heavy.

Re-ran the daemon test with COORD=h: 5/5 chained (txids
e90d3c38…/eb91d05c…/9339ec71…/400d0c63…/b1bea343…), 10.1 s wall time
on public electrum, chain valid, ~293k sats consumed. Combined with
the earlier GMX-coordinated run, that's 10 consecutive chained claims
on ONE reservoir across two sessions with zero collisions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-29 01:55:21 +02:00
Local Dev
b8b110dc97 feat(asm): tier-2 sequencer service drafted + chipnet-proved 5/5
Adds the coordinator-funded HTTP relay from
Parameters/ASM_Native/ASM-single-asset-throughput.md §2a — the
low-latency path that complements the tier-4 wallet retry (0272/0275/
0277) by turning N simultaneous claim requests into an ordered chain
from ONE reservoir. Trust surface: the covenant enforces the swap
math either way; the relay can only order and rebroadcast.

scripts/asm-sequencer/
  server.mjs   — Node HTTP daemon: GET /health, POST /claim, POST /admin/reload-tip
  client.mjs   — reference client (portable to wallet + dApp)
  lib/state.mjs — SequencerState + async mutex over the chain

scripts/chipnet-asm-sequencer-daemon-test.mjs
  spawns daemon, fires 5 parallel /claim requests, verifies txid chain

Chipnet result: 5 wire arrivals within 3 ms produce 5/5 accepted in
daemon-assigned order; each tx's `before.reservoir` matches the
previous tx's txid. Reservoir advanced 5 times from one starting
outpoint, zero collisions. Baseline naïve parallel is 1/5.

Snapshot 0278 documents the trust model, gotchas
(chipnet wallet BCH depletion, cashaddr → tokenaddr conversion), and
the roadmap: wallet-side client wire, then SIGHASH_ANYONECANPAY
user-funded variant, then multi-category.

Wiki handoff (wiki/asm/zero-contention-protocol.md) updated with the
tier-2 status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-28 01:47:40 +02:00
Local Dev
7a4d1072f3 docs(asm): zero-contention protocol handoff for parallel session
Canonical reference for the tier-4 client-side retry that resolves
same-target reservoir races. Covers: what the protocol is, where the
three retry surfaces live (wallet, dApp, scripts), how they stay in
sync, the chipnet proof numbers, deployment state, the WALLET-
COORDINATION boundary, first-run gotcha (fresh provider per attempt),
knobs, and what the protocol deliberately does NOT solve.

Registered in wiki/asm/index.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-27 21:05:38 +02:00
Local Dev
235b358160 feat(asm): tier-4 zero-contention retry — wallet + Potidaea + chipnet proof
Wires the client-side optimistic-chain-plus-retry pattern from
`Parameters/ASM_Native/ASM-single-asset-throughput.md` §2d into both ASM
surfaces (beta wallet AsmCore + Potidaea dApp), plus a shared node.js
retry lib and a chipnet script that proves the fix on live infrastructure.

Same-target reservoir contention (measured 1/6 collision under naïve
parallel broadcast in Parameters/ASM_Native/ASM-no-shared-state-contention.md
§4) now resolves silently: on `txn-mempool-conflict`, the wallet
re-plans against the mempool-inclusive tip and rebroadcasts, bounded to
5 attempts with linear `n × 200 ms` backoff. Fully trustless — no
sequencer, no relay, no coordinator.

Chipnet-proven end-to-end: same 6 wallets, same reservoir tip, same
simultaneous `Promise.all` broadcast — Phase A (baseline) gets 1/6
accepted (reproducing §4 exactly), Phase B (wrapped in withRetry) gets
6/6 accepted, 20 total broadcasts, mean 3.3 attempts, all under 3 s.

Snapshots: 0272 (wallet), 0277 (Potidaea), 0275 (chipnet proof).
Contention doc amended with §4a "Wallet-layer resolution — client-side
retry turns 1/6 into 6/6".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-27 20:57:41 +02:00
Local Dev
1134024d63 fix(consolidate): batch large sources into ≤400-input tx chunks
Wallet a on chipnet had 9,359 dust UTXOs (600-bit fee-signature outputs
from throughput runs). A single tx over that whole set is ~1.4 MB — well
above mempool tx-size limits, so the source would fail hard rather than
consolidate.

Add --batch-inputs=N (default 400, matches chipnet-wallet-sweep.mjs) to
split any source with too many inputs into a sequence of self-contained
txs, each ≤ ~60 KB. When the source has FT/NFT specials (passthrough
mode), the LAST batch carries them + enough plain BCH for its fee;
earlier batches are pure plain-BCH consolidates.

Also fix a latent bug in the batching logic: when a source had NO FT/NFT
specials (redeem-mba or plain-only source), `batches` came out empty
because the `specials.length ? ... : earlyBatches` branch discarded the
plain-only chunks. Now the two branches are separated: specials-present
uses the last-batch carve-out; specials-absent chunks plain into fixed-
size batches directly.

Verified by sweeping the a/b/c dust to h: a → 24 batches (400 inputs
each × 23 + 159 final) recovered 4.03M bits at ~1.57M fee (600-bit inputs
are inherently uneconomical, but the sweep now COMPLETES). b → 1 tx, c
→ 1 tx. All 26 broadcasts OK. a/b/c/d/e/f/g balances now zero; h holds
185.76M plain + 34.20M MBA backing + 50 NFTs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-27 16:05:55 +02:00
Local Dev
b0981741b1 feat(consolidate): multi-source sweep to destination with MBA + NFT passthrough
New scripts/chipnet-consolidate-to-dest.mjs — a general-purpose consolidate
that supersedes the wallet UI's plain-BCH-only ConsolidateFlow. Handles the
three UTXO classes the UI leaves stranded:

  • Plain BCH — merged into one output at the destination.
  • MBA fungible tokens — passthrough (one output per category, amounts summed)
    or --redeem-mba to burn implicitly and recover backing bits as plain BCH.
  • NFTs (incl. mutable, minting-batons) — 1:1 output preserving category,
    commitment, and capability. --leave-nfts to keep them on the source.

Modes:
  default (passthrough) — dest ends up with plain BCH + all FT categories
    + all NFT identities preserved. Source must have some plain BCH to
    cover the miner fee; script auto pre-funds any zero-plain source from
    the destination in ONE small tx before the sweep.
  --redeem-mba — implicit fungible-token burn (no Potidaea call, no swap
    fee, only the standard BCH miner fee). Backing satoshis fold into
    the plain-BCH output. NFTs skipped (they carry commitments).
  --leave-nfts — never sweep NFTs (match old wallet-sweep behaviour).

Also fixes chipnet-wallet-balances.mjs to work with the master-password-
encrypted keystore (calls ensureKeystoreUnlocked() at boot).

Verified end-to-end on chipnet:
  Pre-fund tx 1639ee5d…, then a→h eb541121…, b→h 28037c34…, c→h 5e94663a…,
  d→h d1642b55…, e→h 95dc1f33…, f→h 4e34e699…, g→h 0b82c6ee… — 7/7 OK
  at height 325269. 91.4M bits + 25 FT categories + 50 NFTs consolidated
  into h (which is HD-seed, m/44'/145'/0'/0/0 — WizardConnect-ready).

The WIF-only a-g wallets couldn't do WalletConnect because the shipped
@wizardconnect/wallet SDK requires an xpub via getXpub(path), which only
works with a seed. This script gets those funds onto a seed-backed wallet
without stranding the MBA CashTokens.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-27 16:00:24 +02:00
Local Dev
af580b6234 feat(potidaea): a11y batch + activity-panel refresh + attach-wallet CTA
Batch of 4 shipped in one pass (tasks #21-#24 from the UX review):

1) Public trade panel invalidate on broadcast (bug fix)
   New claims didn't appear in the Activity panel for up to 30s (the
   odActivityCache TTL). Fixed by:
   - Deleting the cache entry for the target MBA's covenant address
     on successful claim broadcast
   - Firing paintOdPublicActivity at 2s / 6s / 14s so the first one
     that beats electrum's mempool inclusion wins
   Redeems don't touch the covenant address, so no cache invalidation
   there — the user's own tx-log entry via logTx handles their side.

2) Welcome overlay a11y
   - div.welcome-btn → <button type="button"> with descriptive
     aria-label ("Create a fresh chipnet wallet in this browser", etc)
   - <a data-welcome-act="dismiss"> → <button class="welcome-link">
   - overlay gets role="dialog" aria-modal="true" aria-labelledby
   - Programmatic focus moves to the primary CTA on open so
     Tab-only visitors land on an actionable target
   - ESC dismisses like clicking "Look around first"
   - :focus-visible outline ring on all CTAs + the link
   Verified live: role=dialog, aria-modal=true, focus lands on primary,
   ESC sets sessionStorage dismiss flag + hides overlay.

3) Attach-wallet CTA on swap card
   Previously the review CTA was always "Review swap →" regardless of
   signer state — no-wallet visitors could type an amount, click
   Review, see the review sheet, click Confirm, then get "no signer"
   error. 4 gestures deep. Now:
   - When !localSigner && wc not paired, the label flips to
     "Attach wallet to swap →" with a matching aria-label
   - Click is intercepted (capture phase) and routed to the wallet
     chip → dropdown menu (openCreateModal as fallback)
   - Re-synced from onSignerChanged so it flips back on attach
   Verified live: "Attach wallet to swap →" with the aria-label
   present when no wallet.

4) :focus-visible rings on stripped inputs
   .amt-in, .side-in input, .fld input, .fld select, .ol-select,
   .kat-sort all previously had outline:0 with no replacement. Added
   :focus-visible rings (2px acid outline for the amount input,
   0 0 0 2px acid halo box-shadow for the form/select inputs).
   :focus-visible only fires on keyboard focus, so mouse users
   don't get an unnecessary halo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-25 08:27:37 +02:00
Local Dev
ae11271670 polish(potidaea): Med → Half quick-size chip + honest done-sheet status
Two small polish wins:

1) Med → Half
   User asked to rename the middle quick-size chip. The math also
   changes: was geometric mean of Min and Max, now exactly half of
   Max (clamped up to Min so it never proposes an amount the covenant
   would reject). Tooltip updated to "Half of your Max — a mid-size
   probe between the covenant's dust floor and your wallet's ceiling."

2) Done sheet no longer lies about confirmations
   The receipt row used to read "Block: chipnet · 1 conf" the instant
   the tx broadcast landed — before any confirmation was actually
   observed. On a chipnet stall or slow endpoint the user would see
   "1 conf" beside a txid the explorer hadn't yet indexed, which is
   the most credibility-sensitive surface in the app claiming a fact
   it did not know.
   Renamed the label from Block → Status and the value to
   "chipnet · broadcast · awaiting confirmation". Honest by default;
   a follow-up can flip it to "chipnet · N conf" once a confirmation
   scan is wired.

Flagged by the UX review — small effort, protects trust in the
one surface users evaluate the whole app on.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-25 00:08:25 +02:00
Local Dev
3a10da7ece fix(potidaea): sticky topbar + sidebar rail actually stick on mobile
Task #15 wired the intent (position: sticky; top: 0/68px) but on live
mobile the topbar and wallet chip still scrolled off with the page.
Reproduced at 375×812, scrollY 1344 → chipY -1265 (below the fold).

Root cause: cosmo-genesis.css sets `html, body { overflow-x: hidden }`
globally. Per CSS spec, any non-visible value on ONE axis forces the
other axis to compute as `auto`, which turns body into its own
scrolling context. position:sticky then sticks to that context, not
the viewport, so the sticky elements scroll along with the page
content.

Fix: override in app.html with
  html { overflow: visible !important; }
  body { overflow: visible !important; }
and leave horizontal clipping to `.app { overflow-x: clip }` at the
560px breakpoint (already added, harmless on wider viewports). Sticky
ancestors now resolve to the viewport where they belong.

Verified at 375×812 after fix: scrollY 1344 → chipY 78 (in view).
Wallet chip + nav rail stay visible through deep scrolls into the
My Transactions / Activity / Notes panels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-24 23:24:07 +02:00
Local Dev
1d68253159 fix(potidaea): MBA→BCH receipt matches preview + redeem detection in hydrate
Two related fixes:

1) Receipt for MBA → BCH showed the pre-fee backing (redeemedBacking)
   instead of the actual BCH received. User saw preview 0.049970 BCH,
   receipt 0.05 BCH — a 3000-bits (miner fee) drift, plus any leftover-
   dust padding on partial redeems.
   Fix in orbital.ts redeemViaLocalSigner: compute
     netToUser = redeemedBacking - MINER_FEE - paddingSats
   and return that as releasedSats. Net BCH gain to the wallet is
   correct regardless of extraSats (the extra plain BCH the user
   contributes as fee headroom comes back out through outputSats, so
   it nets to zero in the wallet-level change).

2) Redeem detection landed in the chain-scan hydrate (task #20). v1 was
   claim-only; v2 uses an input-scan to distinguish "I redeemed" from
   "someone paid me BCH":
   - Fast path: FT of a known category received at user's PKH → CLAIM
   - Candidate redeem: BCH received ≥ 546 bits AND no FT to user of a
     known category
   - Verify: fetch up to 4 input prev-txs; if any input was the user's
     own FT UTXO of a known category, classify as REDEEM (with the
     input-side FT amount minus any leftover output). Otherwise skip.
   Cost: one extra RPC per candidate input, bounded at 4 per tx.

   Classifier validation on wallet D (treasury address with 6854
   lifetime txs, mostly platform-fee BCH receipts):
   - 14 true-positive claims across ASM / SHR745 / ASM2
   - 0 false-positive redeems across 200 non-redeem BCH-receive txs
   The input scan correctly rejects every BCH receipt whose inputs
   don't spend a user-owned FT UTXO.

   For a wallet that actually does MBA→BCH redeems (a swap-side wallet
   rather than a treasury address), those will now hydrate cross-device
   alongside the claims that landed in c3d33ca.

Also added `vin` to the FulcrumVerboseTx interface so the input scan
compiles cleanly. FulcrumVerboseTx.vout[i].value is BCH (not sats) —
fixed the bchReceivedToUser computation to multiply by 1e8 before floor
(latent bug in the v1 classifier that never surfaced because claims
don't read that field).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-24 05:02:44 +02:00
Local Dev
5df3cabd93 feat(potidaea): full WizardConnect surface — QR + open-in-wallet deep link
Existing WC wiring only surfaced the wiz:// URI as a monospace text
blob; users had to copy-paste it into their wallet. Per Riften docs
(https://docs.riftenlabs.com/wizardconnect/) the canonical pairing UX
is QR-first with a deep-link fallback for same-device wallets.

- Added qrcode dep (~10 KB gzipped) — SVG output, no canvas
- QRCode.toString(uri, { type: 'svg', ... }) renders inline SVG in
  the modal, in the acid+ink palette so it reads on the dark surface
- "Open in wallet ↗" primary button uses the wiz:// URI as the anchor
  href — mobile browsers auto-launch the paired wallet on this scheme
- "Copy URI" demoted to secondary for the rare desktop-scan case
- Only rebuild the QR when the URI actually changes (avoids flicker
  on the WcClient's repeated `change` emissions during pairing)
- Hint at the bottom links to the Riften docs so users can install
  a compatible wallet before scanning

The signing path (claimViaWc + retry wrapper) was already wired and
chipnet-proven — this ships only the pairing surface upgrade.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-23 03:31:32 +02:00
Local Dev
c3d33cad85 feat(potidaea): cross-device tx history via chipnet chain-scan
My Transactions was localStorage-only, so a swap made on mobile never
appeared on desktop with the same WIF. Fixed by scanning the chain on
wallet attach (and at boot resume): blockchain.address.get_history →
per-tx verbose fetches → classify → merge into TX_LOG_KEY with
txid-dedupe.

Architecture note (user asked): no dedicated database / indexer
needed. The chain IS the database. Fulcrum's address history +
verbose-tx RPCs give us every historical swap in a bounded number of
calls (bounded here by HYDRATE_MAX_TXS = 100 most-recent per address).
Adding a server-side indexer would violate the lean-endpoint stance
(wiki/infra/lean-utxo-endpoint.md), introduce a custody-adjacent
surface, and buy ~100 ms of speed for something we do once per
session. If a wallet ever accumulates thousands of swaps we can
paginate get_history and cache verbose fetches per txid, but that's a
knob-turn not a re-architecture.

Wiring:
- hydrateTxLogFromChain(signer) fires 300 ms after onSignerChanged
  (attach / switch / disconnect+reattach) and 500 ms after
  bootLocalSigner picks up a resumed wallet. Idempotent — hydrateInFlight
  guard + txid dedupe means duplicate calls are no-ops.
- Parallel worker pool (HYDRATE_CONCURRENCY = 4) so a 100-tx address
  hydrates in ~2-3 s on chipnet.

Classification (v1, claims-only):
- Match the user's PKH against each verbose output's scriptPubKey to
  find outputs going TO the user.
- If an output carries token_data.category matching a KNOWN_MBAS
  entry → it's a CLAIM: pick the largest FT delta as the primary
  swap, derive BCH gross from units × rate, log with fromSym=BCH,
  toSym=<ticker>, chain block time as ts.
- REDEEM detection deferred — burn-in-place txs don't touch the
  covenant address; distinguishing "I redeemed" from "I received a
  BCH payment" without input-scan is fuzzy. v2 task queued.

Storage: hydrateInsert() writes directly to TX_LOG_KEY with the chain
block time (not Date.now()) so the log stays ordered by real chain
time, then re-sorts newest-first and repaints odTxLog.

Verified with wallet D on chipnet: 17 claim entries hydrated across
ASM / SHR745 / ASM2 tickers, first row is a 2026-09-21 claim of 550
ASM for 0.00055 BCH (txid 79084691e78e…). Panel repaints on completion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 18:02:20 +02:00
Local Dev
fe89d88597 feat(potidaea): 6 polish wins + chart-tab design plan
Six shipped items:

1) Tooltips on MIN/MED/MAX chips — hover explains dust floor /
   geometric mid / balance-adjusted cap so users understand why MAX
   isn't the raw balance.

2) USD equivalent on rate line — "1 BCH = 1,000,000 ASM · ≈ $16.29"
   for a 0.05 BCH swap, using the same cached bchUsd the balance
   total already reads. Silently skipped when USD < 0.01.

3) Wallet chip copy — "no wallet attached ▾" → "Attach wallet ▾".
   Same click target, action framing.

4) Sticky wallet chip on mobile — the topbar is already sticky on
   desktop; at ≤940px the sidebar rail was scrolling off but leaving
   a 19px gap under the topbar (body's default 8px margin + partial
   rail). Fixed body margin, made the mobile sidebar rail itself
   sticky at top:0 with its own z-index, pushed the topbar to
   top:68px so both stack cleanly. Wallet chip stays in view during
   scroll on any screen size.

5) Persistent My-transactions tab — the tab (trades / creates / all)
   used to reset to "trades" on every load. Now stored in
   deviant.potidaea.tx-tab.v1; boot syncs the visual "on" state and
   click updates the key.

6) Katalogos search + sort — filter input (ticker / name / category-id
   substring) + sort dropdown (recent / name A→Z / rate low→high) +
   live entry count above the Stoa table. Both filter and sort persist
   via localStorage. Empty search states show a "No matches for X" row.
   Exposes KNOWN_MBAS on window so the search can hit category ids.

Plus a design snapshot (0276) for a future Chart tab: sourcing
strategy (BCH/USD from CoinGecko cache, MBA/USD derived from covenant
rate, NBA/USD from Anthemus fills once live), chart lib pick (uPlot),
placement, storage keys, and open questions parked until we start.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 17:53:29 +02:00
Local Dev
08a43d9cc3 feat(potidaea): min/med/max quick sizes + drop Agora brand + review sheet math
Four connected polish items:

1) Review sheet math synced to the swap card (declared vs receipt fix)
   openReviewSheet was still using the mockup's flat-0.5% formula
   (net = (amt - amt*0.005) * fa.rate / ta.rate) even after the
   swap-card recalc got the exact potidaeaEstimateNet path. Result:
   card showed 100,000 ASM, review sheet said 99,500 ASM, receipt
   confirmed 100,000 — the "declared vs defacto" mismatch the user
   flagged. openReviewSheet now calls the same helper first, falls back
   to the flat approximation only for unknown pairs (USDm etc.).
   Verified: card, review sheet, and receipt all agree at 100,000 ASM
   for 0.1 BCH → ASM.

2) Min / Med / Max quick-size chips
   Replaced the single "Max" text next to the FROM balance with a chip
   row below the amount input. potidaeaSwapSizes(from, to, balFrom,
   balTo) computes each for the current pair:
     - MIN — smallest amount the covenant accepts. Claim: dust floor
       ceil(546*10000/founderBps) bits. Redeem: ceil((dust+miner)/rate)
       units.
     - MAX — largest amount this wallet can actually broadcast.
       Claim: bal is discounted for founder + platform + miner fees
       so the click lands on an amount that will succeed (no dropped
       "insufficient BCH" broadcast). Redeem: full MBA balance.
     - MED — geometric midpoint between MIN and MAX so heavy and light
       traders both land on a sensible probe.
   Verified for 0.5 BCH balance → ASM: MIN 0.000546 (546 units) · MED
   0.016399 · MAX 0.492581 (492,581 units, totalCost 0.49997 BCH — right
   at balance without overshooting).

3) Drop Agora brand link
   Removed the <a class="brand" href="index.html">Agora⌁</a> from the
   sidebar and the "Agora · the marketplace" tagline from side-foot per
   user request; the sidebar now leads with Balances directly. Dead CSS
   for .brand / .tri left in place — harmless, tiny.

4) Mobile check
   Verified 375x812: swap card renders with the chip row inline, no
   overflow; MIN · MED · MAX buttons are readable at 10px monospace
   with equal 1fr widths, and clicking MAX auto-fills 0.492581 BCH so
   the amtIn matches what the wallet can actually broadcast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 17:40:03 +02:00
Local Dev
8ea2cb0c92 feat(potidaea): correct swap math + my-assets row actions + preview ribbon + wizard autosave
Four improvements in one pass:

1) Swap preview matches on-chain result (fixes user-reported drift)
   The mockup's recalc applied a flat 0.5 % fee to the FROM amount, then
   converted. The covenant works differently:
     - CLAIM (BCH → MBA): gross BCH enters covenant; user receives
       floor(gross / rate) MBA units. Fee is an EXTRA BCH output
       deducted from wallet change, NOT from the mint amount.
     - REDEEM (MBA → BCH): user burns units; receives units × rate
       − miner_fee bits. No founder / platform fee.
   User reported "preview showed 999.5 ASM, receipt 1005 ASM" — off by
   the mockup's fake 0.5 %. Now the preview reads 100,000 ASM for
   0.1 BCH → ASM and 0.04997 BCH for 50,000 ASM → BCH, both matching
   handleRealSwapConfirm's integer math.
   main.ts exposes potidaeaEstimateNet(from, to, amt) using KNOWN_MBAS;
   recalc + recalcFromOut call it first, fall back to the mockup's flat
   approximation for unknown pairs (USDm etc.).
   Fee-value display now uses 6 decimals when denominated in BCH (was
   fixed at 2 — rounded 0.0015 BCH to "0.00").

2) My-assets rows get action buttons
   Every row now shows Swap → · Publish → · ↗:
     - Swap → sets from=BCH / to=<ticker> and jumps to swap view.
     - Publish → keeps the existing wiring to openPublishOverlay.
     - ↗ links to chipnet.imaginary.cash/tx/<genesisTxid>.
   loadLocalKnownMbas now includes the genesis txid so the ↗ works
   without a second lookup.

3) Anthemus preview ribbon
   The DEX view is a mockup (hardcoded book / AMM pool / quotes) but was
   sitting silently next to real Potidaea flows. Adds a persistent amber
   banner at the top of #v-anthemus: "Anthemus is a preview. Order book,
   AMM pool and quotes on this page are illustrative — no real orders
   are broadcast." + "Try Potidaea instead ↗" link. Stops eroding trust
   in the shipped surface.

4) Wizard draft autosave
   sessionStorage snapshot of every wizard field (asSym / asName /
   asRate / asFee / asDec / asGlyph + wiz.step / family / cls) on every
   input change and every step transition. Restored on next Artemision
   visit — a stray refresh mid-flow no longer wipes progress. Successful
   broadcast (mockup + real via runArtemisionGenesis) clears the draft
   so the next fresh visit starts clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 08:44:27 +02:00
Local Dev
440416f522 fix(potidaea): welcome overlay hides properly + compact classified errors
Two bugs one commit:

1) Welcome overlay left the swap layout at the bottom of the page
   .welcome-overlay { display: flex; min-height: calc(100vh - 140px) }
   is an author rule (specificity 0,1,0) that outranks the user-agent
   [hidden] { display: none } (same specificity, but user-agent). So
   overlay.hidden = true had no visual effect: the empty container kept
   eating a full viewport and shoved .od-layout below the fold. Reported
   by user with a screenshot showing panels at the very bottom after
   wallet import.
   Fix: explicit .welcome-overlay[hidden] { display: none !important; }
   in the author sheet (specificity 0,2,0 wins). Verified live: overlay
   collapses to display:none / height 0 after import; .od-layout sits
   at y=181 (just under the topbar) as expected.

2) Swap-sheet error was a wall of text with no way to grab it
   setSheetError previously dumped the raw message into a single div
   with textContent. Broadcast / covenant errors can be multi-KB dumps
   (tx hex, cashscript stack, Bitauth trace) and blew up the review
   sheet vertically. Reported by user attempting MBA→BCH redeem.
   Fix:
   - classifySwapError maps common patterns to short code labels:
     E-INSUFFICIENT / E-DUST / E-CHANGE-DUST / E-COVENANT-STATE /
     E-STALE-UTXO / E-FEE-LOW / E-NET / E-COVENANT / E-LEFTOVER-DUST /
     E-REDEEM-SMALL / E-OTHER
   - Compact display: mark + code chip + one-line headline + copy
     button + expand toggle
   - Full text lives in a collapsible block (max-height 240px, scroll)
     that stays hidden until the user clicks the details arrow
   - Copy button writes "[E-CODE] <full text>" to clipboard so a
     support paste starts with the error code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 08:33:34 +02:00
Local Dev
acdfceaf40 feat(potidaea): cold-visit welcome + tx receipts + terminology sweep
Three small polish wins:

1) Cold-visit welcome overlay on the swap view
   No-wallet visitors were landing on a grid of empty panels with only
   a small "no wallet attached" chip to guide them. Adds a full-view
   card with the three attach paths (Create · Import WIF · WalletConnect)
   whenever localSigner is null and the visitor hasn't dismissed for the
   session. Dismiss link ("Look around first →") stores a
   sessionStorage flag so power users aren't nagged again in the same
   tab, but a fresh visit still gets the intro. Wallet attach/detach
   auto-flips the overlay via onSignerChanged.

2) Tx receipt with copy + explorer link
   Swap done sheet, Artemision genesis-done, and Katalogos publish-done
   all rendered the raw txid as plain text. Adds a shared
   renderTxidWithActions helper: short-form display + copy-to-clipboard
   button + "↗" link to chipnet.imaginary.cash/tx/<id>. First-time
   users now have a place to verify their broadcast landed on chain.

3) Terminology sweep
   - "BCH sats" → "BCH bits" (Balances footer copy) per house rule
     "sats"→"bits" for user-visible text.
   - "mint-then-redeem sandwich" → "swap-in-then-swap-out sandwich"
     with a plain-English gloss (DEA-yield v1 limits copy) per
     FIATμ UX rule "swap not mint/redeem in user copy".
   Intentional retentions:
   - "Unbacked token" (NBA class card) — NBA IS the unbacked-token
     family; the rule is MBA→"backed asset", not a blanket ban.
   - "minted through Artemision" — genesis IS a mint (asset creation),
     distinct from mint/redeem swap wording.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 01:57:38 +02:00
Local Dev
0de1deafb7 feat(potidaea): my-assets dashboard block on Balances view
Users had no place to see the assets they'd genesised via the
Artemision wizard — the local cache existed (localStorage
deviant.potidaea.local-assets.v1) but the only surface was implicit:
the ticker appearing in the swap picker after boot. Publishing status
was invisible without switching tabs.

Adds a "My assets · created here" card list at the top of the Balances
view (right home: wallet-scoped dashboard; Katalogos stays a
cross-wallet public directory). Each row shows:
- Icon/glyph
- Ticker + display name
- Class pill: Static · Share · Oracle (matches DVMB template dispatch)
- Status pill: "✓ Listed on Katalogos" (green) or "Local only" (muted)
- Rate (BCH/unit), founder fee %
- Category id chip — click to copy the full 64-char id
- Publish → button on unpublished rows, wired to the existing
  openPublishOverlay so it's one click from dashboard to on-chain
  listing

Also adds a "+ New asset" CTA in the block header that jumps to
Artemision, and an empty-state that links to the wizard for
first-time creators.

paintMyAssets is called on:
- boot (initial render + Balances view can be direct-linked)
- refreshRegistry finish (status pill toggles when a publish lands)
- Artemision genesis broadcast success (new row appears immediately)
- applyView('balances') (route-based navigation refreshes stale pills)

Sub line summarises the two counts: "N assets minted from this wallet · M published".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 01:21:06 +02:00
Local Dev
ec4ed612aa feat(potidaea): bidirectional swap inputs + on-the-go balance clamp
Users could only type in the FROM amount — the TO side was a static div
displaying '≈ 3.9681'. Now both legs are inputs and the covenant's
fixed-rate quote back-computes cleanly either direction:

- amtOut converted from div to <input class="amt-in"> — matches FROM's
  styling and IME behaviour.
- recalc still reads amtIn as the single source of truth for
  handleRealSwapConfirm's amount computation; it skips overwriting
  amtOut while the user is focused there so typing doesn't race.
- recalcFromOut inverts the rate: amt = out * (ta.rate / fa.rate) /
  (1 - FEE_PCT), then reruns recalc to keep every derived field
  (fee split, netVal, etc.) consistent.
- Both directions silently clamp to the FROM balance. Type "5" in BCH
  with only 1.25 BCH held → field auto-corrects to 1.25 mid-typing,
  amtOut follows. Same on the reverse — overshoot in TO caps both.
- Flip button seeds amtIn from the previous amtOut so the trade shape
  survives the swap (was: 1000 USDm → 1000 BCH after flip; now: the
  BCH figure the user just saw becomes the new FROM).

Reason: user asked "impossible to add ASM amount, only BCH amount is
accepted" + "if user types 5 in BCH field, and his balance is only
1.25, the auto-correction should fix it on the go".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 00:09:38 +02:00
Local Dev
3b36d9b48a feat(potidaea): wider Artemision wizard for desktop + larger type
Artemision was inheriting .content's 1120px max-width and desktop
users on 1920px screens saw the wizard occupy ~55% of the viewport
with 12-16px text. Added a per-view content.artemision class wired
through the nav handler alongside the existing content.wide (swap):
- max-width 1120→1520px, padding 34→40px 48px
- steps rail 220→280px, gap 22→32px, larger step glyphs + labels
- wizard title 22→30px, description 12→14px
- tax card names 16→20px, descriptions 11→13px
- form inputs 14→16px, labels 9→11px
- buttons 11→13px, footer 10→12px
- graceful step-down @1400px so mid-sized desktops still fit well
- ≤940px stack behavior preserved (unchanged mobile flow)

Result at 1920×1080: content fills 79% of viewport (was ~55%).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-21 23:48:22 +02:00
Local Dev
ee7b44b9ba fix(potidaea): sell rows survive activity-panel enrichment + offline fallback
The Activity · <pair> panel was showing green-only because:
1. enrichOdActivityRows() only received buy rows; its post-fetch repaint
   overwrote the sell rows merged in from the local tx log.
2. Sell rows were mapped without amountUnits, so the placeholder "local"
   text rendered instead of a real "5,000 ASM" figure.
3. On electrum connect failure the entire panel dropped to an "offline"
   placeholder — even though the user's own redeems live in localStorage
   and cost nothing to render.

Fixes:
- Parse redeemed unit count from the log's fromAmount ("5,000 ASM" → 5000n)
- Pass the FULL merged list to enrichment; skip sell rows internally so
  the verbose-tx scan doesn't overwrite the log-parsed amount with 0
- On fetch failure, render the sell-only list with a "Buy feed offline"
  notice instead of blanking the panel

Structural note: Potidaea redeems burn FT in-place; the redeem tx never
touches the covenant address, so blockchain.address.get_history(covenant)
returns only claims. Fulcrum on chipnet.imaginary.cash doesn't expose a
chain-wide token-category history feed either, so PUBLIC redeems remain
out of reach — this fix restores the SELF redeems that were being
erased, matching the panel's original intent.

Verified locally + on live https://navigate.st/bns/potidaea.asm/ with a
seeded local redeem: 1 sell (red, 5.00K, real time) surviving alongside
29 chipnet buys, and the offline fallback shown when electrum is down.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-20 02:11:34 +02:00
Local Dev
a52f6d930b fix(potidaea): client-side dust-floor check before covenant claim
Root cause of "DeviantOpenMintFT.cash:112 Require statement failed at
input 0 · require(feeSats >= 546)":

The Artemision covenant enforces that the fee output must be at least
546 sats (dust threshold). For an ASM claim, feeSats = grossSats ×
feeBps / 10000. Below the floor the covenant rejects the tx on-chain,
after the user has already gone through Confirm and been shown a
"Broadcasting…" spinner. All the user sees is the CashScript error
URL — hard to interpret.

Fix: validate the fee output size in handleRealSwapConfirm BEFORE we
build or broadcast anything, and if it's under 546 sats surface a
specific inline error naming the minimum claim in both units AND
BCH:

    ✗ Amount too small — the covenant requires a 546 bit fee output;
    below that the tx is rejected on chain. Minimum claim:
    546 ASM (~0.000546 BCH).

The minimum is derived from the covenant's own params:
    minGrossSats = ceil(546 × 10000 / feeBps)
    minUnits     = ceil(minGrossSats / rateSats)

so it stays correct for any covenant we swap in — ASM (rate 100, fee
100 bps) → 546 units; ASM2 (rate 1000, fee 100 bps) → ~55 units.

Verified live: attempting 0.0001 BCH → ASM now surfaces the specific
error before broadcast; no on-chain tx is attempted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 22:14:09 +02:00
Local Dev
31273686a5 feat(anthemus.dex): live cross-nav + BCH/USD/EUR chips + sequential PIN
Three fixes in one turn.

CROSS-NAV — Artemision · Katalogos · Listing · Swap · Deposit all
now actually land somewhere.
- New deep-link protocol: hash form (#tab=… &sub=… &view=…) instead
  of query string. The BNS gateway strips ?query from path-key
  resolution so `wallet/?tab=agora` falls back to the name-root
  landing; hash never hits the gateway.
- src/deepLink.ts::deepLinkParam(name) reads hash first, query second.
  All wallet-side sniffers switched to it (App.tsx tab · AgoraScreen
  panel · AnthemusScreen view · ArtemisionScreen sub · KatalogosPanel
  open=publish).
- ExchangePanel takes a `routes` + `linkTarget` prop; AnthemusKiosk
  populates it with wallet URLs opening in a new tab
  (deviant.x/wallet/#tab=…). Wallet-embedded ExchangePanel keeps
  the old hash-only defaults.
- KatalogosPanel opens the Pinax registration modal automatically
  when the deep link carries open=publish, so the Listing link lands
  on the "add asset" form instead of the readonly directory.

ASSET CHIPS — Favorites/All/Fiat replaced with ★ · BCH · USD · EUR
per user request (Binance-style compact chips).
- Single-select with click-again-to-clear; no active chip = show all.
- ★ chip is icon-only, gold when active; currency chips filter pairs
  whose base OR quote ticker matches. Counts render next to each
  chip and update as favorites change.
- Persists to deviant.anthemus.assetChip; retired the assetTab key.

PIN ENTRY — two pads on one screen is fixed. Set-a-PIN now walks
choose → confirm sequentially: single pad on screen, "Next" advances
after ≥ 4 digits, "Back" returns to edit the first without clearing.
Same PinPad component, two-step state (pinStep) inside Onboarding.

ASSET-BASE FIX — the BNS gateway injects `<base href="/bns/<name>/">`
into every served HTML, which broke the subpath deploy: assets at
`/bns/deviant.x/wallet/assets/…` were resolved against the name root
and 404'd. Now building with DEVIANT_BASE=/bns/deviant.x/wallet/ so
all asset paths are absolute — the gateway's injected base becomes
irrelevant. Cross-bucket loads (anthemus.dex → wallet's assets)
work by design because the origin is the same.

Deployed to bns/deviant.x/wallet/ + bns/deviant.x/anthemus/. Verified
end to end: wallet boots at deviant.x/wallet/, kiosk fires at
anthemus.dex/, chips filter correctly (USD→2 rows, ★→favorites only),
PIN flow shows one pad at a time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 21:38:48 +02:00
Local Dev
4b438ba2cc fix(potidaea): balances auto-populate after import — no manual refresh needed
Import → Attach was calling `void refreshBalance()` once and moving
on. Two things went wrong from that:

  1. Any pending-swap overlays left in memory (from a previous session
     / previous wallet) were still applied to the newly-imported
     wallet's paintBalance, mis-adjusting its bchRow.bal by a delta
     that belonged to a different address.

  2. Electrum's first response on a cold cashaddr subscription is
     often slow (fresh listunspent + include_tokens on a new address).
     A single fire-and-forget refresh can lose the race with the user
     opening the Balances view, leaving the table empty until they
     hit the manual refresh button.

Fix — new onSignerChanged() helper wired into every signer-change
site (import, create, switch, WC-pair). It:
  • Clears pendingSwaps + savePendingSwapsToStorage — no stale
    overlays leak across wallet-change.
  • Fires an immediate refreshBalance, then follow-ups at 700 ms,
    2500 ms, and 6000 ms so a slow first response doesn't strand
    the user on an empty Balances page.

Verified live: deployed bundle contains the 700 / 2500 / 6000 ms
setTimeout retries — no manual refresh needed after import.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 21:15:05 +02:00
Local Dev
b3d22f1f3a fix(potidaea): BCH row excludes sats locked in MBA UTXOs (explorer convention)
Root cause of "0.35 BCH is the non-deducted balance":
summariseBalance was summing EVERY UTXO's sats into bchSats, including
the sats sitting inside FT UTXOs. For an Artemision claim, the covenant
mints an FT UTXO that carries `units × rateSats` of BCH backing right
inside it — those sats belong to the address but are LOCKED WITH THE
TOKEN until it's redeemed / transferred / burnt. Adding them into the
BCH balance made the claim look like it never moved any sats: the plain
BCH pile dropped, but the freshly-minted FT UTXO's backing sats picked
up the same amount, so the total looked identical to pre-swap.

Fix — mirror the split the chipnet explorer at
design/explorer/index.html:184 already uses:

  • UTXOs WITH token_data → their sats go into `ftBackingSats`
    (and a per-category map `ftBackingByCategory`), and the FT
    amounts go into `ftByCategory` as before.
  • UTXOs WITHOUT token_data → their sats go into `bchSats` — this
    is now "plain, spendable BCH" only.

paintBalance already consumed `bchSats` as "the BCH balance"; no code
change needed there — the number just becomes correct.

Verified on the deployed page, wallet with 3 MBA holdings:
  chip                → 0.2110 BCH   (was 0.2899 · plain BCH only now)
  BCH row balance     → 0.2110 BCH
  ASM  row            → 71,800  · backing 0.0718 BCH
  ASM2 row            → 150     · backing 0.0015 BCH
  DEA  row            → 3,750   · backing 0.0325 BCH
  + backed in MBAs    → 0.1058 BCH  (informational)

Sats decrease vs old figure: 0.2899 − 0.2110 = 0.0789 BCH — the pool
of sats that had been double-counted as both "plain BCH" and "inside
the FT UTXO backing".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 09:30:34 +02:00
Local Dev
51944252d4 style(potidaea): mobile — reclaim topbar width, no more chip wrapping
Two mobile-layout issues surfaced on a 375-wide screen:

  1. View title "Potidaea · Swap" wrapped to two lines inside its 98-px
     topbar slot. The sidebar rail already highlights the active view,
     so the topbar title is duplicated information — hidden on phones
     via `.topbar .view-title { display: none }` inside the 560-px
     media query, and the topbar's justify-content is set to flex-end
     so the network badge + wallet chip cluster on the right.

  2. Wallet chip content wrapped inside its 167-px width. The chip
     tries to render dot + address + BCH amount + mode badge (LOCAL /
     WC / DEV) + chevron — the address is already hidden on mobile,
     but the "local" badge (43 px) plus the BCH amount plus punctuation
     still overflowed and stacked vertically. Fix: hide
     `.wc-mode-badge` on phones, add `white-space: nowrap` on the chip
     and its inner <b>, and tighten padding to 6/9. The chip now reads
     as a single-line "● 0.2899 BCH ▾" pill (134 × 28 px).

Verified live at 375×812:
  topbar height        → 49 px  (was 81 px with wrapped title)
  view-title display   → none
  mode-badge display   → none
  wallet chip          → 134×28 px, BCH label single line (14 px tall)
  horizontal overflow  → none

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 09:28:02 +02:00
Local Dev
faa76962d4 polish(potidaea): balances — kill double-count risk + drop CoinGecko note
Two issues the user flagged:

1. The Value column still invited "spendable + backing = totals" math.
   Header read "Value"; BCH row printed "0.3568 BCH", ASM row printed
   "0.2000 BCH". A reader scanning the column added those (0.5568) and
   thought that was the wallet total — even though the top figure
   already showed 0.3568 BCH (spendable only). Renaming the top wasn't
   enough — the two visible BCH numbers looked interchangeable.

   Fix: rename the column to "Backing", and blank BCH's cell with an
   em-dash (BCH doesn't have "backing" — it IS the backing). MBA rows
   still show their redeem-value in BCH. The column now only ever
   holds one meaning: the BCH you'd get back on redeem. No more
   accidental sum of "spendable BCH + backing" reading as a total.

   Footer explanation updated to match:
     "Backing = units × backing rate — the BCH released when the MBA
      is redeemed via Potidaea; not spendable until then."

2. USD subtitle no longer names CoinGecko.
   Was "≈ $88.43 · via CoinGecko" → now "≈ $71.80". The pricing source
   isn't user-facing information; the number stands on its own.

Verified live on the Balances view:
  Header row       → Asset | Class | Balance | Backing
  BCH row Backing  → —
  ASM row Backing  → 0.0718 BCH   (was "0.0718 BCH" labelled Value)
  Total USD        → ≈ $71.80     (no CoinGecko mention)
  DOM search for /CoinGecko/i     → false

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 09:20:18 +02:00
Local Dev
76a641e14f fix(potidaea): honest balance total + drop redeem/claim UI wording
Three joined fixes from user feedback:

1. Balance total was inflated by MBA backing.
   The Balances view summed BCH + every MBA's redeem value into one
   figure ("Total holdings value = 0.5467 BCH") — but a claimed MBA
   does NOT give the user more spendable BCH; the backing is locked
   in the covenant until redeem. The number read as if the user had
   more BCH than they actually did, especially confusing right after
   a claim: spent 0.2 BCH, got 200K ASM, saw the "total" unchanged
   because the ASM's 0.2-BCH backing quietly replaced the spent BCH
   in the sum.

   Fix: the top figure is now labelled "Spendable BCH" and includes
   the BCH row ONLY (0.2899 BCH in the verified live snapshot).
   MBA backing is broken out under it as an informational line:
   "+ backed in MBAs · 0.1058 BCH (redeemable via Potidaea)".
   The line hides itself when no MBAs are held.

   Terminal panel's ≈ $X header (odTotalUsd) uses the same
   spendable-only rule so both surfaces agree.

2. "Redeem + claim" UI wording dropped.
   The review sheet's Route row, the pending-broadcast subtitle, and
   the Settlement info panel all said "redeem + claim · 1 tx" — but a
   BCH → ASM buy is a claim only (no redeem), and the string was
   misleading during broadcast. All three now read:
       Route         → "covenant swap · 1 tx"
       Pending sub   → "Building the swap transaction"
   Neutral, accurate for both directions, and no user-facing "claim"
   / "redeem" jargon (those stay in code comments + tx-log kind).

Verified live:
    Spendable BCH  → 0.2899 BCH  ($71.73)
    Backed in MBAs → 0.1058 BCH  (ASM 0.0718 + ASM2 0.0015 + DEA 0.0325)
    Pending sheet  → "Building the swap transaction"
    No "redeem + claim" text anywhere in the DOM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 09:09:11 +02:00
Local Dev
6d6db198eb fix(potidaea): mobile / tablet layout — undo desktop fit-to-viewport clip
Recent desktop work added `.content.wide { max-height: calc(100vh -
72px); overflow: hidden }` + `.od-layout { height: calc(100vh - 128px)
}` so the six-panel terminal fit inside a 900 px desktop viewport
without scroll. That worked at desktop, but on a phone (375×812) it
capped `.content` at 740 px AND set overflow:hidden — so once the
1200 px breakpoint kicked in and restacked the six panels vertically
(cctop → topright → botright → ccbot → topleft → botleft), everything
past ~740 px was CLIPPED and unreachable. Users literally couldn't
see Notes, My-transactions or Balances on a phone.

Fix — three joined blocks:

  1. @media (max-width: 1200px) undoes the fit-to-viewport contract:
     .content.wide → display:block, min-height:auto, max-height:none,
     overflow:visible; .od-layout → height:auto, grid-template-rows:
     none, overflow:visible. Page now scrolls naturally, all six
     panels are reachable. .od-list-scroll caps re-added (320 px /
     240 px) so a long feed doesn't stretch the page indefinitely.

  2. @media (max-width: 940px) tightens .content.wide padding (16 px)
     and topbar spacing for the sidebar-collapse layout.

  3. @media (max-width: 560px) — phone specifics:
       • body { overflow-x: hidden } so nothing horizontal leaks
       • .content.wide { padding: 12px }, topbar padding 10/14
       • Swap card fonts down to 18 px (amt-in/out), 11 px CTA
       • Panel body/header paddings tightened
       • Market rows tighter (gap: 3px, font: 10px) so From/To/Time
         fit at 375 px width; margin-left:-14px on To keeps the
         "slightly less" middle gap in mini form

Verified on the deployed page:
  Mobile (375×812) :
    contentMaxHeight → none, overflow → visible
    All 6 panels visible, doc height 2018 px, page scrolls
    Swap card width 325 px inside 351 px panel
    No horizontal overflow
  Tablet (768×1024):
    All 6 panels visible at 721 px width, page scrolls
    No horizontal overflow
  Desktop (1600×900):
    contentMaxHeight → 828 px, overflow → hidden
    doc height = viewport height → fits without scroll (unchanged)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 09:04:49 +02:00
Local Dev
cbbc4e689b feat(faucet): master-password unlock (v:3) — one password reveals all wallets
Add a v:3 unlock mode alongside the existing v:1 (WIF) and v:2 (seed-phrase +
BIP-32) paths in the faucet sign-in flow. The new mode keys directly off the
Deviant chipnet-keystore master password, so the SAME password that unlocks
Keys/chipnet-keystore.json also reveals the faucet's saved-address watchlist
after deploy.

build-seed-enc.mjs
- New --master-pw flag (or CHIPNET_KEYSTORE_PASSWORD env). Refuses a password
  shorter than 8 chars; prompts interactively when no env is set.
- v:3 uses PBKDF2-SHA256/200000 with a FRESH 16-byte random salt per build,
  stored inline in the blob. Deliberately different from v:1's fixed
  "deviant-faucet-seed-v1" salt: the password entropy is much lower than a
  raw private key, so a per-build salt kills pre-computed rainbow tables and
  keeps rebuilds independent of stolen older blobs. 200k iters matches the
  Deviant keystore's PBKDF2 parameters — a stolen blob costs ~0.5s per guess.
- The v:2/v:1 paths are unchanged; the blob shape stays the same shape
  ({v, iv, ct}) with kdf+salt fields on v:1/v:3 and a `path` field on v:2.

index.html
- New "Master password" input mode alongside the existing seed-phrase and WIF
  modes. New masterPwToRawKey() reads the salt + iters from the loaded blob
  and derives the AES-256 key via SubtleCrypto — verifies the blob is v:3
  before continuing.
- tryAutoUnlock() now picks the default mode from the deployed blob version:
  v:3 → 'master', else the existing v:2/v:1 probe.
- Sign-in modal grew a third input field; the panel-open focus target
  branches on the current mode.

Verified end-to-end in browser preview:
- v:3 blob loads (2083 ct bytes · fresh 16-byte salt · 200k iters).
- Correct master password → 15 chipnet BCH addresses rendered (7 original +
  8 imported from the extended keystore: h, i, gmx, anthemus-platform,
  potidaea, sirius-x, bns-beacon, bns-tld-beacon).
- Wrong password → opaque "Wrong key — could not decrypt." error, no leak.
- No plaintext faucet-seed.js in the deploy path (already gitignored).

Third consumer of the same master-password crypto envelope this week, after
the shipped Deviant chipnet-keystore encryption
(scripts/chipnet-keystore-{encrypt,decrypt}.mjs) and before the Explorer
which reuses the same primitives at v:1 spec level.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 01:24:40 +02:00
Local Dev
d6cb311066 feat(explorer): master-password unlock + all 15 keystore addresses
Expand the chipnet Address Explorer from a hard-coded 7-wallet list to the full
15-wallet keystore, with an optional master-password gate for public deploys.

Two operating modes, chosen at boot by whether `addresses.enc.json` sits next
to the page:

1. Local / file:// — the inline WALLETS constant renders directly. Same
   frictionless behaviour as before, but the list now includes all 15 wallets
   plus per-wallet `cat` (category: operational | chipnet-test | bns | bns-infra)
   for a category-chip filter above the table.

2. Deployed — ship index.html + addresses.enc.json (spec
   chipnet-explorer-addresses/1: PBKDF2-SHA256/200k + AES-256-GCM, fresh
   16-byte salt per build). On boot the page fetches the blob, wipes the
   inline WALLETS to prevent pre-unlock leak, and shows a full-viewport lock
   overlay. Master password decrypts in-browser via SubtleCrypto — never
   transmitted, never persisted.

New signer: design/explorer/build-addresses-enc.mjs
- --master-pw prompts (or reads CHIPNET_KEYSTORE_PASSWORD env) and encrypts.
- Default source is the inline WALLETS parsed from index.html.
- --from-keystore pulls straight from Keys/chipnet-keystore.json via the
  existing scripts/lib/chipnet-keystore.mjs loader.

Crypto envelope matches the Faucet's v:3 mode (Parameters/dApp/Faucet/) and
the chipnet-keystore/2-encrypted format — same PBKDF2 params, same AES-GCM
authentication, same "fresh salt per build" discipline. Fourth consumer of
the unified master-password integration (spec:
Parameters/dApp/MASTER-KEY-INTEGRATION.md §6.1).

Verified in browser preview (Parameters/dApp/Explorer/ on port 5185):
- Overlay shows on load when addresses.enc.json is present
- Inline WALLETS wiped correctly before unlock (Σ table shows 0)
- Correct password → 15 addresses render with live balances + category chips
- All four categories surface: operational · chipnet-test · bns-infra · bns

Also gitignores both plaintext (addresses.json) and encrypted
(addresses.enc.json) address lists — same discipline as the faucet seed.

Copies design/explorer/index.html into Parameters/dApp/Explorer/index.html so
both mirrors stay identical (the parallel session's convention: design/ is the
source of truth, Parameters/ is the published copy).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 01:18:41 +02:00
Local Dev
30a43433e4 fix(potidaea): kill mockup fake-broadcast + simplify wallet dropdown labels
Two joined fixes from user reports:

1. The BCH-inflation bug had ONE MORE root cause that the overlay work
   couldn't touch: when no wallet was attached, the swap confirm
   button fell through to the mockup's inline confirmSwap. That
   handler mutates fa.bal (BCH) and ta.bal (MBA) as if a real swap
   had happened — so the local balance display moved without ANY tx
   ever going on chain. From the user's side it looked like BCH was
   deducted, then re-added, then the ledger drifted. This was the
   real reason for "BCH not deducted after swapping to ASM".

   Fix: wireSwapConfirmInterceptor now ALWAYS stopImmediatePropagation
   when the real handler doesn't fire, and surfaces a specific error
   on the review sheet instead:

     • pair touches DEA          → "DEA has no mint/burn covenant …"
     • no wallet attached        → "Attach a wallet before swapping …"
     • MBA → MBA composite       → "redeem to BCH first, then claim …"
     • other unsupported pair    → "only BCH ↔ ASM/ASM2/Share is live"

   The mockup's inline confirmSwap can no longer run — every path
   through the interceptor terminates with either a real broadcast or
   an inline error. No local balance can move without a real tx.

2. Wallet-menu labels trimmed per user request. The three attach-a-
   wallet rows are now plain:

     + Create a wallet
     ↧ Import a wallet
     ⌘ Connect a wallet    (was "⌘ Wizard Connect / pair a BCH wallet")

   Sub-descriptions removed on all three so the dropdown reads as
   three clean actions.

Verified live: with a signer active, tapping Confirm on a BCH → ASM
attempt kept bchRow.bal + asmRow.bal exactly the same (mockup
mutation blocked); wallet dropdown shows the three clean labels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 22:37:25 +02:00
Local Dev
47d5ae54ed style(potidaea): my-tx flex layout — content-tight cells, no hollow space
Previous grid `auto 3fr 2fr 3fr` made each data column much wider than
its own text — the amount ("0.12 BCH") sat right-aligned inside a
150-px track, so 100 px of empty column-space stretched to its LEFT.
That empty space is why the numbers read as center-aligned: the eye
saw the value floating in the middle of a wide track, not hugging any
edge.

New layout: display: flex + justify-content: space-between + gap: 4 px.
Each cell hugs its own text width — no hollow left-side. The four
cells distribute across the panel: Pair on the far left, Time on the
far right, From / To centered between them. `margin-left: -28 px` on
the To cell pulls it a little closer to From so the two amounts read
as coupled without touching.

Measured live at panel width 580 px:
  Pair    →  46 px wide (BCH/ASM)
  From    →  53 px wide (0.12 BCH)
  To      →  73 px wide (120,000 ASM)
  Time    →  62 px wide (08:21:31)

Distance between cell right edges:
  Pair → From    → 155 px
  From → To      → 146 px   (~10 px less — the "slightly less" gap)
  To   → Time    → 164 px

Every amount now sits tight against its own content bounds, so the
"right-alignment" the eye is looking for is visible — the amount ends
exactly where the visible cell ends.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 08:22:50 +02:00
Local Dev
79d33b758d style(potidaea): my-tx equal outer gaps, slightly tighter From ↔ To
Grid template changed from `auto 1fr 1fr auto` to `auto 3fr 2fr 3fr`:

  Pair (auto, left)     From (3fr)     To (2fr)     Time (3fr)

  • FROM and TIME tracks are each 3fr — the wider pair. The middle
    TO track is 2fr, a little narrower. Combined with right-aligned
    text in every data cell, the distance between column right edges
    reads: wide (Pair→From) · narrow (From→To) · wide (To→Time).
  • Every data cell keeps text-align: right + overflow: hidden +
    ellipsis, so any amount hugs the right of its own column and
    can't spill into the neighbour.

Measured live at panel width 580 px:
  Pair right edge         →  87 px
  From right edge         → 260 px
  To   right edge         → 380 px
  Time right edge         → 552 px

  Pair → From distance    → 173 px
  From → To  distance     → 120 px    (about 30 % less — "slightly
                                        less" per the brief)
  To   → Time distance    → 172 px    (matches Pair→From)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 08:17:50 +02:00
Local Dev
2f454ea9ca style(potidaea): my-tx From / To split evenly in the middle, no overlap
Grid template changed from `auto 1fr auto auto` back to
`auto 1fr 1fr auto`:

  Pair (auto, left) │ From (1fr) │ To (1fr) │ Time (auto, right)

  • From and To each own an equal 1fr track — no more cluster on the
    right where one long amount could visually crowd the next.
  • Both cells stay text-align: right, so the amount hugs the right
    edge of its own column: From ends around the panel's midline,
    To ends around the right shoulder.
  • overflow: hidden + text-overflow: ellipsis + white-space: nowrap
    guarantee that even a long amount ("0.086252 BCH") truncates
    inside its cell instead of bleeding into the adjacent one.

Measured live at panel width 580 px:
  From column width       →  193 px
  To   column width       →  193 px  (equal split of 1fr : 1fr)
  From → To boundary gap  →   16 px  (matches CSS gap)
  overlap                 →  false

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 08:09:30 +02:00
Local Dev
c37e9139d4 style(potidaea): cluster From / To / Time on the right of my-tx panel
Grid template changed from `auto 1fr 1fr 1fr` (three equal data
columns) to `auto 1fr auto auto`:

  • Pair (col 1, auto) stays on the left.
  • From (col 2, 1fr) absorbs all remaining panel width; its right-
    aligned text hugs the far right of that wide track.
  • To  (col 3, auto) sits immediately after From with a 16 px gap.
  • Time (col 4, auto) sits immediately after To with a 16 px gap.

Result:

    BCH/ASM                              0.12 BCH  120,000 ASM  01:51:55
    ^ pair                                    ^ from   ^ to        ^ time

Measured live at panel width 559 px:
  Pair right edge         →  87 px
  From text right edge    → 385 px
  To   text right edge    → 474 px
  Time text right edge    → 552 px
  From → To visual gap    →  16 px
  To   → Time visual gap  →  16 px

The wide gap between Pair and From now anchors Pair on the left; From,
To, Time cluster tight on the right of the panel, all right-aligned —
matching the request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 01:53:23 +02:00
Local Dev
409d4885fc fix(potidaea): drop premature-confirm overlay drop + equal column gaps + sells
Three issues from user feedback:

1. Balance still reverted after a claim → redeem round-trip.
   Root cause was the just-added `confirmed` shortcut in reapPendingSwaps:
   as soon as `blockchain.transaction.get(txid)` returned success, the
   overlay was dropped. But chipnet Fulcrum can answer transaction.get
   before its own listunspent reflects the same mempool tx — so
   paintBalance ran with no overlay and stamped bchRow.bal back to the
   pre-swap value from stale UTXOs. The redeem then read that inflated
   basis and added the released BCH on top → "more BCH than initial".

   Fix: never drop on `confirmed` alone. The only reliable "settled"
   signal is the endpoint's own UTXO set moving in the expected
   direction (already implemented). Confirmed is kept only for future
   diagnostics.

2. My-tx column gaps unequal.
   Grid was `1fr 1fr 1fr auto` with an auto Time column, so the gap
   between right edges of To and Time was much smaller than the other
   two. Now `auto 1fr 1fr 1fr`: Pair column shrinks to its text, and
   the three data columns each take a third of the remaining width. All
   right-aligned, so the visual gap between consecutive column right
   edges is identical (measured live: 163 / 162 / 162 px).

3. Public activity only showed one side.
   Fulcrum has no chain-wide token-category history RPC — verified live
   against chipnet.imaginary.cash:50004: `blockchain.token.get_history`,
   `token.category.get_history`, `blockchain.token.category.get_history`
   all respond "Unsupported request". So we can't get every user's
   redeems from public data today.

   Interim honest fix: merge the user's OWN redeems (from local tx log)
   into the activity feed as sell rows (red), interleaved with the
   public claims (green) by time. Amount reads "local" for the sell
   entries whose block height isn't in the log. When a shared token
   indexer lands, we swap that data source in — the render path already
   handles both sides via the .sell class.

Verified live on the deployed page: overlay stays until UTXO moves,
one red sell row visible above the green buys, column-header right
edges at [65, 228, 390, 552] — equally spaced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 01:38:58 +02:00
Local Dev
bc9257e2aa fix(potidaea): balance inflation on claim → redeem round-trip + refresh button
Root cause of the "returned me more BCH than I started with" bug:

  1. User broadcasts BCH → ASM claim; applyOptimisticSwapDelta registers
     a pending overlay { bchDelta: +1_013_000, preSwap: 100_000_000 }
     and directly decrements bchRow.bal to 0.98987.
  2. Electrum lags on mempool indexing longer than the 60 s hard
     timeout → reapPendingSwaps drops the overlay entry.
  3. paintBalance now runs with pending: [], effective = reported (still
     stale = 100_000_000) → bchRow.bal snaps back to 1.0 (WRONG).
  4. User broadcasts ASM → BCH redeem; applyOptimisticSwapDelta reads
     currentBchSatsFromAssets = 100_000_000 (the inflated basis) and
     directly adds the released BCH: bchRow.bal → 1.00987.
  5. User sees more BCH than they started with.

Three-part fix:

  (a) Hard timeout raised from 60 s → 30 min. Chipnet mempool inclusion
      + first-confirmation latency is comfortably inside that window; a
      30-min-old pending is genuinely stuck and should age out.
  (b) New reconcilePendingViaTxid pass runs on every refreshBalance:
      calls blockchain.transaction.get(txid) for each unconfirmed entry;
      if the endpoint returns a raw hex, the tx has landed (mempool or
      block) → mark confirmed. reap then drops confirmed entries whether
      or not the reported UTXO set has caught up.
  (c) pendingSwaps persist to localStorage
      (deviant.potidaea.pending-swaps.v1). A page reload mid-flight no
      longer loses the overlay while electrum finishes syncing.

Also: manual refresh button — a small ⟳ pill on the Balances panel
title (od-botright) and a larger version on the v-balances view header.
Both fire refreshBalance() and spin briefly as feedback. Users who
suspect a stale balance can force a re-read from chipnet immediately.

Verified in deployed bundle: 30-min timeout literal present, txid
confirmation path present, localStorage key present, old 60_000 gone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 01:18:43 +02:00
Local Dev
2142be0a96 feat(potidaea): Orbital route pill in panel header + Balances back to sidebar
Three related UX changes:

1. "Atomic · counterparty-free" → "Orbital · counterparty-free".
   Atomic swap already denotes the HTLC-style cross-chain family;
   Potidaea is a same-chain covenant swap and "Orbital" is the
   project's own name for that mechanism (wiki/architecture.md).

2. Route pill moved out of the swap card body and into the center-top
   panel header, next to the buy/sell tabs. Slimmed (4 px vertical
   padding, 10 px horizontal, 10 px type) so it fits comfortably in
   the header alongside the tabs — same acid-green pill treatment,
   just proportioned for the header context.

3. Balances back to the left sidebar, in the pole position (before
   Potidaea). It is a wallet-scoped view but a frequently-visited one,
   so it earns the top slot rather than sitting mixed with the workflow
   tabs. The header pill (hdrBalances) is removed; view-switch logic
   is unchanged (the sidebar navitem uses the same data-view="balances"
   hook as before).

Verified live: route reads "✓ Orbital · counterparty-free" in the
header, no route pill in the swap-card body, sidebar order is
[Balances, Potidaea, Anthemus, Artemision, Katalogos].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 01:11:09 +02:00