Conversation
Full working-tree state of the wip restructure before the planned library rewrite. Review findings and the design blueprint that motivate the rewrite are under docs/reviews/ (2026-08-09 snapshots).
Adds the 2026-08-09 review/design/decision snapshots and the 2026-08-10 final decisions record (the authoritative rewrite spec). Removes the 34 July review-cycle artifacts after a salvage pass folded their carry-overs into the final record; they remain at tag pre-rewrite-baseline.
CCC family 1.17->1.18.2 (covers the integration-audit pin's runtime code plus fixes #445/#448, #446, #451/#452, #453/#454; #444 remains open upstream, mitigated by the HTTP-only fallbacks:[] client rule). In-range refresh across tooling; react override follows the app to 19.2.8; brace-expansion override advanced for new advisories; tsgo pinned (closes C9 with the already-removed stale exclude block); gitignore log/ anchored to /log/. Typecheck clean, 1509 tests pass, audit 12 vulns -> 2 (elliptic ignored-by-policy + nanoid clearing the age gate).
…ct oracle The matcher converted ckb_min_match into UDT units with inverted scales (ceil(m*udtScale/ckbScale) instead of ceil(m*ckbScale/udtScale)), so at asymmetric ratios it emitted partials the deployed limit_order script rejects with InsufficientMatch (over-restrictive in the mainnet regime, under-inclusive in the general one). Adds the entry.rs:116 plain-CKB-delta post-guard as the authoritative check, a freestanding contract oracle in testkit ported line-by-line from the deployed Rust (validate(), deposit_to_ickb; 35 self-tests), oracle adjudication sweeps across four ratio regimes in both directions (11 tests; 4 fail against the unfixed matcher), and pinned contract fixtures: the four iCKB release ELFs (contracts @ ae8a11f) plus the dao binary extracted from mainnet/testnet genesis (byte-identical, provenance in fixtures README).
60 rows generated by the contracts repo's vector-gen crate (byte-exact copies of deposit_to_ickb and validate with build-time drift assertions, commit ae8a11f): 24 deposit-conversion rows incl. exact soft-cap boundaries at three ARs, 36 limit-order rows across both directions, dual-ratio orders, and check-ordering pins. The TS oracle agrees on all 60 (independent double-derivation). Adds node types to testkit for the fixture loader.
Chart: live-tip marker dot, 2px non-scaling mark-spec stroke, tighter mobile y-axis gutter (plot +15% at 390w). Form: brand casing iCKB (was uppercased to ICKB by the grid's blanket uppercase), sentence-case validation errors. Theme: color-scheme dark for UA scrollbars/controls. Explainer: mobile heading scale. Removes an inert text-s utility. Verified via preview snapshots at 1280w/390w, landing and connected (testnet harness) states; a11y inventory and 167 interface tests intact.
250-run seeded properties (VITEST_FC_SEED replayable) across both scale regimes: every emitted partial contract-valid via the oracle, allowance respect, monotone consumption, full-fill shape, empty-below-minimum; min-match boundary band probed explicitly. Core: ickbValue equals the oracle's depositToIckb across both cap branches; convert round-trip loss bounded exactly. Shared oracle helpers extracted for both suites. fast-check ^4.9.0 as root devDependency. 357 tests green, no counterexamples.
Resolves the Phase-2 blocked/unblocked contradiction, re-gates debugger execution to the Phase-2 exit, stages the systemd cutover with a joint rollback pair, derives the journal consumer inventory from the repo, reorders gate/artifact dependencies, slices Phase 3, renames the exported-allowed subtree to shared/ and adds B4 bases to the export contract, records the B4 recipe durably with its re-probe obligation, amends the withdrawal-selection product contract explicitly, and pins the debugger release asset.
FakeClient extends the abstract ccc.Client: every member either consults the in-memory ChainState (cells, headers, transactions, tip, fee stats, send scripting) or throws FakeClientError synchronously — no network path exists, unlike StubClient's live-client fallthrough. Overrides map retained as the error-injection escape hatch; ClientCacheMemory keeps caching paths real. StubClient stays untouched pending call-site migration. One WIP fixture corrected for CCC 1.18's capacity raising (#459): sub-occupancy outputs are illegal on-chain and now normalize at construction.
…valence Fee-drawing properties (0..10^7 + pinned constant) show contract validity of emitted partials is fee-invariant, and a reachability suite proves the entry.rs:116 post-guard is exactly equivalent to the bMinMatch pre-gate for from()-constructed matchers (floor(u*b/a) >= m iff u >= ceil(m*a/b); fee never enters either side): every sub-gate would-be partial adjudicates InsufficientMatch, every admitted partial clears the plain-CKB minimum. Guard retained as defense-in-depth with its comment corrected from 'authoritative' to the proven equivalence.
Per review 003 residuals: section-5 title and open-items reflect the delivered mapping; Phase-2 line carries the debugger exit gate instead of the stale blocker; the sequencing pin replaces the four-consumer coordinated change with the staged cutover over the grep-derived owner inventory (now including tests/support and the supervisor README); shared/ replaces internal/ with the B4 symbols in the root contract and the dropped depcruise rule removed from the body.
No-subpaths confirmed safe (merged barrel adds zero retention); adopts the entity-module-layout rule and the PURE-in-static-block ban; sets the 2KB packed-probe budget for the 3a gate; queues two CCC upstream reports (nested-manifest sideEffects omission, namespace monolith).
…oor gate Replaces the evicted ephemeral B4 evidence (F-006) with a re-runnable probe: tsgo emit + api-extractor over the amendment-7 two-module shape, plus negative controls for ae-forgotten-export and for silent demotion of that gate. The root-export-floor fixture typechecks all 31 contract names against current barrels (type-aware: catches type-only impostors of value names): 4 known-missing flagged for Phase-2 barrel work, 10 pending Phase-2 names tracked, and the contract adopts the existing DAO_OUTPUT_LIMIT name. Re-points at the packed tarball as the 3a exit gate. pnpm probes wires both.
typescript-eslint 8.65 flags vi.fn() spy references typed through ccc.Signer as unbound methods (no this is captured); sonarjs 4.2 does not track node:assert deepEqual through the smoke tests' loops. Five reasoned per-line disables; behavior unchanged, both suites green.
ICKB-014 narrowed to post-deadline presentation in both body sites; Phase-4 body points at the staged cutover amendments instead of the 'coordinated change' phrasing.
F-013: coverage gate green again — the entry.rs:116 mirror is exercised via deliberate direct-constructor desync (the case it defends), and the two FakeClient override/unscripted branches are covered. F-014: root-export floor de-vacuated (DAO_OUTPUT_LIMIT owned by dao and verified) and made nominally complete: result unions and per-entity XBase consts tracked as named pending entries (38 names); provisional status recorded as amendment 14. F-015: committed treeshake reproducer (rollup+esbuild pinned devDeps, pnpm probes): source-scan ban, runtime-breakage reproduction — refined to the truth: rollup DEFAULTS keep the annotated call; the silent class-kept/static-emptied breakage reproduces under moduleSideEffects:false, a consumer config outside our control (amendment 13) — and the 2KiB clean-layout budget. F-016: body reconciled (entity check resolved, Phase-5 line, ICKB-025 supersession wording). F-017/018/019 recorded as amendments 15-17: durable signed-bytes owner keyed by chain/account/hash/inputs; full dependency-identity congruence as fail-closed signing prerequisite; per-consumer confirmation-depth table proposed for maintainer ratification.
…scribe Keeps the dispatch describe under the 80-line function cap.
…ema (review 007 F-005 note)
…05, amendment 18)
F-015: brace-aware static-block scanner with a nested-brace negative control (the naive first-closing-brace regex provably misses deep annotations); amendment-12 barrel claim narrowed to its exploratory status with the packed 3a gate as the enforced form; probes wired into the required gate (pnpm lint -> lint:probes). F-016: resolved open-item removed; the treeshake gate names its real command instead of the nonexistent check:deep. F-017: the inviolable keep-list itself now requires the exact signed bytes (amendment-15 lifecycle), not hash-only persistence.
…-019)
Supersedes the per-consumer depth table: depth 0 everywhere; automated
entities restart from clean state on revert (client reconstruction
replaces the ICKB-020 scoped cache rollback for stack consumers); the
interface communicates state transitions honestly and recovers via the
amendment-15 signed bytes; wait({depth}) stays published for off-chain-
dependent integrators; ICKB-008 recheck becomes the reorg monitor.
Merges the settling-copy residual into the ICKB-014 interface-
communication task.
…cation Recovers e39f713 (cherry-picked: the chaining + clean-state-restart ratification had been orphaned by a silently-failed push plus a shared-checkout reset — push verification hardened hereafter). F-005: amendment 11 now states the distinct-UID/LoadCredential rule it was cited for; amendment 18's mainnet-rejection claim corrected to harness protection (separate never-funded mainnet keys are the asset boundary). F-014: IckbErrorCode ratified and added to the floor manifest (39 names). F-015: the static-block scanner is TypeScript-AST based with nested-brace AND lexical (string-brace) negative controls. F-016: section-1 floor marked provisional-until-3a; section-2 congruence upgraded to the amendment-16 full identity binding. F-017: appendix ICKB-007 constraint upgraded to exact signed bytes. F-019: amendment 15 gains the chained-ancestor retention lifecycle (parent bytes durable until all descendants terminal). F-021: amendment 19 — every mandatory gate lands with its command and CI membership in the slice that introduces it.
The wait only lets a turn journal its own commit; the next turn a minute later rebuilds from committed state either way. A withdrawal request is built inside the pool's fifteen-minute lock-up window, so a ten-minute wait on a stuck one left five minutes to rebuild it before the deposit locked for another thirty days. Seven days of testnet journals: 116 commits, 77 s at most, 54 s at p90. Decision 52(aj)(1).
…default exponent is 36 A fill was sized to the balances, so a fully fillable order met with a balance a little short left a scrap under the ten-fee floor that no turn took, and for a buyer DAO growth then buried it. `bestFill` now caps a partial's payment at `bMaxMatch - bMinMatch`, skips the order when that cap is under `bMinMatch`, and takes an affordable completion untouched (a completion of any size is contract-valid). With it the SDK's default minimum match goes from 2^33 to 2^36 shannons (about 687 CKB), so a fresh remainder clears the floor 22 times over even at a 100,000 fee rate; orders under about 1,375 CKB are taken whole or left, a band that must stay under the bot's 2,000 iCKB refill line. Decision 52(aj)(2).
The order folder is one folder per on-chain script like the rest of the
SDK will be: `order/{cells,info,ratio,master,order_data,relative,
entity_validity,matcher,fill,fee,conversion,order}.ts`, no internal
barrel. Adopted from the two independent code reviews (2026-09-19):
- `OrderMatcher` is built only by `from()`: the public constructor, its
parameter assert, the two "direct construction" guards in `match()`
and their reachability suite are gone, with the dead real-ratio fields
and `compareRealRatioDesc` (the bot ranks by `netOf`).
- `greatestBoundedFractionAtMost` is one loop of twenty lines; an
exhaustive small-bound oracle (566,250 cases plus Uint64 boundaries)
pins it, written against the old loop first.
- `OrderManager` owns its scan and transaction code as methods; the scan
returns the group or `undefined` instead of six skip reasons nobody
read, and `findOrders` returns an array (it always resolved every
group before yielding).
- The five "already being spent" loops become one invariant in
`completeTransaction`, which names an input spent twice; the order
layer's two are gone here, the DAO layer's follow with its dissolution.
- Dead members: `Info.ckb2UdtCompare`/`udt2CkbCompare` (callers compare
ratios), `Ratio.applyFee` (its fee scaling lives in `quoteConversion`),
`OrderCell.isDualRatio`, `OrderCell.maturity` (the projection asks the
estimate), `MasterCell.from`, `OrderManager.isMaster`,
`Info.create`'s third argument, `cellInputLikeFrom` on this path.
- `minBigInt`, `maxBigInt`, `ceilDiv` once in `utils`.
…lans, one book Adopted from the two independent code reviews and the user's decisions of 2026-09-18/19 (decision 52(ak)): - `IckbSdk` is one class in `sdk/src/sdk.ts` (four abstract layers, one leaf, no second implementor); `request`, `collect` and `estimate` are gone, their one caller each rewired. - `conversion/` drops the `sdk_` prefix: types, plans, estimate, maturity, projection, withdrawal_ring, fundable_walk, error; `signerAccountLocks` lives with the signer code in `send/`. - Direct first: iCKB-to-CKB plans are tried longest direct prefix first, the remainder as an order; the maturity-bucket ranking, its plan fields and surplus helpers are deleted (supersedes 46(a); the ranking also let an order leg of unknown maturity outrank a dated direct plan). - One book, two filters: `system.orderPool` is every order past par, the wallet's own included; `user.orders` every order it owns. - The withdrawal ring is one file over `IckbDepositCell`, selection is positional, the planner filters anchors first; `buildBaseTransaction` takes the context's collect steps and an optional withdrawal request. - Records with one field or one implementor go: `PoolDepositState`, `IckbToCkbOrderEstimate` (the estimate carries its notice), `BuildBaseTransactionOptions`, `LockCells`, `FundableCompletion`, the two-record pool pipeline, `expectedChainIdentity`, `isIckbError`, `IckbError.retryable`. - The barrel is the twenty names the interface reads plus the types their signatures need; the interface matches errors by class.
…ing fetch first The SDK touches only iCKB deposits and iCKB withdrawals, never raw DAO ones, and every `DaoManager` operation had exactly one caller in an iCKB manager (user, 2026-09-19). Now `logic.ts` owns deposits and receipts, `owned_owner.ts` withdrawal requests and withdrawals, both over the DAO script deps they carry; `dao.ts` holds the plain shared rules (deposit data, claim epoch, the lock-up window, the output and header-index limits); `udt.ts` the token and the receipt payload it values. Each batch reads the distinct transaction and block headers it needs once, then decodes with pure functions, so the three promise caches, their re-wraps and the batch cache options are gone; a deposit is valued in one pass. Deleted with them: the out-point branches of the cell converters, `DaoOutputLimitIndeterminateError` (an oversized transaction with an unresolved input is refused as a DAO one), the entity classes behind the owner and receipt payloads, `cellInputLikeFrom`, the brand on `IckbDepositCell`, the per-layer spent-input loops. `getConfig` returns the managers flat and the lock-up window is settled once in `getL1AccountState`. The core and dao test suites are rewritten on the new modules; vitest now includes every test directory. Decision 52(ak)(9).
…whole, and fold their folders - The config reader parses the RPC URL once and returns its credential-free identity beside the URL; the control-character loop, the credential rule and `publicRpcEndpointIdentity` are gone (the URL string reaches only the client, the journal gets the identity fields). - One catch in the bot entry point: `runBotTurn` propagates; the built and skipped events carry the decision only, whose `actions` the README's jq lines now read. - The stimulus turn writes its log record directly instead of through a callback; `transactionShape` has one owner in `shared/format.ts`. - Test seams out of production signatures: the emitter's `write` hook (tests read stdout), `matchTurn`'s partial cap, the sampler's samples per year, `readBotRuntimeConfig`. - Layout: `bot/runtime/*` and `bot/policy/*` fold into `bot/`, the shared and stimulus barrels and `bot/failure.ts` go; tests mirror it. Decision 52(ak).
…tion down to the form, parked queries R17: the form draft is isCkb2Udt and text, two states in WalletGate and two props down the same path; the C/I prefix, symbol2Direction and direction2Symbol are gone. R18: actionMessage takes the preview and a twelve-flag record; Action folds its four status helpers, timeUntilMaturity moves into actionStatus.ts, the click handler and attempt ownership are a hook and one function. R19: parseAmountInput is one anchored expression. R22: L1StateType carries the SDK projection whole and the form reads it; AssetDisplay nests the balance triple. R26: rateChartView returns what the chart reads. N6: no test seams in production signatures (pending store initial, RateChart now, cccConnection storage). N7: the submitting record holds the promise. N11: RootConfig and QuoteState used directly. N16: skipToken parks the quote and preview queries. Gate green; preview checked at 1280 and 390 with the private-key harness.
…methods it found are gone scripts/tooling/dead-members.ts loads the root and interface programs and reports a public class member no production file references through the class, a type it implements, or an interface of its name (const-class statics resolve through the typed export variable); overrides exempt, testkit excluded. Wired into lint:knip. First run: Ratio/Info/Relative/OrderData.isValid() and entity_validity.ts were reachable only from tests; the tests assert validate() directly (52(ak)(10)).
…ates are the supply A seller's date: one deposit's CKB now, the bot's working capital, unless a fillable seller has sat on the book for over a turn (the origin's block number, read for free from the master's transaction, dates the order), then each pool deposit at its real claim date, less the deposits the same plan withdraws directly; demand is the order's CKB plus every fillable seller priced better, at the DAO ratio; the first covering date plus one turn, else waiting for liquidity. A buyer's date: one turn plus one per cap of net demand ahead, the cap at the current ratio. Book orders count only when fillsWhole would take them. BOT_TURN_MS names the ten minutes. SystemState.ckbAvailable, ckbMaturing, CkbCumulative, poolCkb and binarySearch are gone; poolDeposits is the supply. Contract: sdk/docs/pool_maturity_estimates.md; decisions 52(ai)(15).
…e queries one, the page one Chart: six rateChart* files become RateChart.tsx (the component and its unscaled labels) and model.ts (samples, scale, ticks, view). Queries: the four query/ files become app/queries.ts. The quote helper joins formState.ts, the button style, chain reader and wallet label join shared/utils.ts (react-refresh keeps component files to components), the header portal and section grid become one WalletPage the three wallet pages render through, and chain.ts, walletGateState.ts and walletGateSupport.tsx fold into WalletGate.tsx. interfaceLanding.tsx and staticWalletApp.tsx take their component's name. Tests follow the layout; the element search reads named slot props.
StubClient is the one double: FakeClient and ChainState go (679 lines plus 447 of self-tests), the legacy findCellsOnChain adapter with stub: cursors becomes pagedCells, a page handler over a cell list sliced the way the SDK's findCells reads it, and composedClient mirrors the client the connector hands the interface: a ccc.Proxy.Base(ccc.Client) wrapper (ClientWithFeeRate, connector 2.0.0) that is a ccc.Client but not a ClientJsonRpc, so the SDK's typed read paths are production paths and the tests keep reaching them. Consult: T3 child astra-testkit-doubles-2026-09-19, claims verified against the installed sources (52(ak)(11)).
sdk/src/index.ts is the only barrel: every importer of utils/index.ts names the owning file (chain, codec, utils). The bot's readBotState, one importer, lives in turn.ts and its tests in the turn's.
The thresholds give 2.2 (1.2 deposits in the inventory band, one deposit plus the reserve to refill); the half deposit of CKB on top keeps serving sellers while a withdrawal waits for its claim date, the steady state observed on testnet (52(aj)).
… bot refuses a credentialed RPC URL The seller estimate subtracted the CKB an order had already received from what it still needs, so a half-filled order read ten minutes with no CKB in sight (Opus 5, Astra). The bot's RPC URL parser now rejects userinfo, whitespace and control characters as the README promised; a credentialed URL reached the transport, whose error text carries it (Astra, Sol). Docs corrected: the root README's obsolete completion boundary and page-size sections and the src/core layout in both READMEs, the policy note's runtime/ path, the interface README's quote module, the generator's fee set, its ten-minute wait and its refused count (all five reviews). Deleted: an inline copy of pushHeaderDep, the readyPoolDeposits sorting wrapper, the generator's origin re-reads now that OrderGroup.blockNumber carries the block, a stray doc comment and a doubled import (end-game consultation 2026-09-19).
…stor; the app watches an uncertain send The connector hands the interface a composition proxy that fails instanceof ccc.ClientJsonRpc, so the wait read transaction bodies, and CCC returns nothing for a rejected transaction's null body: a rejection looked pending until the timeout and the session stayed locked (Astra, Fable 5, Fable 5.1). jsonRpcRequestor finds the requestor the proxy forwards; the wait and the header reader use it, so the interface sees rejections and takes the status-only header reads the bot takes. After a send whose answer was lost the app now enters the same confirmation window on the hash it recorded, instead of reporting a failure and waiting for a click; a resend could not tell what became of the first copy (Sol; Astra consult astra-send-wait-2026-09-19). The stub client answers the raw status poll from the scripted body.
…ount as ahead; dual-ratio orders are dropped at the scan End-game decisions 2, 6 and 7 (user, 2026-09-19/20; recorded as decisions amendment 52(al) in the closing commit). Every iCKB is backed by a pool deposit that matures within one DAO cycle, so a sell order at par is always covered by the pool: the unknown-date path (`maturity-unavailable`, `maturityEstimateUnavailable`, "waiting for CKB liquidity", `shownMaturityText`, the projection filter and the plan branch) is deleted end to end; `maturity()` returns a date, and an order asking above par reads the pool's last claim date plus one turn. The notice keeps its one kind and loses the `kind` field. A fillable order at the same price counts as ahead (the bot fills ties in an order of its own choosing), and an order already on the book is left out of its own queue by out point. A dual-ratio order, valid on chain but placed by nothing in the stack, is dropped at the order scan, so nothing downstream knows it. `CI=true pnpm check` exit 0 on this tree.
… and iCKB there, at amount zero End-game decision 4 (user, 2026-09-20; Astra consult `astra-foreign-amount-2026-09-20`; recorded as decisions amendment 52(al) in the closing commit, superseding 52(af)'s amount clause). A transaction to another address carries only native CKB and iCKB, never a converting position, since the new wallet may not read those, so it takes no amount: once the destination is another wallet's the amount reads "0" with "all CKB and iCKB go to the address above" under it, max and the direction switch are off, and the typed text comes back when the address is cleared (derived, no new state). The button reads "send all CKB and iCKB"; the status line says what is sent, that converting funds stay here, and "run it again until everything has been sent" when the size budget left cells behind. "Ready:" reads "now". The app's transaction boundary refuses a non-zero request to another address before reaching the SDK. The SDK result carries `isSweepComplete`, whether every liquid cell became an input. A test pins the order the completer already had: iCKB cells first, plain last, the fee always from plain, so several transactions always fund each other. While a transaction is prepared, signed or awaited the address reads as plain text with "cannot change during the transaction" under it. The word "move" is not used on screen (user): it named nothing the user knows; "send" is the wallet word. Layout checked in the browser at 1280 and 390. `CI=true pnpm check` exit 0 on this tree.
…icy, checked again at send time End-game decision 5 (user, 2026-09-19/20; Astra consult `astra-signing-deadline-2026-09-19`, two runs; recorded as decisions amendment 52(al) in the closing commit). A withdrawal request committed after its claim locks the deposit for another cycle, and a wallet may hold the signature for a while. `LockUpPolicy` names a caller's rules in epochs, the chain's clock: a selection window (`minLockUp`, `maxLockUp`) and a `broadcastReserve`. `WALLET_LOCK_UP` (claims two hours to three days out, sent while ninety minutes remain) is the app's and the generator's; `BOT_LOCK_UP` (twenty minutes to an hour, fifteen) is the bot's and, since the bot makes the requests, the rule the estimate dates the pool's supply on. The SDK default window is gone: `getL1AccountState` takes the policy and carries it on `system.lockUp`. Pool deposits carry their sampled `claimEpoch`; readers judge readiness with `depositMaturity` and the policy (`readyDeposits`), the ring reads the claim (one cycle round, so a roll changes no segment), and the ready guard in the base builder is gone with the sender's check subsuming it. Every builder that requests withdrawals puts `broadcastBefore`, the earliest selected claim less the reserve, on its result; `signAndSendTransaction` reads one fresh tip after the signature and the fee check and throws `TransactionExpiredError` without sending once the tip's epoch has reached it. The app releases the preview with "The withdrawal timing changed while you were signing. Nothing was sent. Review the refreshed preview and sign again."; the bot's turn fails and the next one rebuilds. `CI=true pnpm check` exit 0 on this tree.
…dment 52(al) records the end game End-game decision 8 (user, 2026-09-20): the sweep checks the size budget before each input, so the last one may overshoot it; accepted, and the policy and the constant's comment say so. Amendment 52(al) records the end-game consultation (five reviewers, Grok unavailable), the eight decisions settled one at a time with their rejected alternatives and accepted tradeoffs, and the commits that built them. `CI=true pnpm check` exit 0 on this tree.
…hatever the reason Decision 52(am) (user, 2026-09-20). The generator's thirty-day rule moves into the SDK as `isStale`, beside `isRefused`; the app counts a stale order with the fulfilled and refused ones, so the user's next transaction melts it and returns the funds. Dust under a bot's fill cost, a remainder another matcher leaves, an ask above the market: a sell of real size is rightly never refused (52(q)), but these could sit for ever. The generator keeps drawing dust and now exercises the collection path. Rejected: a horizon of DAO growth on the fill check (an assumption about the DAO rate, and it would melt a deliberate above-market ask); age past the order's own estimate (a bot outage would cancel every waiting order). `CI=true pnpm check` exit 0 on this tree.
…l deposit headers go first Decisions amendment 52(an) (user, 2026-09-20), from Grok 4.6's end-game review (`grok46-endgame-2026-09-20` over `7a08df98`: no confirmed defects), each suggestion verified against the code. The order scan's cache-first origin read trusted a cached response without a block, so the app's long-lived client kept a lagging node's "pending" answer for the session: the order never counted as old and the estimate read it as uncommitted. As CCC's own readers do, the cache is trusted only once the entry carries a block number; no new traffic in the steady state. Only phase 2 names a header by index, read as one byte by the deployed DAO script, so `withdraw` moves its distinct deposit headers to the front of `headerDeps`: the limit counts those alone, the projection's ready batch is the first 256 matured withdrawals whatever the receipts and requests, and the request-ahead-of-withdrawals residual is gone. Nothing else in the stack reads a header by position, each built transaction passes the step once and last, and every base transaction starts empty; `buildBaseTransaction` says so for integrators. Two record corrections: the estimate doc's ten-minute lock-up floor is the bot's twenty since 52(al)(5); 52(al) deleted an inline copy of `pushHeaderDep`, not the function. Order expiration is settled as 52(am) after a Fable 5 consult. `CI=true pnpm check` exit 0 on this tree.
… guards its cursor; one fit loop, one status parser Decisions amendment 52(an), second round (user, 2026-09-20; fresh Opus 5 and Grok 4.6 audits over `f113ef91`, no confirmed defects, every suggestion verified). `daoClaimEpoch` existed because CCC rolled a cycle on equal fractions; the pinned 1.20.0 `calcDaoClaimEpoch` is the same strict comparison, so the copy is gone and the dao.c model suite pins the installed CCC on every run. A non-empty page always moves the indexer's cursor, so `findCells` fails with a named error when a full page brings it back unchanged, instead of looping for ever on a broken node in the app's public pool. One withdrawal fit loop (`fitWithdrawalDeposits`, the caller sorts) replaces the bot's copy; one `rawTransactionStatus` serves the wait and the header read; the order scan's second cache write, `RING_EPOCHS` and the generator's second manager set are gone; the phase-2 `since` carries the packed epoch number rather than a hex that matched by codec coincidence. Stale comments and one README sentence corrected. `CI=true pnpm check` exit 0 on this tree.
Clean-up settled one item at a time (user, 2026-09-20; recorded in decisions amendment 52(an)). Deleted: the closed findings file, the rewrite overview (the root README points at the decision record), the CCC audit companion (its rows built, superseded or contradicted; its six standing CCC and node facts went to the maintainer's cross-project notes for re-verification), PRODUCT.md, the SDK's .npmignore, the two package-level .gitignore files, the private packages' licence sections, ten uncalled package scripts and four root ones, the sampler's tee into a file, a dead eslint override for a folder that no longer exists, seven stale ignore lines. Three small source files are folded into their neighbours: IckbError and the fundable walk into sdk.ts, signerAccountLocks into the sender. Every folder that is not source or tests has a README (scripts, patches, the two docs folders, the vectors' provenance note renamed); the interface's public folder is described in its README since Vite would publish one there. The runtime-config test makes its temporary directory under the OS temp dir; the manual-run examples name the key directory the units use. `CI=true pnpm check` exit 0 on this tree.
The three-day window of 52(v) held only while the SDK was unpublished; seven days is the standing policy before the fork (user, 2026-09-14 and 2026-09-20). A frozen install verifies every lockfile entry against the policy and passes: nothing installed is younger than a week. `CI=true pnpm check` exit 0 on this tree.
The pre-rewrite stack left @ickb/sdk on npm at 1000.0.82, so the 0.1.0 this branch declared could not publish. 9000.0.0 sorts above everything published, so the first release takes the latest tag with no dist-tag surgery, and one number across the repo drops the 1001.0.0 placeholder that marked a package unpublished. Recorded as decisions amendment 52(ao); it supersedes 52(ad)'s first-version clause.
Read the changelogs at ckb-devrel/ccc 3e9087267bd8. Nothing in 1.20.1, 1.21.0 or 1.22.0 fixes a defect on a path we use: the one correctness fix in the range is 1.20.1's UDT input selection, and the stack never calls it, funding completion from the cells the state read returned and using CCC only for completeFeeChangeToLock. Recorded as decisions amendment 52(ap), which closes the bump slice 52(v) left open. The minimumReleaseAge comment still described the three-day window that was restored to seven days before it; it now says what the value is.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This replaces the whole TypeScript stack: one published SDK, three single-turn actors, one app, and a test kit.
The previous stack grew faster than it settled: thirteen workspaces, a supervisor and a launcher around the bot, five published packages, and transaction logic spread so thin that following one conversion from the click to the signed bytes meant reading most of the repository. Reading it that way turned up a real bug: the order matcher inverted a conversion. It is fixed here and pinned by an oracle ported from the contracts, and the rest of that code is gone.
Users get an app that converts CKB to iCKB and back, shows them one date for everything they have converting, and collects what is ready with the next transaction they sign. Integrators get
@ickb/sdk: one entry point, one dependency, plain sampled state and a transaction they can complete, sign and send. Operators get three processes that run one turn and exit. Every choice below, with the rejected alternatives and the accepted tradeoffs, is written down indocs/, which is the authority; this description is the map.Shape
Thirteen workspaces become four:
sdk(the published library),sdk/node(the bot, the testnet stimulus generator, the mainnet rate sampler),testkit(private test helpers),interface(the app). Gone:packages/core,dao,order,utils,node-utils,testkit, andapps/bot,tester,sampler,supervisor,interface.@ickb/sdkis the only published package: one entry point, 25 exports,@ckb-ccc/coreas its single dependency, browser safe. One file per on-chain script (udt.ts,logic.ts,owned_owner.ts,dao.ts,order/), the conversion workflow inconversion/, the send path insend/, and no barrels except the index.What the SDK does
getL1AccountStatetakes one tip and reads the book, the pool and every account lock with uncached exact-lock scans, sorting the cells here. It is complete and uncapped: a big book makes the read slower, never partial.ae8a11f, plus golden vectors generated by their Rust crate. If either port drifts, the suite fails.dao.c; the 64-output limit; the one-byte header index, with withdrawal deposit headers placed first so only they count against it.The maturity estimates explain how an order gets its date.
What the actors do
One turn is one process: read state, decide, send at most one transaction, exit
0on a skip or a commit and1on any failure. The supervisor, the launcher, the tester and the generated config files are deleted. Restarting is systemd's job, the unit is the config, and the signing key stays in a0600file the unit names. It never reaches an environment value, an event or a log. The bot policy says what a turn does, in order.What the app does
One preview per settled amount, built from exact wallet state and completed as a real transaction, then refreshed and rebuilt once more right before the wallet pops up. The balance row says what is in wallet, what is converting and what is collectable, and one "Ready:" line dates everything in flight. Every transaction the user signs also collects their converted funds, including orders the market will never fill and orders sitting for thirty days. The address in the header is also the destination: point it at another wallet and it sends the CKB and iCKB there, with no amount to type. That is the way out for a key-only wallet that has no xUDT transfer of its own.
Decisions
These are the ones that shape how the code reads. The rest, with the rejected alternatives, are in
docs/.For the user:
For the integrator:
@ckb-ccc/coreis the only thing it pulls in.9000.0.0. The old stack left@ickb/sdkon npm at1000.0.82, so a sane number could not publish. This one sorts above everything and takes the tag without a fight.For the operator:
no_gainmeans the book offers nothing worth its fees at any size;unfunded_gainmeans it does but the balances cannot pay. A watcher can tell a dull market from an empty wallet.For the repository:
pnpm checkis the audit, the full lint and the interface build. Coverage is 100% on both source trees, and a dead-member lint sits beside knip because knip sees exports, not members.