Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`.
Expand Down
6 changes: 5 additions & 1 deletion apps/interface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions apps/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -47,6 +48,7 @@
"dependencies": {
"@ckb-ccc/ccc": "catalog:",
"@ickb/core": "workspace:*",
"@ickb/dao": "workspace:*",
"@ickb/order": "workspace:*",
"@ickb/sdk": "workspace:*",
"@ickb/utils": "workspace:*",
Expand Down
31 changes: 30 additions & 1 deletion apps/interface/src/Action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export default function Action({
txInfo.estimatedMaturity,
l1State.tipTimestamp,
);
const shownMaturity = txInfo.conversionNotice?.maturityEstimateUnavailable
? "Waiting for CKB liquidity"
: maturity;

return (
<span className="grid grid-cols-2 items-center justify-items-center gap-y-4">
Expand Down Expand Up @@ -106,10 +109,36 @@ export default function Action({
</button>
</Progress>
{failure !== "" ? <span className="col-span-2 text-center text-red-400">{failure}</span> : null}
{txInfo.conversionNotice ? <DustConversionNotice notice={txInfo.conversionNotice} /> : null}
<span className="leading-relaxed font-bold tracking-wider">Fee:</span>
<span>{toText(txInfo.fee)} CKB</span>
<span className="leading-relaxed font-bold tracking-wider">Maturity:</span>
<span>{maturity}</span>
<span>{shownMaturity}</span>
</span>
);
}

function DustConversionNotice({
notice,
}: {
notice: NonNullable<TxInfo["conversionNotice"]>;
}): JSX.Element {
if (notice.kind === "maturity-unavailable") {
return (
<span className="col-span-2 rounded border border-amber-400/60 px-3 py-2 text-center text-sm leading-relaxed text-amber-200">
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.
</span>
);
}

return (
<span className="col-span-2 rounded border border-amber-400/60 px-3 py-2 text-center text-sm leading-relaxed text-amber-200">
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.
</span>
);
}
Expand Down
32 changes: 32 additions & 0 deletions apps/interface/src/Connector.test.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
30 changes: 22 additions & 8 deletions apps/interface/src/Connector.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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();
Expand All @@ -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(),
];
))];
Comment thread
phroi marked this conversation as resolved.
Comment thread
phroi marked this conversation as resolved.

return {
...rootConfig,
Expand Down
34 changes: 34 additions & 0 deletions apps/interface/src/connectorQueryKey.ts
Original file line number Diff line number Diff line change
@@ -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<object, number>();

export function objectIdentityKey(value: object): number {
const existing = objectKeys.get(value);
if (existing !== undefined) {
return existing;
}

const key = nextObjectKey;
nextObjectKey += 1;
objectKeys.set(value, key);
return key;
}
13 changes: 3 additions & 10 deletions apps/interface/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ccc, JoyId } from "@ckb-ccc/ccc";
import { getConfig, IckbSdk } from "@ickb/sdk";
import Connector from "./Connector.tsx";
import type { RootConfig } from "./utils.ts";
import { parseWalletChain, type RootConfig } from "./utils.ts";
import appIcon from "/favicon.png?url";

const appName = "iCKB DApp";

function createRootConfig(chain: "mainnet" | "testnet"): RootConfig {
const config = getConfig(chain);
const { managers } = config;

return {
chain,
Expand All @@ -21,12 +20,6 @@ function createRootConfig(chain: "mainnet" | "testnet"): RootConfig {
? new ccc.ClientPublicMainnet()
: new ccc.ClientPublicTestnet(),
sdk: IckbSdk.fromConfig(config),
managers: {
ickbUdt: managers.ickbUdt,
logic: managers.logic,
ownedOwner: managers.ownedOwner,
order: managers.order,
},
};
}

Expand All @@ -36,8 +29,8 @@ const rootConfigs = {
};

export function startApp(walletChain: string): void {
const [walletName, chain] = walletChain.split("_");
const rootConfig = chain === "mainnet" ? rootConfigs.mainnet : rootConfigs.testnet;
const { walletName, chain } = parseWalletChain(walletChain);
const rootConfig = rootConfigs[chain];

const signerInfo = JoyId.getJoyIdSigners(
rootConfig.cccClient,
Expand Down
Loading
Loading