diff --git a/.dependency-cruiser.jsonc b/.dependency-cruiser.jsonc new file mode 100644 index 00000000..187625a9 --- /dev/null +++ b/.dependency-cruiser.jsonc @@ -0,0 +1,131 @@ +{ + "extends": "dependency-cruiser/configs/recommended-strict", + "forbidden": [ + { + "name": "no-orphans", + "from": { + "orphan": true, + "pathNot": [ + "(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$", + "\\.d\\.(c|m)?ts$", + "(^|/)tsconfig\\.json$", + "(^|/)(?:babel|webpack)\\.config\\.(?:js|cjs|mjs|ts|json)$", + "^scripts/run-node-tests[.](?:mts|ts)$", + "^scripts/tooling/build/rewrite-dts-imports[.](?:mts|ts)$", + "^scripts/tooling/dead-members[.]ts$", + "^scripts/tooling/eslint/plugins[.]ts$", + "^scripts/test/.+[.](?:mts|ts)$", + "^sdk/node/test/.+[.]ts$", + "^(?:sdk|sdk/node|testkit|interface)/vitest[.]config[.]mts$", + "^interface/vite[.]config[.]ts$", + "^interface/eslint[.]config[.]mts$", + ], + }, + }, + { + "name": "not-prod-to-test-source", + "comment": "Production source must not import test-only files. Keep test fixtures behind test/ boundaries.", + "severity": "error", + "from": { + "path": "^(?:(?:sdk|sdk/node|testkit|interface)/src/|scripts/)", + "pathNot": [ + "(?:^|/)test/", + "[.]test[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.][^/]+[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:_test_|_test_constants[.]|_test_fixtures[.]|_test_support[.])[^/]*$", + ], + }, + "to": { + "path": [ + "(?:^|/)test/", + "[.]test[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.][^/]+[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:_test_|_test_constants[.]|_test_fixtures[.]|_test_support[.])[^/]*$", + ], + }, + }, + { + "name": "not-prod-to-testkit", + "comment": "Production source must not import @ickb/testkit. Keep test harness dependencies under test/.", + "severity": "error", + "from": { + "path": "^(?:sdk|sdk/node|testkit|interface)/src/", + "pathNot": [ + "^testkit/src/", + "(?:^|/)test/", + "[.]test[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:Suite[0-9]*[.]test|Suite|Test(?:Assertions|CommandFixtures|Constants|Fixtures|ProcessFixtures|RunFixtures|Support)|TestFixtures|TestSupport)[.][^/]+[.](?:mts|ts|tsx)$", + "(?:^|/)[^/]*(?:_test_|_test_constants[.]|_test_fixtures[.]|_test_support[.])[^/]*$", + ], + }, + "to": { + "path": "^testkit/src/", + }, + }, + { + "name": "oracle-independent", + "comment": "The contract oracle imports nothing from the implementation it judges (amendment 44).", + "severity": "error", + "from": { "path": "^testkit/src/contract_oracle[.]ts$" }, + "to": {}, + }, + { + "name": "node-actors-apart", + "comment": "The bot, the sampler, and the stimulus generator share only src/shared; they never import each other.", + "severity": "error", + "from": { "path": "^sdk/node/src/(bot|sampler|stimulus)/" }, + "to": { + "path": "^sdk/node/src/(?:bot|sampler|stimulus)/", + "pathNot": "^sdk/node/src/$1/", + }, + }, + { + "name": "node-shared-neutral", + "comment": "src/shared serves every actor and depends on none of them.", + "severity": "error", + "from": { "path": "^sdk/node/src/shared/" }, + "to": { "path": "^sdk/node/src/(?:bot|sampler|stimulus)" }, + }, + { + "name": "sdk-browser-safe", + "comment": "The SDK runs in the browser: no Node built-ins (decisions §1).", + "severity": "error", + "from": { "path": "^sdk/src/" }, + "to": { "dependencyTypes": ["core"] }, + }, + { + "name": "sdk-not-node", + "comment": "The published SDK never imports the Node actors that live beside it (amendment 52).", + "severity": "error", + "from": { "path": "^sdk/src/" }, + "to": { "path": "^sdk/node/" }, + }, + { + "name": "no-library-to-actor", + "comment": "The SDK and the test kit must not depend on the actors.", + "severity": "error", + "from": { "path": "^(?:sdk/src|testkit)/" }, + "to": { "path": "^(?:interface|sdk/node)/" }, + }, + { + "name": "no-actor-to-actor", + "comment": "The interface and the Node actors must not depend on each other.", + "severity": "error", + "from": { "path": "^(interface|sdk/node)/" }, + "to": { "path": "^(?:interface|sdk/node)/", "pathNot": "^$1/" }, + }, + ], + "options": { + "exclude": { + "path": "(?:^|/)(?:coverage|dist)/", + }, + "includeOnly": "^(sdk|testkit|interface|scripts)/", + "tsConfig": { + "fileName": "tsconfig.json", + }, + "tsPreCompilationDeps": "specify", + }, +} diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 7b518c0d..26e945fc 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -5,19 +5,91 @@ on: push: branches: [master] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest + outputs: + interface: ${{ steps.releases.outputs.interface }} + sdk: ${{ steps.releases.outputs.sdk }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: - node-version: 24 - - uses: pnpm/action-setup@v4 - - - name: Install jq for fork bootstrap - run: sudo apt-get update -qq && sudo apt-get install -y -qq --no-install-recommends jq + persist-credentials: false + # The release rule below reads the previous commit. + fetch-depth: 2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + # The declared Node floor; the example units run the current release. + node-version: 22.19.0 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 + - name: Install pinned dependencies + run: pnpm install --frozen-lockfile - name: Run pnpm check run: pnpm check + + # A release is a push to the default branch whose commit changes a package's version + # field (decisions amendment 52(ad)). Every such push is one squash commit, so the + # previous commit is the comparison; a path absent there counts as changed. + - id: releases + if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + run: | + released() { + now=$(node -p "require('./$1/package.json').version") + before=$(git show "HEAD^:$1/package.json" 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).version" 2>/dev/null || echo none) + if [ "$now" != "$before" ]; then echo true; else echo false; fi + } + echo "interface=$(released interface)" >> "$GITHUB_OUTPUT" + echo "sdk=$(released sdk)" >> "$GITHUB_OUTPUT" + - if: steps.releases.outputs.interface == 'true' + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 + with: + path: interface/dist + + # The interface bundle the gate just built goes live at ickb.org (decisions amendment + # 52(ab)); a failed run leaves the previous deployment live. + deploy: + needs: build + if: needs.build.outputs.interface == 'true' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - id: deployment + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 + + # npm trusted publishing: the id token stands in for a registry token, and npm records + # the provenance of this run with the package (decisions amendment 52(ad)). + publish: + needs: build + if: needs.build.outputs.sdk == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 22.19.0 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 + + - name: Install pinned dependencies + run: pnpm install --frozen-lockfile + - name: Build the SDK + run: pnpm build + # The checkout is a detached commit, so pnpm's branch and remote checks do not apply. + - name: Publish @ickb/sdk + run: pnpm --filter @ickb/sdk publish --provenance --access public --no-git-checks diff --git a/.gitignore b/.gitignore index 74e2f24e..27814d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,15 +2,9 @@ **/node_modules/ # Artifacts +**/coverage/ **/dist/ -log/ -apps/*/log_*.json - -# Local runtime config files -config/ - -# Local logs -logs/ +**/*.tsbuildinfo # Local scratch files -.scratch/ +/private/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..433b6fdf --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +**/node_modules/ +**/dist/ +**/*.tsbuildinfo +coverage/ +.git/ +pnpm-lock.yaml diff --git a/README.md b/README.md index 9bee59af..f07e1d56 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,15 @@ iCKB Stack is the monorepo for the current TypeScript iCKB libraries and apps built on top of [CCC](https://github.com/ckb-devrel/ccc). -## Transaction Completion Boundary - -`@ickb/sdk` builders still return partial `ccc.Transaction` values. Callers explicitly choose when to finalize, and the shared completion path now also lives in `@ickb/sdk`. - -Callers own the final completion pipeline: +Every design decision behind this tree, with its rejected alternatives, is in the [decision record](docs/stack-rewrite/decisions.md). The rest of this README describes the repository. -1. Build the partial transaction through `IckbSdk` and the package managers. -2. Before send, call `sdk.completeTransaction(...)` or `completeIckbTransaction(...)` from `@ickb/sdk`. -3. Only then send the transaction. +## Transaction Completion Boundary -Withdrawal requests built from public pool ready deposits may include `requiredLiveDeposits`. `@ickb/sdk` adds those cells as live `cell_dep` checks so a transaction fails if a protected pool anchor disappears before inclusion. +`IckbSdk.buildConversionTransaction(...)` returns a completed transaction: the SDK completes each candidate plan against the signer's committed cells and returns the first fundable one, so a caller only signs and sends (`signAndSendTransaction`) and waits (`waitTransaction`). The managers behind it are internal to the package. -## Scan Page Size Boundary +## Cell Scans -Stack cell scans that feed account state, pool state, order books, or maturity estimates use a per-request page size. SDK state APIs expose it as `cellPageSize`; lower-level scan wrappers expose it as `pageSize` and pass it to CCC as `limit`. +Every Stack cell scan, for account state, the pool, the order book, or the maturity estimate, is one uncached paging loop at a fixed page of 400 cells that stops on the first short page; there is no page-size knob (decisions amendment 52(c)). ## User Lock Assumption @@ -26,67 +20,42 @@ Current stack flows assume user-owned cells are protected by locks whose signatu Apps: -- `apps/bot`: Node order-fulfillment and rebalance bot for matching profitable orders, collecting owned orders, completing receipts and withdrawals, and rebalancing pool exposure. -- `apps/interface`: Browser interface for CCC wallet connection, conversion previews, transaction completion, signing, sending, and confirmation. -- `apps/sampler`: Mainnet sampling utility that writes historical iCKB exchange-rate CSV output. -- `apps/supervisor`: Deterministic live testnet supervisor for bounded bot/tester stress cycles, ignored artifacts, and incident bundles. -- `apps/tester`: Node simulator that creates random conversion orders to exercise the order and conversion flows. +- `sdk/node`: the bot, the testnet stimulus generator, and the mainnet rate sampler, three entrypoints in one Node workspace sharing chain preflight, config, and logging. +- `interface`: Browser interface for CCC wallet connection, conversion previews, transaction completion, signing, sending, and confirmation. -The Node app packages (`@ickb/bot`, `@ickb/sampler`, and `@ickb/tester`) publish their built entrypoints for distribution, but the supported reusable API surface lives in the packages below. `@ickb/interface` is a deployable browser app package and does not expose a library entrypoint. +Apps are private workspace runtimes and run from source under Node 22.19+ or Vite. The supported reusable API surface lives in the packages below. Stack package `build` scripts emit `dist/` for publishing reusable packages only; local development, tests, live supervisor runs, and bot deployments use TypeScript source directly. Packages: -- `packages/core`: iCKB protocol primitives, cells, UDT conversion helpers, and low-level transaction builders. -- `packages/dao`: Nervos DAO cell classification, readiness, deposit, request, and withdrawal helpers. -- `packages/node-utils`: Private Node app utilities for env parsing, RPC client setup, signer locks, sleeps, and JSON logs. -- `packages/order`: UDT limit-order entities, grouping, matching, minting, melting, and deployed-script confusion mitigation. -- `packages/sdk`: Stack-level SDK that composes core, DAO, and order packages into account state, conversion planning, completion, sending, and confirmation helpers. -- `packages/testkit`: Private test helpers and fixtures for workspace tests. -- `packages/utils`: Shared low-level utilities such as complete-scan enforcement, binary search, collection helpers, and bounded subset selection. +- `sdk`: the one published package. One file per on-chain script at the top of `src` (`udt.ts` the iCKB token, `logic.ts` deposits and receipts, `owned_owner.ts` withdrawal requests and withdrawals, `dao.ts` the shared Nervos DAO rules); `src/order` the UDT limit-order entities, matching, minting, and melting; `src/conversion` the state read, projection, estimates, conversion plans, the withdrawal ring, and the completion walk; `src/send` signing, sending, and confirmation; and `src/utils` the one uncached cell paging loop and shared helpers. +- `testkit`: Private test helpers and fixtures for workspace tests. ## Dependencies CCC packages are normal package dependencies resolved through `pnpm-workspace.yaml` catalog entries and `pnpm-lock.yaml`. From a plain checkout, run `pnpm install`; no local CCC fork, build step, or workspace alias is required. -`pnpm check` is the validation gate. It always runs with `CI=true`. - -## Live Testnet Supervisor - -Provide ignored bounded configs, then run the supervisor from the repo root: - -```bash -pnpm live:supervisor -``` +A release is a merge to the default branch that changes a package's `version` field: `sdk` then publishes to npm through trusted publishing with provenance, and `interface` deploys to GitHub Pages; both from the check workflow, after the gate passed on that commit. Other merges release nothing. -By default the supervisor uses ignored `config/bot-testnet.json` and `config/tester-testnet.json`, writes standalone artifacts under ignored `logs/live-supervisor//` paths, and runs deterministic bounded bot/tester commands only. +`pnpm check` is the validation gate: the audit, the full `pnpm lint` (typecheck, format, duplication, knip, architecture, forgotten exports, publish check, coverage, ESLint, Node script tests), and the interface build, all with `CI=true`. It runs against the installed dependencies; CI installs them from the pinned lockfile in a fresh checkout first. -Rebuild disposable live configs from `ICKB_TESTNET_BOT_PRIVATE_KEY` and `ICKB_TESTNET_TESTER_PRIVATE_KEY` with `pnpm live:config-from-env -- --force` when they are missing or stale; `ICKB_TESTNET_RPC_URL` is optional. The supervisor does not patch, verify, rebuild, relaunch, or invoke an LLM; external loops and operators consume `summary.json` between runs. +## Live Testnet Validation -`pnpm live:preflight -- --config config/bot-testnet.json --role bot` prints public balance evidence for funding checks. Use `key.recommendedAddress` as the funding address, then rerun preflight and check `balances.CKB.available`, `balances.CKB.reserve`, `balances.CKB.spendable`, `balances.CKB.projectedAvailable`, `balances.CKB.total`, and `capital.minimumCkbCapital`; `available` and `spendable` are actual plain-cell values, while `projectedAvailable` and `total` are projected accounting values. For machine-readable JSON without package-manager output, run `node scripts/ickb-live-preflight.mjs --config config/bot-testnet.json --role bot` directly. +Validation is operator-driven. Each actor runs one turn as a process and exits with its outcome; the operator, a person or a model, reads the JSON on stdout and decides the next action. There is no launcher, supervisor, summary, or automated cadence. -For repeated bounded invocations, keep loop-owned options before `--` and supervisor options after it. The loop owns child run directories through `--out-root`, so do not pass supervisor `--out-dir` after `--`: +The bot reads `BOT_CHAIN`, the optional `BOT_RPC_URL`, and the key file named by `BOT_PRIVATE_KEY_FILE`; the stimulus generator reads the same under `STIMULUS_` and refuses any chain but testnet. A key file holds one lowercase `0x` key; keep it outside the checkout, in `~/.config/ickb-bot/` as the units do. Without an RPC URL the actors use CCC's public endpoints for the chain, WebSocket first with HTTPS fallbacks; a configured URL is the only endpoint. Private keys are for signing only and never reach events, errors, or logs. ```bash -pnpm live:supervisor:loop --max-runs 1 -- --scenario standard-cycle --max-cycles 1 +export BOT_CHAIN=testnet BOT_PRIVATE_KEY_FILE=~/.config/ickb-bot/testnet.key +export STIMULUS_CHAIN=testnet STIMULUS_PRIVATE_KEY_FILE=~/.config/ickb-bot/stimulus-testnet.key +node sdk/node/src/bot.ts +node sdk/node/src/stimulus.ts ``` -By default the loop prebuilds bot, tester, and supervisor runtime before the first run. Use loop-owned `--skip-build` only when another wrapper has already built those artifacts. Use loop-owned `--child-timeout-seconds` to bound the outer supervisor child process when running long watches; keep it long enough for the whole supervisor invocation, including actor preflights and actor commands, so the supervisor remains alive to enforce its own `--command-timeout-seconds` process-group cleanup. - -For continuous live matching, use the dynamic external loop. It reads only tester preflight balance summaries, chooses `all-ckb-limit-order` when `CKB.available >= 3001`, otherwise chooses `ickb-to-ckb-limit-order` with `--tester-fee 1 --tester-fee-base 1000` when `CKB.available >= 2100` and `ICKB.available >= 100`, otherwise leaves the tester scenario as `auto`, then runs bounded `scripts/ickb-supervisor-loop.mjs` chunks: - -```bash -pnpm live:supervisor:dynamic-loop -``` - -Dynamic validation sessions default to ignored `log/validation/dynamic-