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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ iCKB Stack is the monorepo for the current TypeScript iCKB libraries and apps bu

## Transaction Completion Boundary

`@ickb/sdk` stops at protocol-specific transaction construction. It returns partial `ccc.Transaction` values and does not finalize iCKB UDT balance, CKB capacity, or fees on behalf of the caller.
`@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:

1. Use `getConfig(...).managers.ickbUdt` to finish iCKB UDT completion.
2. Then run CCC-native CKB capacity and fee completion.
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.

## User Lock Assumption

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.

## Local CCC Workflow

The shared CCC baseline lives in `forks/ccc/pin/` and materializes into `forks/ccc/repo/`.
Expand Down
10 changes: 3 additions & 7 deletions apps/bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,15 @@ async function main(): Promise<void> {

const chain = parseChain(CHAIN);
const client = createClient(chain, RPC_URL);
const { managers, bots } = getConfig(chain);
const config = getConfig(chain);
const { managers } = config;
const signer = new ccc.SignerCkbPrivateKey(client, BOT_PRIVATE_KEY);
const primaryLock = (await signer.getRecommendedAddressObj()).script;
const runtime: Runtime = {
chain,
client,
signer,
sdk: new IckbSdk(
managers.ownedOwner,
managers.logic,
managers.order,
bots,
),
sdk: IckbSdk.fromConfig(config),
managers,
primaryLock,
};
Expand Down
10 changes: 3 additions & 7 deletions apps/interface/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import appIcon from "/favicon.png?url";
const appName = "iCKB DApp";

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

return {
chain,
Expand All @@ -19,12 +20,7 @@ function createRootConfig(chain: "mainnet" | "testnet"): RootConfig {
chain === "mainnet"
? new ccc.ClientPublicMainnet()
: new ccc.ClientPublicTestnet(),
sdk: new IckbSdk(
managers.ownedOwner,
managers.logic,
managers.order,
bots,
),
sdk: IckbSdk.fromConfig(config),
managers: {
ickbUdt: managers.ickbUdt,
logic: managers.logic,
Expand Down
10 changes: 3 additions & 7 deletions apps/tester/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,15 @@ async function main(): Promise<void> {

const chain = parseChain(CHAIN);
const client = createClient(chain, RPC_URL);
const { managers, bots } = getConfig(chain);
const config = getConfig(chain);
const { managers } = config;
const signer = new ccc.SignerCkbPrivateKey(client, TESTER_PRIVATE_KEY);
const primaryLock = (await signer.getRecommendedAddressObj()).script;
const runtime: Runtime = {
chain,
client,
signer,
sdk: new IckbSdk(
managers.ownedOwner,
managers.logic,
managers.order,
bots,
),
sdk: IckbSdk.fromConfig(config),
managers,
primaryLock,
accountLocks: dedupeScripts(
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"build": "pnpm -r --filter !./apps/** --filter '!./forks/**' build",
"build:all": "pnpm -r --filter '!./forks/**' build",
"check": "CI=true pnpm check:base",
"check:base": "pnpm clean:deep && pnpm check:ccc-overrides && pnpm forks:bootstrap && pnpm install && pnpm forks:ccc && pnpm forks:ccc:smoke && pnpm lint && pnpm build:all && pnpm test:ci",
"check:base": "pnpm clean:deep && pnpm check:ccc-overrides && pnpm forks:bootstrap && pnpm install && pnpm forks:ccc && pnpm forks:ccc:smoke && pnpm madge && pnpm lint && pnpm build:all && pnpm test:ci",
"check:ccc-overrides": "node scripts/check-ccc-overrides.mjs",
"check:fresh": "rm -f pnpm-lock.yaml && pnpm check",
"test": "vitest",
"test:ci": "vitest run && node --test scripts/*.test.mjs",
"madge": "madge --circular --extensions ts,tsx --ts-config ./tsconfig.json --exclude '^forks/' packages apps",
"test": "NODE_OPTIONS='--disable-warning=DEP0040' vitest",
"test:ci": "NODE_OPTIONS='--disable-warning=DEP0040' vitest run && node --test scripts/*.test.mjs && pnpm test:ccc",
"test:ccc": "NODE_OPTIONS='--disable-warning=DEP0040' vitest run --root forks/ccc/repo --project @ckb-ccc/core && NODE_OPTIONS='--disable-warning=DEP0040' vitest run --root forks/ccc/repo --project @ckb-ccc/udt -t 'infoFrom|isUdt'",
"lint": "pnpm -r --filter '!./forks/**' lint",
"clean": "rm -fr dist packages/*/dist apps/*/dist",
"clean:deep": "pnpm clean && rm -fr node_modules packages/*/node_modules apps/*/node_modules forks/ccc/repo/packages/*/tsconfig.tsbuildinfo",
Expand All @@ -30,6 +32,7 @@
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.24",
"madge": "^8.0.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"vitest": "^3.2.4"
Expand Down
10 changes: 10 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ graph TD;
click D "https://github.com/ickb/stack/tree/master/packages/core" "Go to @ickb/core"
```

## Partial Transactions

`@ickb/core` transaction builders stop at protocol-specific construction.

If a caller will send the returned transaction, it still must:

1. Finish iCKB UDT completion.
2. Finish CCC-native CKB capacity and fee completion.
3. Check `ccc.isDaoOutputLimitExceeded(...)` before send.

## Epoch Semantic Versioning

This repository follows [Epoch Semantic Versioning](https://antfu.me/posts/epoch-semver). In short ESV aims to provide a more nuanced and effective way to communicate software changes, allowing for better user understanding and smoother upgrades.
Expand Down
7 changes: 4 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
}
},
"scripts": {
"test": "vitest",
"test:ci": "vitest run",
"test": "NODE_OPTIONS='--disable-warning=DEP0040' vitest",
"test:ci": "NODE_OPTIONS='--disable-warning=DEP0040' vitest run",
"build": "tsc",
"lint": "eslint ./src",
"clean": "rm -fr dist",
Expand All @@ -55,6 +55,7 @@
"@ckb-ccc/core": "catalog:",
"@ckb-ccc/udt": "catalog:",
"@ickb/dao": "workspace:*",
"@ickb/utils": "workspace:*"
"@ickb/utils": "workspace:*",
"tslib": "^2.8.1"
}
}
135 changes: 134 additions & 1 deletion packages/core/src/cells.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ccc } from "@ckb-ccc/core";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { DaoManager } from "@ickb/dao";
import { receiptCellFrom } from "./cells.js";
import { ReceiptData } from "./entities.js";
Expand Down Expand Up @@ -151,4 +151,137 @@ describe("receipt prefix decoding", () => {
expect(info.capacity).toBe(ccc.fixedPointFrom(100082));
expect(info.count).toBe(1);
});

it("fetches receipt and deposit headers concurrently", async () => {
const logic = script("33");
const dao = script("44");
const header = ccc.ClientBlockHeader.from(headerLike(10000000000000000n));
const receipt = receiptCell(
receiptOutputData(2, ccc.fixedPointFrom(100000)),
logic,
);
const deposit = ccc.Cell.from({
outPoint: { txHash: byte32FromByte("88"), index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(100082),
lock: logic,
type: dao,
},
outputData: "0x0000000000000000",
});
const ickbUdt = new IckbUdt(
{ txHash: byte32FromByte("44"), index: 0n },
script("55"),
{ txHash: byte32FromByte("66"), index: 0n },
logic,
new DaoManager(dao, []),
);
let resolveReceipt!: (res: { header: ccc.ClientBlockHeader }) => void;
let resolveDeposit!: (res: { header: ccc.ClientBlockHeader }) => void;
const receiptFetch = new Promise<{ header: ccc.ClientBlockHeader }>((resolve) => {
resolveReceipt = resolve;
});
const depositFetch = new Promise<{ header: ccc.ClientBlockHeader }>((resolve) => {
resolveDeposit = resolve;
});
const requests: ccc.Hex[] = [];
const client = {
getTransactionWithHeader: async (txHash: ccc.Hex) => {
requests.push(txHash);
return txHash === receipt.outPoint.txHash ? receiptFetch : depositFetch;
},
} as unknown as ccc.Client;

const infoPromise = ickbUdt.infoFrom(client, [receipt, deposit]);

await vi.waitFor(() => {
expect(requests).toEqual([
receipt.outPoint.txHash,
deposit.outPoint.txHash,
]);
});
resolveDeposit({ header });
await Promise.resolve();
resolveReceipt({ header });

const info = await infoPromise;

expect(info.balance).toBe(
ickbValue(ccc.fixedPointFrom(100000), header) * 2n -
ickbValue(deposit.capacityFree, header),
);
expect(info.capacity).toBe(
receipt.cellOutput.capacity + deposit.cellOutput.capacity,
);
expect(info.count).toBe(2);
});

it("deduplicates repeated transaction header lookups", async () => {
const logic = script("33");
const dao = script("44");
const txHash = byte32FromByte("88");
const header = ccc.ClientBlockHeader.from(headerLike(10000000000000000n));
const receipt = ccc.Cell.from({
outPoint: { txHash, index: 0n },
cellOutput: {
capacity: ccc.fixedPointFrom(100082),
lock: script("22"),
type: logic,
},
outputData: receiptOutputData(2, ccc.fixedPointFrom(100000)),
});
const deposit = ccc.Cell.from({
outPoint: { txHash, index: 1n },
cellOutput: {
capacity: ccc.fixedPointFrom(100082),
lock: logic,
type: dao,
},
outputData: "0x0000000000000000",
});
const ickbUdt = new IckbUdt(
{ txHash: byte32FromByte("44"), index: 0n },
script("55"),
{ txHash: byte32FromByte("66"), index: 0n },
logic,
new DaoManager(dao, []),
);
let calls = 0;
const client = {
getTransactionWithHeader: async () => {
calls += 1;
await Promise.resolve();
return { header };
},
} as unknown as ccc.Client;

const info = await ickbUdt.infoFrom(client, [receipt, deposit]);

expect(calls).toBe(1);
expect(info.balance).toBe(
ickbValue(ccc.fixedPointFrom(100000), header) * 2n -
ickbValue(deposit.capacityFree, header),
);
});

it("adds xUDT and logic code deps explicitly", () => {
const logic = script("33");
const xudtCode = { txHash: byte32FromByte("44"), index: 1n };
const logicCode = { txHash: byte32FromByte("66"), index: 2n };
const ickbUdt = new IckbUdt(
xudtCode,
script("55"),
logicCode,
logic,
new DaoManager(script("77"), []),
);

const tx = ickbUdt.addCellDeps(ccc.Transaction.default());

expect(tx.cellDeps).toHaveLength(2);
expect(tx.cellDeps[0]?.depType).toBe("code");
expect(tx.cellDeps[0]?.outPoint.eq(ccc.OutPoint.from(xudtCode))).toBe(true);
expect(tx.cellDeps[1]?.depType).toBe("code");
expect(tx.cellDeps[1]?.outPoint.eq(ccc.OutPoint.from(logicCode))).toBe(true);
});
});
28 changes: 6 additions & 22 deletions packages/core/src/cells.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { ccc } from "@ckb-ccc/core";
import { type TransactionHeader, type ValueComponents } from "@ickb/utils";
import { OwnerData, ReceiptData } from "./entities.js";
import { ickbValue } from "./udt.js";
import { daoCellFrom, type DaoCell } from "@ickb/dao";
import type { DaoDepositCell, DaoWithdrawalRequestCell } from "@ickb/dao";

export interface IckbDepositCell extends DaoCell {
export interface IckbDepositCell extends DaoDepositCell {
/**
* A symbol property indicating that this cell is a Ickb Deposit Cell.
* This property is always set to true.
Expand All @@ -15,23 +15,7 @@ export interface IckbDepositCell extends DaoCell {
// Symbol to represent the isIckbDeposit property of Ickb Deposit Cells
const isIckbDepositSymbol = Symbol("isIckbDeposit");

/**
* Creates an IckbDepositCell from the provided parameters.
*
* @param options - The options to create a DaoCell. It can be one of the following:
* - An object omitting "interests" and "maturity" from DaoCell.
* - An object containing a cell, isDeposit flag, client, and an optional tip.
* - An object containing an outpoint, isDeposit flag, client, and an optional tip.
* - an instance of `DaoCell`.
*
* @returns A promise that resolves to a IckbDepositCell instance.
*
* @throws Error if the cell is not found.
*/
export async function ickbDepositCellFrom(
options: Parameters<typeof daoCellFrom>[0] | DaoCell,
): Promise<IckbDepositCell> {
const daoCell = "maturity" in options ? options : await daoCellFrom(options);
export function ickbDepositCellFrom(daoCell: DaoDepositCell): IckbDepositCell {
return {
...daoCell,
udtValue: ickbValue(daoCell.cell.capacityFree, daoCell.headers[0].header),
Expand Down Expand Up @@ -105,7 +89,7 @@ export async function receiptCellFrom(
*/
export class WithdrawalGroup implements ValueComponents {
constructor(
public owned: DaoCell,
public owned: DaoWithdrawalRequestCell,
public owner: OwnerCell,
) {}

Expand All @@ -121,10 +105,10 @@ export class WithdrawalGroup implements ValueComponents {
/**
* Gets the UDT value of the group.
*
* @returns The UDT amount as a `ccc.FixedPoint`, derived from the owned cell.
* @returns The iCKB amount represented by the owned withdrawal request.
*/
get udtValue(): ccc.FixedPoint {
return this.owned.udtValue;
return ickbValue(this.owned.cell.capacityFree, this.owned.headers[0].header);
}
}

Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export * from "./cells.js";
export {
OwnerCell,
receiptCellFrom,
WithdrawalGroup,
type IckbDepositCell,
type ReceiptCell,
} from "./cells.js";
export * from "./entities.js";
export * from "./logic.js";
export * from "./owned_owner.js";
Expand Down
Loading
Loading