Skip to content

iCKB Stack rewrite: one SDK, single-turn actors, one interface - #63

Open
phroi wants to merge 252 commits into
masterfrom
wip
Open

phroi wants to merge 252 commits into
masterfrom
wip

Conversation

@phroi

@phroi phroi commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

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 in docs/, 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, and apps/bot, tester, sampler, supervisor, interface.

@ickb/sdk is the only published package: one entry point, 25 exports, @ckb-ccc/core as its single dependency, browser safe. One file per on-chain script (udt.ts, logic.ts, owned_owner.ts, dao.ts, order/), the conversion workflow in conversion/, the send path in send/, and no barrels except the index.

What the SDK does

  • One read of the chain. getL1AccountState takes 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.
  • Plans that degrade instead of failing. Candidates are built most direct first, and completion takes the first one the wallet can actually fund. Short of CKB the plan turns into fewer direct withdrawals and a bigger standing order, not an error.
  • Completion never scans. It funds from the cells the state read already gave it, sweeps the rest of the account while the transaction stays under about 64 KiB, and puts change in a new plain cell.
  • The send owns its bytes. The local hash is the identity whatever the node replies, the signer cannot change ordered inputs, outputs or output data, the fee ceiling values inputs from the transaction before signing, and an ambiguous broadcast gets watched, never resent.
  • The wait ends. One bounded window, then the caller rebuilds from committed state.
  • Matching is checked against the contract. A TypeScript oracle ported from the contracts at ae8a11f, plus golden vectors generated by their Rust crate. If either port drifts, the suite fails.
  • DAO rules as the deployed script has them. Claim epochs from CCC, rechecked every run against a transcription of the deployed dao.c; the 64-output limit; the one-byte header index, with withdrawal deposit headers placed first so only they count against it.
  • Withdrawal timing is the caller's policy. A selection window and a broadcast reserve, both in epochs: tight for the bot, generous for a wallet where a human signs by hand. After signing, one fresh tip decides: a request that would commit after its claim is refused, not sent.

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 0 on a skip or a commit and 1 on 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 a 0600 file 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:

  • A conversion is two steps, not a position. One leg takes a DAO cycle, the other takes a fill. The app dates both and never asks the user to track them.
  • The default order fee is 0.01%. At 0.001% DAO growth put a CKB-to-iCKB order under par within four hours, so no bot would touch it. 0.01% is about two days of yield, long enough to notice a stalled bot.
  • An order thirty days on the book comes home. Dust, a remainder another matcher left, or an ask above the market can sit for a month. The next transaction the user signs melts it and returns the funds.
  • Nothing the user signs cancels a live conversion. The old model melted the standing order on every new one. That made the collect button destructive and it reset the signal that tells a seller nobody is filling.
  • CKB figures mean what the wallet holds. Capacity locked inside iCKB cells counts. Before, an address holding only iCKB read "0 CKB available" while its own Max button built a funded transaction from that same capacity.

For the integrator:

  • One package, one entry point, one dependency. Five published packages became one, and @ckb-ccc/core is the only thing it pulls in.
  • The SDK hands back state and a transaction, not a workflow. The caller completes it, signs it and sends it, and sets its own withdrawal timing policy.
  • Version 9000.0.0. The old stack left @ickb/sdk on npm at 1000.0.82, so a sane number could not publish. This one sorts above everything and takes the tag without a fight.
  • CCC stays at 1.20.0. Nothing in the releases up to 1.22.0 fixes a defect on a path this stack uses. The only correctness fix in the range is in UDT input selection, which it never calls.

For the operator:

  • One turn is one process. No supervisor, no launcher, no generated config. systemd restarts it, and a turn that hangs dies on the unit's own clock.
  • The journal is the alerting surface. Neither actor has a notification channel, because every condition worth waking someone for spans turns and only the journal sees across turns. The README shows the pipe.
  • The bot says why it did nothing. no_gain means the book offers nothing worth its fees at any size; unfunded_gain means it does but the balances cannot pay. A watcher can tell a dull market from an empty wallet.
  • The generator melts what the bot will not take. It judges its own orders with the bot's matcher, so the testnet book stays a book and not a graveyard.

For the repository:

  • One green gate per commit. pnpm check is 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.
  • pnpm 12, seven-day release age. Trusted publishing needs the 12 line. A freshly published package cannot be installed until it has had a week to be caught.

phroi added 30 commits August 10, 2026 02:17
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.
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.
…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.
@phroi phroi closed this Sep 20, 2026
@phroi
phroi deleted the wip branch September 20, 2026 15:39
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.
@phroi phroi reopened this Sep 21, 2026
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.
Replace the chronological rewrite journal with rationale in the owning package docs and local comments. Add the contributor agreement and its CLAUDE.md symlink. Keep current rules beside their consumers rather than requiring parallel README, register, and comment accounts.

Preserve the deployed-contract and CCC constraints, and correct stale descriptions of DAO limits, confirmation, collection, and actor behavior. Document mining-reward maturity as an accepted limitation and whole-transaction sizing as an open design issue. Executable behavior remains unchanged.

Consolidate the documentation work and follow-up limitation notes into one concern. CI=true pnpm check passed: 807 Vitest tests, four native tests, required coverage, lint, API, publish, and interface build checks.
The unit setup and update procedure told operators to run `pnpm node:install`,
but no such script exists, so pnpm fails with `Command "node:install" not
found`. The update procedure stops the services before that step, leaving them
down. Use the root README's install command instead.
The liquid-cell sweep checked a 64 KiB prepared-size budget by serializing the
whole transaction before every added input, so its cost grew quadratically: an
offline probe took about 13 s near 1,500 inputs with no RPC. The 64 KiB was a
round compaction target, not a validity bound, and a same-lock input has a fixed
size (44 bytes, 52 with an extra witness slot), so a count says the same thing
without serializing.

The iCKB and plain sweeps now share one limit of 1,000 total inputs, about
50 KB and the one size measured, far under the node's 512,000-byte limit.
Required funding and the fee may still pass it. Rejected: a hand-maintained
per-input byte counter (duplicates CCC's layout) and 2,000 inputs (script
cycles and wallet limits unmeasured). Whole-transaction sizing stays a separate,
documented open issue.

The tests now prove the sweep stops at the limit, that required iCKB funding
and the fee still pass it, and that a move reports a cut-short sweep.
The property asserted `plan.deposit !== undefined || pendingCkb > 0n`, with
pendingCkb drawn from [0, 3 deposits) and never passed to planRebalance. It was
nonzero in every checked sample, so the assertion held whatever the planner did:
disabling deposits in planRebalance left it green.

The planner never sees pending withdrawals, which only add CKB on their way
back, so the property is now checked with nothing pending: a funded account
below the refill line must plan a low_ickb deposit. With deposits disabled the
test fails.
Several tests passed or failed for reasons unrelated to their titles. They
pinned constants to their own literals, a component to its function name, a
fee threshold to its formula and a style to its CSS value. One exercised only
the test's own stub client, and one duplicated other coverage.

Harmless retuning broke the pins while the documented relations stayed
unchecked. The policy test now asserts the rule in sdk/node/docs/policy.md:
twice the default minimum match, converted at the genesis rate (the worst
case), stays below the refill line. The entrypoint test checks the mounted
component by identity. The rest are deleted: their behavior is covered
elsewhere, and changing the fee factor fails six maturity-preview tests.

Each rewritten test fails when its production line is broken. Reviewed
independently; no production code changes.
convert already rejects a ratio that is not populated, so both scales are
positive and the divisor is at least 1. A zero amount then yields 0 / b or
(b - 1) / b, both 0, in either direction and rounding mode. The early return
changed nothing, and its test, titled "covers zero conversion", could not fail
without it.

The zero test now asserts the formula in all four direction and rounding
combinations. The unrelated compare assertion keeps its own test, since it is
the one case that exercises cross-multiplication.
The fixture README cited contracts commit ae8a11fa, which has no
scripts/vector-gen, and gave absolute paths into one machine's checkout, so
the fixture could not be regenerated from the cited revision. Pin c951927e,
where the generator is tracked, link its README for the mechanism and the
regeneration steps instead of restating them, and record that regenerating
there reproduces the committed fixture byte for byte.
The authorization-boundary link pointed at the report's old file name at a
fixed contracts commit. The report was renamed to ICKB-Audit-Report.md and
keeps evolving on contracts master, so link the current section there.
The ring split the DAO cycle into nextPowerOfTwo(deposit count) segments.
A deposit created without iCKB Logic running carries no iCKB, so zero-value
deposits could multiply the segments for the cost of their occupied capacity
alone, and the bot's ring_coverage rule then deposited on nearly every tip
(2,700 of 2,880 tips in a reproduction, against 180).

The count now follows pool iCKB, one segment per two deposit caps. Two caps
rather than one: a covered ring holds about one cap per segment, which with
one cap per segment sits exactly at the next power of two, so the bot's own
fills after a doubling would cross it and double the ring again. With two
caps of room, fills cause at most one doubling. The bot's seeding check now
tests pool value too, so a pool holding only zero-value deposits still seeds.
Rationale and rejected alternatives are in sdk/node/docs/policy.md.
The segment count is one loop: double while the segments hold fewer than
two caps each. Two helpers for ceiling division and next power of two
said the same thing in more code. The policy document keeps the rule,
the two-caps reason and the rejected alternatives in one sentence each;
the runaway story lives in b202638.
A withdrawal transaction carries at most DAO_OUTPUT_LIMIT / 2 requests, a
request and its owner marker each. The user planner applied that cap after
fitWithdrawalDeposits; the bot did not, so its completion walk built and
discarded every oversized prefix first. A deposit made without iCKB Logic
running carries no iCKB and always fits the amount, so a flood of them
made that walk long on every turn.

fitWithdrawalDeposits now stops at the cap itself, for both callers. No
prefix beyond it could ever build, so outcomes do not change. Zero-value
deposits are otherwise taken as they come: each returns its occupied
capacity with interest and burns no iCKB, a positive payout for the user
and the bot alike, so neither filters them.
A deposit made without iCKB Logic running carries no iCKB and always fits
a withdrawal chain, so a flood of them with early claims can take every
request slot ahead of valued deposits. Withdrawing one returns its 82 CKB
occupied capacity, the same a valued deposit returns, so the withdrawer's
only cost is the order fee on the displaced iCKB, under an eighth of the
82 CKB per slot the flood costs its sender, paid to the bot. Sorting
valued deposits first was rejected: one shannon of free capacity makes a
deposit count as valued at the same cost, the threshold objection already
recorded for segment counting.
…sories

pnpm audit reported 23 advisories on the unchanged lockfile. Twenty-one
have patched versions inside the ranges their dependents declare and older
than the seven-day release age, so they get security pins in the existing
overrides block: axios 1.20.0 (twelve advisories via @ckb-ccc/spore, not
imported by the interface), shell-quote 1.11.0 (React Native dev tooling
under @ckb-ccc/connector), and the build tooling brace-expansion 5.0.12,
fast-uri 3.1.8, source-map-js 1.2.2 and smol-toml 1.9.0. The two with no
patch are ignored with their reason: braces under the same React Native
chain as the existing image-size ignores, sprintf-js under
@microsoft/api-extractor's argparse. Gate exit 0, 808 tests.
pnpm update across the workspace, CCC excluded: the README keeps the
tested CCC 1.20.0 set until a relevant upstream fix is evaluated on its
own, and the first run moved the catalog to core 1.23.0, so the update
was redone with the @ckb-ccc scope excluded and the catalog restored.
eslint-plugin-regexp's exact pin moved 3.1.0 to 3.3.1; the minor is
older than the release age. @types/node follows its tilde range to
22.20.4. A stale registry metadata cache made typescript-eslint 8.71.0
unresolvable until the @typescript-eslint cache entries were deleted.
Gate exit 0, 808 tests.
Every CCC release in the range is older than the seven-day release age.
CCC 1.23 sizes mol.union dynamically and rejects it as a struct field,
so OrderData failed to load: MasterCodec now uses mol.fixedUnion, whose
Relative and OutPoint variants are both 36 bytes, the same wire format.
The transport mock assertion in the waitTransaction test matches the
payload alone, since the client now passes optional request options as
a second argument. The README paragraph keeping 1.20.0 is replaced; the
connector-react move from 2.0 to 2.2 is not covered by the gate and
awaits live wallet testing. Gate exit 0, 808 tests.
Both vitest packages move together, as the coverage provider follows
the runner's major. The vitest@<4.1.0 security override no longer
matches anything and is removed. Gate exit 0, 808 tests, coverage 100%.

TypeScript 7.0.2 was tried and reverted: the package exports only its
version, without the compiler API that typescript-eslint, api-extractor
and scripts/tooling/dead-members.ts use, and both tools declare
typescript <6.1.0. The README records this beside the dependency policy.
The unopinionated preset gained prefer-ternary, prefer-minimal-ternary,
prefer-simplified-conditions and prefer-logical-operator-over-ternary,
which flagged 37 if/else returns and ternary placements. Ternary shape
is a per-site readability call, and the rewrite the minimal-ternary
rule wants in sdk/src/order/master.ts would lose the discriminated
union narrowing, so the four are turned off beside the existing
exceptions. Gate exit 0, 808 tests.
A 0.x minor, so outside the caret range and moved by hand. The
@vitejs/plugin-react peer range ^0.145.0 was already unmet at 0.147;
the interface builds and its tests pass on 0.152. Gate exit 0.
The dynamic-size union change landed in CCC 1.23.0, not 1.21, as the
installed changelog records under #573; the master codec comment, the
README and the CCC commit message said 1.21. api-extractor bundles
its own TypeScript and declares no peer range, so the TypeScript 7 note
names typescript-eslint alone. Unicorn 76 added one rule to the preset
and broadened three that were already in it. CCC 1.22 gave transports a
per-request abort signal, so the waitTransaction comments now say the
in-flight client operation receives none, which is the fact that keeps
late settlements handled. Found by the Fable 5.1 and Opus 5.5 audits of
the dependency commits; no behavior change, gate exit 0.
prefer-minimal-ternary, prefer-simplified-conditions and
prefer-logical-operator-over-ternary are back on. Nine test fixtures
move the ternary inside the array literal, the self-exclusion in
maturity.ts reads as a disjunction, masterFrom uses a guard clause so
the discriminated union keeps narrowing, and the vite host becomes a
guarded assignment, since the two ternary shapes for it each trip one
of the rules. prefer-ternary stays off: unicorn 76 extended it to a
guard clause before the final return, which turns a chain of guard
clauses into an if followed by a ternary. Gate exit 0, 808 tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant