From ad3633cc4ebea43547d51aa5bcce2c409690b28d Mon Sep 17 00:00:00 2001
From: phroi <90913182+phroi@users.noreply.github.com>
Date: Tue, 12 May 2026 13:07:35 +0000
Subject: [PATCH 1/3] fix(apps): move runtime flows onto SDK boundaries
---
README.md | 21 +
apps/interface/README.md | 6 +-
apps/interface/package.json | 6 +-
apps/interface/src/Action.tsx | 31 +-
apps/interface/src/Connector.test.ts | 32 ++
apps/interface/src/Connector.tsx | 30 +-
apps/interface/src/connectorQueryKey.ts | 34 ++
apps/interface/src/main.tsx | 13 +-
apps/interface/src/queries.test.ts | 152 ++++++--
apps/interface/src/queries.ts | 175 +++++++--
apps/interface/src/transaction.test.ts | 424 +++++++++------------
apps/interface/src/transaction.ts | 288 +++-----------
apps/interface/src/utils.test.ts | 27 ++
apps/interface/src/utils.ts | 37 +-
apps/interface/vite.config.ts | 6 +-
apps/interface/vitest.config.mts | 20 +
apps/sampler/.gitignore | 1 +
apps/sampler/README.md | 24 +-
apps/sampler/package.json | 3 +-
apps/sampler/src/index.test.ts | 105 +++++
apps/sampler/src/index.ts | 66 ++--
apps/sampler/tsconfig.build.json | 9 +
apps/tester/.gitignore | 3 +-
apps/tester/README.md | 6 +-
apps/tester/package.json | 6 +-
apps/tester/src/freshMatchableOrderSkip.ts | 54 +++
apps/tester/src/index.test.ts | 87 ++++-
apps/tester/src/index.ts | 197 +++-------
apps/tester/src/runtime.test.ts | 92 +++--
apps/tester/src/runtime.ts | 7 +-
apps/tester/tsconfig.build.json | 9 +
apps/tester/vitest.config.mts | 8 +
package.json | 1 +
packages/node-utils/.gitignore | 3 +
packages/node-utils/README.md | 11 +
packages/node-utils/package.json | 48 +++
packages/node-utils/src/index.test.ts | 147 +++++++
packages/node-utils/src/index.ts | 124 ++++++
packages/node-utils/tsconfig.json | 12 +
packages/node-utils/vitest.config.mts | 3 +
pnpm-lock.yaml | 34 ++
41 files changed, 1539 insertions(+), 823 deletions(-)
create mode 100644 apps/interface/src/Connector.test.ts
create mode 100644 apps/interface/src/connectorQueryKey.ts
create mode 100644 apps/interface/src/utils.test.ts
create mode 100644 apps/sampler/.gitignore
create mode 100644 apps/sampler/src/index.test.ts
create mode 100644 apps/sampler/tsconfig.build.json
create mode 100644 apps/tester/src/freshMatchableOrderSkip.ts
create mode 100644 apps/tester/tsconfig.build.json
create mode 100644 packages/node-utils/.gitignore
create mode 100644 packages/node-utils/README.md
create mode 100644 packages/node-utils/package.json
create mode 100644 packages/node-utils/src/index.test.ts
create mode 100644 packages/node-utils/src/index.ts
create mode 100644 packages/node-utils/tsconfig.json
create mode 100644 packages/node-utils/vitest.config.mts
diff --git a/README.md b/README.md
index f6b49c1..21236b5 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,27 @@ Stack cell scans that feed account state, pool state, order books, or maturity e
Current stack flows assume user-owned cells are protected by locks whose signatures bind the whole transaction, such as standard `sighash` wallet flows. Passing a raw `ccc.Script` is only safe when that lock gives the same output and recipient binding. Delegated-signature or OTX-style locks are integration-specific and must account for the weak-lock boundary documented in the iCKB whitepaper and contracts audit.
+## Workspace Map
+
+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/tester`: Node simulator that creates random conversion orders to exercise the order and conversion flows.
+
+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.
+
+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.
+
## Local CCC Workflow
The shared CCC baseline lives in `forks/ccc/pin/` and materializes into `forks/ccc/repo/`.
diff --git a/apps/interface/README.md b/apps/interface/README.md
index 05c8c89..03d085b 100644
--- a/apps/interface/README.md
+++ b/apps/interface/README.md
@@ -40,7 +40,11 @@ pnpm --filter ./apps/interface build
Like `dev`, the build script refreshes those workspace package `dist/` outputs first so a clean checkout does not rely on stale generated files.
-The interface now uses CCC-native wallet connection and transaction completion. Protocol-specific transaction construction comes from `@ickb/sdk`, then the app completes iCKB UDT balance, CKB capacity, and fees before sending.
+The interface now uses CCC-native wallet connection and transaction completion. Protocol-specific conversion planning and partial transaction construction come from `@ickb/sdk`; the app maps domain results to UI copy, calls `sdk.completeTransaction(...)`, and then sends.
+
+## Small iCKB Balances
+
+For iCKB-to-CKB requests below the normal order preview threshold, the interface automatically builds a discounted dust order instead of adding another confirmation step. The preview shows the tiny iCKB input, approximate CKB output, and matcher incentive inline before the normal wallet signature. This path is useful when the user mainly wants to recover CKB capacity locked in an iCKB xUDT cell; the user accepts or rejects the exact terms by signing or cancelling the transaction.
## Licensing
diff --git a/apps/interface/package.json b/apps/interface/package.json
index 775db34..30063fe 100644
--- a/apps/interface/package.json
+++ b/apps/interface/package.json
@@ -22,8 +22,8 @@
"type": "module",
"private": false,
"scripts": {
- "dev": "pnpm --filter @ickb/core --filter @ickb/order --filter @ickb/sdk --filter @ickb/utils build && vite",
- "build": "pnpm --filter @ickb/core --filter @ickb/order --filter @ickb/sdk --filter @ickb/utils build && tsc && vite build",
+ "dev": "pnpm --filter @ickb/core --filter @ickb/dao --filter @ickb/order --filter @ickb/sdk --filter @ickb/utils build && vite",
+ "build": "pnpm --filter @ickb/core --filter @ickb/dao --filter @ickb/order --filter @ickb/sdk --filter @ickb/utils build && tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ci": "vitest run",
@@ -32,6 +32,7 @@
"clean:deep": "rm -fr dist node_modules"
},
"devDependencies": {
+ "@ickb/testkit": "workspace:*",
"@babel/preset-react": "^7.27.1",
"@tailwindcss/vite": "^4.1.14",
"@types/node": "^22.18.11",
@@ -47,6 +48,7 @@
"dependencies": {
"@ckb-ccc/ccc": "catalog:",
"@ickb/core": "workspace:*",
+ "@ickb/dao": "workspace:*",
"@ickb/order": "workspace:*",
"@ickb/sdk": "workspace:*",
"@ickb/utils": "workspace:*",
diff --git a/apps/interface/src/Action.tsx b/apps/interface/src/Action.tsx
index 3e4c230..514cd56 100644
--- a/apps/interface/src/Action.tsx
+++ b/apps/interface/src/Action.tsx
@@ -71,6 +71,9 @@ export default function Action({
txInfo.estimatedMaturity,
l1State.tipTimestamp,
);
+ const shownMaturity = txInfo.conversionNotice?.maturityEstimateUnavailable
+ ? "Waiting for CKB liquidity"
+ : maturity;
return (
@@ -106,10 +109,36 @@ export default function Action({
{failure !== "" ? {failure} : null}
+ {txInfo.conversionNotice ? : null}
Fee:
{toText(txInfo.fee)} CKB
Maturity:
- {maturity}
+ {shownMaturity}
+
+ );
+}
+
+function DustConversionNotice({
+ notice,
+}: {
+ notice: NonNullable;
+}): JSX.Element {
+ if (notice.kind === "maturity-unavailable") {
+ return (
+
+ Conversion terms: this order converts {toText(notice.inputIckb)} iCKB to
+ about {toText(notice.outputCkb)} CKB with {toText(notice.incentiveCkb)}
+ CKB matcher incentive. The current pool state does not provide a maturity estimate yet.
+
+ );
+ }
+
+ return (
+
+ Small-balance conversion: this discounted order converts{" "}
+ {toText(notice.inputIckb)} iCKB to about {toText(notice.outputCkb)} CKB
+ with {toText(notice.incentiveCkb)} CKB matcher incentive, helping recover
+ locked iCKB cell capacity after the order is fulfilled or collected.
);
}
diff --git a/apps/interface/src/Connector.test.ts b/apps/interface/src/Connector.test.ts
new file mode 100644
index 0000000..af0bbce
--- /dev/null
+++ b/apps/interface/src/Connector.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from "vitest";
+import { connectorWalletConfigQueryKey } from "./connectorQueryKey.ts";
+import type { RootConfig } from "./utils.ts";
+
+describe("connectorWalletConfigQueryKey", () => {
+ it("keys wallet config by root config identity, wallet, and signer object", () => {
+ const rootConfig = {
+ chain: "testnet",
+ cccClient: {},
+ queryClient: {},
+ sdk: {},
+ } as RootConfig;
+ const signerA = 1;
+ const signerB = 2;
+ const key = connectorWalletConfigQueryKey(rootConfig, "JoyID", signerA, 0);
+
+ expect(key[0]).toBe("testnet");
+ expect(key.slice(4)).toEqual(["JoyID", signerA, 0, "walletConfig"]);
+ expect(connectorWalletConfigQueryKey(rootConfig, "JoyID", signerB, 0)).not.toEqual(
+ key,
+ );
+ expect(connectorWalletConfigQueryKey(rootConfig, "JoyID", signerA, 1)).not.toEqual(
+ key,
+ );
+ expect(connectorWalletConfigQueryKey({
+ ...rootConfig,
+ sdk: {} as RootConfig["sdk"],
+ }, "JoyID", signerA, 0)).not.toEqual(
+ key,
+ );
+ });
+});
diff --git a/apps/interface/src/Connector.tsx b/apps/interface/src/Connector.tsx
index 91054ee..59d1d35 100644
--- a/apps/interface/src/Connector.tsx
+++ b/apps/interface/src/Connector.tsx
@@ -1,9 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { ccc } from "@ckb-ccc/ccc";
+import { unique } from "@ickb/utils";
+import { useEffect, useState } from "react";
import type { JSX } from "react/jsx-runtime";
import App from "./App.tsx";
import { EmptyDashboard } from "./Dashboard.tsx";
import Progress from "./Progress.tsx";
+import {
+ connectorWalletConfigQueryKey,
+ objectIdentityKey,
+} from "./connectorQueryKey.ts";
import { errorMessageOf, type RootConfig } from "./utils.ts";
export default function Connector({
@@ -15,12 +21,25 @@ export default function Connector({
signer: ccc.Signer;
walletName: string;
}): JSX.Element {
+ const signerKey = objectIdentityKey(signer);
+ const [signerVersion, setSignerVersion] = useState(0);
+ useEffect(
+ () => signer.onReplaced(() => {
+ setSignerVersion((version) => version + 1);
+ }),
+ [signer],
+ );
const {
isPending,
error,
data: walletConfig,
} = useQuery({
- queryKey: [rootConfig.chain, "walletConfig"],
+ queryKey: connectorWalletConfigQueryKey(
+ rootConfig,
+ walletName,
+ signerKey,
+ signerVersion,
+ ),
queryFn: async () => {
if (!(await signer.isConnected())) {
await signer.connect();
@@ -31,14 +50,9 @@ export default function Connector({
signer.getAddressObjs(),
]);
- let accountLocks = [recommendedAddressObj, ...addressObjs].map(({ script }) =>
+ const accountLocks = [...unique([recommendedAddressObj, ...addressObjs].map(({ script }) =>
ccc.Script.from(script),
- );
-
- // Keep unique account locks, preferred one is the first one.
- accountLocks = [
- ...new Map(accountLocks.map((script) => [script.toHex(), script])).values(),
- ];
+ ))];
return {
...rootConfig,
diff --git a/apps/interface/src/connectorQueryKey.ts b/apps/interface/src/connectorQueryKey.ts
new file mode 100644
index 0000000..b97cad0
--- /dev/null
+++ b/apps/interface/src/connectorQueryKey.ts
@@ -0,0 +1,34 @@
+import type { RootConfig } from "./utils.ts";
+
+export function connectorWalletConfigQueryKey(
+ rootConfig: RootConfig,
+ walletName: string,
+ signerKey: number,
+ signerVersion: number,
+): readonly [RootConfig["chain"], number, number, number, string, number, number, "walletConfig"] {
+ return [
+ rootConfig.chain,
+ objectIdentityKey(rootConfig.cccClient),
+ objectIdentityKey(rootConfig.queryClient),
+ objectIdentityKey(rootConfig.sdk),
+ walletName,
+ signerKey,
+ signerVersion,
+ "walletConfig",
+ ] as const;
+}
+
+let nextObjectKey = 1;
+const objectKeys = new WeakMap