From 65f35068ce206c5d16a4ece32dab455c7242ecaf Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 14 Aug 2025 22:32:43 +0800 Subject: [PATCH 01/81] chore: publish canary by pr comment --- .github/workflows/publish-canary.yaml | 65 +++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-canary.yaml b/.github/workflows/publish-canary.yaml index 18fceb9b7..cc68986f0 100644 --- a/.github/workflows/publish-canary.yaml +++ b/.github/workflows/publish-canary.yaml @@ -2,17 +2,44 @@ name: Release to NPM Canary permissions: contents: write + pull-requests: write -on: workflow_dispatch +on: + workflow_dispatch: + issue_comment: + types: [created] -concurrency: ${{ github.workflow }}-${{ github.ref }} +concurrency: ${{ github.workflow }}-${{ github.event_name == 'issue_comment' && github.event.issue.number || github.ref }} jobs: release: name: Release + if: > + github.event_name == 'workflow_dispatch' || + (github.event.issue.pull_request && + contains(fromJSON('["MEMBER", "OWNER", "COLLABORATOR"]'), github.event.comment.author_association) && + startsWith(github.event.comment.body, '/canary')) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Get PR head SHA for comment trigger + if: github.event_name == 'issue_comment' + id: get_pr_head_sha + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: context.issue.number, + }); + core.setOutput('sha', pr.head.sha); + + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'issue_comment' && steps.get_pr_head_sha.outputs.sha || github.ref }} + - uses: actions/setup-node@v4 with: node-version: 22 @@ -37,3 +64,35 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Comment on PR with Success + if: github.event_name == 'issue_comment' && steps.changesets.outputs.published == 'true' + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const workflow_url = `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`; + + const publishedPackages = JSON.parse(${{ toJSON(steps.changesets.outputs.publishedPackages) }}); + + let body = `🚀 Canary version published successfully! [View workflow run](${workflow_url})\n\n`; + body += "The following packages have been published to npm:\n"; + for (const pkg of publishedPackages) { + body += `* \`${pkg.name}@${pkg.version}\`\n`; + } + + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + + - name: Comment on PR on Failure + if: failure() && github.event_name == 'issue_comment' + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const workflow_url = `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`; + + const body = `❌ Canary version deployment failed. [View workflow run](${workflow_url})`; + + await github.rest.issues.createComment({ owner, repo, issue_number, body }); From 705b27d9fce6ccb0f13f19fcf359df62c6b73b26 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 14 Aug 2025 20:28:08 +0800 Subject: [PATCH 02/81] fix(core): avoid circular dependency --- .changeset/clean-chefs-roll.md | 6 ++++++ packages/core/src/ckb/hash.ts | 28 +++++++++++++++++++++++++++ packages/core/src/ckb/index.ts | 1 + packages/core/src/hasher/hasherCkb.ts | 26 ------------------------- 4 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 .changeset/clean-chefs-roll.md create mode 100644 packages/core/src/ckb/hash.ts diff --git a/.changeset/clean-chefs-roll.md b/.changeset/clean-chefs-roll.md new file mode 100644 index 000000000..7e5f0c8fa --- /dev/null +++ b/.changeset/clean-chefs-roll.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": patch +--- + +fix(core): avoid circular dependency + \ No newline at end of file diff --git a/packages/core/src/ckb/hash.ts b/packages/core/src/ckb/hash.ts new file mode 100644 index 000000000..e754703c6 --- /dev/null +++ b/packages/core/src/ckb/hash.ts @@ -0,0 +1,28 @@ +import { hashCkb } from "../hasher/hasherCkb.js"; +import { Hex } from "../hex/index.js"; +import { NumLike, numLeToBytes } from "../num/index.js"; +import { CellInput, CellInputLike } from "./transaction.js"; + +/** + * Computes the Type ID hash of the given data. + * @public + * + * @param cellInputLike - The first cell input of the transaction. + * @param outputIndex - The output index of the Type ID cell. + * @returns The hexadecimal string representation of the hash. + * + * @example + * ```typescript + * const hash = hashTypeId(cellInput, outputIndex); // Outputs something like "0x..." + * ``` + */ + +export function hashTypeId( + cellInputLike: CellInputLike, + outputIndex: NumLike, +): Hex { + return hashCkb( + CellInput.from(cellInputLike).toBytes(), + numLeToBytes(outputIndex, 8), + ); +} diff --git a/packages/core/src/ckb/index.ts b/packages/core/src/ckb/index.ts index 7b8a9df08..7d20b37d0 100644 --- a/packages/core/src/ckb/index.ts +++ b/packages/core/src/ckb/index.ts @@ -1,3 +1,4 @@ +export * from "./hash.js"; export * from "./script.js"; export * from "./transaction.js"; export * from "./transactionErrors.js"; diff --git a/packages/core/src/hasher/hasherCkb.ts b/packages/core/src/hasher/hasherCkb.ts index c23f83d1a..c32ec569c 100644 --- a/packages/core/src/hasher/hasherCkb.ts +++ b/packages/core/src/hasher/hasherCkb.ts @@ -1,8 +1,6 @@ import { blake2b } from "@noble/hashes/blake2b"; import { BytesLike, bytesFrom } from "../bytes/index.js"; -import { CellInput, CellInputLike } from "../ckb/index.js"; import { Hex, hexFrom } from "../hex/index.js"; -import { NumLike, numLeToBytes } from "../num/index.js"; import { CKB_BLAKE2B_PERSONAL } from "./advanced.js"; import { Hasher } from "./hasher.js"; @@ -81,27 +79,3 @@ export function hashCkb(...data: BytesLike[]): Hex { data.forEach((d) => hasher.update(d)); return hasher.digest(); } - -/** - * Computes the Type ID hash of the given data. - * @public - * - * @param cellInputLike - The first cell input of the transaction. - * @param outputIndex - The output index of the Type ID cell. - * @returns The hexadecimal string representation of the hash. - * - * @example - * ```typescript - * const hash = hashTypeId(cellInput, outputIndex); // Outputs something like "0x..." - * ``` - */ - -export function hashTypeId( - cellInputLike: CellInputLike, - outputIndex: NumLike, -): Hex { - return hashCkb( - CellInput.from(cellInputLike).toBytes(), - numLeToBytes(outputIndex, 8), - ); -} From b8a07c1ff78f73561f024d0f7a160152a9bafe54 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sat, 21 Jun 2025 05:40:50 +0800 Subject: [PATCH 03/81] feat(core): `Signer.findCellsOnChain` --- .changeset/clean-shoes-thank.md | 6 +++++ packages/core/src/signer/signer/index.ts | 34 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 .changeset/clean-shoes-thank.md diff --git a/.changeset/clean-shoes-thank.md b/.changeset/clean-shoes-thank.md new file mode 100644 index 000000000..00b6d9101 --- /dev/null +++ b/.changeset/clean-shoes-thank.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): `Signer.findCellsOnChain` + \ No newline at end of file diff --git a/packages/core/src/signer/signer/index.ts b/packages/core/src/signer/signer/index.ts index fdc372a33..5d5c7b147 100644 --- a/packages/core/src/signer/signer/index.ts +++ b/packages/core/src/signer/signer/index.ts @@ -6,6 +6,7 @@ import { Client, ClientFindTransactionsGroupedResponse, ClientFindTransactionsResponse, + ClientIndexerSearchKeyFilterLike, } from "../../client/index.js"; import { Hex } from "../../hex/index.js"; import { Num } from "../../num/index.js"; @@ -239,6 +240,39 @@ export abstract class Signer { ); } + /** + * Find cells of this signer + * + * @param filter - The filter for the search key. + * @param withData - Whether to include cell data in the response. + * @param order - The order of the returned cells, can be "asc" or "desc". + * @param limit - The maximum number of cells for every querying chunk. + * @returns A async generator that yields all matching cells + */ + async *findCellsOnChain( + filter: ClientIndexerSearchKeyFilterLike, + withData?: boolean | null, + order?: "asc" | "desc", + limit?: number, + ): AsyncGenerator { + const scripts = await this.getAddressObjs(); + for (const { script } of scripts) { + for await (const cell of this.client.findCellsOnChain( + { + script, + scriptType: "lock", + filter, + scriptSearchMode: "exact", + withData, + }, + order, + limit, + )) { + yield cell; + } + } + } + /** * Find cells of this signer * From 7cd7696feb991f0027c9c8b6d58de7f51ef516a6 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 15 Jun 2025 01:01:37 +0800 Subject: [PATCH 04/81] feat(core): auto capacity completion --- .changeset/green-news-behave.md | 6 + packages/core/src/ckb/transaction.test.ts | 484 ++++++++++++++++++++++ packages/core/src/ckb/transaction.ts | 242 +++++++++-- 3 files changed, 694 insertions(+), 38 deletions(-) create mode 100644 .changeset/green-news-behave.md diff --git a/.changeset/green-news-behave.md b/.changeset/green-news-behave.md new file mode 100644 index 000000000..8dc7156eb --- /dev/null +++ b/.changeset/green-news-behave.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): auto capacity completion + \ No newline at end of file diff --git a/packages/core/src/ckb/transaction.test.ts b/packages/core/src/ckb/transaction.test.ts index 47c7e4b84..6271943ae 100644 --- a/packages/core/src/ckb/transaction.test.ts +++ b/packages/core/src/ckb/transaction.test.ts @@ -644,4 +644,488 @@ describe("Transaction", () => { ); }); }); + + describe("Automatic Capacity Completion", () => { + describe("CellOutput.from", () => { + it("should use explicit capacity when provided", () => { + const cellOutput = ccc.CellOutput.from({ + capacity: 1000n, + lock, + }); + + expect(cellOutput.capacity).toBe(1000n); + }); + + it("should calculate capacity automatically when capacity is 0", () => { + const outputData = "0x1234"; // 2 bytes + const cellOutput = ccc.CellOutput.from( + { + capacity: 0n, + lock, + }, + outputData, + ); + + const expectedCapacity = cellOutput.occupiedSize + 2; // occupiedSize + outputData length + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + + it("should calculate capacity automatically when capacity is omitted", () => { + const outputData = "0x5678"; // 2 bytes + const cellOutput = ccc.CellOutput.from( + { + lock, + }, + outputData, + ); + + const expectedCapacity = cellOutput.occupiedSize + 2; // occupiedSize + outputData length + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + + it("should handle empty outputData in automatic calculation", () => { + const outputData = "0x"; // 0 bytes + const cellOutput = ccc.CellOutput.from( + { + lock, + }, + outputData, + ); + + const expectedCapacity = cellOutput.occupiedSize; // occupiedSize + 0 + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + + it("should handle long outputData in automatic calculation", () => { + const outputData = "0x" + "12".repeat(100); // 100 bytes + const cellOutput = ccc.CellOutput.from( + { + lock, + }, + outputData, + ); + + const expectedCapacity = cellOutput.occupiedSize + 100; // occupiedSize + outputData length + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + + it("should calculate capacity with type script", () => { + const outputData = "0x1234"; // 2 bytes + const cellOutput = ccc.CellOutput.from( + { + lock, + type, + }, + outputData, + ); + + const expectedCapacity = cellOutput.occupiedSize + 2; // occupiedSize (including type) + outputData length + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + + it("should not auto-calculate when capacity is explicitly provided even with outputData", () => { + const outputData = "0x1234"; // 2 bytes + const explicitCapacity = 5000n; + const cellOutput = ccc.CellOutput.from( + { + capacity: explicitCapacity, + lock, + }, + outputData, + ); + + expect(cellOutput.capacity).toBe(explicitCapacity); + }); + + it("should handle the overloaded signature correctly", () => { + // Test the overloaded signature where capacity is omitted and outputData is required + const outputData = "0xabcd"; + const cellOutput = ccc.CellOutput.from( + { + lock, + type, + }, + outputData, + ); + + const expectedCapacity = cellOutput.occupiedSize + 2; + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + }); + + describe("Transaction.from", () => { + it("should create transaction with automatic capacity calculation for outputs", () => { + const outputsData = ["0x1234", "0x567890"]; + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + }, + { + lock, + type, + }, + ], + outputsData, + }); + + // First output: lock only + 2 bytes data + const expectedCapacity1 = 8 + lock.occupiedSize + 2; // capacity field + lock + outputData + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity1), + ); + + // Second output: lock + type + 3 bytes data + const expectedCapacity2 = 8 + lock.occupiedSize + type.occupiedSize + 3; // capacity field + lock + type + outputData + expect(tx.outputs[1].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity2), + ); + + expect(tx.outputsData).toEqual([ + ccc.hexFrom("0x1234"), + ccc.hexFrom("0x567890"), + ]); + }); + + it("should handle mixed explicit and automatic capacity calculation", () => { + const outputsData = ["0x12", "0x3456"]; + const explicitCapacity = 5000n; + const tx = ccc.Transaction.from({ + outputs: [ + { + capacity: explicitCapacity, + lock, + }, + { + lock, + }, + ], + outputsData, + }); + + // First output: explicit capacity + expect(tx.outputs[0].capacity).toBe(explicitCapacity); + + // Second output: automatic calculation + const expectedCapacity2 = 8 + lock.occupiedSize + 2; + expect(tx.outputs[1].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity2), + ); + }); + + it("should handle empty outputsData array", () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + }, + ], + outputsData: [], + }); + + // Should use empty data for calculation + const expectedCapacity = 8 + lock.occupiedSize + 0; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity), + ); + expect(tx.outputsData).toEqual([ccc.hexFrom("0x")]); + }); + + it("should handle missing outputsData", () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + }, + ], + }); + + // Should use empty data for calculation + const expectedCapacity = 8 + lock.occupiedSize + 0; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity), + ); + expect(tx.outputsData).toEqual([ccc.hexFrom("0x")]); + }); + + it("should handle more outputsData than outputs", () => { + const outputsData = ["0x12", "0x34", "0x56"]; + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + }, + ], + outputsData, + }); + + // First output should use first outputData + const expectedCapacity = 8 + lock.occupiedSize + 1; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity), + ); + + // All outputsData should be preserved + expect(tx.outputsData).toEqual([ + ccc.hexFrom("0x12"), + ccc.hexFrom("0x34"), + ccc.hexFrom("0x56"), + ]); + }); + }); + + describe("Transaction.addOutput", () => { + it("should add output with automatic capacity calculation", () => { + const tx = ccc.Transaction.default(); + const outputData = "0x1234"; + + const outputCount = tx.addOutput( + { + lock, + }, + outputData, + ); + + expect(outputCount).toBe(1); + expect(tx.outputs.length).toBe(1); + + const expectedCapacity = 8 + lock.occupiedSize + 2; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity), + ); + expect(tx.outputsData[0]).toBe(ccc.hexFrom(outputData)); + }); + + it("should add output with type script and automatic capacity calculation", () => { + const tx = ccc.Transaction.default(); + const outputData = "0x567890"; + + tx.addOutput( + { + lock, + type, + }, + outputData, + ); + + const expectedCapacity = 8 + lock.occupiedSize + type.occupiedSize + 3; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity), + ); + expect(tx.outputsData[0]).toBe(ccc.hexFrom(outputData)); + }); + + it("should add output with explicit capacity", () => { + const tx = ccc.Transaction.default(); + const outputData = "0x12"; + const explicitCapacity = 10000n; + + tx.addOutput( + { + capacity: explicitCapacity, + lock, + }, + outputData, + ); + + expect(tx.outputs[0].capacity).toBe(explicitCapacity); + expect(tx.outputsData[0]).toBe(ccc.hexFrom(outputData)); + }); + + it("should add output with default empty outputData", () => { + const tx = ccc.Transaction.default(); + + tx.addOutput({ + lock, + }); + + const expectedCapacity = 8 + lock.occupiedSize + 0; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity), + ); + expect(tx.outputsData[0]).toBe(ccc.hexFrom("0x")); + }); + + it("should add multiple outputs with automatic capacity calculation", () => { + const tx = ccc.Transaction.default(); + + tx.addOutput({ lock }, "0x12"); + tx.addOutput({ lock, type }, "0x3456"); + + expect(tx.outputs.length).toBe(2); + + // First output + const expectedCapacity1 = 8 + lock.occupiedSize + 1; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity1), + ); + + // Second output + const expectedCapacity2 = 8 + lock.occupiedSize + type.occupiedSize + 2; + expect(tx.outputs[1].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity2), + ); + + expect(tx.outputsData).toEqual([ + ccc.hexFrom("0x12"), + ccc.hexFrom("0x3456"), + ]); + }); + }); + + describe("Edge Cases and Error Handling", () => { + it("should handle CellOutput instance passed to CellOutput.from", () => { + const originalOutput = ccc.CellOutput.from({ + capacity: 1000n, + lock, + }); + + const result = ccc.CellOutput.from(originalOutput); + expect(result).toBe(originalOutput); // Should return the same instance + }); + + it("should handle Cell instance passed to Cell.from", () => { + const originalCell = ccc.Cell.from({ + outPoint: { + txHash: "0x" + "0".repeat(64), + index: 0, + }, + cellOutput: { + capacity: 1000n, + lock, + }, + outputData: "0x", + }); + + const result = ccc.Cell.from(originalCell); + expect(result).toBe(originalCell); // Should return the same instance + }); + + it("should handle Transaction instance passed to Transaction.from", () => { + const originalTx = ccc.Transaction.from({ + outputs: [{ capacity: 1000n, lock }], + outputsData: ["0x"], + }); + + const result = ccc.Transaction.from(originalTx); + expect(result).toBe(originalTx); // Should return the same instance + }); + + it("should calculate minimum capacity correctly", () => { + // Test with minimal lock script + const minimalLock = ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "data", + args: "0x", + }); + + const cellOutput = ccc.CellOutput.from( + { + lock: minimalLock, + }, + "0x", + ); + + // Minimum capacity should be 8 (capacity field) + lock.occupiedSize + 0 (empty data) + const expectedMinCapacity = 8 + minimalLock.occupiedSize; + expect(cellOutput.capacity).toBe( + ccc.fixedPointFrom(expectedMinCapacity), + ); + }); + + it("should handle very large outputData", () => { + // Create 1KB of data + const largeData = "0x" + "ff".repeat(1024); + const cellOutput = ccc.CellOutput.from( + { + lock, + }, + largeData, + ); + + const expectedCapacity = 8 + lock.occupiedSize + 1024; + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + + it("should handle null type script correctly", () => { + const cellOutput = ccc.CellOutput.from( + { + lock, + type: null, + }, + "0x1234", + ); + + // Should not include type script in calculation + const expectedCapacity = 8 + lock.occupiedSize + 2; + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + expect(cellOutput.type).toBeUndefined(); + }); + + it("should handle empty outputData in overloaded signature", () => { + // This tests the overloaded signature where outputData is required + const cellOutput = ccc.CellOutput.from( + { + lock, + }, + "0x", // Empty data + ); + + // Should treat empty outputData as 0 bytes + const expectedCapacity = 8 + lock.occupiedSize + 0; + expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); + }); + + it("should verify occupiedSize calculation includes all components", () => { + const cellOutput = ccc.CellOutput.from({ + capacity: 1000n, + lock, + type, + }); + + // occupiedSize should include capacity field (8 bytes) + lock + type + const expectedOccupiedSize = 8 + lock.occupiedSize + type.occupiedSize; + expect(cellOutput.occupiedSize).toBe(expectedOccupiedSize); + }); + + it("should verify Cell occupiedSize includes outputData", () => { + const outputData = "0x123456"; + const cell = ccc.Cell.from({ + outPoint: { + txHash: "0x" + "0".repeat(64), + index: 0, + }, + cellOutput: { + capacity: 1000n, + lock, + }, + outputData, + }); + + // Cell occupiedSize should include CellOutput occupiedSize + outputData length + const expectedOccupiedSize = cell.cellOutput.occupiedSize + 3; // 3 bytes of data + expect(cell.occupiedSize).toBe(expectedOccupiedSize); + }); + + it("should calculate capacityFree correctly", () => { + const outputData = "0x1234"; + const explicitCapacity = 1000n; + const cell = ccc.Cell.from({ + outPoint: { + txHash: "0x" + "0".repeat(64), + index: 0, + }, + cellOutput: { + capacity: explicitCapacity, + lock, + }, + outputData, + }); + + const expectedFreeCapacity = + explicitCapacity - ccc.fixedPointFrom(cell.occupiedSize); + expect(cell.capacityFree).toBe(expectedFreeCapacity); + }); + }); + }); }); diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index e1a38ac67..95fef2d38 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -235,29 +235,58 @@ export class CellOutput extends mol.Entity.Base() { /** * Creates a CellOutput instance from a CellOutputLike object. + * This method supports automatic capacity calculation when capacity is 0 or omitted. * * @param cellOutput - A CellOutputLike object or an instance of CellOutput. + * Can also be an object without capacity when outputData is provided. + * @param outputData - Optional output data used for automatic capacity calculation. + * When provided and capacity is 0, the capacity will be calculated + * as occupiedSize + outputData.length. * @returns A CellOutput instance. * * @example * ```typescript - * const cellOutput = CellOutput.from({ + * // Basic usage with explicit capacity + * const cellOutput1 = CellOutput.from({ * capacity: 1000n, * lock: { codeHash: "0x...", hashType: "type", args: "0x..." }, * type: { codeHash: "0x...", hashType: "type", args: "0x..." } * }); + * + * // Automatic capacity calculation + * const cellOutput2 = CellOutput.from({ + * lock: { codeHash: "0x...", hashType: "type", args: "0x..." } + * }, "0x1234"); // Capacity will be calculated automatically * ``` */ - static from(cellOutput: CellOutputLike): CellOutput { + static from(cellOutput: CellOutputLike, outputData?: HexLike): CellOutput; + static from( + cellOutput: Omit & + Partial>, + outputData: HexLike, + ): CellOutput; + static from( + cellOutput: Omit & + Partial>, + outputData?: HexLike, + ): CellOutput { if (cellOutput instanceof CellOutput) { return cellOutput; } - return new CellOutput( - numFrom(cellOutput.capacity), + const output = new CellOutput( + numFrom(cellOutput.capacity ?? 0), Script.from(cellOutput.lock), apply(Script.from, cellOutput.type), ); + + if (output.capacity === Zero) { + output.capacity = fixedPointFrom( + output.occupiedSize + bytesFrom(outputData ?? "0x").length, + ); + } + + return output; } /** @@ -308,9 +337,37 @@ export class Cell { /** * Creates a Cell instance from a CellLike object. + * This method accepts either `outPoint` or `previousOutput` to specify the cell's location, + * and supports automatic capacity calculation for the cell output. * - * @param cell - A CellLike object or an instance of Cell. + * @param cell - A CellLike object or an instance of Cell. The object can use either: + * - `outPoint`: For referencing a cell output + * - `previousOutput`: For referencing a cell input (alternative name for outPoint) + * The cellOutput can omit capacity for automatic calculation. * @returns A Cell instance. + * + * @example + * ```typescript + * // Using outPoint with explicit capacity + * const cell1 = Cell.from({ + * outPoint: { txHash: "0x...", index: 0 }, + * cellOutput: { + * capacity: 1000n, + * lock: { codeHash: "0x...", hashType: "type", args: "0x..." } + * }, + * outputData: "0x" + * }); + * + * // Using previousOutput with automatic capacity calculation + * const cell2 = Cell.from({ + * previousOutput: { txHash: "0x...", index: 0 }, + * cellOutput: { + * lock: { codeHash: "0x...", hashType: "type", args: "0x..." } + * // capacity will be calculated automatically + * }, + * outputData: "0x1234" + * }); + * ``` */ static from(cell: CellLike): Cell { @@ -320,7 +377,7 @@ export class Cell { return new Cell( OutPoint.from("outPoint" in cell ? cell.outPoint : cell.previousOutput), - CellOutput.from(cell.cellOutput), + CellOutput.from(cell.cellOutput, cell.outputData), hexFrom(cell.outputData), ); } @@ -1001,6 +1058,9 @@ export class Transaction extends mol.Entity.Base< /** * Copy every properties from another transaction. + * This method replaces all properties of the current transaction with those from the provided transaction. + * + * @param txLike - The transaction-like object to copy properties from. * * @example * ```typescript @@ -1092,19 +1152,9 @@ export class Transaction extends mol.Entity.Base< return tx; } const outputs = - tx.outputs?.map((output, i) => { - const o = CellOutput.from({ - ...output, - capacity: output.capacity ?? 0, - }); - if (o.capacity === Zero) { - o.capacity = fixedPointFrom( - o.occupiedSize + - (apply(bytesFrom, tx.outputsData?.[i])?.length ?? 0), - ); - } - return o; - }) ?? []; + tx.outputs?.map((output, i) => + CellOutput.from(output, tx.outputsData?.[i] ?? []), + ) ?? []; const outputsData = outputs.map((_, i) => hexFrom(tx.outputsData?.[i] ?? "0x"), ); @@ -1332,7 +1382,7 @@ export class Transaction extends mol.Entity.Base< * * @param scriptLike - The script associated with the transaction, represented as a ScriptLike object. * @param client - The client for complete extra infos in the transaction. - * @returns A promise that resolves to the prepared transaction + * @returns A promise that resolves to the found index, or undefined if no matching input is found. * * @example * ```typescript @@ -1359,7 +1409,7 @@ export class Transaction extends mol.Entity.Base< * * @param scriptLike - The script associated with the transaction, represented as a ScriptLike object. * @param client - The client for complete extra infos in the transaction. - * @returns A promise that resolves to the prepared transaction + * @returns A promise that resolves to the found index, or undefined if no matching input is found. * * @example * ```typescript @@ -1472,14 +1522,14 @@ export class Transaction extends mol.Entity.Base< * Set output data at index. * * @param index - The index of the output data. - * @param witness - The data to set. + * @param data - The data to set. * * @example * ```typescript - * await tx.setOutputDataAt(0, "0x00"); + * tx.setOutputDataAt(0, "0x00"); * ``` */ - setOutputDataAt(index: number, witness: HexLike): void { + setOutputDataAt(index: number, data: HexLike): void { if (this.outputsData.length < index) { this.outputsData.push( ...Array.from( @@ -1489,7 +1539,7 @@ export class Transaction extends mol.Entity.Base< ); } - this.outputsData[index] = hexFrom(witness); + this.outputsData[index] = hexFrom(data); } /** @@ -1564,16 +1614,7 @@ export class Transaction extends mol.Entity.Base< Partial>, outputData: HexLike = "0x", ): number { - const output = CellOutput.from({ - ...outputLike, - capacity: outputLike.capacity ?? 0, - }); - if (output.capacity === Zero) { - output.capacity = fixedPointFrom( - output.occupiedSize + bytesFrom(outputData).length, - ); - } - const len = this.outputs.push(output); + const len = this.outputs.push(CellOutput.from(outputLike, outputData)); this.setOutputDataAt(len - 1, outputData); return len; @@ -2073,6 +2114,38 @@ export class Transaction extends mol.Entity.Base< } } + /** + * Completes the transaction fee by adding inputs and creating a change output with the specified lock script. + * This is a convenience method that automatically creates a change cell with the provided lock script + * when there's excess capacity after paying the transaction fee. + * + * @param from - The signer to complete inputs from and prepare the transaction. + * @param change - The lock script for the change output cell. + * @param feeRate - Optional fee rate in shannons per 1000 bytes. If not provided, it will be fetched from the client. + * @param filter - Optional filter for selecting cells when adding inputs. + * @param options - Optional configuration object. + * @param options.feeRateBlockRange - Block range for fee rate calculation when feeRate is not provided. + * @param options.maxFeeRate - Maximum allowed fee rate. + * @param options.shouldAddInputs - Whether to add inputs automatically. Defaults to true. + * @returns A promise that resolves to a tuple containing: + * - The number of inputs added during the process + * - A boolean indicating whether change outputs were created (true) or fee was paid without change (false) + * + * @example + * ```typescript + * const changeScript = Script.from({ + * codeHash: "0x...", + * hashType: "type", + * args: "0x..." + * }); + * + * const [addedInputs, hasChange] = await tx.completeFeeChangeToLock( + * signer, + * changeScript, + * 1000n // 1000 shannons per 1000 bytes + * ); + * ``` + */ completeFeeChangeToLock( from: Signer, change: ScriptLike, @@ -2104,6 +2177,33 @@ export class Transaction extends mol.Entity.Base< ); } + /** + * Completes the transaction fee using the signer's recommended address for change. + * This is a convenience method that automatically uses the signer's recommended + * address as the change destination, making it easier to complete transactions + * without manually specifying a change address. + * + * @param from - The signer to complete inputs from and prepare the transaction. + * @param feeRate - Optional fee rate in shannons per 1000 bytes. If not provided, it will be fetched from the client. + * @param filter - Optional filter for selecting cells when adding inputs. + * @param options - Optional configuration object. + * @param options.feeRateBlockRange - Block range for fee rate calculation when feeRate is not provided. + * @param options.maxFeeRate - Maximum allowed fee rate. + * @param options.shouldAddInputs - Whether to add inputs automatically. Defaults to true. + * @returns A promise that resolves to a tuple containing: + * - The number of inputs added during the process + * - A boolean indicating whether change outputs were created (true) or fee was paid without change (false) + * + * @example + * ```typescript + * const [addedInputs, hasChange] = await tx.completeFeeBy( + * signer, + * 1000n // 1000 shannons per 1000 bytes + * ); + * + * // Change will automatically go to signer's recommended address + * ``` + */ async completeFeeBy( from: Signer, feeRate?: NumLike, @@ -2119,6 +2219,35 @@ export class Transaction extends mol.Entity.Base< return this.completeFeeChangeToLock(from, script, feeRate, filter, options); } + /** + * Completes the transaction fee by adding excess capacity to an existing output. + * Instead of creating a new change output, this method adds any excess capacity + * to the specified existing output in the transaction. + * + * @param from - The signer to complete inputs from and prepare the transaction. + * @param index - The index of the existing output to add excess capacity to. + * @param feeRate - Optional fee rate in shannons per 1000 bytes. If not provided, it will be fetched from the client. + * @param filter - Optional filter for selecting cells when adding inputs. + * @param options - Optional configuration object. + * @param options.feeRateBlockRange - Block range for fee rate calculation when feeRate is not provided. + * @param options.maxFeeRate - Maximum allowed fee rate. + * @param options.shouldAddInputs - Whether to add inputs automatically. Defaults to true. + * @returns A promise that resolves to a tuple containing: + * - The number of inputs added during the process + * - A boolean indicating whether change was applied (true) or fee was paid without change (false) + * + * @throws {Error} When the specified output index doesn't exist. + * + * @example + * ```typescript + * // Add excess capacity to the first output (index 0) + * const [addedInputs, hasChange] = await tx.completeFeeChangeToOutput( + * signer, + * 0, // Output index + * 1000n // 1000 shannons per 1000 bytes + * ); + * ``` + */ completeFeeChangeToOutput( from: Signer, index: NumLike, @@ -2148,7 +2277,26 @@ export class Transaction extends mol.Entity.Base< } /** - * Calculate Nervos DAO profit between two blocks + * Calculate Nervos DAO profit between two blocks. + * This function computes the profit earned from a Nervos DAO deposit + * based on the capacity and the time period between deposit and withdrawal. + * + * @param profitableCapacity - The capacity that earns profit (total capacity minus occupied capacity). + * @param depositHeaderLike - The block header when the DAO deposit was made. + * @param withdrawHeaderLike - The block header when the DAO withdrawal is made. + * @returns The profit amount in CKB (capacity units). + * + * @example + * ```typescript + * const profit = calcDaoProfit( + * ccc.fixedPointFrom(100), // 100 CKB profitable capacity + * depositHeader, + * withdrawHeader + * ); + * console.log(`Profit: ${profit} shannons`); + * ``` + * + * @see {@link https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md | Nervos DAO RFC} */ export function calcDaoProfit( profitableCapacity: NumLike, @@ -2167,8 +2315,26 @@ export function calcDaoProfit( } /** - * Calculate claimable epoch for Nervos DAO withdrawal - * See https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md + * Calculate claimable epoch for Nervos DAO withdrawal. + * This function determines the earliest epoch when a Nervos DAO withdrawal + * can be claimed based on the deposit and withdrawal epochs. + * + * @param depositHeader - The block header when the DAO deposit was made. + * @param withdrawHeader - The block header when the DAO withdrawal was initiated. + * @returns The epoch when the withdrawal can be claimed, represented as [number, index, length]. + * + * @example + * ```typescript + * const claimEpoch = calcDaoClaimEpoch(depositHeader, withdrawHeader); + * console.log(`Can claim at epoch: ${claimEpoch[0]}, index: ${claimEpoch[1]}, length: ${claimEpoch[2]}`); + * ``` + * + * @remarks + * The Nervos DAO has a minimum lock period of 180 epochs (~30 days). + * This function calculates the exact epoch when the withdrawal becomes claimable + * based on the deposit epoch and withdrawal epoch timing. + * + * @see {@link https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md | Nervos DAO RFC} */ export function calcDaoClaimEpoch( depositHeader: ClientBlockHeaderLike, From e900c59f291332a285201e265fcf4f2b2b8430e7 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 15 Jun 2025 03:12:25 +0800 Subject: [PATCH 05/81] perf(core): optimize Transaction.completeFee --- .changeset/empty-shrimps-buy.md | 6 ++++ packages/core/src/ckb/transaction.ts | 41 ++++++++++++++++++---------- 2 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 .changeset/empty-shrimps-buy.md diff --git a/.changeset/empty-shrimps-buy.md b/.changeset/empty-shrimps-buy.md new file mode 100644 index 000000000..15ccfd8a4 --- /dev/null +++ b/.changeset/empty-shrimps-buy.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": patch +--- + +perf(core): optimize Transaction.completeFee + \ No newline at end of file diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 95fef2d38..ea1c1e554 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -2039,21 +2039,22 @@ export class Transaction extends mol.Entity.Base< let leastFee = Zero; let leastExtraCapacity = Zero; - + let collected = 0; + + // === + // Usually, for the worst situation, three iterations are needed + // 1. First attempt to complete the transaction. + // 2. Not enough capacity for the change cell. + // 3. Fee increased by the change cell. + // === while (true) { - const tx = this.clone(); - const collected = await (async () => { + collected += await (async () => { if (!(options?.shouldAddInputs ?? true)) { - const fee = - (await tx.getFee(from.client)) - leastFee - leastExtraCapacity; - if (fee < Zero) { - throw new ErrorTransactionInsufficientCapacity(-fee); - } return 0; } try { - return await tx.completeInputsByCapacity( + return await this.completeInputsByCapacity( from, leastFee + leastExtraCapacity, filter, @@ -2072,21 +2073,33 @@ export class Transaction extends mol.Entity.Base< } })(); - await from.prepareTransaction(tx); + const fee = await this.getFee(from.client); + if (fee < leastFee + leastExtraCapacity) { + // Not enough capacity are collected, it should only happens when shouldAddInputs is false + throw new ErrorTransactionInsufficientCapacity( + leastFee + leastExtraCapacity - fee, + { isForChange: leastExtraCapacity !== Zero }, + ); + } + + await from.prepareTransaction(this); if (leastFee === Zero) { // The initial fee is calculated based on prepared transaction - leastFee = tx.estimateFee(feeRate); + // This should only happens during the first iteration + leastFee = this.estimateFee(feeRate); } - const fee = await tx.getFee(from.client); // The extra capacity paid the fee without a change + // leastExtraCapacity should be 0 here, otherwise we should failed in the previous check + // So this only happens in the first iteration if (fee === leastFee) { - this.copy(tx); return [collected, false]; } + // Invoke the change function on a transaction multiple times may cause problems, so we clone it + const tx = this.clone(); const needed = numFrom(await Promise.resolve(change(tx, fee - leastFee))); - // No enough extra capacity to create new cells for change if (needed > Zero) { + // No enough extra capacity to create new cells for change, collect inputs again leastExtraCapacity = needed; continue; } From 439380360a053c511c5ef741b3afeca77da9096a Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 17 Aug 2025 03:14:53 +0800 Subject: [PATCH 06/81] feat(udt): `Udt.complete*` methods --- .changeset/plenty-ads-rush.md | 6 + .changeset/shy-horses-agree.md | 6 + packages/core/src/ckb/transaction.ts | 12 + packages/core/src/ckb/transactionErrors.ts | 3 + packages/ssri/src/executor.ts | 8 + packages/udt/package.json | 2 +- packages/udt/src/udt/index.test.ts | 1001 +++++++++++++++ packages/udt/src/udt/index.ts | 1295 +++++++++++++++++++- packages/udt/src/udtPausable/index.ts | 6 +- packages/udt/vitest.config.ts | 10 + vitest.config.ts | 4 +- 11 files changed, 2292 insertions(+), 61 deletions(-) create mode 100644 .changeset/plenty-ads-rush.md create mode 100644 .changeset/shy-horses-agree.md create mode 100644 packages/udt/src/udt/index.test.ts create mode 100644 packages/udt/vitest.config.ts diff --git a/.changeset/plenty-ads-rush.md b/.changeset/plenty-ads-rush.md new file mode 100644 index 000000000..8db5049cd --- /dev/null +++ b/.changeset/plenty-ads-rush.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/ssri": minor +--- + +feat(ssri): `ExecutorResponse.mapAsync` + \ No newline at end of file diff --git a/.changeset/shy-horses-agree.md b/.changeset/shy-horses-agree.md new file mode 100644 index 000000000..f42efc551 --- /dev/null +++ b/.changeset/shy-horses-agree.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/udt": minor +--- + +feat(udt): `Udt.complete*` methods + diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index ea1c1e554..2bfd24ef3 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1737,6 +1737,12 @@ export class Transaction extends mol.Entity.Base< ); } + /** + * @deprecated Use `Udt.getInputsBalance` from `@ckb-ccc/udt` instead + * @param client + * @param type + * @returns + */ async getInputsUdtBalance(client: Client, type: ScriptLike): Promise { return reduceAsync( this.inputs, @@ -1752,6 +1758,11 @@ export class Transaction extends mol.Entity.Base< ); } + /** + * @deprecated Use `Udt.getOutputsBalance` from `@ckb-ccc/udt` instead + * @param type + * @returns + */ getOutputsUdtBalance(type: ScriptLike): Num { return this.outputs.reduce((acc, output, i) => { if (!output.type?.eq(type)) { @@ -1869,6 +1880,7 @@ export class Transaction extends mol.Entity.Base< * This method succeeds only if enough balance is collected. * * It will try to collect at least two inputs, even when the first input already contains enough balance, to avoid extra occupation fees introduced by the change cell. An edge case: If the first cell has the same amount as the output, a new cell is not needed. + * @deprecated Use `Udt.completeInputsByBalance` from `@ckb-ccc/udt` instead * @param from - The signer to complete the inputs. * @param type - The type script of the UDT. * @param balanceTweak - The tweak of the balance. diff --git a/packages/core/src/ckb/transactionErrors.ts b/packages/core/src/ckb/transactionErrors.ts index 09c1cb745..26057669a 100644 --- a/packages/core/src/ckb/transactionErrors.ts +++ b/packages/core/src/ckb/transactionErrors.ts @@ -22,6 +22,9 @@ export class ErrorTransactionInsufficientCapacity extends Error { } } +/** + * @deprecated Use `ErrorUdtInsufficientCoin` from `@ckb-ccc/udt` instead. + */ export class ErrorTransactionInsufficientCoin extends Error { public readonly amount: Num; public readonly type: Script; diff --git a/packages/ssri/src/executor.ts b/packages/ssri/src/executor.ts index ccbf98c94..c31518ac5 100644 --- a/packages/ssri/src/executor.ts +++ b/packages/ssri/src/executor.ts @@ -63,6 +63,14 @@ export class ExecutorResponse { throw new ExecutorErrorDecode(JSON.stringify(err)); } } + + async mapAsync(fn: (res: T) => Promise): Promise> { + try { + return new ExecutorResponse(await fn(this.res), this.cellDeps); + } catch (err) { + throw new ExecutorErrorDecode(JSON.stringify(err)); + } + } } /** diff --git a/packages/udt/package.json b/packages/udt/package.json index b268b3a0b..cdc1c6941 100644 --- a/packages/udt/package.json +++ b/packages/udt/package.json @@ -23,7 +23,7 @@ } }, "scripts": { - "test": "jest", + "test": "vitest", "build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json && copyfiles -u 2 misc/basedirs/**/* .", "lint": "eslint ./src", "format": "prettier --write . && eslint --fix ./src" diff --git a/packages/udt/src/udt/index.test.ts b/packages/udt/src/udt/index.test.ts new file mode 100644 index 000000000..00433e894 --- /dev/null +++ b/packages/udt/src/udt/index.test.ts @@ -0,0 +1,1001 @@ +import { ccc } from "@ckb-ccc/core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Udt } from "./index.js"; + +let client: ccc.Client; +let signer: ccc.Signer; +let lock: ccc.Script; +let type: ccc.Script; +let udt: Udt; + +beforeEach(async () => { + client = new ccc.ClientPublicTestnet(); + signer = new ccc.SignerCkbPublicKey( + client, + "0x026f3255791f578cc5e38783b6f2d87d4709697b797def6bf7b3b9af4120e2bfd9", + ); + lock = (await signer.getRecommendedAddressObj()).script; + + type = await ccc.Script.fromKnownScript( + client, + ccc.KnownScript.XUdt, + "0xf8f94a13dfe1b87c10312fb9678ab5276eefbe1e0b2c62b4841b1f393494eff2", + ); + + // Create UDT instance + udt = new Udt( + { + txHash: + "0x4e2e832e0b1e7b5994681b621b00c1e65f577ee4b440ef95fa07db9bb3d50269", + index: 0, + }, + type, + ); +}); + +describe("Udt", () => { + describe("completeInputsByBalance", () => { + // Mock cells with 100 UDT each (10 cells total = 1000 UDT) + let mockUdtCells: ccc.Cell[]; + + beforeEach(async () => { + // Create mock cells after type is initialized + mockUdtCells = Array.from({ length: 10 }, (_, i) => + ccc.Cell.from({ + outPoint: { + txHash: `0x${"0".repeat(63)}${i.toString(16)}`, + index: 0, + }, + cellOutput: { + capacity: ccc.fixedPointFrom(142), + lock, + type, + }, + outputData: ccc.numLeToBytes(100, 16), // 100 UDT tokens + }), + ); + }); + + beforeEach(() => { + // Mock the findCells method to return our mock UDT cells + vi.spyOn(signer, "findCells").mockImplementation( + async function* (filter) { + if (filter.script && ccc.Script.from(filter.script).eq(type)) { + for (const cell of mockUdtCells) { + yield cell; + } + } + }, + ); + + // Mock client.getCell to return the cell data for inputs + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + const cell = mockUdtCells.find((c) => c.outPoint.eq(outPoint)); + return cell; + }); + }); + + it("should return 0 when no UDT balance is needed", async () => { + const tx = ccc.Transaction.from({ + outputs: [], + }); + + const { addedCount } = await udt.completeInputsByBalance(tx, signer); + expect(addedCount).toBe(0); + }); + + it("should collect exactly the required UDT balance", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + type, + }, + ], + outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT + }); + + const { addedCount } = await udt.completeInputsByBalance(tx, signer); + + // Should add 2 cells (200 UDT total) to have at least 2 inputs + expect(addedCount).toBe(2); + expect(tx.inputs.length).toBe(2); + + // Verify the inputs are UDT cells + const inputBalance = await udt.getInputsBalance(tx, client); + expect(inputBalance).toBe(ccc.numFrom(200)); + }); + + it("should collect exactly one cell when amount matches exactly", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + type, + }, + ], + outputsData: [ccc.numLeToBytes(100, 16)], // Need exactly 100 UDT + }); + + const { addedCount } = await udt.completeInputsByBalance(tx, signer); + + // Should add only 1 cell since it matches exactly + expect(addedCount).toBe(1); + expect(tx.inputs.length).toBe(1); + + const inputBalance = await udt.getInputsBalance(tx, client); + expect(inputBalance).toBe(ccc.numFrom(100)); + }); + + it("should handle balanceTweak parameter", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + type, + }, + ], + outputsData: [ccc.numLeToBytes(100, 16)], // Need 100 UDT + }); + + // Add 50 extra UDT requirement via balanceTweak + const { addedCount } = await udt.completeInputsByBalance(tx, signer, 50); + + // Should add 2 cells to cover 150 UDT total requirement + expect(addedCount).toBe(2); + expect(tx.inputs.length).toBe(2); + + const inputBalance = await udt.getInputsBalance(tx, client); + expect(inputBalance).toBe(ccc.numFrom(200)); + }); + + it("should return 0 when existing inputs already satisfy the requirement", async () => { + const tx = ccc.Transaction.from({ + inputs: [ + { + previousOutput: mockUdtCells[0].outPoint, + }, + { + previousOutput: mockUdtCells[1].outPoint, + }, + ], + outputs: [ + { + lock, + type, + }, + ], + outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT, already have 200 + }); + + const { addedCount } = await udt.completeInputsByBalance(tx, signer); + + // Should not add any inputs since we already have enough + expect(addedCount).toBe(0); + expect(tx.inputs.length).toBe(2); + }); + + it("should throw error when insufficient UDT balance available", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + type, + }, + ], + outputsData: [ccc.numLeToBytes(1500, 16)], // Need 1500 UDT, only have 1000 available + }); + + await expect(udt.completeInputsByBalance(tx, signer)).rejects.toThrow( + "Insufficient coin, need 500 extra coin", + ); + }); + + it("should handle multiple UDT outputs correctly", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + type, + }, + { + lock, + type, + }, + ], + outputsData: [ + ccc.numLeToBytes(100, 16), // First output: 100 UDT + ccc.numLeToBytes(150, 16), // Second output: 150 UDT + ], // Total: 250 UDT needed + }); + + const { addedCount } = await udt.completeInputsByBalance(tx, signer); + + // Should add 3 cells to cover 250 UDT requirement (300 UDT total) + expect(addedCount).toBe(3); + expect(tx.inputs.length).toBe(3); + + const inputBalance = await udt.getInputsBalance(tx, client); + expect(inputBalance).toBe(ccc.numFrom(300)); + + const outputBalance = await udt.getOutputsBalance(tx, client); + expect(outputBalance).toBe(ccc.numFrom(250)); + }); + + it("should skip cells that are already used as inputs", async () => { + // Pre-add one of the mock cells as input + const tx = ccc.Transaction.from({ + inputs: [ + { + previousOutput: mockUdtCells[0].outPoint, + }, + ], + outputs: [ + { + lock, + type, + }, + ], + outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT, already have 100 + }); + + const { addedCount } = await udt.completeInputsByBalance(tx, signer); + + // Should add 1 more cell (since we already have 1 input with 100 UDT) + expect(addedCount).toBe(1); + expect(tx.inputs.length).toBe(2); + + const inputBalance = await udt.getInputsBalance(tx, client); + expect(inputBalance).toBe(ccc.numFrom(200)); + }); + + it("should add one cell when user needs less than one cell", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { + lock, + type, + }, + ], + outputsData: [ccc.numLeToBytes(50, 16)], // Need only 50 UDT (less than one cell) + }); + + const { addedCount } = await udt.completeInputsByBalance(tx, signer); + + // UDT completeInputsByBalance adds minimum inputs needed + expect(addedCount).toBe(1); + expect(tx.inputs.length).toBe(1); + + const inputBalance = await udt.getInputsBalance(tx, client); + expect(inputBalance).toBe(ccc.numFrom(100)); + }); + }); + + describe("completeInputsAll", () => { + // Mock cells with 100 UDT each (5 cells total = 500 UDT) + let mockUdtCells: ccc.Cell[]; + + beforeEach(async () => { + // Create mock cells after type is initialized + mockUdtCells = Array.from({ length: 5 }, (_, i) => + ccc.Cell.from({ + outPoint: { + txHash: `0x${"a".repeat(63)}${i.toString(16)}`, + index: 0, + }, + cellOutput: { + capacity: ccc.fixedPointFrom(142 + i * 10), // Varying capacity: 142, 152, 162, 172, 182 + lock, + type, + }, + outputData: ccc.numLeToBytes(100, 16), // 100 UDT tokens each + }), + ); + }); + + beforeEach(() => { + // Mock the findCells method to return our mock UDT cells + vi.spyOn(signer, "findCells").mockImplementation( + async function* (filter) { + if (filter.script && ccc.Script.from(filter.script).eq(type)) { + for (const cell of mockUdtCells) { + yield cell; + } + } + }, + ); + + // Mock client.getCell to return the cell data for inputs + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + const cell = mockUdtCells.find((c) => c.outPoint.eq(outPoint)); + return cell; + }); + }); + + it("should add all available UDT cells to empty transaction", async () => { + const tx = ccc.Transaction.from({ + outputs: [], + }); + + const { tx: completedTx, addedCount } = await udt.completeInputsAll( + tx, + signer, + ); + + // Should add all 5 available UDT cells + expect(addedCount).toBe(5); + expect(completedTx.inputs.length).toBe(5); + + // Verify total UDT balance is 500 (5 cells × 100 UDT each) + const inputBalance = await udt.getInputsBalance(completedTx, client); + expect(inputBalance).toBe(ccc.numFrom(500)); + + // Verify all cells were added by checking outpoints + const addedOutpoints = completedTx.inputs.map( + (input) => input.previousOutput, + ); + for (const cell of mockUdtCells) { + expect(addedOutpoints.some((op) => op.eq(cell.outPoint))).toBe(true); + } + }); + + it("should add all available UDT cells to transaction with outputs", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { lock, type }, + { lock, type }, + ], + outputsData: [ + ccc.numLeToBytes(150, 16), // 150 UDT + ccc.numLeToBytes(200, 16), // 200 UDT + ], // Total: 350 UDT needed + }); + + const { tx: completedTx, addedCount } = await udt.completeInputsAll( + tx, + signer, + ); + + // Should add all 5 available UDT cells regardless of output requirements + expect(addedCount).toBe(5); + expect(completedTx.inputs.length).toBe(5); + + // Verify total UDT balance is 500 (all available) + const inputBalance = await udt.getInputsBalance(completedTx, client); + expect(inputBalance).toBe(ccc.numFrom(500)); + + // Verify output balance is still 350 + const outputBalance = await udt.getOutputsBalance(completedTx, client); + expect(outputBalance).toBe(ccc.numFrom(350)); + + // Should have 150 UDT excess balance (500 - 350) + const balanceBurned = await udt.getBalanceBurned(completedTx, client); + expect(balanceBurned).toBe(ccc.numFrom(150)); + }); + + it("should skip cells already used as inputs", async () => { + // Pre-add 2 of the mock cells as inputs + const tx = ccc.Transaction.from({ + inputs: [ + { previousOutput: mockUdtCells[0].outPoint }, + { previousOutput: mockUdtCells[1].outPoint }, + ], + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(100, 16)], + }); + + const { tx: completedTx, addedCount } = await udt.completeInputsAll( + tx, + signer, + ); + + // Should add the remaining 3 cells (cells 2, 3, 4) + expect(addedCount).toBe(3); + expect(completedTx.inputs.length).toBe(5); // 2 existing + 3 added + + // Verify total UDT balance is still 500 (all 5 cells) + const inputBalance = await udt.getInputsBalance(completedTx, client); + expect(inputBalance).toBe(ccc.numFrom(500)); + }); + + it("should return 0 when all UDT cells are already used as inputs", async () => { + // Pre-add all mock cells as inputs + const tx = ccc.Transaction.from({ + inputs: mockUdtCells.map((cell) => ({ previousOutput: cell.outPoint })), + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(100, 16)], + }); + + const { tx: completedTx, addedCount } = await udt.completeInputsAll( + tx, + signer, + ); + + // Should not add any new inputs + expect(addedCount).toBe(0); + expect(completedTx.inputs.length).toBe(5); // Same as before + + // Verify total UDT balance is still 500 + const inputBalance = await udt.getInputsBalance(completedTx, client); + expect(inputBalance).toBe(ccc.numFrom(500)); + }); + + it("should handle transaction with no UDT outputs", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { lock }, // Non-UDT output + ], + outputsData: ["0x"], + }); + + const { tx: completedTx, addedCount } = await udt.completeInputsAll( + tx, + signer, + ); + + // Should add all 5 UDT cells even though no UDT outputs + expect(addedCount).toBe(5); + expect(completedTx.inputs.length).toBe(5); + + // All 500 UDT will be "burned" since no UDT outputs + const balanceBurned = await udt.getBalanceBurned(completedTx, client); + expect(balanceBurned).toBe(ccc.numFrom(500)); + }); + + it("should work with mixed input types", async () => { + // Create a non-UDT cell + const nonUdtCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "f".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(1000), + lock, + // No type script + }, + outputData: "0x", + }); + + // Pre-add the non-UDT cell as input + const tx = ccc.Transaction.from({ + inputs: [{ previousOutput: nonUdtCell.outPoint }], + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(100, 16)], + }); + + // Mock getCell to handle both UDT and non-UDT cells + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + const outPointObj = ccc.OutPoint.from(outPoint); + if (outPointObj.eq(nonUdtCell.outPoint)) { + return nonUdtCell; + } + return mockUdtCells.find((c) => c.outPoint.eq(outPointObj)); + }); + + const { tx: completedTx, addedCount } = await udt.completeInputsAll( + tx, + signer, + ); + + // Should add all 5 UDT cells + expect(addedCount).toBe(5); + expect(completedTx.inputs.length).toBe(6); // 1 non-UDT + 5 UDT + + // Verify only UDT balance is counted + const inputBalance = await udt.getInputsBalance(completedTx, client); + expect(inputBalance).toBe(ccc.numFrom(500)); + }); + + it("should handle empty cell collection gracefully", async () => { + // Mock findCells to return no cells + vi.spyOn(signer, "findCells").mockImplementation(async function* () { + // Return no cells + }); + + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(100, 16)], + }); + + const { tx: completedTx, addedCount } = await udt.completeInputsAll( + tx, + signer, + ); + + // Should not add any inputs + expect(addedCount).toBe(0); + expect(completedTx.inputs.length).toBe(0); + + // UDT balance should be 0 + const inputBalance = await udt.getInputsBalance(completedTx, client); + expect(inputBalance).toBe(ccc.numFrom(0)); + }); + }); + + describe("getInputsBalance", () => { + it("should calculate total UDT balance from inputs", async () => { + const mockCells = [ + ccc.Cell.from({ + outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, + outputData: ccc.numLeToBytes(100, 16), // 100 UDT + }), + ccc.Cell.from({ + outPoint: { txHash: "0x" + "1".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, + outputData: ccc.numLeToBytes(200, 16), // 200 UDT + }), + ]; + + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + return mockCells.find((c) => c.outPoint.eq(outPoint)); + }); + + const tx = ccc.Transaction.from({ + inputs: [ + { previousOutput: mockCells[0].outPoint }, + { previousOutput: mockCells[1].outPoint }, + ], + }); + + const balance = await udt.getInputsBalance(tx, client); + expect(balance).toBe(ccc.numFrom(300)); // 100 + 200 + }); + + it("should ignore inputs without matching type script", async () => { + const mockCells = [ + ccc.Cell.from({ + outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, + outputData: ccc.numLeToBytes(100, 16), // 100 UDT + }), + ccc.Cell.from({ + outPoint: { txHash: "0x" + "1".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(142), lock }, // No type script + outputData: "0x", + }), + ]; + + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + return mockCells.find((c) => c.outPoint.eq(outPoint)); + }); + + const tx = ccc.Transaction.from({ + inputs: [ + { previousOutput: mockCells[0].outPoint }, + { previousOutput: mockCells[1].outPoint }, + ], + }); + + const balance = await udt.getInputsBalance(tx, client); + expect(balance).toBe(ccc.numFrom(100)); // Only the UDT cell + }); + }); + + describe("getOutputsBalance", () => { + it("should calculate total UDT balance from outputs", async () => { + const tx = ccc.Transaction.from({ + outputs: [ + { lock, type }, + { lock, type }, + { lock }, // No type script + ], + outputsData: [ + ccc.numLeToBytes(100, 16), // 100 UDT + ccc.numLeToBytes(200, 16), // 200 UDT + "0x", // Not UDT + ], + }); + + const balance = await udt.getOutputsBalance(tx, client); + expect(balance).toBe(ccc.numFrom(300)); // 100 + 200, ignoring non-UDT output + }); + + it("should return 0 when no UDT outputs", async () => { + const tx = ccc.Transaction.from({ + outputs: [{ lock }], // No type script + outputsData: ["0x"], + }); + + const balance = await udt.getOutputsBalance(tx, client); + expect(balance).toBe(ccc.numFrom(0)); + }); + }); + + describe("completeChangeToLock", () => { + let mockUdtCells: ccc.Cell[]; + + beforeEach(() => { + mockUdtCells = Array.from({ length: 5 }, (_, i) => + ccc.Cell.from({ + outPoint: { + txHash: `0x${"0".repeat(63)}${i.toString(16)}`, + index: 0, + }, + cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, + outputData: ccc.numLeToBytes(100, 16), // 100 UDT each + }), + ); + + vi.spyOn(signer, "findCells").mockImplementation( + async function* (filter) { + if (filter.script && ccc.Script.from(filter.script).eq(type)) { + for (const cell of mockUdtCells) { + yield cell; + } + } + }, + ); + + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + return mockUdtCells.find((c) => c.outPoint.eq(outPoint)); + }); + }); + + it("should add change output when there's excess UDT balance", async () => { + const changeLock = ccc.Script.from({ + codeHash: "0x" + "9".repeat(64), + hashType: "type", + args: "0x1234", + }); + + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT + }); + + const completedTx = await udt.completeChangeToLock( + tx, + signer, + changeLock, + ); + + // Should have original output + change output + expect(completedTx.outputs.length).toBe(2); + expect(completedTx.outputs[1].lock.eq(changeLock)).toBe(true); + expect(completedTx.outputs[1].type?.eq(type)).toBe(true); + + // Change should be 50 UDT (200 input - 150 output) + const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[1]); + expect(changeAmount).toBe(ccc.numFrom(50)); + }); + + it("should not add change when no excess balance", async () => { + const changeLock = ccc.Script.from({ + codeHash: "0x" + "9".repeat(64), + hashType: "type", + args: "0x1234", + }); + + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(200, 16)], // Need exactly 200 UDT + }); + + const completedTx = await udt.completeChangeToLock( + tx, + signer, + changeLock, + ); + + // Should only have original output + expect(completedTx.outputs.length).toBe(1); + }); + }); + + describe("completeBy", () => { + it("should use signer's recommended address for change", async () => { + const mockUdtCells = Array.from({ length: 3 }, (_, i) => + ccc.Cell.from({ + outPoint: { + txHash: `0x${"0".repeat(63)}${i.toString(16)}`, + index: 0, + }, + cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, + outputData: ccc.numLeToBytes(100, 16), + }), + ); + + vi.spyOn(signer, "findCells").mockImplementation( + async function* (filter) { + if (filter.script && ccc.Script.from(filter.script).eq(type)) { + for (const cell of mockUdtCells) { + yield cell; + } + } + }, + ); + + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + return mockUdtCells.find((c) => c.outPoint.eq(outPoint)); + }); + + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(150, 16)], + }); + + const completedTx = await udt.completeBy(tx, signer); + + // Should have change output with signer's lock + expect(completedTx.outputs.length).toBe(2); + expect(completedTx.outputs[1].lock.eq(lock)).toBe(true); // Same as signer's lock + }); + }); + + describe("complete method with capacity handling", () => { + let mockUdtCells: ccc.Cell[]; + + beforeEach(() => { + // Create mock cells with different capacity values + mockUdtCells = [ + // Cell 0: 100 UDT, 142 CKB capacity (minimum for UDT cell) + ccc.Cell.from({ + outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, + outputData: ccc.numLeToBytes(100, 16), + }), + // Cell 1: 100 UDT, 200 CKB capacity (extra capacity) + ccc.Cell.from({ + outPoint: { txHash: "0x" + "1".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(200), lock, type }, + outputData: ccc.numLeToBytes(100, 16), + }), + // Cell 2: 100 UDT, 300 CKB capacity (more extra capacity) + ccc.Cell.from({ + outPoint: { txHash: "0x" + "2".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(300), lock, type }, + outputData: ccc.numLeToBytes(100, 16), + }), + ]; + + vi.spyOn(signer, "findCells").mockImplementation( + async function* (filter) { + if (filter.script && ccc.Script.from(filter.script).eq(type)) { + for (const cell of mockUdtCells) { + yield cell; + } + } + }, + ); + + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + return mockUdtCells.find((c) => c.outPoint.eq(outPoint)); + }); + }); + + it("should add extra UDT cells when change output requires additional capacity", async () => { + const changeLock = ccc.Script.from({ + codeHash: "0x" + "9".repeat(64), + hashType: "type", + args: "0x1234", + }); + + // Create a transaction that needs 50 UDT (less than one cell) + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(50, 16)], + }); + + const completedTx = await udt.completeChangeToLock( + tx, + signer, + changeLock, + ); + + // Should have original output + change output + expect(completedTx.outputs.length).toBe(2); + + // Verify inputs were added to cover both UDT balance and capacity requirements + expect(completedTx.inputs.length).toBe(2); + + // Check that change output has correct UDT balance (should be input - 50) + const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[1]); + const inputBalance = await udt.getInputsBalance(completedTx, client); + expect(changeAmount).toBe(inputBalance - ccc.numFrom(50)); + + // Verify change output has correct type script + expect(completedTx.outputs[1].type?.eq(type)).toBe(true); + expect(completedTx.outputs[1].lock.eq(changeLock)).toBe(true); + + // Key assertion: verify that capacity is sufficient (positive fee) + const fee = await completedTx.getFee(client); + expect(fee).toBeGreaterThanOrEqual(ccc.Zero); + }); + + it("should handle capacity tweak parameter in completeInputsByBalance", async () => { + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(50, 16)], // Need 50 UDT + }); + + // Add extra capacity requirement via capacityTweak that's reasonable + const extraCapacityNeeded = ccc.fixedPointFrom(1000); // Reasonable capacity requirement + const { addedCount } = await udt.completeInputsByBalance( + tx, + signer, + ccc.Zero, // No extra UDT balance needed + extraCapacityNeeded, // Extra capacity needed + ); + + // Should add cells to cover the capacity requirement + expect(addedCount).toBeGreaterThan(2); + + // Should have added at least one cell with capacity + expect(await udt.getInputsBalance(tx, client)).toBeGreaterThan(ccc.Zero); + }); + + it("should handle the two-phase capacity completion in complete method", async () => { + const changeLock = ccc.Script.from({ + codeHash: "0x" + "9".repeat(64), + hashType: "type", + args: "0x1234", + }); + + // Create a transaction that will need change + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(50, 16)], // Need 50 UDT, will have 50 UDT change + }); + + // Track the calls to completeInputsByBalance to verify two-phase completion + const completeInputsByBalanceSpy = vi.spyOn( + udt, + "completeInputsByBalance", + ); + + const completedTx = await udt.completeChangeToLock( + tx, + signer, + changeLock, + ); + + // Should have called completeInputsByBalance twice: + // 1. First call: initial UDT balance completion + // 2. Second call: with extraCapacity for change output + expect(completeInputsByBalanceSpy).toHaveBeenCalledTimes(2); + + // Verify the second call included extraCapacity parameter + const secondCall = completeInputsByBalanceSpy.mock.calls[1]; + expect(secondCall[2]).toBe(ccc.Zero); // balanceTweak should be 0 + expect(secondCall[3]).toBeGreaterThan(ccc.Zero); // capacityTweak should be > 0 (change output capacity) + + // Should have change output + expect(completedTx.outputs.length).toBe(2); + const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[1]); + expect(changeAmount).toBe( + (await udt.getInputsBalance(completedTx, client)) - ccc.numFrom(50), + ); // 100 input - 50 output = 50 change + + completeInputsByBalanceSpy.mockRestore(); + }); + + it("should handle completeChangeToOutput correctly", async () => { + // Create a transaction with an existing UDT output that will receive change + const tx = ccc.Transaction.from({ + outputs: [ + { lock, type }, // This will be the change output + ], + outputsData: [ + ccc.numLeToBytes(50, 16), // Initial amount in change output + ], + }); + + const completedTx = await udt.completeChangeToOutput(tx, signer, 0); // Use first output as change + + // Should have added inputs + expect(completedTx.inputs.length).toBeGreaterThan(0); + + // The first output should now contain the original amount plus any excess from inputs + const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[0]); + const inputBalance = await udt.getInputsBalance(completedTx, client); + + // Change output should have: original amount + excess from inputs + // Since we only have one output, all input balance should go to it + expect(changeAmount).toBe(inputBalance); + expect(changeAmount).toBeGreaterThan(ccc.numFrom(50)); // More than the original amount + }); + + it("should throw error when change output is not a UDT cell", async () => { + const tx = ccc.Transaction.from({ + outputs: [{ lock }], // No type script - not a UDT cell + outputsData: ["0x"], + }); + + await expect(udt.completeChangeToOutput(tx, signer, 0)).rejects.toThrow( + "Change output must be a UDT cell", + ); + }); + + it("should handle insufficient capacity gracefully", async () => { + // Mock to return cells with very low capacity + const lowCapacityCells = [ + ccc.Cell.from({ + outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, + cellOutput: { capacity: ccc.fixedPointFrom(61), lock, type }, // Very low capacity + outputData: ccc.numLeToBytes(100, 16), + }), + ]; + + vi.spyOn(signer, "findCells").mockImplementation( + async function* (filter) { + if (filter.script && ccc.Script.from(filter.script).eq(type)) { + for (const cell of lowCapacityCells) { + yield cell; + } + } + }, + ); + + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + return lowCapacityCells.find((c) => c.outPoint.eq(outPoint)); + }); + + const changeLock = ccc.Script.from({ + codeHash: "0x" + "9".repeat(64), + hashType: "type", + args: "0x1234", + }); + + const tx = ccc.Transaction.from({ + outputs: [{ lock, type }], + outputsData: [ccc.numLeToBytes(50, 16)], + }); + + // Should still complete successfully even with capacity constraints + // The UDT logic should focus on UDT balance completion + const completedTx = await udt.completeChangeToLock( + tx, + signer, + changeLock, + ); + + expect(completedTx.inputs.length).toBe(1); + expect(completedTx.outputs.length).toBe(2); // Original + change + + expect(await completedTx.getFee(client)).toBeLessThan(0n); + }); + + it("should handle capacity calculation when transaction has non-UDT inputs with high capacity", async () => { + // Create a non-UDT cell with very high capacity + const nonUdtCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "f".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(10000), // Very high capacity (100 CKB) + lock, + // No type script - this is a regular CKB cell + }, + outputData: "0x", // Empty data + }); + + // Create a transaction that already has the non-UDT input + const tx = ccc.Transaction.from({ + inputs: [ + { previousOutput: nonUdtCell.outPoint }, // Pre-existing non-UDT input + ], + outputs: [ + { lock, type }, // UDT output requiring 50 UDT + ], + outputsData: [ + ccc.numLeToBytes(50, 16), // Need 50 UDT + ], + }); + + // Mock getCell to return both UDT and non-UDT cells + vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { + const outPointObj = ccc.OutPoint.from(outPoint); + if (outPointObj.eq(nonUdtCell.outPoint)) { + return nonUdtCell; + } + return mockUdtCells.find((c) => c.outPoint.eq(outPointObj)); + }); + + const resultTx = await udt.completeBy(tx, signer); + + // Should add exactly 2 UDT cell to satisfy the 50 UDT requirement & extra occupation from the change cell + expect(resultTx.inputs.length).toBe(3); // 1 non-UDT + 2 UDT + + // Verify UDT balance is satisfied + const inputBalance = await udt.getInputsBalance(resultTx, client); + expect(inputBalance).toBe(ccc.numFrom(200)); + }); + }); +}); diff --git a/packages/udt/src/udt/index.ts b/packages/udt/src/udt/index.ts index 005df649d..715a964cb 100644 --- a/packages/udt/src/udt/index.ts +++ b/packages/udt/src/udt/index.ts @@ -1,6 +1,194 @@ import { ccc } from "@ckb-ccc/core"; import { ssri } from "@ckb-ccc/ssri"; +/** + * Error thrown when there are insufficient UDT coins to complete a transaction. + * This error provides detailed information about the shortfall, including the + * exact amount needed, the UDT type script, and an optional custom reason. + * + * @public + * @category Error + * @category UDT + * + * @example + * ```typescript + * // This error is typically thrown automatically by UDT methods + * try { + * await udt.completeInputsByBalance(tx, signer); + * } catch (error) { + * if (error instanceof ErrorUdtInsufficientCoin) { + * console.log(`Error: ${error.message}`); + * console.log(`Shortfall: ${error.amount} UDT tokens`); + * console.log(`UDT type script: ${error.type.toHex()}`); + * } + * } + * ``` + */ +export class ErrorUdtInsufficientCoin extends Error { + /** + * The amount of UDT coins that are insufficient (shortfall amount). + * This represents how many more UDT tokens are needed to complete the operation. + */ + public readonly amount: ccc.Num; + + /** + * The type script of the UDT that has insufficient balance. + * This identifies which specific UDT token is lacking sufficient funds. + */ + public readonly type: ccc.Script; + + /** + * Creates a new ErrorUdtInsufficientCoin instance. + * + * @param info - Configuration object for the error + * @param info.amount - The amount of UDT coins that are insufficient (shortfall amount) + * @param info.type - The type script of the UDT that has insufficient balance + * @param info.reason - Optional custom reason message. If not provided, a default message will be generated + * + * @example + * ```typescript + * // Manual creation (typically not needed as the error is thrown automatically) + * const error = new ErrorUdtInsufficientCoin({ + * amount: ccc.numFrom(1000), + * type: udtScript, + * reason: "Custom insufficient balance message" + * }); + * + * // More commonly, catch the error when it's thrown by UDT methods + * try { + * const result = await udt.completeInputsByBalance(tx, signer); + * } catch (error) { + * if (error instanceof ErrorUdtInsufficientCoin) { + * // Handle the insufficient balance error + * console.error(`Insufficient UDT: need ${error.amount} more tokens`); + * } + * } + * ``` + * + * @remarks + * The error message format depends on whether a custom reason is provided: + * - With custom reason: "Insufficient coin, {custom reason}" + * - Without custom reason: "Insufficient coin, need {amount} extra coin" + */ + constructor(info: { + amount: ccc.NumLike; + type: ccc.ScriptLike; + reason?: string; + }) { + const amount = ccc.numFrom(info.amount); + const type = ccc.Script.from(info.type); + super(`Insufficient coin, ${info.reason ?? `need ${amount} extra coin`}`); + this.amount = amount; + this.type = type; + } +} + +/** + * Configuration object type for UDT instances. + * This type defines the optional configuration parameters that can be passed + * when creating a UDT instance to customize its behavior. + * + * @public + * @category Configuration + * @category UDT + */ +export type UdtConfigLike = { + /** + * Optional SSRI executor instance for advanced UDT operations. + * When provided, enables SSRI-compliant features like metadata queries + * and advanced transfer operations. + */ + executor?: ssri.Executor | null; + + /** + * Optional custom search filter for finding UDT cells. + * If not provided, a default filter will be created that matches + * cells with the UDT's type script and valid output data length. + */ + filter?: ccc.ClientIndexerSearchKeyFilterLike | null; +}; + +/** + * Configuration class for UDT instances. + * This class provides a structured way to handle UDT configuration parameters + * and includes factory methods for creating instances from configuration-like objects. + * + * @public + * @category Configuration + * @category UDT + * + * @example + * ```typescript + * // Create configuration with executor + * const config = new UdtConfig(ssriExecutor); + * + * // Create configuration with both executor and filter + * const config = new UdtConfig( + * ssriExecutor, + * ccc.ClientIndexerSearchKeyFilter.from({ + * script: udtScript, + * outputDataLenRange: [16, 32] + * }) + * ); + * + * // Create from configuration-like object + * const config = UdtConfig.from({ + * executor: ssriExecutor, + * filter: { script: udtScript, outputDataLenRange: [16, "0xffffffff"] } + * }); + * ``` + */ +export class UdtConfig { + /** + * Creates a new UdtConfig instance. + * + * @param executor - Optional SSRI executor for advanced UDT operations + * @param filter - Optional search filter for finding UDT cells + */ + constructor( + public readonly executor?: ssri.Executor, + public readonly filter?: ccc.ClientIndexerSearchKeyFilter, + ) {} + + /** + * Creates a UdtConfig instance from a configuration-like object. + * This factory method provides a convenient way to create UdtConfig instances + * from plain objects, automatically converting filter-like objects to proper + * ClientIndexerSearchKeyFilter instances. + * + * @param configLike - Configuration-like object containing executor and/or filter + * @returns A new UdtConfig instance with the specified configuration + * + * @example + * ```typescript + * // Create from object with executor only + * const config = UdtConfig.from({ executor: ssriExecutor }); + * + * // Create from object with filter only + * const config = UdtConfig.from({ + * filter: { + * script: udtScript, + * outputDataLenRange: [16, "0xffffffff"] + * } + * }); + * + * // Create from object with both + * const config = UdtConfig.from({ + * executor: ssriExecutor, + * filter: { script: udtScript, outputDataLenRange: [16, 32] } + * }); + * ``` + */ + static from(configLike: UdtConfigLike) { + return new UdtConfig( + configLike.executor ?? undefined, + configLike.filter + ? ccc.ClientIndexerSearchKeyFilter.from(configLike.filter) + : undefined, + ); + } +} + /** * Represents a User Defined Token (UDT) script compliant with the SSRI protocol. * @@ -13,35 +201,144 @@ import { ssri } from "@ckb-ccc/ssri"; * @category Token */ export class Udt extends ssri.Trait { + /** + * The type script that uniquely identifies this UDT token. + * This script is used to distinguish UDT cells from other cell types and + * to identify which cells belong to this specific UDT token. + * + * @remarks + * The script contains: + * - `codeHash`: Hash of the UDT script code + * - `hashType`: How the code hash should be interpreted ("type" or "data") + * - `args`: Arguments that make this UDT unique (often contains token-specific data) + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * console.log(`UDT script hash: ${udt.script.hash()}`); + * console.log(`UDT args: ${udt.script.args}`); + * + * // Check if a cell belongs to this UDT + * const isUdt = udt.isUdt(cell); + * ``` + */ public readonly script: ccc.Script; + /** + * The search filter used to find UDT cells controlled by signers. + * This filter is automatically configured to match cells with this UDT's type script + * and appropriate output data length (minimum 16 bytes for UDT balance storage). + * + * @remarks + * The filter includes: + * - `script`: Set to this UDT's type script + * - `outputDataLenRange`: [16, "0xffffffff"] to ensure valid UDT cells + * + * This filter is used internally by methods like: + * - `calculateInfo()` and `calculateBalance()` for scanning all UDT cells + * - `completeInputs()` and related methods for finding suitable input cells + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // The filter is used internally, but you can access it if needed + * console.log(`Filter script: ${udt.filter.script?.hash()}`); + * console.log(`Output data range: ${udt.filter.outputDataLenRange}`); + * + * // Manually find cells using the same filter + * for await (const cell of signer.findCells(udt.filter)) { + * console.log(`Found UDT cell with balance: ${ccc.udtBalanceFrom(cell.outputData)}`); + * } + * ``` + */ + public readonly filter: ccc.ClientIndexerSearchKeyFilter; + /** * Constructs a new UDT (User Defined Token) script instance. - * By default it is a SSRI-compliant UDT. By providing `xudtType`, it is compatible with the legacy xUDT. + * By default it is a SSRI-compliant UDT. This class supports both SSRI-compliant UDTs and legacy sUDT/xUDT standard tokens. + * + * @param code - The script code cell outpoint of the UDT. This points to the cell containing the UDT script code + * @param script - The type script of the UDT that uniquely identifies this token + * @param config - Optional configuration object for advanced settings + * @param config.executor - The SSRI executor instance for advanced UDT operations. If provided, enables SSRI-compliant features + * @param config.filter - Custom search filter for finding UDT cells. If not provided, a default filter will be created * - * @param executor - The SSRI executor instance. - * @param code - The script code cell of the UDT. - * @param script - The type script of the UDT. * @example * ```typescript - * const udt = new Udt(executor, code, script); + * // Basic UDT instance + * const udt = new Udt( + * { txHash: "0x...", index: 0 }, // code outpoint + * { codeHash: "0x...", hashType: "type", args: "0x..." } // type script + * ); + * + * // UDT with SSRI executor for advanced features + * const ssriUdt = new Udt( + * codeOutPoint, + * typeScript, + * { executor: ssriExecutor } + * ); + * + * // UDT with custom filter (advanced usage) + * const customUdt = new Udt( + * codeOutPoint, + * typeScript, + * { + * filter: { + * script: typeScript, + * outputDataLenRange: [16, 32], // Only cells with 16-32 bytes output data + * } + * } + * ); * ``` + * + * @remarks + * **Default Filter Behavior:** + * If no custom filter is provided, a default filter is created with: + * - `script`: Set to the provided UDT type script + * - `outputDataLenRange`: [16, "0xffffffff"] to match valid UDT cells + * + * **SSRI Compliance:** + * When an executor is provided, the UDT instance can use SSRI-compliant features like: + * - Advanced transfer operations + * - Metadata queries (name, symbol, decimals, icon) + * - Custom UDT logic execution + * + * **Legacy Support:** + * Even without an executor, the UDT class supports basic operations for legacy sUDT/xUDT tokens. */ constructor( code: ccc.OutPointLike, script: ccc.ScriptLike, - config?: { - executor?: ssri.Executor | null; - } | null, + config?: UdtConfigLike | null, ) { super(code, config?.executor); this.script = ccc.Script.from(script); + this.filter = ccc.ClientIndexerSearchKeyFilter.from( + config?.filter ?? { + script: this.script, + outputDataLenRange: [16, "0xffffffff"], + }, + ); } /** * Retrieves the human-readable name of the User Defined Token. + * This method queries the UDT script to get the token's display name, + * which is typically used in user interfaces and wallets. * - * @returns A promise resolving to the token's name. + * @param context - Optional script execution context for additional parameters + * @returns A promise resolving to an ExecutorResponse containing the token's name, + * or undefined if the name is not available or the script doesn't support this method + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const nameResponse = await udt.name(); + * if (nameResponse.res) { + * console.log(`Token name: ${nameResponse.res}`); + * } + * ``` */ async name( context?: ssri.ContextScript, @@ -60,8 +357,22 @@ export class Udt extends ssri.Trait { } /** - * Retrieves the symbol of the UDT. - * @returns The symbol of the UDT. + * Retrieves the symbol (ticker) of the User Defined Token. + * The symbol is typically a short abbreviation used to identify the token, + * similar to stock ticker symbols (e.g., "BTC", "ETH", "USDT"). + * + * @param context - Optional script execution context for additional parameters + * @returns A promise resolving to an ExecutorResponse containing the token's symbol, + * or undefined if the symbol is not available or the script doesn't support this method + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const symbolResponse = await udt.symbol(); + * if (symbolResponse.res) { + * console.log(`Token symbol: ${symbolResponse.res}`); + * } + * ``` */ async symbol( context?: ssri.ContextScript, @@ -85,8 +396,24 @@ export class Udt extends ssri.Trait { } /** - * Retrieves the decimals of the UDT. - * @returns The decimals of the UDT. + * Retrieves the number of decimal places for the User Defined Token. + * This value determines how the token amount should be displayed and interpreted. + * For example, if decimals is 8, then a balance of 100000000 represents 1.0 tokens. + * + * @param context - Optional script execution context for additional parameters + * @returns A promise resolving to an ExecutorResponse containing the number of decimals, + * or undefined if decimals are not specified or the script doesn't support this method + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const decimalsResponse = await udt.decimals(); + * if (decimalsResponse.res !== undefined) { + * console.log(`Token decimals: ${decimalsResponse.res}`); + * // Convert raw amount to human-readable format + * const humanReadable = rawAmount / (10 ** Number(decimalsResponse.res)); + * } + * ``` */ async decimals( context?: ssri.ContextScript, @@ -110,8 +437,25 @@ export class Udt extends ssri.Trait { } /** - * Retrieves the icon of the UDT - * @returns The icon of the UDT. + * Retrieves the icon URL or data URI for the User Defined Token. + * This can be used to display a visual representation of the token in user interfaces. + * The returned value may be a URL pointing to an image file or a data URI containing + * the image data directly. + * + * @param context - Optional script execution context for additional parameters + * @returns A promise resolving to an ExecutorResponse containing the icon URL/data, + * or undefined if no icon is available or the script doesn't support this method + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const iconResponse = await udt.icon(); + * if (iconResponse.res) { + * // Use the icon in UI + * const imgElement = document.createElement('img'); + * imgElement.src = iconResponse.res; + * } + * ``` */ async icon( context?: ssri.ContextScript, @@ -129,13 +473,67 @@ export class Udt extends ssri.Trait { return ssri.ExecutorResponse.new(undefined); } + /** + * Adds the UDT script code as a cell dependency to the transaction. + * This method ensures that the transaction includes the necessary cell dependency + * for the UDT script code, which is required for any transaction that uses this UDT. + * + * @param txLike - The transaction to add the cell dependency to + * @returns A new transaction with the UDT code cell dependency added + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // Create a basic transaction + * let tx = ccc.Transaction.from({ + * outputs: [{ lock: recipientLock, type: udt.script }], + * outputsData: [ccc.numLeToBytes(100, 16)] + * }); + * + * // Add UDT code dependency + * tx = udt.addCellDeps(tx); + * + * // Now the transaction can be completed and sent + * await tx.completeInputsByCapacity(signer); + * await tx.completeFeeBy(signer); + * ``` + * + * @remarks + * **When to Use:** + * - When manually constructing transactions that involve UDT cells + * - Before sending any transaction that creates or consumes UDT cells + * - This is automatically called by methods like `transfer()` and `mint()` + * + * **Cell Dependency Details:** + * - Adds the UDT script code outpoint as a "code" type dependency + * - This allows the transaction to reference and execute the UDT script + * - Required for script validation during transaction processing + * + * **Note:** Most high-level UDT methods automatically add this dependency, + * so manual usage is typically only needed for custom transaction construction. + */ + addCellDeps(txLike: ccc.TransactionLike): ccc.Transaction { + const tx = ccc.Transaction.from(txLike); + tx.addCellDeps({ + outPoint: this.code, + depType: "code", + }); + return tx; + } + /** * Transfers UDT to specified addresses. - * @param tx - Transfer on the basis of an existing transaction to achieve combined actions. If not provided, a new transaction will be created. - * @param transfers - The array of transfers. - * @param transfers.to - The receiver of token. - * @param transfers.amount - The amount of token to the receiver. - * @returns The transaction result. + * This method creates a transaction that transfers UDT tokens to one or more recipients. + * It can build upon an existing transaction to achieve combined actions. + * + * @param signer - The signer that will authorize and potentially pay for the transaction + * @param transfers - Array of transfer operations to perform + * @param transfers.to - The lock script of the recipient who will receive the tokens + * @param transfers.amount - The amount of tokens to transfer to this recipient (in smallest unit) + * @param tx - Optional existing transaction to build upon. If not provided, a new transaction will be created + * @returns A promise resolving to an ExecutorResponse containing the transaction with transfer operations + * * @tag Mutation - This method represents a mutation of the onchain state and will return a transaction object. * @example * ```typescript @@ -154,12 +552,12 @@ export class Udt extends ssri.Trait { * }, * ); * - * const { res: tx } = await udtTrait.transfer( + * const { res: tx } = await udt.transfer( * signer, * [{ to, amount: 100 }], * ); * - * const completedTx = udt.completeUdtBy(tx, signer); + * const completedTx = await udt.completeBy(tx, signer); * await completedTx.completeInputsByCapacity(signer); * await completedTx.completeFeeBy(signer); * const transferTxHash = await signer.sendTransaction(completedTx); @@ -208,21 +606,47 @@ export class Udt extends ssri.Trait { } resTx = ssri.ExecutorResponse.new(transfer); } - resTx.res.addCellDeps({ - outPoint: this.code, - depType: "code", - }); - return resTx; + + return resTx.map((tx) => this.addCellDeps(tx)); } /** - * Mints new tokens to specified addresses. See the example in `transfer` as they are similar. - * @param tx - Optional existing transaction to build upon - * @param mints - Array of mints - * @param mints.to - receiver of token - * @param mints.amount - amount to the receiver - * @returns The transaction containing the mint operation + * Mints new tokens to specified addresses. + * This method creates new UDT tokens and assigns them to the specified recipients. + * The minting operation requires appropriate permissions and may be restricted + * based on the UDT's implementation. + * + * @param signer - The signer that will authorize and potentially pay for the transaction + * @param mints - Array of mint operations to perform + * @param mints.to - The lock script of the recipient who will receive the minted tokens + * @param mints.amount - The amount of tokens to mint for this recipient (in smallest unit) + * @param tx - Optional existing transaction to build upon. If not provided, a new transaction will be created + * @returns A promise resolving to an ExecutorResponse containing the transaction with mint operations + * * @tag Mutation - This method represents a mutation of the onchain state + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const { script: recipientLock } = await ccc.Address.fromString(recipientAddress, signer.client); + * + * const mintResponse = await udt.mint( + * signer, + * [ + * { to: recipientLock, amount: ccc.fixedPointFrom(1000) }, // Mint 1000 tokens + * { to: anotherLock, amount: ccc.fixedPointFrom(500) } // Mint 500 tokens + * ] + * ); + * + * // Complete the transaction + * const tx = mintResponse.res; + * await tx.completeInputsByCapacity(signer); + * await tx.completeFeeBy(signer, changeLock); + * + * const txHash = await signer.sendTransaction(tx); + * ``` + * + * @throws May throw if the signer doesn't have minting permissions or if the UDT doesn't support minting */ async mint( signer: ccc.Signer, @@ -267,40 +691,803 @@ export class Udt extends ssri.Trait { } resTx = ssri.ExecutorResponse.new(mint); } - resTx.res.addCellDeps({ - outPoint: this.code, - depType: "code", + + return resTx.map((tx) => this.addCellDeps(tx)); + } + + /** + * Checks if a cell is a valid UDT cell for this token. + * A valid UDT cell must have this UDT's type script and contain at least 16 bytes of output data + * (the minimum required for storing the UDT balance as a 128-bit little-endian integer). + * + * @param cellOutputLike - The cell output to check + * @param outputData - The output data of the cell + * @returns True if the cell is a valid UDT cell for this token, false otherwise + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const cellOutput = { lock: someLock, type: udt.script }; + * const outputData = ccc.numLeToBytes(1000, 16); // 1000 UDT balance + * + * const isValid = udt.isUdt({ cellOutput, outputData }); + * console.log(`Is valid UDT cell: ${isValid}`); // true + * ``` + * + * @remarks + * The method checks two conditions: + * 1. The cell's type script matches this UDT's script + * 2. The output data is at least 16 bytes long (required for UDT balance storage) + */ + isUdt(cell: { cellOutput: ccc.CellOutputLike; outputData: ccc.HexLike }) { + return ( + (ccc.CellOutput.from(cell.cellOutput).type?.eq(this.script) ?? false) && + ccc.bytesFrom(cell.outputData).length >= 16 + ); + } + + /** + * Retrieves comprehensive information about UDT inputs in a transaction. + * This method analyzes all input cells and returns detailed statistics including + * total UDT balance, total capacity occupied, and the number of UDT cells. + * + * @param txLike - The transaction to analyze + * @param client - The client to fetch input cell data + * @returns A promise resolving to an object containing: + * - balance: Total UDT balance from all input cells + * - capacity: Total capacity occupied by all UDT input cells + * - count: Number of UDT input cells + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const tx = ccc.Transaction.from(existingTransaction); + * + * const inputsInfo = await udt.getInputsInfo(tx, client); + * console.log(`UDT inputs: ${inputsInfo.count} cells`); + * console.log(`Total UDT balance: ${inputsInfo.balance}`); + * console.log(`Total capacity: ${inputsInfo.capacity}`); + * ``` + * + * @remarks + * This method provides more comprehensive information than `getInputsBalance`, + * making it useful for transaction analysis, fee calculation, and UI display. + * Only cells with this UDT's type script are included in the statistics. + */ + async getInputsInfo( + txLike: ccc.TransactionLike, + client: ccc.Client, + ): Promise<{ + balance: ccc.Num; + capacity: ccc.Num; + count: number; + }> { + const tx = ccc.Transaction.from(txLike); + const [balance, capacity, count] = await ccc.reduceAsync( + tx.inputs, + async (acc, input) => { + const { cellOutput, outputData } = await input.getCell(client); + if (!this.isUdt({ cellOutput, outputData })) { + return acc; + } + + return [ + acc[0] + ccc.udtBalanceFrom(outputData), + acc[1] + cellOutput.capacity, + acc[2] + 1, + ]; + }, + [ccc.Zero, ccc.Zero, 0], + ); + + return { + balance, + capacity, + count, + }; + } + + /** + * Calculates the total UDT balance from all inputs in a transaction. + * This method examines each input cell and sums up the UDT amounts + * for cells that have this UDT's type script. + * + * @param txLike - The transaction to analyze + * @param client - The client to fetch input cell data + * @returns A promise resolving to the total UDT balance from all inputs + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const tx = ccc.Transaction.from(existingTransaction); + * + * const inputBalance = await udt.getInputsBalance(tx, client); + * console.log(`Total UDT input balance: ${inputBalance}`); + * ``` + * + * @remarks + * This method only counts inputs that have the same type script as this UDT instance. + * Inputs without a type script or with different type scripts are ignored. + */ + async getInputsBalance( + txLike: ccc.TransactionLike, + client: ccc.Client, + ): Promise { + return (await this.getInputsInfo(txLike, client)).balance; + } + + /** + * Retrieves comprehensive information about UDT outputs in a transaction. + * This method analyzes all output cells and returns detailed statistics including + * total UDT balance, total capacity occupied, and the number of UDT cells. + * + * @param txLike - The transaction to analyze + * @param _client - The client parameter (unused for outputs since data is already available) + * @returns A promise resolving to an object containing: + * - balance: Total UDT balance from all output cells + * - capacity: Total capacity occupied by all UDT output cells + * - count: Number of UDT output cells + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const tx = ccc.Transaction.from({ + * outputs: [ + * { lock: recipientLock, type: udt.script }, + * { lock: changeLock, type: udt.script } + * ], + * outputsData: [ + * ccc.numLeToBytes(1000, 16), // 1000 UDT to recipient + * ccc.numLeToBytes(500, 16) // 500 UDT as change + * ] + * }); + * + * const outputsInfo = await udt.getOutputsInfo(tx, client); + * console.log(`UDT outputs: ${outputsInfo.count} cells`); + * console.log(`Total UDT balance: ${outputsInfo.balance}`); // 1500 + * console.log(`Total capacity: ${outputsInfo.capacity}`); + * ``` + * + * @remarks + * This method provides more comprehensive information than `getOutputsBalance`, + * making it useful for transaction validation, analysis, and UI display. + * Only cells with this UDT's type script are included in the statistics. + * This is an async method for consistency with `getInputsInfo`, though it doesn't + * actually need to fetch data since output information is already available. + */ + async getOutputsInfo( + txLike: ccc.TransactionLike, + _client: ccc.Client, + ): Promise<{ + balance: ccc.Num; + capacity: ccc.Num; + count: number; + }> { + const tx = ccc.Transaction.from(txLike); + const [balance, capacity, count] = tx.outputs.reduce( + (acc, output, i) => { + if ( + !this.isUdt({ cellOutput: output, outputData: tx.outputsData[i] }) + ) { + return acc; + } + + return [ + acc[0] + ccc.udtBalanceFrom(tx.outputsData[i]), + acc[1] + output.capacity, + acc[2] + 1, + ]; + }, + [ccc.Zero, ccc.Zero, 0], + ); + + return { + balance, + capacity, + count, + }; + } + + /** + * Calculates the total UDT balance from all outputs in a transaction. + * This method examines each output cell and sums up the UDT amounts + * for cells that have this UDT's type script. + * + * @param txLike - The transaction to analyze + * @param client - The client parameter (passed to getOutputsInfo for consistency) + * @returns A promise resolving to the total UDT balance from all outputs + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const tx = ccc.Transaction.from({ + * outputs: [ + * { lock: recipientLock, type: udt.script }, + * { lock: changeLock, type: udt.script } + * ], + * outputsData: [ + * ccc.numLeToBytes(1000, 16), // 1000 UDT to recipient + * ccc.numLeToBytes(500, 16) // 500 UDT as change + * ] + * }); + * + * const outputBalance = await udt.getOutputsBalance(tx, client); + * console.log(`Total UDT output balance: ${outputBalance}`); // 1500 + * ``` + * + * @remarks + * This method only counts outputs that have the same type script as this UDT instance. + * Outputs without a type script or with different type scripts are ignored. + * This method is a convenience wrapper around `getOutputsInfo` that returns only the balance. + */ + async getOutputsBalance( + txLike: ccc.TransactionLike, + client: ccc.Client, + ): Promise { + return (await this.getOutputsInfo(txLike, client)).balance; + } + + /** + * Calculates the net UDT balance that would be burned (destroyed) in a transaction. + * This is the difference between the total UDT balance in inputs and outputs. + * A positive value indicates UDT tokens are being burned, while a negative value + * indicates more UDT is being created than consumed (which may require minting permissions). + * + * @param txLike - The transaction to analyze + * @param client - The client to fetch input cell data + * @returns A promise resolving to the net UDT balance burned (inputs - outputs) + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * const tx = ccc.Transaction.from(existingTransaction); + * + * const burned = await udt.getBalanceBurned(tx, client); + * if (burned > 0) { + * console.log(`${burned} UDT tokens will be burned`); + * } else if (burned < 0) { + * console.log(`${-burned} UDT tokens will be created`); + * } else { + * console.log('UDT balance is conserved'); + * } + * ``` + * + * @remarks + * This method is useful for: + * - Validating transaction balance conservation + * - Calculating how much UDT is being destroyed in burn operations + * - Detecting minting operations (negative burned balance) + * - Ensuring sufficient UDT inputs are provided for transfers + */ + async getBalanceBurned( + txLike: ccc.TransactionLike, + client: ccc.Client, + ): Promise { + const tx = ccc.Transaction.from(txLike); + return ( + (await this.getInputsBalance(tx, client)) - + (await this.getOutputsBalance(tx, client)) + ); + } + + /** + * Low-level method to complete UDT inputs for a transaction using a custom accumulator function. + * This method provides maximum flexibility for input selection by allowing custom logic + * through the accumulator function. It's primarily used internally by other completion methods. + * + * @template T - The type of the accumulator value + * @param txLike - The transaction to complete with UDT inputs + * @param from - The signer that will provide UDT inputs + * @param accumulator - Function that determines when to stop adding inputs based on accumulated state + * @param init - Initial value for the accumulator + * @returns A promise resolving to an object containing: + * - tx: The transaction with added inputs + * - addedCount: Number of inputs that were added + * - accumulated: Final accumulator value (undefined if target was reached) + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // Custom accumulator to track both balance and capacity + * const result = await udt.completeInputs( + * tx, + * signer, + * ([balanceAcc, capacityAcc], cell) => { + * const balance = ccc.udtBalanceFrom(cell.outputData); + * const newBalance = balanceAcc + balance; + * const newCapacity = capacityAcc + cell.cellOutput.capacity; + * + * // Stop when we have enough balance and capacity + * return newBalance >= requiredBalance && newCapacity >= requiredCapacity + * ? undefined // Stop adding inputs + * : [newBalance, newCapacity]; // Continue with updated accumulator + * }, + * [ccc.Zero, ccc.Zero] // Initial [balance, capacity] + * ); + * ``` + * + * @remarks + * This is a low-level method that most users won't need to call directly. + * Use `completeInputsByBalance` for typical UDT input completion needs. + * The accumulator function should return `undefined` to stop adding inputs, + * or return an updated accumulator value to continue. + */ + async completeInputs( + txLike: ccc.TransactionLike, + from: ccc.Signer, + accumulator: ( + acc: T, + v: ccc.Cell, + i: number, + array: ccc.Cell[], + ) => Promise | T | undefined, + init: T, + ): Promise<{ + tx: ccc.Transaction; + addedCount: number; + accumulated?: T; + }> { + const tx = ccc.Transaction.from(txLike); + const res = await tx.completeInputs(from, this.filter, accumulator, init); + + return { + ...res, + tx, + }; + } + + /** + * Completes UDT inputs for a transaction to satisfy both UDT balance and capacity requirements. + * This method implements intelligent input selection that considers both UDT token balance + * and cell capacity constraints, optimizing for minimal cell usage while meeting all requirements. + * It uses sophisticated balance calculations and early exit optimizations for efficiency. + * + * @param txLike - The transaction to complete with UDT inputs + * @param from - The signer that will provide UDT inputs + * @param balanceTweak - Optional additional UDT balance requirement beyond outputs (default: 0) + * @param capacityTweak - Optional additional CKB capacity requirement beyond outputs (default: 0) + * @returns A promise resolving to an object containing: + * - tx: The modified transaction with added UDT inputs + * - addedCount: Number of UDT input cells that were added + * + * @throws {ErrorUdtInsufficientCoin} When there are insufficient UDT cells to cover the required balance + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // Basic usage: add inputs to cover UDT outputs + * const tx = ccc.Transaction.from({ + * outputs: [{ lock: recipientLock, type: udt.script }], + * outputsData: [ccc.numLeToBytes(1000, 16)] + * }); + * + * const { tx: completedTx, addedCount } = await udt.completeInputsByBalance(tx, signer); + * console.log(`Added ${addedCount} UDT inputs to cover 1000 UDT requirement`); + * + * // Advanced usage: with balance and capacity tweaks + * const { tx: advancedTx, addedCount: advancedCount } = await udt.completeInputsByBalance( + * tx, + * signer, + * ccc.numFrom(100), // Extra 100 UDT balance needed + * ccc.fixedPointFrom(5000) // Extra 5000 capacity needed + * ); + * ``` + * + * @remarks + * This method implements sophisticated dual-constraint input selection with the following logic: + * + * **Balance Calculations:** + * - UDT balance deficit: `inputBalance - outputBalance - balanceTweak` + * - Capacity balance with fee optimization: `min(inputCapacity - outputCapacity, estimatedFee) - capacityTweak` + * - The capacity calculation tries to avoid extra occupation by UDT cells and compress UDT state + * + * **Early Exit Optimization:** + * - Returns immediately with `addedCount: 0` if both balance and capacity constraints are satisfied + * - Avoids unnecessary input addition when existing inputs are sufficient + * + * **Smart Input Selection:** + * - Uses accumulator pattern to track both UDT balance and capacity during selection + * - Continues adding inputs until both constraints are satisfied: `balanceAcc >= 0 && capacityAcc >= 0` + * - Prioritizes providing sufficient capacity through UDT cells to avoid extra non-UDT inputs + * + * **Error Handling:** + * - Throws `ErrorUdtInsufficientCoin` with exact shortfall amount if insufficient UDT balance + * - Only throws error if UDT balance cannot be satisfied (capacity issues don't cause errors) + */ + async completeInputsByBalance( + txLike: ccc.TransactionLike, + from: ccc.Signer, + balanceTweak?: ccc.NumLike, + capacityTweak?: ccc.NumLike, + ): Promise<{ + addedCount: number; + tx: ccc.Transaction; + }> { + const tx = ccc.Transaction.from(txLike); + const { balance: inBalance, capacity: inCapacity } = + await this.getInputsInfo(tx, from.client); + const { balance: outBalance, capacity: outCapacity } = + await this.getOutputsInfo(tx, from.client); + + const balanceBurned = + inBalance - outBalance - ccc.numFrom(balanceTweak ?? 0); + // Try to avoid extra occupation by UDT and also try to compress UDT state + const capacityBurned = + ccc.numMin(inCapacity - outCapacity, await tx.getFee(from.client)) - + ccc.numFrom(capacityTweak ?? 0); + + if (balanceBurned >= ccc.Zero && capacityBurned >= ccc.Zero) { + return { addedCount: 0, tx }; + } + + const { + tx: txRes, + addedCount, + accumulated, + } = await this.completeInputs( + tx, + from, + ([balanceAcc, capacityAcc], { cellOutput: { capacity }, outputData }) => { + const balance = ccc.udtBalanceFrom(outputData); + const balanceBurned = balanceAcc + balance; + const capacityBurned = capacityAcc + capacity; + + // Try to provide enough capacity with UDT cells to avoid extra occupation + return balanceBurned >= ccc.Zero && capacityBurned >= ccc.Zero + ? undefined + : [balanceBurned, capacityBurned]; + }, + [balanceBurned, capacityBurned], + ); + + if (accumulated === undefined || accumulated[0] >= ccc.Zero) { + return { tx: txRes, addedCount }; + } + + throw new ErrorUdtInsufficientCoin({ + amount: -accumulated[0], + type: this.script, }); - return resTx; } - async completeChangeToLock( + /** + * Adds ALL available UDT cells from the signer as inputs to the transaction. + * Unlike `completeInputsByBalance` which adds only the minimum required inputs, + * this method collects every available UDT cell that the signer controls, + * regardless of the transaction's actual UDT requirements. + * + * @param txLike - The transaction to add UDT inputs to + * @param from - The signer that will provide all available UDT inputs + * @returns A promise resolving to an object containing: + * - tx: The transaction with all available UDT inputs added + * - addedCount: Number of UDT input cells that were added + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // Create a transaction (can be empty or have existing outputs) + * const tx = ccc.Transaction.from({ + * outputs: [{ lock: recipientLock, type: udt.script }], + * outputsData: [ccc.numLeToBytes(100, 16)] // Send 100 UDT + * }); + * + * // Add ALL available UDT cells as inputs + * const { tx: completedTx, addedCount } = await udt.completeInputsAll(tx, signer); + * console.log(`Added ${addedCount} UDT cells as inputs`); + * + * // The transaction now contains all UDT cells the signer controls + * const totalInputBalance = await udt.getInputsBalance(completedTx, client); + * console.log(`Total UDT input balance: ${totalInputBalance}`); + * ``` + * + * @remarks + * **Use Cases:** + * - **UDT Consolidation**: Combining multiple small UDT cells into fewer larger ones + * - **Complete Balance Transfer**: Moving all UDT tokens from one address to another + * - **Wallet Cleanup**: Reducing the number of UDT cells for better wallet performance + * - **Batch Operations**: When you need to process all UDT holdings at once + * + * **Important Considerations:** + * - This method will likely create a large excess balance that needs to be handled with change outputs + * - The resulting transaction may be large and expensive due to many inputs + * - Use `completeInputsByBalance` instead if you only need specific amounts + * - Always handle the excess balance with appropriate change outputs after calling this method + * + * **Behavior:** + * - Adds every UDT cell that the signer controls and that isn't already used in the transaction + * - The accumulator tracks total capacity of added cells (used internally for optimization) + * - Does not stop until all available UDT cells are added + * - Skips cells that are already present as inputs in the transaction + */ + async completeInputsAll( + txLike: ccc.TransactionLike, + from: ccc.Signer, + ): Promise<{ + addedCount: number; + tx: ccc.Transaction; + }> { + const tx = ccc.Transaction.from(txLike); + + return this.completeInputs( + tx, + from, + (acc, { cellOutput: { capacity } }) => acc + capacity, + ccc.Zero, + ); + } + + /** + * Completes a UDT transaction by adding inputs and handling change with a custom change function. + * This is a low-level method that provides maximum flexibility for handling UDT transaction completion. + * The change function is called to handle excess UDT balance and can return the capacity cost of the change. + * + * @param txLike - The transaction to complete + * @param signer - The signer that will provide UDT inputs + * @param change - Function to handle excess UDT balance. Called with (tx, balance, shouldModify) + * where shouldModify indicates if the function should actually modify the transaction + * @param options - Optional configuration + * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true + * @returns A promise resolving to the completed transaction + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * const completedTx = await udt.complete( + * tx, + * signer, + * (tx, balance, shouldModify) => { + * if (shouldModify && balance > 0) { + * // Add change output + * const changeData = ccc.numLeToBytes(balance, 16); + * tx.addOutput({ lock: changeLock, type: udt.script }, changeData); + * return ccc.CellOutput.from({ lock: changeLock, type: udt.script }, changeData).capacity; + * } + * return 0; + * } + * ); + * ``` + * + * @remarks + * The change function is called twice: + * 1. First with shouldModify=false to calculate capacity requirements + * 2. Then with shouldModify=true to actually modify the transaction + * This two-phase approach ensures proper input selection considering capacity requirements. + */ + async complete( txLike: ccc.TransactionLike, signer: ccc.Signer, - change: ccc.ScriptLike, + change: ( + tx: ccc.Transaction, + balance: ccc.Num, + shouldModify: boolean, + ) => Promise | ccc.NumLike, + options?: { shouldAddInputs?: boolean }, + ): Promise { + let tx = this.addCellDeps(ccc.Transaction.from(txLike)); + + /* === Figure out the balance to change === */ + if (options?.shouldAddInputs ?? true) { + tx = (await this.completeInputsByBalance(tx, signer)).tx; + } + + const balanceBurned = await this.getBalanceBurned(tx, signer.client); + + if (balanceBurned < ccc.Zero) { + throw new ErrorUdtInsufficientCoin({ + amount: -balanceBurned, + type: this.script, + }); + } else if (balanceBurned === ccc.Zero) { + return tx; + } + /* === Some balance need to change === */ + + if (!(options?.shouldAddInputs ?? true)) { + await Promise.resolve(change(tx, balanceBurned, true)); + return tx; + } + + // Different with `Transaction.completeFee`, we don't need the modified tx to track updated fee + // So one attempt should be enough + const extraCapacity = ccc.numFrom( + await Promise.resolve(change(tx, balanceBurned, false)), + ); // Extra capacity introduced by change cell + tx = ( + await this.completeInputsByBalance(tx, signer, ccc.Zero, extraCapacity) + ).tx; + + const balanceToChange = await this.getBalanceBurned(tx, signer.client); + await Promise.resolve(change(tx, balanceToChange, true)); + + return tx; + } + + /** + * Completes a UDT transaction by adding change to an existing output at the specified index. + * This method modifies an existing UDT output in the transaction to include any excess + * UDT balance as change, rather than creating a new change output. + * + * @param txLike - The transaction to complete + * @param signer - The signer that will provide UDT inputs + * @param indexLike - The index of the output to modify with change balance + * @param options - Optional configuration + * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true + * @returns A promise resolving to the completed transaction + * + * @throws {Error} When the specified output is not a valid UDT cell + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // Create transaction with a UDT output that will receive change + * const tx = ccc.Transaction.from({ + * outputs: [ + * { lock: recipientLock, type: udt.script }, + * { lock: changeLock, type: udt.script } // This will receive change + * ], + * outputsData: [ + * ccc.numLeToBytes(1000, 16), // Send 1000 UDT + * ccc.numLeToBytes(0, 16) // Change output starts with 0 + * ] + * }); + * + * // Complete with change going to output index 1 + * const completedTx = await udt.completeChangeToOutput(tx, signer, 1); + * // Output 1 now contains the excess UDT balance + * ``` + * + * @remarks + * This method is useful when you want to consolidate change into an existing output + * rather than creating a new output, which can save on transaction size and fees. + * The specified output must already be a valid UDT cell with this UDT's type script. + */ + async completeChangeToOutput( + txLike: ccc.TransactionLike, + signer: ccc.Signer, + indexLike: ccc.NumLike, + options?: { shouldAddInputs?: boolean }, ) { const tx = ccc.Transaction.from(txLike); + const index = Number(ccc.numFrom(indexLike)); + const outputData = ccc.bytesFrom(tx.outputsData[index]); - await tx.completeInputsByUdt(signer, this.script); - const balanceDiff = - (await tx.getInputsUdtBalance(signer.client, this.script)) - - tx.getOutputsUdtBalance(this.script); - if (balanceDiff > ccc.Zero) { - tx.addOutput( - { - lock: change, - type: this.script, - }, - ccc.numLeToBytes(balanceDiff, 16), - ); + if (!this.isUdt({ cellOutput: tx.outputs[index], outputData })) { + throw new Error("Change output must be a UDT cell"); } - return tx; + return this.complete( + tx, + signer, + (tx, balance, shouldModify) => { + if (shouldModify) { + const balanceData = ccc.numLeToBytes( + ccc.udtBalanceFrom(outputData) + balance, + 16, + ); + + tx.outputsData[index] = ccc.hexFrom( + ccc.bytesConcatTo([], balanceData, outputData.slice(16)), + ); + } + + return 0; + }, + options, + ); + } + + /** + * Completes a UDT transaction by adding necessary inputs and handling change. + * This method automatically adds UDT inputs to cover the required output amounts + * and creates a change output if there's excess UDT balance. + * + * @param tx - The transaction to complete, containing UDT outputs + * @param signer - The signer that will provide UDT inputs + * @param changeLike - The lock script where any excess UDT balance should be sent as change + * @param options - Optional configuration for the completion process + * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true + * @returns A promise resolving to the completed transaction with inputs and change output added + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // Create a transaction with UDT outputs + * const tx = ccc.Transaction.from({ + * outputs: [ + * { lock: recipientLock, type: udt.script } + * ], + * outputsData: [ccc.numLeToBytes(1000, 16)] // Send 1000 UDT + * }); + * + * // Complete with change going to sender's address + * const { script: changeLock } = await signer.getRecommendedAddressObj(); + * const completedTx = await udt.completeChangeToLock(tx, signer, changeLock); + * + * // The transaction now has: + * // - Sufficient UDT inputs to cover the 1000 UDT output + * // - A change output if there was excess UDT balance + * ``` + * + * @remarks + * This method performs the following operations: + * 1. Adds UDT inputs using `completeInputsByBalance` + * 2. Calculates the difference between input and output UDT balances + * 3. Creates a change output if there's excess UDT balance + */ + async completeChangeToLock( + tx: ccc.TransactionLike, + signer: ccc.Signer, + changeLike: ccc.ScriptLike, + options?: { shouldAddInputs?: boolean }, + ) { + const change = ccc.Script.from(changeLike); + + return this.complete( + tx, + signer, + (tx, balance, shouldModify) => { + const balanceData = ccc.numLeToBytes(balance, 16); + const changeOutput = ccc.CellOutput.from( + { lock: change, type: this.script }, + balanceData, + ); + if (shouldModify) { + tx.addOutput(changeOutput, balanceData); + } + + return changeOutput.capacity; + }, + options, + ); } - async completeBy(tx: ccc.TransactionLike, from: ccc.Signer) { + /** + * Completes a UDT transaction using the signer's recommended address for change. + * This is a convenience method that automatically uses the signer's recommended + * address as the change destination, making it easier to complete UDT transactions + * without manually specifying a change address. + * + * @param tx - The transaction to complete, containing UDT outputs + * @param from - The signer that will provide UDT inputs and receive change + * @param options - Optional configuration for the completion process + * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true + * @returns A promise resolving to the completed transaction with inputs and change output added + * + * @example + * ```typescript + * const udt = new Udt(codeOutPoint, scriptConfig); + * + * // Create a transfer transaction + * const transferResponse = await udt.transfer( + * signer, + * [{ to: recipientLock, amount: 1000 }] + * ); + * + * // Complete the transaction (change will go to signer's address) + * const completedTx = await udt.completeBy(transferResponse.res, signer); + * + * // Add capacity inputs and fee + * await completedTx.completeInputsByCapacity(signer); + * await completedTx.completeFeeBy(signer, changeLock); + * + * const txHash = await signer.sendTransaction(completedTx); + * ``` + * + * @see {@link completeChangeToLock} for more control over the change destination + */ + async completeBy( + tx: ccc.TransactionLike, + from: ccc.Signer, + options?: { shouldAddInputs?: boolean }, + ) { const { script } = await from.getRecommendedAddressObj(); - return this.completeChangeToLock(tx, from, script); + return this.completeChangeToLock(tx, from, script, options); } } diff --git a/packages/udt/src/udtPausable/index.ts b/packages/udt/src/udtPausable/index.ts index 84215cce3..46fb7dda5 100644 --- a/packages/udt/src/udtPausable/index.ts +++ b/packages/udt/src/udtPausable/index.ts @@ -1,6 +1,6 @@ import { ccc } from "@ckb-ccc/core"; import { ssri } from "@ckb-ccc/ssri"; -import { Udt } from "../udt/index.js"; +import { Udt, UdtConfigLike } from "../udt/index.js"; /** * Represents a UDT (User Defined Token) with pausable functionality. @@ -11,9 +11,7 @@ export class UdtPausable extends Udt { constructor( code: ccc.OutPointLike, script: ccc.ScriptLike, - config: { - executor: ssri.Executor; - }, + config: UdtConfigLike & { executor: ssri.Executor }, ) { super(code, script, config); } diff --git a/packages/udt/vitest.config.ts b/packages/udt/vitest.config.ts new file mode 100644 index 000000000..dc6a58785 --- /dev/null +++ b/packages/udt/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + coverage: { + include: ["src/**/*.ts"], + }, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts index 7af3d4a84..35bbdef14 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,9 +2,9 @@ import { defineConfig, coverageConfigDefaults } from "vitest/config"; export default defineConfig({ test: { - projects: ["packages/core"], + projects: ["packages/core", "packages/udt"], coverage: { - include: ["packages/core"], + include: ["packages/core", "packages/udt"], exclude: [ "**/dist/**", "**/dist.commonjs/**", From 1c51782c02bfd36549dd0ff19a4347f8faa6360a Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 17 Aug 2025 10:46:12 +0800 Subject: [PATCH 07/81] docs: CONTRIBUTING.md --- .github/pull_request_template.md | 20 +++++ CONTRIBUTING.md | 134 +++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..152574cca --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +--- +name: Pull Request +about: Propose a change to ccc +title: '' +labels: '' +assignees: '' + +--- + + + + + +**Please describe your changes in detail:** + + + +**Does this pull request close any issues?** + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..f8afdb244 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,134 @@ +# Contributing to ccc + +First off, thank you for considering contributing to ccc! We appreciate your help in making ccc a better tool. + +## Where do I go from here? + +If you've noticed a bug or have a feature request, we'd appreciate it if you [make one](https://github.com/ckb-devrel/ccc/issues/new)! It's generally best if you get confirmation of your bug or approval for your feature request this way before starting to code. + +## Guiding Principles + +When contributing to ccc, please keep the following principles in mind. These guidelines are designed to ensure that ccc is not only a powerful tool but also a pleasure to use for developers. + +* **Design for the Majority**: Prioritize the most common use cases. Strive to make these scenarios as intuitive and frictionless as possible. +* **Usability Over Performance**: While performance is important, the ease of use and overall developer experience should always be the primary consideration. A slight performance trade-off is acceptable if it leads to a more intuitive API or a clearer development process. +* **Good Developer Experience is Key**: Code that simply "works" is not enough. Aim to write code that is a joy to work with. This includes clear naming, comprehensive documentation, and a logical, predictable API. A positive developer experience is a feature in itself. + +In summary, our goal is to create a tool that is both powerful and developer-friendly. By focusing on the most common use cases, prioritizing usability, and striving for a great developer experience, we can build a better ccc for everyone. + +### Fork & create a branch + +If you'd like to contribute a fix, you can [fork ccc](https://github.com/ckb-devrel/ccc/fork) and create a branch with a descriptive name. + +A good branch name would be in the format of `/`, for example: + +```sh +git checkout -b feat/add-new-api +``` + +Or for a bug fix: + +```sh +git checkout -b fix/resolve-memory-leak +``` + +### Get the project running + +To get the project running on your local machine, please run: +```sh +pnpm install +``` + +### Make your changes + +Now you can modify the code to fix the bug or add the feature. + +### Run tests + +Please make sure the tests pass before you commit your changes. + +```sh +pnpm test +``` + +### Lint and format your code + +We use ESLint for linting and Prettier for formatting. Please ensure your code is clean before committing. + +```sh +pnpm lint +pnpm format +``` + +### Commit your changes + +Please add a changeset for your changes. This is how we track changes and generate changelogs. + +```sh +pnpm changeset +``` + +This will ask you a few questions and then generate a file in the `.changeset` directory. Please add this file to your commit. + +### Commit Message Format + +We use the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for our commit messages. This allows for easier automation of changelog generation and versioning. + +Here are a few examples of good commit messages: + +- A commit that fixes a bug: + ``` + fix(parser): handle multi-byte characters + ``` +- A commit that adds a new feature: + ``` + feat(api): add new endpoint for user profiles + ``` +- A commit that includes a breaking change: + ``` + feat(auth)!: remove support for deprecated authentication method + + BREAKING CHANGE: The old authentication method is no longer supported. Please upgrade to the new method. + ``` + +### Push to your fork and submit a pull request + +After pushing your changes to your fork, please submit a pull request to the `master` branch of the `ckb-devrel/ccc` repository. + +After submitting the pull request, our team will review it. We may suggest some changes or improvements or alternatives. + +### Keep Pull Requests Small and Focused + +We prefer small, atomic pull requests over large ones. This makes them easier to review, test, and merge, which means your contributions can be integrated faster. A good pull request is like a single, logical commit. + +Here are some more detailed tips for keeping your pull requests small and focused: + +* **One PR, One Concern:** Each pull request should address a single, well-defined issue or feature. Avoid mixing bug fixes, new features, and refactoring in the same pull request. For example, if you find a bug while working on a new feature, first create a PR to fix the bug, and then create a separate PR for the feature. + +* **Break Down Large Features:** If you're implementing a complex feature, don't try to do it all in one giant PR. Instead, break it down into smaller, incremental steps. For example, if you're adding a new UI component, you could have separate PRs for: + 1. The basic component structure and styling. + 2. The component's state management. + 3. The component's integration with the rest of the application. + +* **Separate Refactoring from Features/Fixes:** If you need to refactor existing code to implement your change, do it in a separate PR *before* you start working on the feature or fix. This makes it clear what changes are refactoring and what changes are new functionality, making the review process much smoother. For example, if you need to rename a function that your new feature will use, submit a PR with just the rename first. + +* **Keep an Eye on the Diff:** As you work, regularly check the size of your diff (`git diff`). If it's getting too large (e.g., more than a few hundred lines of changes), it's a good sign that you should probably split your work into multiple PRs. + +By following these guidelines, you'll not only make the review process easier for us, but you'll also likely find it easier to manage your own work. + +### Interacting with the Gemini Review Bot + +We use a Gemini-powered bot to help with pull request reviews. Here's how to interact with it: + +* **Read Gemini's review comments:** The bot will often provide useful suggestions for improving your code. Please read its comments carefully. +* **Request a re-review:** After you've updated your pull request based on the feedback, you can ask the bot to review your changes again by leaving a comment with `/gemini review`. + +To help your pull request get accepted, please consider the following: + +- Write tests. +- Follow our existing style. +- Write a good commit message. + +### Publishing a Canary Release (for maintainers) + +Maintainers can publish a canary release to npm by commenting `/canary` on a pull request. This will trigger a workflow that builds and publishes a new version to npm with the `canary` tag. \ No newline at end of file From 2bfc53e23c38f094700ee3c6dba49db99afc2e59 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 17 Aug 2025 10:54:33 +0800 Subject: [PATCH 08/81] docs: pull request template --- .github/pull_request_template.md | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 152574cca..44cd724c4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,20 +1,3 @@ ---- -name: Pull Request -about: Propose a change to ccc -title: '' -labels: '' -assignees: '' - ---- - - - -**Please describe your changes in detail:** - - - -**Does this pull request close any issues?** - - \ No newline at end of file +- [ ] I have read the [**Contributing Guidelines**](CONTRIBUTING.md) From eb1fdb369f907694d2bd6567d237d826c7d6a4a5 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 17 Aug 2025 11:49:09 +0800 Subject: [PATCH 09/81] fix(playground): some messages are not displyed --- packages/playground/src/app/context.tsx | 59 +++++++++++++++---------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/playground/src/app/context.tsx b/packages/playground/src/app/context.tsx index 13833582f..830325cae 100644 --- a/packages/playground/src/app/context.tsx +++ b/packages/playground/src/app/context.tsx @@ -30,6 +30,13 @@ function WalletIcon({ ); } +export type Messages = [ + "error" | "info", + string, + unknown[], + ReactNode | undefined, +][]; + export const APP_CONTEXT = createContext< | { enabledAnimate: boolean; @@ -42,7 +49,7 @@ export const APP_CONTEXT = createContext< disconnect: () => void; openAction: ReactNode; - messages: ["error" | "info", string, unknown[], ReactNode | undefined][]; + messages: Messages; clearMessage: () => void; sendMessage: ( level: "error" | "info", @@ -84,33 +91,34 @@ export function AppProvider({ children }: { children: React.ReactNode }) { signer?.getInternalAddress().then((a) => setAddress(a)); }, [signer]); - const [{ messages }, setMessages] = useState<{ - messages: ["error" | "info", string, unknown[], ReactNode | undefined][]; - cachedMessages: number; - }>({ messages: [], cachedMessages: 0 }); + const [messages, setMessages] = useState([]); + const cachedMessagesRef = React.useRef([]); + const messagesTimeoutRef = React.useRef(null); const sendMessage = useCallback( - (level: "error" | "info", title: string, msgs: unknown[]) => - messages.push([level, title, msgs, undefined]), - [messages], + (level: "error" | "info", title: string, msgs: unknown[]) => { + cachedMessagesRef.current.push([level, title, msgs, undefined]); + + if (messagesTimeoutRef.current) { + return; + } + + messagesTimeoutRef.current = window.setTimeout(() => { + const toAdd = cachedMessagesRef.current.splice(0); + setMessages((prevMessages) => [...prevMessages, ...toAdd]); + messagesTimeoutRef.current = null; + }, 100); + }, + [], ); useEffect(() => { - const interval = setInterval(() => { - setMessages((messages) => { - if (messages.messages.length === messages.cachedMessages) { - return messages; - } - - return { - messages: [...messages.messages], - cachedMessages: messages.messages.length, - }; - }); - }, 100); - - return () => clearInterval(interval); - }, [setMessages]); + return () => { + if (messagesTimeoutRef.current) { + clearTimeout(messagesTimeoutRef.current); + } + }; + }, []); useEffect(() => { const handler = (event: PromiseRejectionEvent) => { @@ -154,7 +162,10 @@ export function AppProvider({ children }: { children: React.ReactNode }) { ), messages, - clearMessage: () => setMessages({ messages: [], cachedMessages: 0 }), + clearMessage: () => { + setMessages([]); + cachedMessagesRef.current.length = 0; + }, sendMessage, createSender: (title) => ({ log: (...msgs) => sendMessage("info", title, msgs), From b4f64cec5b5b98e34cf17af676c951c7e40ce85e Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 17 Aug 2025 21:23:02 +0800 Subject: [PATCH 10/81] feat(core): multiple scripts for `SignerCkbScriptReadonly` --- .changeset/ten-ties-kiss.md | 6 ++++ .../src/signer/ckb/signerCkbScriptReadonly.ts | 32 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 .changeset/ten-ties-kiss.md diff --git a/.changeset/ten-ties-kiss.md b/.changeset/ten-ties-kiss.md new file mode 100644 index 000000000..163263fc4 --- /dev/null +++ b/.changeset/ten-ties-kiss.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): multiple scripts for `SignerCkbScriptReadonly` + \ No newline at end of file diff --git a/packages/core/src/signer/ckb/signerCkbScriptReadonly.ts b/packages/core/src/signer/ckb/signerCkbScriptReadonly.ts index fa9df8b1b..57d170bb1 100644 --- a/packages/core/src/signer/ckb/signerCkbScriptReadonly.ts +++ b/packages/core/src/signer/ckb/signerCkbScriptReadonly.ts @@ -4,31 +4,46 @@ import { Client } from "../../client/index.js"; import { Signer, SignerSignType, SignerType } from "../signer/index.js"; /** - * A class extending Signer that provides read-only access to a CKB script. - * This class does not support signing operations. + * A read-only signer for a CKB script. It can be used to get addresses, + * but not to sign transactions. This is useful when you want to watch an address + * without having the private key. + * * @public */ export class SignerCkbScriptReadonly extends Signer { + /** + * The type of the signer. + */ get type(): SignerType { return SignerType.CKB; } + /** + * The sign type of the signer. + * As this is a read-only signer, the sign type is {@link SignerSignType.Unknown}. + */ get signType(): SignerSignType { return SignerSignType.Unknown; } - private readonly script: Script; + /** + * The scripts associated with the signer. + */ + public readonly scripts: Script[]; /** * Creates an instance of SignerCkbScriptReadonly. * * @param client - The client instance used for communication. - * @param script - The script associated with the signer. + * @param scripts - The scripts associated with the signer. Can be a single script, an array of scripts, or multiple script arguments. */ - constructor(client: Client, script: ScriptLike) { + constructor(client: Client, ...scripts: (ScriptLike | ScriptLike[])[]) { super(client); - this.script = Script.from(script); + this.scripts = scripts.flat().map(Script.from); + if (this.scripts.length === 0) { + throw new Error("SignerCkbScriptReadonly requires at least one script."); + } } /** @@ -71,8 +86,9 @@ export class SignerCkbScriptReadonly extends Signer { * const addressObjs = await signer.getAddressObjs(); // Outputs the array of Address objects * ``` */ - async getAddressObjs(): Promise { - return [Address.fromScript(this.script, this.client)]; + return this.scripts.map((script) => + Address.fromScript(script, this.client), + ); } } From e4965606bc892ef0b05f3004933a568b45e6a2ef Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 17 Aug 2025 06:18:12 +0800 Subject: [PATCH 11/81] feat(core): add `CellAny` It's definitely a mistake to name `CellOnChain` `Cell`, but there is nothing we can do with that right now. To avoid more duplicate code, `CellAny` was added to represent a cell that's on-chain or off-chain. --- .changeset/salty-apples-check.md | 9 + packages/core/src/ckb/transaction.ts | 334 +++++++++++++----- .../core/src/client/jsonRpc/transformers.ts | 7 +- packages/ssri/src/executor.ts | 11 +- 4 files changed, 276 insertions(+), 85 deletions(-) create mode 100644 .changeset/salty-apples-check.md diff --git a/.changeset/salty-apples-check.md b/.changeset/salty-apples-check.md new file mode 100644 index 000000000..4e6c19b57 --- /dev/null +++ b/.changeset/salty-apples-check.md @@ -0,0 +1,9 @@ +--- +"@ckb-ccc/core": minor +"@ckb-ccc/ssri": patch +--- + +feat(core): add `CellAny` + +It's definitely a mistake to name `CellOnChain` `Cell`, but there is nothing we can do with that right now. To avoid more duplicate code, `CellAny` was added to represent a cell that's on-chain or off-chain. + diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 2bfd24ef3..df0e84c34 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -198,7 +198,7 @@ export class OutPoint extends mol.Entity.Base() { * @public */ export type CellOutputLike = { - capacity: NumLike; + capacity?: NumLike | null; lock: ScriptLike; type?: ScriptLike | null; }; @@ -238,7 +238,6 @@ export class CellOutput extends mol.Entity.Base() { * This method supports automatic capacity calculation when capacity is 0 or omitted. * * @param cellOutput - A CellOutputLike object or an instance of CellOutput. - * Can also be an object without capacity when outputData is provided. * @param outputData - Optional output data used for automatic capacity calculation. * When provided and capacity is 0, the capacity will be calculated * as occupiedSize + outputData.length. @@ -259,16 +258,9 @@ export class CellOutput extends mol.Entity.Base() { * }, "0x1234"); // Capacity will be calculated automatically * ``` */ - static from(cellOutput: CellOutputLike, outputData?: HexLike): CellOutput; static from( - cellOutput: Omit & - Partial>, - outputData: HexLike, - ): CellOutput; - static from( - cellOutput: Omit & - Partial>, - outputData?: HexLike, + cellOutput: CellOutputLike, + outputData?: HexLike | null, ): CellOutput { if (cellOutput instanceof CellOutput) { return cellOutput; @@ -307,34 +299,186 @@ export const CellOutputVec = mol.vector(CellOutput); /** * @public + * Represents a cell-like object that may or may not be on-chain. + * It can optionally have an `outPoint` (or `previousOutput`). + * This is used as a flexible input for creating `CellAny` instances. + * @see CellAny */ -export type CellLike = ( - | { - outPoint: OutPointLike; - } - | { previousOutput: OutPointLike } -) & { +export type CellAnyLike = { + outPoint?: OutPointLike | null; + previousOutput?: OutPointLike | null; cellOutput: CellOutputLike; - outputData: HexLike; + outputData?: HexLike | null; }; /** + * Represents a CKB cell that can be either on-chain (with an `outPoint`) or off-chain (without an `outPoint`). + * This class provides a unified interface for handling cells before they are included in a transaction, + * or for cells that are already part of the blockchain state. + * * @public */ -export class Cell { +export class CellAny { /** - * Creates an instance of Cell. + * Creates an instance of CellAny. * - * @param outPoint - The output point of the cell. * @param cellOutput - The cell output of the cell. * @param outputData - The output data of the cell. + * @param outPoint - The optional output point of the cell. If provided, the cell is considered on-chain. */ constructor( - public outPoint: OutPoint, public cellOutput: CellOutput, public outputData: Hex, + public outPoint?: OutPoint, ) {} + /** + * Creates a `CellAny` instance from a `CellAnyLike` object. + * This factory method provides a convenient way to create `CellAny` instances + * from plain objects, automatically handling the optional `outPoint` or `previousOutput`. + * + * @param cell - A `CellAnyLike` object. + * @returns A new `CellAny` instance. + * + * @example + * ```typescript + * // Create an off-chain cell (e.g., a new output) + * const offChainCell = CellAny.from({ + * cellOutput: { capacity: 1000n, lock: lockScript }, + * outputData: "0x" + * }); + * + * // Create an on-chain cell from an input + * const onChainCell = CellAny.from({ + * outPoint: { txHash: "0x...", index: 0 }, + * cellOutput: { capacity: 2000n, lock: lockScript }, + * outputData: "0x1234" + * }); + * ``` + */ + static from(cell: CellAnyLike): CellAny { + if (cell instanceof CellAny) { + return cell; + } + + return new CellAny( + CellOutput.from(cell.cellOutput, cell.outputData), + hexFrom(cell.outputData ?? "0x"), + apply(OutPoint.from, cell.outPoint ?? cell.previousOutput), + ); + } + + /** + * Calculates the total occupied size of the cell in bytes. + * This includes the size of the `CellOutput` structure plus the size of the `outputData`. + * + * @returns The total occupied size in bytes. + */ + get occupiedSize() { + return this.cellOutput.occupiedSize + bytesFrom(this.outputData).byteLength; + } + + /** + * Calculates the free capacity of the cell. + * Free capacity is the total capacity minus the capacity occupied by the cell's structure and data. + * + * @returns The free capacity in shannons as a `Num`. + */ + get capacityFree() { + return this.cellOutput.capacity - fixedPointFrom(this.occupiedSize); + } + + /** + * Checks if the cell is a Nervos DAO cell and optionally checks its phase. + * + * @param client - A CKB client instance to fetch known script information. + * @param phase - Optional phase to check: "deposited" or "withdrew". + * If omitted, it checks if the cell is a DAO cell regardless of phase. + * @returns A promise that resolves to `true` if the cell is a matching Nervos DAO cell, `false` otherwise. + */ + async isNervosDao( + client: Client, + phase?: "deposited" | "withdrew", + ): Promise { + const { type } = this.cellOutput; + + const daoType = await client.getKnownScript(KnownScript.NervosDao); + if ( + !type || + type.codeHash !== daoType.codeHash || + type.hashType !== daoType.hashType + ) { + // Non Nervos DAO cell + return false; + } + + const hasWithdrew = numFrom(this.outputData) !== Zero; + return ( + !phase || + (phase === "deposited" && !hasWithdrew) || + (phase === "withdrew" && hasWithdrew) + ); + } + + /** + * Clones the `CellAny` instance. + * + * @returns A new `CellAny` instance that is a deep copy of the current one. + * + * @example + * ```typescript + * const clonedCell = cellAny.clone(); + * ``` + */ + clone(): CellAny { + return new CellAny( + this.cellOutput.clone(), + this.outputData, + this.outPoint?.clone(), + ); + } +} + +/** + * Represents a cell-like object that is guaranteed to be on-chain. + * It must have an `outPoint` (or its alias `previousOutput`). + * This is used as a type constraint for creating `Cell` instances. + * @see Cell + * @public + */ +export type CellLike = CellAnyLike & + ( + | { + outPoint: OutPointLike; + previousOutput?: undefined | null; + } + | { + outPoint?: undefined | null; + previousOutput: OutPointLike; + } + ); +/** + * Represents an on-chain CKB cell, which is a `CellAny` that is guaranteed to have an `outPoint`. + * This class is typically used for cells that are already part of the blockchain state, such as transaction inputs. + * @public + */ +export class Cell extends CellAny { + /** + * Creates an instance of an on-chain Cell. + * + * @param outPoint - The output point of the cell. + * @param cellOutput - The cell output of the cell. + * @param outputData - The output data of the cell. + */ + + constructor( + public outPoint: OutPoint, + cellOutput: CellOutput, + outputData: Hex, + ) { + super(cellOutput, outputData, outPoint); + } + /** * Creates a Cell instance from a CellLike object. * This method accepts either `outPoint` or `previousOutput` to specify the cell's location, @@ -376,25 +520,10 @@ export class Cell { } return new Cell( - OutPoint.from("outPoint" in cell ? cell.outPoint : cell.previousOutput), + OutPoint.from(cell.outPoint ?? cell.previousOutput), CellOutput.from(cell.cellOutput, cell.outputData), - hexFrom(cell.outputData), - ); - } - - get capacityFree() { - const occupiedSize = fixedPointFrom( - this.cellOutput.occupiedSize + bytesFrom(this.outputData).length, + hexFrom(cell.outputData ?? "0x"), ); - return this.cellOutput.capacity - occupiedSize; - } - - /** - * Occupied bytes of a cell on chain - * It's CellOutput.occupiedSize + bytesFrom(outputData).byteLength - */ - get occupiedSize() { - return this.cellOutput.occupiedSize + bytesFrom(this.outputData).byteLength; } /** @@ -426,30 +555,22 @@ export class Cell { return calcDaoProfit(this.capacityFree, depositHeader, withdrawHeader); } - async isNervosDao( - client: Client, - phase?: "deposited" | "withdrew", - ): Promise { - const { type } = this.cellOutput; - - const daoType = await client.getKnownScript(KnownScript.NervosDao); - if ( - !type || - type.codeHash !== daoType.codeHash || - type.hashType !== daoType.hashType - ) { - // Non Nervos DAO cell - return false; - } - - const hasWithdrew = numFrom(this.outputData) !== Zero; - return ( - !phase || - (phase === "deposited" && !hasWithdrew) || - (phase === "withdrew" && hasWithdrew) - ); - } - + /** + * Retrieves detailed information about a Nervos DAO cell, including its deposit and withdrawal headers. + * + * @param client - A CKB client instance to fetch cell and header data. + * @returns A promise that resolves to an object containing header information. + * - If not a DAO cell, returns `{}`. + * - If a deposited DAO cell, returns `{ depositHeader }`. + * - If a withdrawn DAO cell, returns `{ depositHeader, withdrawHeader }`. + * + * @throws If the cell is a DAO cell but its corresponding headers cannot be fetched. + * + * @example + * ```typescript + * const daoInfo = await cell.getNervosDaoInfo(client); + * ``` + */ async getNervosDaoInfo(client: Client): Promise< // Non Nervos DAO cell | { @@ -1583,39 +1704,92 @@ export class Transaction extends mol.Entity.Base< * await tx.getOutput(0); * ``` */ - getOutput(index: NumLike): - | { - cellOutput: CellOutput; - outputData: Hex; - } - | undefined { + getOutput(index: NumLike): CellAny | undefined { const i = Number(numFrom(index)); if (i >= this.outputs.length) { return; } - return { + return CellAny.from({ cellOutput: this.outputs[i], outputData: this.outputsData[i] ?? "0x", - }; + }); } + /** - * Add output + * Provides an iterable over the transaction's output cells. + * + * This getter is a convenient way to iterate through all the output cells (`CellAny`) + * of the transaction, combining the `outputs` and `outputsData` arrays. + * It can be used with `for...of` loops or other iterable-consuming patterns. * - * @param outputLike - The cell output to add - * @param outputData - optional output data + * @public + * @category Getter + * @returns An `Iterable` that yields each output cell of the transaction. * * @example * ```typescript - * await tx.addOutput(cellOutput, "0xabcd"); + * for (const cell of tx.outputCells) { + * console.log(`Output cell capacity: ${cell.cellOutput.capacity}`); + * } * ``` */ + get outputCells(): Iterable { + const { outputs, outputsData } = this; + + function* generator(): Generator { + for (let i = 0; i < outputs.length; i++) { + yield CellAny.from({ + cellOutput: outputs[i], + outputData: outputsData[i] ?? "0x", + }); + } + } + + return generator(); + } + + /** + * Adds an output to the transaction. + * + * This method supports two overloads for adding an output: + * 1. By providing a `CellAnyLike` object, which encapsulates both `cellOutput` and `outputData`. + * 2. By providing a `CellOutputLike` object and an optional `outputData`. + * + * @param cellOrOutputLike - A cell-like object containing both cell output and data, or just the cell output object. + * @param outputDataLike - Optional data for the cell output. Defaults to "0x" if not provided in the first argument. + * @returns The new number of outputs in the transaction. + * + * @example + * ```typescript + * // 1. Add an output using a CellAnyLike object + * const newLength1 = tx.addOutput({ + * cellOutput: { lock: recipientLock }, // capacity is calculated automatically + * outputData: "0x1234", + * }); + * + * // 2. Add an output using CellOutputLike and data separately + * const newLength2 = tx.addOutput({ lock: recipientLock }, "0xabcd"); + * ``` + */ + addOutput(cellLike: CellAnyLike): number; + addOutput( + outputLike: CellOutputLike, + outputDataLike?: HexLike | null, + ): number; addOutput( - outputLike: Omit & - Partial>, - outputData: HexLike = "0x", + cellOrOutputLike: CellAnyLike | CellOutputLike, + outputDataLike?: HexLike | null, ): number { - const len = this.outputs.push(CellOutput.from(outputLike, outputData)); - this.setOutputDataAt(len - 1, outputData); + const cell = + "cellOutput" in cellOrOutputLike + ? CellAny.from(cellOrOutputLike) + : CellAny.from({ + cellOutput: cellOrOutputLike, + outputData: outputDataLike, + }); + + const len = this.outputs.push(cell.cellOutput); + this.setOutputDataAt(len - 1, cell.outputData); return len; } diff --git a/packages/core/src/client/jsonRpc/transformers.ts b/packages/core/src/client/jsonRpc/transformers.ts index 4f46bc7ae..ded06bd80 100644 --- a/packages/core/src/client/jsonRpc/transformers.ts +++ b/packages/core/src/client/jsonRpc/transformers.ts @@ -124,7 +124,12 @@ export class JsonRpcTransformers { since: cellInput.since, }); } - static cellOutputFrom(cellOutput: CellOutputLike): JsonRpcCellOutput { + static cellOutputFrom( + cellOutputLike: CellOutputLike, + outputData?: HexLike | null, + ): JsonRpcCellOutput { + const cellOutput = CellOutput.from(cellOutputLike, outputData); + return { capacity: numToHex(cellOutput.capacity), lock: JsonRpcTransformers.scriptFrom(cellOutput.lock), diff --git a/packages/ssri/src/executor.ts b/packages/ssri/src/executor.ts index c31518ac5..b7ae02d44 100644 --- a/packages/ssri/src/executor.ts +++ b/packages/ssri/src/executor.ts @@ -4,13 +4,13 @@ import { getMethodPath } from "./utils.js"; export type ContextTransaction = { script?: ccc.ScriptLike | null; - cell?: Omit | null; + cell?: ccc.CellAnyLike | null; tx: ccc.TransactionLike; }; export type ContextCell = { script?: ccc.ScriptLike | null; - cell: Omit; + cell: ccc.CellAnyLike; tx?: undefined | null; }; @@ -175,14 +175,17 @@ export class ExecutorJsonRpc extends Executor { ]; } if (context?.cell) { + const cell = ccc.CellAny.from(context.cell); + return [ "run_script_level_cell", [ { cell_output: cccA.JsonRpcTransformers.cellOutputFrom( - ccc.CellOutput.from(context.cell.cellOutput), + cell.cellOutput, + cell.outputData, ), - hex_data: ccc.hexFrom(context.cell.outputData), + hex_data: ccc.hexFrom(cell.outputData), }, ], ]; From 5530fa960736834477b5b84ac0330e0085cee8d5 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Mon, 18 Aug 2025 01:39:28 +0800 Subject: [PATCH 12/81] feat(core): `reduce` and `reduceAsync` for `Iterable` --- .changeset/tangy-memes-sit.md | 6 + packages/core/src/utils/index.test.ts | 178 ++++++++++++++++++++++++++ packages/core/src/utils/index.ts | 120 +++++++++++++---- 3 files changed, 278 insertions(+), 26 deletions(-) create mode 100644 .changeset/tangy-memes-sit.md create mode 100644 packages/core/src/utils/index.test.ts diff --git a/.changeset/tangy-memes-sit.md b/.changeset/tangy-memes-sit.md new file mode 100644 index 000000000..1ee2f07d5 --- /dev/null +++ b/.changeset/tangy-memes-sit.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": major +--- + +feat(core): `reduce` and `reduceAsync` for `Iterable` + diff --git a/packages/core/src/utils/index.test.ts b/packages/core/src/utils/index.test.ts new file mode 100644 index 000000000..34dd53162 --- /dev/null +++ b/packages/core/src/utils/index.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, it } from "vitest"; +import { reduce, reduceAsync } from "./index.js"; + +// Helper to create an async iterable for testing +async function* createAsyncIterable(items: T[]): AsyncIterable { + for (const item of items) { + // Simulate a small delay for each item + await new Promise((resolve) => setTimeout(resolve, 1)); + yield item; + } +} + +describe("reduce", () => { + it("should reduce an array of numbers to their sum", () => { + const values = [1, 2, 3, 4]; + const result = reduce(values, (acc, val) => acc + val); + expect(result).toBe(10); + }); + + it("should reduce with a given initial value", () => { + const values = [1, 2, 3, 4]; + const result = reduce(values, (acc, val) => acc + val, 10); + expect(result).toBe(20); + }); + + it("should handle different accumulator and value types", () => { + const values = ["a", "bb", "ccc"]; + const result = reduce(values, (acc, val) => acc + val.length, 0); + expect(result).toBe(6); + }); + + it("should return the initial value for an empty array", () => { + const values: number[] = []; + const result = reduce(values, (acc, val) => acc + val, 100); + expect(result).toBe(100); + }); + + it("should throw a TypeError for an empty array with no initial value", () => { + const values: number[] = []; + expect(() => reduce(values, (acc, val) => acc + val)).toThrow( + "Reduce of empty iterator with no initial value", + ); + }); + + it("should keep the previous result if accumulator returns null or undefined", () => { + const values = [1, 2, 3, 4]; + const result = reduce( + values, + (acc, val) => { + // Only add odd numbers + return val % 2 !== 0 ? acc + val : null; + }, + 0, + ); + // 0+1=1, 1 (ignore 2), 1+3=4, 4 (ignore 4) + expect(result).toBe(4); + }); + + it("should work with other iterables like Set", () => { + const values = new Set([1, 2, 3, 4]); + const result = reduce(values, (acc, val) => acc * val, 1); + expect(result).toBe(24); + }); + + it("should pass correct index to the accumulator", () => { + const values = ["a", "b", "c"]; + const indicesWithInit: number[] = []; + reduce( + values, + (_acc, _val, i) => { + indicesWithInit.push(i); + }, + "", + ); + expect(indicesWithInit).toEqual([0, 1, 2]); + + const indicesWithoutInit: number[] = []; + reduce(values, (_acc, _val, i) => { + indicesWithoutInit.push(i); + }); + // First call is for the second element, so index is 1 + expect(indicesWithoutInit).toEqual([1, 2]); + }); +}); + +describe("reduceAsync", () => { + it("should work with a sync iterable and sync accumulator", async () => { + const values = [1, 2, 3, 4]; + const result = await reduceAsync(values, (acc, val) => acc + val); + expect(result).toBe(10); + }); + + it("should work with a sync iterable and async accumulator", async () => { + const values = [1, 2, 3, 4]; + const result = await reduceAsync( + values, + async (acc, val) => { + await new Promise((resolve) => setTimeout(resolve, 1)); + return acc + val; + }, + 0, + ); + expect(result).toBe(10); + }); + + it("should work with an async iterable and sync accumulator", async () => { + const values = createAsyncIterable([1, 2, 3, 4]); + const result = await reduceAsync(values, (acc, val) => acc + val, 0); + expect(result).toBe(10); + }); + + it("should work with an async iterable and async accumulator", async () => { + const values = createAsyncIterable([1, 2, 3, 4]); + const result = await reduceAsync( + values, + async (acc, val) => { + await new Promise((resolve) => setTimeout(resolve, 1)); + return acc + val; + }, + 0, + ); + expect(result).toBe(10); + }); + + it("should work with a promise as an initial value", async () => { + const values = [1, 2, 3, 4]; + const init = Promise.resolve(10); + const result = await reduceAsync(values, (acc, val) => acc + val, init); + expect(result).toBe(20); + }); + + it("should throw a TypeError for an empty iterable with no initial value", async () => { + const values: number[] = []; + await expect(reduceAsync(values, (acc, val) => acc + val)).rejects.toThrow( + "Reduce of empty iterator with no initial value", + ); + }); + + it("should return the initial value for an empty async iterable", async () => { + const values = createAsyncIterable([]); + const result = await reduceAsync(values, (acc, val) => acc + val, 100); + expect(result).toBe(100); + }); + + it("should keep previous result if async accumulator returns null", async () => { + const values = createAsyncIterable([1, 2, 3, 4]); + const result = await reduceAsync( + values, + async (acc, val) => { + return val % 2 !== 0 ? acc + val : Promise.resolve(null); + }, + 0, + ); + expect(result).toBe(4); + }); + + it("should pass correct index to the accumulator", async () => { + const values = ["a", "b", "c"]; + const indicesWithInit: number[] = []; + await reduceAsync( + values, + (acc, _val, i) => { + indicesWithInit.push(i); + return acc; + }, + "", + ); + expect(indicesWithInit).toEqual([0, 1, 2]); + + const indicesWithoutInit: number[] = []; + await reduceAsync(values, (acc, _val, i) => { + indicesWithoutInit.push(i); + return acc; + }); + // First call is for the second element, so index is 1 + expect(indicesWithoutInit).toEqual([1, 2]); + }); +}); diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index e9e73364b..08f859d65 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -99,73 +99,141 @@ export function apply( } /** - * Similar to Array.reduce, but the accumulator can returns Promise. + * Similar to Array.reduce, but works on any iterable. * @public * - * @param values - The array to be reduced. - * @param accumulator - A callback to be called for each value. If it returns null, the previous result will be kept. + * @param values - The iterable to be reduced. + * @param accumulator - A callback to be called for each value. If it returns null or undefined, the previous result will be kept. + * @returns The accumulated result. + */ +export function reduce( + values: Iterable, + accumulator: (a: T, b: T, i: number) => T | undefined | null | void, +): T; +/** + * Similar to Array.reduce, but works on any iterable. + * @public + * + * @param values - The iterable to be reduced. + * @param accumulator - A callback to be called for each value. If it returns null or undefined, the previous result will be kept. + * @param init - The initial value. + * @returns The accumulated result. + */ +export function reduce( + values: Iterable, + accumulator: (a: T, b: V, i: number) => T | undefined | null | void, + init: T, +): T; +/** + * Similar to Array.reduce, but works on any iterable. + * @public + * + * @param values - The iterable to be reduced. + * @param accumulator - A callback to be called for each value. If it returns null or undefined, the previous result will be kept. + * @param init - The initial value. + * @returns The accumulated result. + */ +export function reduce( + values: Iterable | Iterable, + accumulator: (a: T, b: T | V, i: number) => T | undefined | null | void, + init?: T, +): T { + const hasInit = arguments.length > 2; + + let acc: T = init as T; // The compiler thinks `acc` isn't assigned without this. Since `T` might be nullable, we should not use non-null assertion here. + let i = 0; + + for (const value of values) { + if (!hasInit && i === 0) { + acc = value as T; + i++; + continue; + } + + acc = accumulator(acc, value, i) ?? acc; + i++; + } + + if (!hasInit && i === 0) { + throw new TypeError("Reduce of empty iterator with no initial value"); + } + + return acc; +} + +/** + * Similar to Array.reduce, but works on async iterables and the accumulator can return a Promise. + * @public + * + * @param values - The iterable or async iterable to be reduced. + * @param accumulator - A callback to be called for each value. If it returns null or undefined, the previous result will be kept. * @returns The accumulated result. */ export async function reduceAsync( - values: T[], + values: Iterable | AsyncIterable, accumulator: ( a: T, b: T, + i: number, ) => Promise | T | undefined | null | void, ): Promise; /** - * Similar to Array.reduce, but the accumulator can returns Promise. + * Similar to Array.reduce, but works on async iterables and the accumulator can return a Promise. * @public * - * @param values - The array to be reduced. - * @param accumulator - A callback to be called for each value. If it returns null, the previous result will be kept. + * @param values - The iterable or async iterable to be reduced. + * @param accumulator - A callback to be called for each value. If it returns null or undefined, the previous result will be kept. * @param init - The initial value. * @returns The accumulated result. */ export async function reduceAsync( - values: V[], + values: Iterable | AsyncIterable, accumulator: ( a: T, b: V, i: number, - values: V[], ) => Promise | T | undefined | null | void, init: T | Promise, ): Promise; /** - * Similar to Array.reduce, but the accumulator can returns Promise. + * Similar to Array.reduce, but works on async iterables and the accumulator can return a Promise. * @public * - * @param values - The array to be reduced. - * @param accumulator - A callback to be called for each value. If it returns null, the previous result will be kept. + * @param values - The iterable or async iterable to be reduced. + * @param accumulator - A callback to be called for each value. If it returns null or undefined, the previous result will be kept. * @param init - The initial value. * @returns The accumulated result. */ export async function reduceAsync( - values: (V | T)[], + values: Iterable | AsyncIterable | Iterable | AsyncIterable, accumulator: ( a: T, b: T | V, i: number, - values: (V | T)[], ) => Promise | T | undefined | null | void, init?: T | Promise, ): Promise { - if (init === undefined) { - if (values.length === 0) { - throw new TypeError("Reduce of empty array with no initial value"); + const hasInit = arguments.length > 2; + + let acc: T = (await Promise.resolve(init)) as T; // The compiler thinks `acc` isn't assigned without this. Since `T` might be nullable, we should not use non-null assertion here. + let i = 0; + + for await (const value of values) { + if (!hasInit && i === 0) { + acc = value as T; + i++; + continue; } - init = values[0] as T; - values = values.slice(1); + + acc = (await accumulator(acc, value, i)) ?? acc; + i++; + } + + if (!hasInit && i === 0) { + throw new TypeError("Reduce of empty iterator with no initial value"); } - return values.reduce( - (current: Promise, b: T | V, i, array) => - current.then((v) => - Promise.resolve(accumulator(v, b, i, array)).then((r) => r ?? v), - ), - Promise.resolve(init), - ); + return acc; } export function sleep(ms: NumLike) { From b90cd5b4a1728a756d3219259890558e4f2c6f94 Mon Sep 17 00:00:00 2001 From: Phroi <90913182+phroi@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:12:06 +0700 Subject: [PATCH 13/81] feat(mol): add support for fixed-size Union (#174) * feat(mol): add support for fixed-size Union * feat(mol): clarify fixed-size Union TypeDoc Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/core/src/molecule/codec.ts | 63 +++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/core/src/molecule/codec.ts b/packages/core/src/molecule/codec.ts index cf0af43a6..4e80d7a1d 100644 --- a/packages/core/src/molecule/codec.ts +++ b/packages/core/src/molecule/codec.ts @@ -459,34 +459,71 @@ type UnionDecoded< : never; /** - * Union is a dynamic-size type. - * Serializing a union has two steps: - * - Serialize an item type id in bytes as a 32 bit unsigned integer in little-endian. The item type id is the index of the inner items, and it's starting at 0. - * - Serialize the inner item. - * @param codecLayout the union item record - * @param fields the custom item type id record + * Constructs a union codec that can serialize and deserialize values tagged with a type identifier. + * + * If all variants have the same fixed size, the resulting union codec is fixed-size (header + payload). + * Otherwise, it falls back to a dynamic-size codec. + * + * Serialization format: + * 1. 4-byte little-endian unsigned integer for the variant index. + * 2. Encoded bytes of the selected variant. + * + * @typeParam T + * A record mapping variant names to codecs. + * @param codecLayout + * An object whose keys are variant names and values are codecs for each variant. + * @param fields + * Optional mapping from variant names to custom numeric IDs. If omitted, the index + * of each variant in `codecLayout` is used as its ID. + * + * * @example - * // without custom id - * union({ cafe: Uint8, bee: Uint8 }) - * // with custom id - * union({ cafe: Uint8, bee: Uint8 }, { cafe: 0xcafe, bee: 0xbee }) + * // Dynamic union without custom numeric IDs + * union({ cafe: Uint8, bee: Uint16 }) + * + * // Dynamic union with custom numeric IDs + * union({ cafe: Uint8, bee: Uint16 }, { cafe: 0xcafe, bee: 0xbee }) + * + * // Fixed-size union without custom numeric IDs + * const PaddedUint8 = struct({ data : u8, padding : u8 }) + * union({ cafe: PaddedUint8, bee: Uint16 }); + * + * // Fixed-size union with custom numeric IDs + * union({ cafe: PaddedUint8, bee: Uint16 }, { cafe: 0xcafe, bee: 0xbee }) */ + export function union>>( codecLayout: T, fields?: Record, ): Codec, UnionDecoded> { - const keys = Object.keys(codecLayout); + const entries = Object.entries(codecLayout); + + // Determine if all variants have a fixed and equal byteLength. + let byteLength: number | undefined; + if (entries.length > 0) { + const firstLen = entries[0][1].byteLength; + if ( + firstLen !== undefined && + entries.every(([, { byteLength: len }]) => len === firstLen) + ) { + // Add 4 bytes for the type header + byteLength = firstLen + 4; + } + } return Codec.from({ + byteLength, encode({ type, value }) { const typeStr = type.toString(); const codec = codecLayout[typeStr]; if (!codec) { throw new Error( - `union: invalid type, expected ${keys.toString()}, but got ${typeStr}`, + `union: invalid type, expected ${entries.map((e) => e[0]).toString()}, but got ${typeStr}`, ); } - const fieldId = fields ? (fields[typeStr] ?? -1) : keys.indexOf(typeStr); + const fieldId = fields + ? (fields[typeStr] ?? -1) + : entries.findIndex((e) => e[0] === typeStr); if (fieldId < 0) { throw new Error(`union: invalid field id ${fieldId} of ${typeStr}`); } From 265af7d889cea4123f4a10214a82c22765641cbf Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 21 Aug 2025 08:13:21 +0800 Subject: [PATCH 14/81] fix: `prettier-vscode` doesn't work with `import` --- config/prettier.config.mjs | 2 +- packages/ccc/prettier.config.mjs | 2 +- packages/ckb-ccc/prettier.config.mjs | 2 +- packages/connector-react/prettier.config.mjs | 2 +- packages/connector/prettier.config.mjs | 2 +- packages/core/prettier.config.mjs | 2 +- packages/demo/prettier.config.mjs | 5 +---- packages/eip6963/prettier.config.mjs | 2 +- packages/examples/prettier.config.mjs | 2 +- packages/faucet/prettier.config.mjs | 2 +- packages/joy-id/prettier.config.mjs | 2 +- packages/lumos-patches/prettier.config.mjs | 2 +- packages/nip07/prettier.config.mjs | 2 +- packages/okx/prettier.config.mjs | 2 +- packages/playground/prettier.config.mjs | 5 +---- packages/rei/prettier.config.mjs | 2 +- packages/shell/prettier.config.mjs | 2 +- packages/spore/prettier.config.mjs | 2 +- packages/ssri/prettier.config.mjs | 2 +- packages/udt/prettier.config.mjs | 2 +- packages/uni-sat/prettier.config.mjs | 2 +- packages/utxo-global/prettier.config.mjs | 2 +- packages/xverse/prettier.config.mjs | 2 +- 23 files changed, 23 insertions(+), 29 deletions(-) diff --git a/config/prettier.config.mjs b/config/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/config/prettier.config.mjs +++ b/config/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/ccc/prettier.config.mjs b/packages/ccc/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/ccc/prettier.config.mjs +++ b/packages/ccc/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/ckb-ccc/prettier.config.mjs b/packages/ckb-ccc/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/ckb-ccc/prettier.config.mjs +++ b/packages/ckb-ccc/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/connector-react/prettier.config.mjs b/packages/connector-react/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/connector-react/prettier.config.mjs +++ b/packages/connector-react/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/connector/prettier.config.mjs b/packages/connector/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/connector/prettier.config.mjs +++ b/packages/connector/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/core/prettier.config.mjs b/packages/core/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/core/prettier.config.mjs +++ b/packages/core/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/demo/prettier.config.mjs b/packages/demo/prettier.config.mjs index da7e5eaf2..edf37446c 100644 --- a/packages/demo/prettier.config.mjs +++ b/packages/demo/prettier.config.mjs @@ -7,10 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [ - import.meta.resolve("prettier-plugin-organize-imports/index.js"), - import.meta.resolve("prettier-plugin-tailwindcss"), - ], + plugins: ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"], }; export default config; diff --git a/packages/eip6963/prettier.config.mjs b/packages/eip6963/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/eip6963/prettier.config.mjs +++ b/packages/eip6963/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/examples/prettier.config.mjs b/packages/examples/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/examples/prettier.config.mjs +++ b/packages/examples/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/faucet/prettier.config.mjs b/packages/faucet/prettier.config.mjs index 3dbe282d7..7be7ba0bc 100644 --- a/packages/faucet/prettier.config.mjs +++ b/packages/faucet/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; diff --git a/packages/joy-id/prettier.config.mjs b/packages/joy-id/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/joy-id/prettier.config.mjs +++ b/packages/joy-id/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/lumos-patches/prettier.config.mjs b/packages/lumos-patches/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/lumos-patches/prettier.config.mjs +++ b/packages/lumos-patches/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/nip07/prettier.config.mjs b/packages/nip07/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/nip07/prettier.config.mjs +++ b/packages/nip07/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/okx/prettier.config.mjs b/packages/okx/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/okx/prettier.config.mjs +++ b/packages/okx/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/playground/prettier.config.mjs b/packages/playground/prettier.config.mjs index da7e5eaf2..edf37446c 100644 --- a/packages/playground/prettier.config.mjs +++ b/packages/playground/prettier.config.mjs @@ -7,10 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [ - import.meta.resolve("prettier-plugin-organize-imports/index.js"), - import.meta.resolve("prettier-plugin-tailwindcss"), - ], + plugins: ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"], }; export default config; diff --git a/packages/rei/prettier.config.mjs b/packages/rei/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/rei/prettier.config.mjs +++ b/packages/rei/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/shell/prettier.config.mjs b/packages/shell/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/shell/prettier.config.mjs +++ b/packages/shell/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/spore/prettier.config.mjs b/packages/spore/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/spore/prettier.config.mjs +++ b/packages/spore/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/ssri/prettier.config.mjs b/packages/ssri/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/ssri/prettier.config.mjs +++ b/packages/ssri/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/udt/prettier.config.mjs b/packages/udt/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/udt/prettier.config.mjs +++ b/packages/udt/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/uni-sat/prettier.config.mjs b/packages/uni-sat/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/uni-sat/prettier.config.mjs +++ b/packages/uni-sat/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/utxo-global/prettier.config.mjs b/packages/utxo-global/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/utxo-global/prettier.config.mjs +++ b/packages/utxo-global/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file diff --git a/packages/xverse/prettier.config.mjs b/packages/xverse/prettier.config.mjs index 054c23e8f..af1401cbb 100644 --- a/packages/xverse/prettier.config.mjs +++ b/packages/xverse/prettier.config.mjs @@ -7,7 +7,7 @@ const config = { singleQuote: false, trailingComma: "all", - plugins: [import.meta.resolve("prettier-plugin-organize-imports")], + plugins: ["prettier-plugin-organize-imports"], }; export default config; \ No newline at end of file From 4675e6f53269c9670e5a9c35a3d39c96ccb9ff36 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 21 Aug 2025 10:20:22 +0800 Subject: [PATCH 15/81] chore: move release branch from `master` to `release` --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index de4f8e6a3..1b0dfbce5 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -7,7 +7,7 @@ permissions: on: push: branches: - - master + - release - "releases/**" concurrency: ${{ github.workflow }}-${{ github.ref }} From 638e5dc575b75270ab1e8d1572470820a72af57a Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 21 Aug 2025 05:03:05 +0800 Subject: [PATCH 16/81] fix(core)!: `getFeeRateStatistics` may returns `null` on devnet --- .changeset/sixty-games-scream.md | 5 +++++ packages/core/src/client/client.ts | 4 ++-- packages/core/src/client/jsonRpc/client.ts | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/sixty-games-scream.md diff --git a/.changeset/sixty-games-scream.md b/.changeset/sixty-games-scream.md new file mode 100644 index 000000000..e419f74a7 --- /dev/null +++ b/.changeset/sixty-games-scream.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": major +--- + +fix(core)!: `getFeeRateStatistics` may returns `null` on devnet diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index 6aee1c992..87de6a687 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -54,13 +54,13 @@ export abstract class Client { abstract getFeeRateStatistics( blockRange?: NumLike, - ): Promise<{ mean: Num; median: Num }>; + ): Promise<{ mean?: Num; median?: Num }>; async getFeeRate( blockRange?: NumLike, options?: { maxFeeRate?: NumLike }, ): Promise { const feeRate = numMax( - (await this.getFeeRateStatistics(blockRange)).median, + (await this.getFeeRateStatistics(blockRange)).median ?? Zero, DEFAULT_MIN_FEE_RATE, ); diff --git a/packages/core/src/client/jsonRpc/client.ts b/packages/core/src/client/jsonRpc/client.ts index a30e8651b..b40db55db 100644 --- a/packages/core/src/client/jsonRpc/client.ts +++ b/packages/core/src/client/jsonRpc/client.ts @@ -131,9 +131,9 @@ export abstract class ClientJsonRpc extends Client { getFeeRateStatistics = this.buildSender( "get_fee_rate_statistics", [(n: NumLike) => apply(numFrom, n)], - ({ mean, median }: { mean: NumLike; median: NumLike }) => ({ - mean: numFrom(mean), - median: numFrom(median), + (res: { mean: NumLike; median: NumLike } | null | undefined) => ({ + mean: apply(numFrom, res?.mean), + median: apply(numFrom, res?.median), }), ) as Client["getFeeRateStatistics"]; From f547718b5da4c9bb7e2d29f4df5586088d4f3e0b Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 28 Aug 2025 14:06:22 +0800 Subject: [PATCH 17/81] docs: fix images in README.md --- README.md | 12 ++++++------ packages/docs/docs/CCC.mdx | 14 ++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b7dc74817..7101738e7 100644 --- a/README.md +++ b/README.md @@ -158,14 +158,14 @@ pnpm run dev ## Who uses CCC? -| [](https://nervdao.com/) | [](https://utxo.global/) | [](https://mobit.app/) | [](https://omiga.io/) | -| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| [](https://nervdao.com/) | [](https://utxo.global/) | [](https://mobit.app/) | [](https://omiga.io/) | +| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -| [](https://www.nervape.com/) | [](https://utxoswap.xyz/) | [](https://d.id/) | [](https://bool.network/) | -| ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| [](https://www.nervape.com/) | [](https://utxoswap.xyz/) | [](https://d.id/) | [](https://bool.network/) | +| -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| [](https://world3.ai/) | [](https://catnip.rgbcat.io/) | -| ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| [](https://world3.ai/) | [](https://catnip.rgbcat.io/) | +| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ## FAQs diff --git a/packages/docs/docs/CCC.mdx b/packages/docs/docs/CCC.mdx index e6217ded3..605d6e298 100644 --- a/packages/docs/docs/CCC.mdx +++ b/packages/docs/docs/CCC.mdx @@ -195,14 +195,16 @@ pnpm run dev ## Who uses CCC? -| [](https://nervdao.com/) | [](https://utxo.global/) | [](https://mobit.app/) | [](https://omiga.io/) | -| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +export const projectImg = { height: "50px" }; -| [](https://www.nervape.com/) | [](https://utxoswap.xyz/) | [](https://d.id/) | [](https://bool.network/) | -| ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| [](https://nervdao.com/) | [](https://utxo.global/) | [](https://mobit.app/) | [](https://omiga.io/) | +| --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| [](https://world3.ai/) | [](https://catnip.rgbcat.io/) | -| ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| [](https://www.nervape.com/) | [](https://utxoswap.xyz/) | [](https://d.id/) | [](https://bool.network/) | +| ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | + +| [](https://world3.ai/) | [](https://catnip.rgbcat.io/) | +| ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ## FAQs From f82f9afde1b59111e19170ca7e95cc09871d8460 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Sun, 17 Aug 2025 21:28:50 +0800 Subject: [PATCH 18/81] feat(core): default `Signer.prepareTransaction` --- .changeset/old-eagles-bake.md | 6 ++++++ packages/core/src/signer/signer/index.ts | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .changeset/old-eagles-bake.md diff --git a/.changeset/old-eagles-bake.md b/.changeset/old-eagles-bake.md new file mode 100644 index 000000000..70b9e8d47 --- /dev/null +++ b/.changeset/old-eagles-bake.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": patch +--- + +feat(core): default `Signer.prepareTransaction` + \ No newline at end of file diff --git a/packages/core/src/signer/signer/index.ts b/packages/core/src/signer/signer/index.ts index 5d5c7b147..819af378d 100644 --- a/packages/core/src/signer/signer/index.ts +++ b/packages/core/src/signer/signer/index.ts @@ -459,14 +459,23 @@ export abstract class Signer { } /** - * prepare a transaction before signing. This method is not implemented and should be overridden by subclasses. + * Prepares a transaction before signing. + * This method can be overridden by subclasses to perform any necessary steps, + * such as adding cell dependencies or witnesses, before the transaction is signed. + * The default implementation converts the {@link TransactionLike} object to a {@link Transaction} object + * without modification. * - * @param _ - The transaction to prepare, represented as a TransactionLike object. - * @returns A promise that resolves to the prepared Transaction object. - * @throws Will throw an error if not implemented. + * @remarks + * Note that this default implementation does not add any cell dependencies or dummy witnesses. + * This may lead to an underestimation of transaction size and fees if used with methods + * like `Transaction.completeFee`. Subclasses for signers that are intended to sign + * transactions should override this method to perform necessary preparations. + * + * @param tx - The transaction to prepare. + * @returns A promise that resolves to the prepared {@link Transaction} object. */ - prepareTransaction(_: TransactionLike): Promise { - throw Error("Signer.prepareTransaction not implemented"); + async prepareTransaction(tx: TransactionLike): Promise { + return Transaction.from(tx); } /** From 99a203699dac9d7b4a79a5a17b64513d56c907c2 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Tue, 9 Sep 2025 18:36:58 +0800 Subject: [PATCH 19/81] Revert "feat(udt): `Udt.complete*` methods" This reverts commit 439380360a053c511c5ef741b3afeca77da9096a. It will be moved to a new branch `feat/udt` --- .changeset/plenty-ads-rush.md | 6 - .changeset/shy-horses-agree.md | 6 - packages/core/src/ckb/transaction.ts | 12 - packages/core/src/ckb/transactionErrors.ts | 3 - packages/ssri/src/executor.ts | 8 - packages/udt/package.json | 2 +- packages/udt/src/udt/index.test.ts | 1001 --------------- packages/udt/src/udt/index.ts | 1295 +------------------- packages/udt/src/udtPausable/index.ts | 6 +- packages/udt/vitest.config.ts | 10 - vitest.config.ts | 4 +- 11 files changed, 61 insertions(+), 2292 deletions(-) delete mode 100644 .changeset/plenty-ads-rush.md delete mode 100644 .changeset/shy-horses-agree.md delete mode 100644 packages/udt/src/udt/index.test.ts delete mode 100644 packages/udt/vitest.config.ts diff --git a/.changeset/plenty-ads-rush.md b/.changeset/plenty-ads-rush.md deleted file mode 100644 index 8db5049cd..000000000 --- a/.changeset/plenty-ads-rush.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@ckb-ccc/ssri": minor ---- - -feat(ssri): `ExecutorResponse.mapAsync` - \ No newline at end of file diff --git a/.changeset/shy-horses-agree.md b/.changeset/shy-horses-agree.md deleted file mode 100644 index f42efc551..000000000 --- a/.changeset/shy-horses-agree.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@ckb-ccc/udt": minor ---- - -feat(udt): `Udt.complete*` methods - diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index df0e84c34..fcaab9135 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1911,12 +1911,6 @@ export class Transaction extends mol.Entity.Base< ); } - /** - * @deprecated Use `Udt.getInputsBalance` from `@ckb-ccc/udt` instead - * @param client - * @param type - * @returns - */ async getInputsUdtBalance(client: Client, type: ScriptLike): Promise { return reduceAsync( this.inputs, @@ -1932,11 +1926,6 @@ export class Transaction extends mol.Entity.Base< ); } - /** - * @deprecated Use `Udt.getOutputsBalance` from `@ckb-ccc/udt` instead - * @param type - * @returns - */ getOutputsUdtBalance(type: ScriptLike): Num { return this.outputs.reduce((acc, output, i) => { if (!output.type?.eq(type)) { @@ -2054,7 +2043,6 @@ export class Transaction extends mol.Entity.Base< * This method succeeds only if enough balance is collected. * * It will try to collect at least two inputs, even when the first input already contains enough balance, to avoid extra occupation fees introduced by the change cell. An edge case: If the first cell has the same amount as the output, a new cell is not needed. - * @deprecated Use `Udt.completeInputsByBalance` from `@ckb-ccc/udt` instead * @param from - The signer to complete the inputs. * @param type - The type script of the UDT. * @param balanceTweak - The tweak of the balance. diff --git a/packages/core/src/ckb/transactionErrors.ts b/packages/core/src/ckb/transactionErrors.ts index 26057669a..09c1cb745 100644 --- a/packages/core/src/ckb/transactionErrors.ts +++ b/packages/core/src/ckb/transactionErrors.ts @@ -22,9 +22,6 @@ export class ErrorTransactionInsufficientCapacity extends Error { } } -/** - * @deprecated Use `ErrorUdtInsufficientCoin` from `@ckb-ccc/udt` instead. - */ export class ErrorTransactionInsufficientCoin extends Error { public readonly amount: Num; public readonly type: Script; diff --git a/packages/ssri/src/executor.ts b/packages/ssri/src/executor.ts index b7ae02d44..d4548b8bd 100644 --- a/packages/ssri/src/executor.ts +++ b/packages/ssri/src/executor.ts @@ -63,14 +63,6 @@ export class ExecutorResponse { throw new ExecutorErrorDecode(JSON.stringify(err)); } } - - async mapAsync(fn: (res: T) => Promise): Promise> { - try { - return new ExecutorResponse(await fn(this.res), this.cellDeps); - } catch (err) { - throw new ExecutorErrorDecode(JSON.stringify(err)); - } - } } /** diff --git a/packages/udt/package.json b/packages/udt/package.json index a9c27d3a0..7b54dcb1b 100644 --- a/packages/udt/package.json +++ b/packages/udt/package.json @@ -23,7 +23,7 @@ } }, "scripts": { - "test": "vitest", + "test": "jest", "build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json && copyfiles -u 2 misc/basedirs/**/* .", "lint": "eslint ./src", "format": "prettier --write . && eslint --fix ./src" diff --git a/packages/udt/src/udt/index.test.ts b/packages/udt/src/udt/index.test.ts deleted file mode 100644 index 00433e894..000000000 --- a/packages/udt/src/udt/index.test.ts +++ /dev/null @@ -1,1001 +0,0 @@ -import { ccc } from "@ckb-ccc/core"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { Udt } from "./index.js"; - -let client: ccc.Client; -let signer: ccc.Signer; -let lock: ccc.Script; -let type: ccc.Script; -let udt: Udt; - -beforeEach(async () => { - client = new ccc.ClientPublicTestnet(); - signer = new ccc.SignerCkbPublicKey( - client, - "0x026f3255791f578cc5e38783b6f2d87d4709697b797def6bf7b3b9af4120e2bfd9", - ); - lock = (await signer.getRecommendedAddressObj()).script; - - type = await ccc.Script.fromKnownScript( - client, - ccc.KnownScript.XUdt, - "0xf8f94a13dfe1b87c10312fb9678ab5276eefbe1e0b2c62b4841b1f393494eff2", - ); - - // Create UDT instance - udt = new Udt( - { - txHash: - "0x4e2e832e0b1e7b5994681b621b00c1e65f577ee4b440ef95fa07db9bb3d50269", - index: 0, - }, - type, - ); -}); - -describe("Udt", () => { - describe("completeInputsByBalance", () => { - // Mock cells with 100 UDT each (10 cells total = 1000 UDT) - let mockUdtCells: ccc.Cell[]; - - beforeEach(async () => { - // Create mock cells after type is initialized - mockUdtCells = Array.from({ length: 10 }, (_, i) => - ccc.Cell.from({ - outPoint: { - txHash: `0x${"0".repeat(63)}${i.toString(16)}`, - index: 0, - }, - cellOutput: { - capacity: ccc.fixedPointFrom(142), - lock, - type, - }, - outputData: ccc.numLeToBytes(100, 16), // 100 UDT tokens - }), - ); - }); - - beforeEach(() => { - // Mock the findCells method to return our mock UDT cells - vi.spyOn(signer, "findCells").mockImplementation( - async function* (filter) { - if (filter.script && ccc.Script.from(filter.script).eq(type)) { - for (const cell of mockUdtCells) { - yield cell; - } - } - }, - ); - - // Mock client.getCell to return the cell data for inputs - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - const cell = mockUdtCells.find((c) => c.outPoint.eq(outPoint)); - return cell; - }); - }); - - it("should return 0 when no UDT balance is needed", async () => { - const tx = ccc.Transaction.from({ - outputs: [], - }); - - const { addedCount } = await udt.completeInputsByBalance(tx, signer); - expect(addedCount).toBe(0); - }); - - it("should collect exactly the required UDT balance", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { - lock, - type, - }, - ], - outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT - }); - - const { addedCount } = await udt.completeInputsByBalance(tx, signer); - - // Should add 2 cells (200 UDT total) to have at least 2 inputs - expect(addedCount).toBe(2); - expect(tx.inputs.length).toBe(2); - - // Verify the inputs are UDT cells - const inputBalance = await udt.getInputsBalance(tx, client); - expect(inputBalance).toBe(ccc.numFrom(200)); - }); - - it("should collect exactly one cell when amount matches exactly", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { - lock, - type, - }, - ], - outputsData: [ccc.numLeToBytes(100, 16)], // Need exactly 100 UDT - }); - - const { addedCount } = await udt.completeInputsByBalance(tx, signer); - - // Should add only 1 cell since it matches exactly - expect(addedCount).toBe(1); - expect(tx.inputs.length).toBe(1); - - const inputBalance = await udt.getInputsBalance(tx, client); - expect(inputBalance).toBe(ccc.numFrom(100)); - }); - - it("should handle balanceTweak parameter", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { - lock, - type, - }, - ], - outputsData: [ccc.numLeToBytes(100, 16)], // Need 100 UDT - }); - - // Add 50 extra UDT requirement via balanceTweak - const { addedCount } = await udt.completeInputsByBalance(tx, signer, 50); - - // Should add 2 cells to cover 150 UDT total requirement - expect(addedCount).toBe(2); - expect(tx.inputs.length).toBe(2); - - const inputBalance = await udt.getInputsBalance(tx, client); - expect(inputBalance).toBe(ccc.numFrom(200)); - }); - - it("should return 0 when existing inputs already satisfy the requirement", async () => { - const tx = ccc.Transaction.from({ - inputs: [ - { - previousOutput: mockUdtCells[0].outPoint, - }, - { - previousOutput: mockUdtCells[1].outPoint, - }, - ], - outputs: [ - { - lock, - type, - }, - ], - outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT, already have 200 - }); - - const { addedCount } = await udt.completeInputsByBalance(tx, signer); - - // Should not add any inputs since we already have enough - expect(addedCount).toBe(0); - expect(tx.inputs.length).toBe(2); - }); - - it("should throw error when insufficient UDT balance available", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { - lock, - type, - }, - ], - outputsData: [ccc.numLeToBytes(1500, 16)], // Need 1500 UDT, only have 1000 available - }); - - await expect(udt.completeInputsByBalance(tx, signer)).rejects.toThrow( - "Insufficient coin, need 500 extra coin", - ); - }); - - it("should handle multiple UDT outputs correctly", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { - lock, - type, - }, - { - lock, - type, - }, - ], - outputsData: [ - ccc.numLeToBytes(100, 16), // First output: 100 UDT - ccc.numLeToBytes(150, 16), // Second output: 150 UDT - ], // Total: 250 UDT needed - }); - - const { addedCount } = await udt.completeInputsByBalance(tx, signer); - - // Should add 3 cells to cover 250 UDT requirement (300 UDT total) - expect(addedCount).toBe(3); - expect(tx.inputs.length).toBe(3); - - const inputBalance = await udt.getInputsBalance(tx, client); - expect(inputBalance).toBe(ccc.numFrom(300)); - - const outputBalance = await udt.getOutputsBalance(tx, client); - expect(outputBalance).toBe(ccc.numFrom(250)); - }); - - it("should skip cells that are already used as inputs", async () => { - // Pre-add one of the mock cells as input - const tx = ccc.Transaction.from({ - inputs: [ - { - previousOutput: mockUdtCells[0].outPoint, - }, - ], - outputs: [ - { - lock, - type, - }, - ], - outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT, already have 100 - }); - - const { addedCount } = await udt.completeInputsByBalance(tx, signer); - - // Should add 1 more cell (since we already have 1 input with 100 UDT) - expect(addedCount).toBe(1); - expect(tx.inputs.length).toBe(2); - - const inputBalance = await udt.getInputsBalance(tx, client); - expect(inputBalance).toBe(ccc.numFrom(200)); - }); - - it("should add one cell when user needs less than one cell", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { - lock, - type, - }, - ], - outputsData: [ccc.numLeToBytes(50, 16)], // Need only 50 UDT (less than one cell) - }); - - const { addedCount } = await udt.completeInputsByBalance(tx, signer); - - // UDT completeInputsByBalance adds minimum inputs needed - expect(addedCount).toBe(1); - expect(tx.inputs.length).toBe(1); - - const inputBalance = await udt.getInputsBalance(tx, client); - expect(inputBalance).toBe(ccc.numFrom(100)); - }); - }); - - describe("completeInputsAll", () => { - // Mock cells with 100 UDT each (5 cells total = 500 UDT) - let mockUdtCells: ccc.Cell[]; - - beforeEach(async () => { - // Create mock cells after type is initialized - mockUdtCells = Array.from({ length: 5 }, (_, i) => - ccc.Cell.from({ - outPoint: { - txHash: `0x${"a".repeat(63)}${i.toString(16)}`, - index: 0, - }, - cellOutput: { - capacity: ccc.fixedPointFrom(142 + i * 10), // Varying capacity: 142, 152, 162, 172, 182 - lock, - type, - }, - outputData: ccc.numLeToBytes(100, 16), // 100 UDT tokens each - }), - ); - }); - - beforeEach(() => { - // Mock the findCells method to return our mock UDT cells - vi.spyOn(signer, "findCells").mockImplementation( - async function* (filter) { - if (filter.script && ccc.Script.from(filter.script).eq(type)) { - for (const cell of mockUdtCells) { - yield cell; - } - } - }, - ); - - // Mock client.getCell to return the cell data for inputs - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - const cell = mockUdtCells.find((c) => c.outPoint.eq(outPoint)); - return cell; - }); - }); - - it("should add all available UDT cells to empty transaction", async () => { - const tx = ccc.Transaction.from({ - outputs: [], - }); - - const { tx: completedTx, addedCount } = await udt.completeInputsAll( - tx, - signer, - ); - - // Should add all 5 available UDT cells - expect(addedCount).toBe(5); - expect(completedTx.inputs.length).toBe(5); - - // Verify total UDT balance is 500 (5 cells × 100 UDT each) - const inputBalance = await udt.getInputsBalance(completedTx, client); - expect(inputBalance).toBe(ccc.numFrom(500)); - - // Verify all cells were added by checking outpoints - const addedOutpoints = completedTx.inputs.map( - (input) => input.previousOutput, - ); - for (const cell of mockUdtCells) { - expect(addedOutpoints.some((op) => op.eq(cell.outPoint))).toBe(true); - } - }); - - it("should add all available UDT cells to transaction with outputs", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { lock, type }, - { lock, type }, - ], - outputsData: [ - ccc.numLeToBytes(150, 16), // 150 UDT - ccc.numLeToBytes(200, 16), // 200 UDT - ], // Total: 350 UDT needed - }); - - const { tx: completedTx, addedCount } = await udt.completeInputsAll( - tx, - signer, - ); - - // Should add all 5 available UDT cells regardless of output requirements - expect(addedCount).toBe(5); - expect(completedTx.inputs.length).toBe(5); - - // Verify total UDT balance is 500 (all available) - const inputBalance = await udt.getInputsBalance(completedTx, client); - expect(inputBalance).toBe(ccc.numFrom(500)); - - // Verify output balance is still 350 - const outputBalance = await udt.getOutputsBalance(completedTx, client); - expect(outputBalance).toBe(ccc.numFrom(350)); - - // Should have 150 UDT excess balance (500 - 350) - const balanceBurned = await udt.getBalanceBurned(completedTx, client); - expect(balanceBurned).toBe(ccc.numFrom(150)); - }); - - it("should skip cells already used as inputs", async () => { - // Pre-add 2 of the mock cells as inputs - const tx = ccc.Transaction.from({ - inputs: [ - { previousOutput: mockUdtCells[0].outPoint }, - { previousOutput: mockUdtCells[1].outPoint }, - ], - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(100, 16)], - }); - - const { tx: completedTx, addedCount } = await udt.completeInputsAll( - tx, - signer, - ); - - // Should add the remaining 3 cells (cells 2, 3, 4) - expect(addedCount).toBe(3); - expect(completedTx.inputs.length).toBe(5); // 2 existing + 3 added - - // Verify total UDT balance is still 500 (all 5 cells) - const inputBalance = await udt.getInputsBalance(completedTx, client); - expect(inputBalance).toBe(ccc.numFrom(500)); - }); - - it("should return 0 when all UDT cells are already used as inputs", async () => { - // Pre-add all mock cells as inputs - const tx = ccc.Transaction.from({ - inputs: mockUdtCells.map((cell) => ({ previousOutput: cell.outPoint })), - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(100, 16)], - }); - - const { tx: completedTx, addedCount } = await udt.completeInputsAll( - tx, - signer, - ); - - // Should not add any new inputs - expect(addedCount).toBe(0); - expect(completedTx.inputs.length).toBe(5); // Same as before - - // Verify total UDT balance is still 500 - const inputBalance = await udt.getInputsBalance(completedTx, client); - expect(inputBalance).toBe(ccc.numFrom(500)); - }); - - it("should handle transaction with no UDT outputs", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { lock }, // Non-UDT output - ], - outputsData: ["0x"], - }); - - const { tx: completedTx, addedCount } = await udt.completeInputsAll( - tx, - signer, - ); - - // Should add all 5 UDT cells even though no UDT outputs - expect(addedCount).toBe(5); - expect(completedTx.inputs.length).toBe(5); - - // All 500 UDT will be "burned" since no UDT outputs - const balanceBurned = await udt.getBalanceBurned(completedTx, client); - expect(balanceBurned).toBe(ccc.numFrom(500)); - }); - - it("should work with mixed input types", async () => { - // Create a non-UDT cell - const nonUdtCell = ccc.Cell.from({ - outPoint: { txHash: "0x" + "f".repeat(64), index: 0 }, - cellOutput: { - capacity: ccc.fixedPointFrom(1000), - lock, - // No type script - }, - outputData: "0x", - }); - - // Pre-add the non-UDT cell as input - const tx = ccc.Transaction.from({ - inputs: [{ previousOutput: nonUdtCell.outPoint }], - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(100, 16)], - }); - - // Mock getCell to handle both UDT and non-UDT cells - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - const outPointObj = ccc.OutPoint.from(outPoint); - if (outPointObj.eq(nonUdtCell.outPoint)) { - return nonUdtCell; - } - return mockUdtCells.find((c) => c.outPoint.eq(outPointObj)); - }); - - const { tx: completedTx, addedCount } = await udt.completeInputsAll( - tx, - signer, - ); - - // Should add all 5 UDT cells - expect(addedCount).toBe(5); - expect(completedTx.inputs.length).toBe(6); // 1 non-UDT + 5 UDT - - // Verify only UDT balance is counted - const inputBalance = await udt.getInputsBalance(completedTx, client); - expect(inputBalance).toBe(ccc.numFrom(500)); - }); - - it("should handle empty cell collection gracefully", async () => { - // Mock findCells to return no cells - vi.spyOn(signer, "findCells").mockImplementation(async function* () { - // Return no cells - }); - - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(100, 16)], - }); - - const { tx: completedTx, addedCount } = await udt.completeInputsAll( - tx, - signer, - ); - - // Should not add any inputs - expect(addedCount).toBe(0); - expect(completedTx.inputs.length).toBe(0); - - // UDT balance should be 0 - const inputBalance = await udt.getInputsBalance(completedTx, client); - expect(inputBalance).toBe(ccc.numFrom(0)); - }); - }); - - describe("getInputsBalance", () => { - it("should calculate total UDT balance from inputs", async () => { - const mockCells = [ - ccc.Cell.from({ - outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, - outputData: ccc.numLeToBytes(100, 16), // 100 UDT - }), - ccc.Cell.from({ - outPoint: { txHash: "0x" + "1".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, - outputData: ccc.numLeToBytes(200, 16), // 200 UDT - }), - ]; - - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - return mockCells.find((c) => c.outPoint.eq(outPoint)); - }); - - const tx = ccc.Transaction.from({ - inputs: [ - { previousOutput: mockCells[0].outPoint }, - { previousOutput: mockCells[1].outPoint }, - ], - }); - - const balance = await udt.getInputsBalance(tx, client); - expect(balance).toBe(ccc.numFrom(300)); // 100 + 200 - }); - - it("should ignore inputs without matching type script", async () => { - const mockCells = [ - ccc.Cell.from({ - outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, - outputData: ccc.numLeToBytes(100, 16), // 100 UDT - }), - ccc.Cell.from({ - outPoint: { txHash: "0x" + "1".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(142), lock }, // No type script - outputData: "0x", - }), - ]; - - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - return mockCells.find((c) => c.outPoint.eq(outPoint)); - }); - - const tx = ccc.Transaction.from({ - inputs: [ - { previousOutput: mockCells[0].outPoint }, - { previousOutput: mockCells[1].outPoint }, - ], - }); - - const balance = await udt.getInputsBalance(tx, client); - expect(balance).toBe(ccc.numFrom(100)); // Only the UDT cell - }); - }); - - describe("getOutputsBalance", () => { - it("should calculate total UDT balance from outputs", async () => { - const tx = ccc.Transaction.from({ - outputs: [ - { lock, type }, - { lock, type }, - { lock }, // No type script - ], - outputsData: [ - ccc.numLeToBytes(100, 16), // 100 UDT - ccc.numLeToBytes(200, 16), // 200 UDT - "0x", // Not UDT - ], - }); - - const balance = await udt.getOutputsBalance(tx, client); - expect(balance).toBe(ccc.numFrom(300)); // 100 + 200, ignoring non-UDT output - }); - - it("should return 0 when no UDT outputs", async () => { - const tx = ccc.Transaction.from({ - outputs: [{ lock }], // No type script - outputsData: ["0x"], - }); - - const balance = await udt.getOutputsBalance(tx, client); - expect(balance).toBe(ccc.numFrom(0)); - }); - }); - - describe("completeChangeToLock", () => { - let mockUdtCells: ccc.Cell[]; - - beforeEach(() => { - mockUdtCells = Array.from({ length: 5 }, (_, i) => - ccc.Cell.from({ - outPoint: { - txHash: `0x${"0".repeat(63)}${i.toString(16)}`, - index: 0, - }, - cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, - outputData: ccc.numLeToBytes(100, 16), // 100 UDT each - }), - ); - - vi.spyOn(signer, "findCells").mockImplementation( - async function* (filter) { - if (filter.script && ccc.Script.from(filter.script).eq(type)) { - for (const cell of mockUdtCells) { - yield cell; - } - } - }, - ); - - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - return mockUdtCells.find((c) => c.outPoint.eq(outPoint)); - }); - }); - - it("should add change output when there's excess UDT balance", async () => { - const changeLock = ccc.Script.from({ - codeHash: "0x" + "9".repeat(64), - hashType: "type", - args: "0x1234", - }); - - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT - }); - - const completedTx = await udt.completeChangeToLock( - tx, - signer, - changeLock, - ); - - // Should have original output + change output - expect(completedTx.outputs.length).toBe(2); - expect(completedTx.outputs[1].lock.eq(changeLock)).toBe(true); - expect(completedTx.outputs[1].type?.eq(type)).toBe(true); - - // Change should be 50 UDT (200 input - 150 output) - const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[1]); - expect(changeAmount).toBe(ccc.numFrom(50)); - }); - - it("should not add change when no excess balance", async () => { - const changeLock = ccc.Script.from({ - codeHash: "0x" + "9".repeat(64), - hashType: "type", - args: "0x1234", - }); - - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(200, 16)], // Need exactly 200 UDT - }); - - const completedTx = await udt.completeChangeToLock( - tx, - signer, - changeLock, - ); - - // Should only have original output - expect(completedTx.outputs.length).toBe(1); - }); - }); - - describe("completeBy", () => { - it("should use signer's recommended address for change", async () => { - const mockUdtCells = Array.from({ length: 3 }, (_, i) => - ccc.Cell.from({ - outPoint: { - txHash: `0x${"0".repeat(63)}${i.toString(16)}`, - index: 0, - }, - cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, - outputData: ccc.numLeToBytes(100, 16), - }), - ); - - vi.spyOn(signer, "findCells").mockImplementation( - async function* (filter) { - if (filter.script && ccc.Script.from(filter.script).eq(type)) { - for (const cell of mockUdtCells) { - yield cell; - } - } - }, - ); - - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - return mockUdtCells.find((c) => c.outPoint.eq(outPoint)); - }); - - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(150, 16)], - }); - - const completedTx = await udt.completeBy(tx, signer); - - // Should have change output with signer's lock - expect(completedTx.outputs.length).toBe(2); - expect(completedTx.outputs[1].lock.eq(lock)).toBe(true); // Same as signer's lock - }); - }); - - describe("complete method with capacity handling", () => { - let mockUdtCells: ccc.Cell[]; - - beforeEach(() => { - // Create mock cells with different capacity values - mockUdtCells = [ - // Cell 0: 100 UDT, 142 CKB capacity (minimum for UDT cell) - ccc.Cell.from({ - outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(142), lock, type }, - outputData: ccc.numLeToBytes(100, 16), - }), - // Cell 1: 100 UDT, 200 CKB capacity (extra capacity) - ccc.Cell.from({ - outPoint: { txHash: "0x" + "1".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(200), lock, type }, - outputData: ccc.numLeToBytes(100, 16), - }), - // Cell 2: 100 UDT, 300 CKB capacity (more extra capacity) - ccc.Cell.from({ - outPoint: { txHash: "0x" + "2".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(300), lock, type }, - outputData: ccc.numLeToBytes(100, 16), - }), - ]; - - vi.spyOn(signer, "findCells").mockImplementation( - async function* (filter) { - if (filter.script && ccc.Script.from(filter.script).eq(type)) { - for (const cell of mockUdtCells) { - yield cell; - } - } - }, - ); - - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - return mockUdtCells.find((c) => c.outPoint.eq(outPoint)); - }); - }); - - it("should add extra UDT cells when change output requires additional capacity", async () => { - const changeLock = ccc.Script.from({ - codeHash: "0x" + "9".repeat(64), - hashType: "type", - args: "0x1234", - }); - - // Create a transaction that needs 50 UDT (less than one cell) - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(50, 16)], - }); - - const completedTx = await udt.completeChangeToLock( - tx, - signer, - changeLock, - ); - - // Should have original output + change output - expect(completedTx.outputs.length).toBe(2); - - // Verify inputs were added to cover both UDT balance and capacity requirements - expect(completedTx.inputs.length).toBe(2); - - // Check that change output has correct UDT balance (should be input - 50) - const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[1]); - const inputBalance = await udt.getInputsBalance(completedTx, client); - expect(changeAmount).toBe(inputBalance - ccc.numFrom(50)); - - // Verify change output has correct type script - expect(completedTx.outputs[1].type?.eq(type)).toBe(true); - expect(completedTx.outputs[1].lock.eq(changeLock)).toBe(true); - - // Key assertion: verify that capacity is sufficient (positive fee) - const fee = await completedTx.getFee(client); - expect(fee).toBeGreaterThanOrEqual(ccc.Zero); - }); - - it("should handle capacity tweak parameter in completeInputsByBalance", async () => { - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(50, 16)], // Need 50 UDT - }); - - // Add extra capacity requirement via capacityTweak that's reasonable - const extraCapacityNeeded = ccc.fixedPointFrom(1000); // Reasonable capacity requirement - const { addedCount } = await udt.completeInputsByBalance( - tx, - signer, - ccc.Zero, // No extra UDT balance needed - extraCapacityNeeded, // Extra capacity needed - ); - - // Should add cells to cover the capacity requirement - expect(addedCount).toBeGreaterThan(2); - - // Should have added at least one cell with capacity - expect(await udt.getInputsBalance(tx, client)).toBeGreaterThan(ccc.Zero); - }); - - it("should handle the two-phase capacity completion in complete method", async () => { - const changeLock = ccc.Script.from({ - codeHash: "0x" + "9".repeat(64), - hashType: "type", - args: "0x1234", - }); - - // Create a transaction that will need change - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(50, 16)], // Need 50 UDT, will have 50 UDT change - }); - - // Track the calls to completeInputsByBalance to verify two-phase completion - const completeInputsByBalanceSpy = vi.spyOn( - udt, - "completeInputsByBalance", - ); - - const completedTx = await udt.completeChangeToLock( - tx, - signer, - changeLock, - ); - - // Should have called completeInputsByBalance twice: - // 1. First call: initial UDT balance completion - // 2. Second call: with extraCapacity for change output - expect(completeInputsByBalanceSpy).toHaveBeenCalledTimes(2); - - // Verify the second call included extraCapacity parameter - const secondCall = completeInputsByBalanceSpy.mock.calls[1]; - expect(secondCall[2]).toBe(ccc.Zero); // balanceTweak should be 0 - expect(secondCall[3]).toBeGreaterThan(ccc.Zero); // capacityTweak should be > 0 (change output capacity) - - // Should have change output - expect(completedTx.outputs.length).toBe(2); - const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[1]); - expect(changeAmount).toBe( - (await udt.getInputsBalance(completedTx, client)) - ccc.numFrom(50), - ); // 100 input - 50 output = 50 change - - completeInputsByBalanceSpy.mockRestore(); - }); - - it("should handle completeChangeToOutput correctly", async () => { - // Create a transaction with an existing UDT output that will receive change - const tx = ccc.Transaction.from({ - outputs: [ - { lock, type }, // This will be the change output - ], - outputsData: [ - ccc.numLeToBytes(50, 16), // Initial amount in change output - ], - }); - - const completedTx = await udt.completeChangeToOutput(tx, signer, 0); // Use first output as change - - // Should have added inputs - expect(completedTx.inputs.length).toBeGreaterThan(0); - - // The first output should now contain the original amount plus any excess from inputs - const changeAmount = ccc.udtBalanceFrom(completedTx.outputsData[0]); - const inputBalance = await udt.getInputsBalance(completedTx, client); - - // Change output should have: original amount + excess from inputs - // Since we only have one output, all input balance should go to it - expect(changeAmount).toBe(inputBalance); - expect(changeAmount).toBeGreaterThan(ccc.numFrom(50)); // More than the original amount - }); - - it("should throw error when change output is not a UDT cell", async () => { - const tx = ccc.Transaction.from({ - outputs: [{ lock }], // No type script - not a UDT cell - outputsData: ["0x"], - }); - - await expect(udt.completeChangeToOutput(tx, signer, 0)).rejects.toThrow( - "Change output must be a UDT cell", - ); - }); - - it("should handle insufficient capacity gracefully", async () => { - // Mock to return cells with very low capacity - const lowCapacityCells = [ - ccc.Cell.from({ - outPoint: { txHash: "0x" + "0".repeat(64), index: 0 }, - cellOutput: { capacity: ccc.fixedPointFrom(61), lock, type }, // Very low capacity - outputData: ccc.numLeToBytes(100, 16), - }), - ]; - - vi.spyOn(signer, "findCells").mockImplementation( - async function* (filter) { - if (filter.script && ccc.Script.from(filter.script).eq(type)) { - for (const cell of lowCapacityCells) { - yield cell; - } - } - }, - ); - - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - return lowCapacityCells.find((c) => c.outPoint.eq(outPoint)); - }); - - const changeLock = ccc.Script.from({ - codeHash: "0x" + "9".repeat(64), - hashType: "type", - args: "0x1234", - }); - - const tx = ccc.Transaction.from({ - outputs: [{ lock, type }], - outputsData: [ccc.numLeToBytes(50, 16)], - }); - - // Should still complete successfully even with capacity constraints - // The UDT logic should focus on UDT balance completion - const completedTx = await udt.completeChangeToLock( - tx, - signer, - changeLock, - ); - - expect(completedTx.inputs.length).toBe(1); - expect(completedTx.outputs.length).toBe(2); // Original + change - - expect(await completedTx.getFee(client)).toBeLessThan(0n); - }); - - it("should handle capacity calculation when transaction has non-UDT inputs with high capacity", async () => { - // Create a non-UDT cell with very high capacity - const nonUdtCell = ccc.Cell.from({ - outPoint: { txHash: "0x" + "f".repeat(64), index: 0 }, - cellOutput: { - capacity: ccc.fixedPointFrom(10000), // Very high capacity (100 CKB) - lock, - // No type script - this is a regular CKB cell - }, - outputData: "0x", // Empty data - }); - - // Create a transaction that already has the non-UDT input - const tx = ccc.Transaction.from({ - inputs: [ - { previousOutput: nonUdtCell.outPoint }, // Pre-existing non-UDT input - ], - outputs: [ - { lock, type }, // UDT output requiring 50 UDT - ], - outputsData: [ - ccc.numLeToBytes(50, 16), // Need 50 UDT - ], - }); - - // Mock getCell to return both UDT and non-UDT cells - vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => { - const outPointObj = ccc.OutPoint.from(outPoint); - if (outPointObj.eq(nonUdtCell.outPoint)) { - return nonUdtCell; - } - return mockUdtCells.find((c) => c.outPoint.eq(outPointObj)); - }); - - const resultTx = await udt.completeBy(tx, signer); - - // Should add exactly 2 UDT cell to satisfy the 50 UDT requirement & extra occupation from the change cell - expect(resultTx.inputs.length).toBe(3); // 1 non-UDT + 2 UDT - - // Verify UDT balance is satisfied - const inputBalance = await udt.getInputsBalance(resultTx, client); - expect(inputBalance).toBe(ccc.numFrom(200)); - }); - }); -}); diff --git a/packages/udt/src/udt/index.ts b/packages/udt/src/udt/index.ts index 715a964cb..005df649d 100644 --- a/packages/udt/src/udt/index.ts +++ b/packages/udt/src/udt/index.ts @@ -1,194 +1,6 @@ import { ccc } from "@ckb-ccc/core"; import { ssri } from "@ckb-ccc/ssri"; -/** - * Error thrown when there are insufficient UDT coins to complete a transaction. - * This error provides detailed information about the shortfall, including the - * exact amount needed, the UDT type script, and an optional custom reason. - * - * @public - * @category Error - * @category UDT - * - * @example - * ```typescript - * // This error is typically thrown automatically by UDT methods - * try { - * await udt.completeInputsByBalance(tx, signer); - * } catch (error) { - * if (error instanceof ErrorUdtInsufficientCoin) { - * console.log(`Error: ${error.message}`); - * console.log(`Shortfall: ${error.amount} UDT tokens`); - * console.log(`UDT type script: ${error.type.toHex()}`); - * } - * } - * ``` - */ -export class ErrorUdtInsufficientCoin extends Error { - /** - * The amount of UDT coins that are insufficient (shortfall amount). - * This represents how many more UDT tokens are needed to complete the operation. - */ - public readonly amount: ccc.Num; - - /** - * The type script of the UDT that has insufficient balance. - * This identifies which specific UDT token is lacking sufficient funds. - */ - public readonly type: ccc.Script; - - /** - * Creates a new ErrorUdtInsufficientCoin instance. - * - * @param info - Configuration object for the error - * @param info.amount - The amount of UDT coins that are insufficient (shortfall amount) - * @param info.type - The type script of the UDT that has insufficient balance - * @param info.reason - Optional custom reason message. If not provided, a default message will be generated - * - * @example - * ```typescript - * // Manual creation (typically not needed as the error is thrown automatically) - * const error = new ErrorUdtInsufficientCoin({ - * amount: ccc.numFrom(1000), - * type: udtScript, - * reason: "Custom insufficient balance message" - * }); - * - * // More commonly, catch the error when it's thrown by UDT methods - * try { - * const result = await udt.completeInputsByBalance(tx, signer); - * } catch (error) { - * if (error instanceof ErrorUdtInsufficientCoin) { - * // Handle the insufficient balance error - * console.error(`Insufficient UDT: need ${error.amount} more tokens`); - * } - * } - * ``` - * - * @remarks - * The error message format depends on whether a custom reason is provided: - * - With custom reason: "Insufficient coin, {custom reason}" - * - Without custom reason: "Insufficient coin, need {amount} extra coin" - */ - constructor(info: { - amount: ccc.NumLike; - type: ccc.ScriptLike; - reason?: string; - }) { - const amount = ccc.numFrom(info.amount); - const type = ccc.Script.from(info.type); - super(`Insufficient coin, ${info.reason ?? `need ${amount} extra coin`}`); - this.amount = amount; - this.type = type; - } -} - -/** - * Configuration object type for UDT instances. - * This type defines the optional configuration parameters that can be passed - * when creating a UDT instance to customize its behavior. - * - * @public - * @category Configuration - * @category UDT - */ -export type UdtConfigLike = { - /** - * Optional SSRI executor instance for advanced UDT operations. - * When provided, enables SSRI-compliant features like metadata queries - * and advanced transfer operations. - */ - executor?: ssri.Executor | null; - - /** - * Optional custom search filter for finding UDT cells. - * If not provided, a default filter will be created that matches - * cells with the UDT's type script and valid output data length. - */ - filter?: ccc.ClientIndexerSearchKeyFilterLike | null; -}; - -/** - * Configuration class for UDT instances. - * This class provides a structured way to handle UDT configuration parameters - * and includes factory methods for creating instances from configuration-like objects. - * - * @public - * @category Configuration - * @category UDT - * - * @example - * ```typescript - * // Create configuration with executor - * const config = new UdtConfig(ssriExecutor); - * - * // Create configuration with both executor and filter - * const config = new UdtConfig( - * ssriExecutor, - * ccc.ClientIndexerSearchKeyFilter.from({ - * script: udtScript, - * outputDataLenRange: [16, 32] - * }) - * ); - * - * // Create from configuration-like object - * const config = UdtConfig.from({ - * executor: ssriExecutor, - * filter: { script: udtScript, outputDataLenRange: [16, "0xffffffff"] } - * }); - * ``` - */ -export class UdtConfig { - /** - * Creates a new UdtConfig instance. - * - * @param executor - Optional SSRI executor for advanced UDT operations - * @param filter - Optional search filter for finding UDT cells - */ - constructor( - public readonly executor?: ssri.Executor, - public readonly filter?: ccc.ClientIndexerSearchKeyFilter, - ) {} - - /** - * Creates a UdtConfig instance from a configuration-like object. - * This factory method provides a convenient way to create UdtConfig instances - * from plain objects, automatically converting filter-like objects to proper - * ClientIndexerSearchKeyFilter instances. - * - * @param configLike - Configuration-like object containing executor and/or filter - * @returns A new UdtConfig instance with the specified configuration - * - * @example - * ```typescript - * // Create from object with executor only - * const config = UdtConfig.from({ executor: ssriExecutor }); - * - * // Create from object with filter only - * const config = UdtConfig.from({ - * filter: { - * script: udtScript, - * outputDataLenRange: [16, "0xffffffff"] - * } - * }); - * - * // Create from object with both - * const config = UdtConfig.from({ - * executor: ssriExecutor, - * filter: { script: udtScript, outputDataLenRange: [16, 32] } - * }); - * ``` - */ - static from(configLike: UdtConfigLike) { - return new UdtConfig( - configLike.executor ?? undefined, - configLike.filter - ? ccc.ClientIndexerSearchKeyFilter.from(configLike.filter) - : undefined, - ); - } -} - /** * Represents a User Defined Token (UDT) script compliant with the SSRI protocol. * @@ -201,144 +13,35 @@ export class UdtConfig { * @category Token */ export class Udt extends ssri.Trait { - /** - * The type script that uniquely identifies this UDT token. - * This script is used to distinguish UDT cells from other cell types and - * to identify which cells belong to this specific UDT token. - * - * @remarks - * The script contains: - * - `codeHash`: Hash of the UDT script code - * - `hashType`: How the code hash should be interpreted ("type" or "data") - * - `args`: Arguments that make this UDT unique (often contains token-specific data) - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * console.log(`UDT script hash: ${udt.script.hash()}`); - * console.log(`UDT args: ${udt.script.args}`); - * - * // Check if a cell belongs to this UDT - * const isUdt = udt.isUdt(cell); - * ``` - */ public readonly script: ccc.Script; - /** - * The search filter used to find UDT cells controlled by signers. - * This filter is automatically configured to match cells with this UDT's type script - * and appropriate output data length (minimum 16 bytes for UDT balance storage). - * - * @remarks - * The filter includes: - * - `script`: Set to this UDT's type script - * - `outputDataLenRange`: [16, "0xffffffff"] to ensure valid UDT cells - * - * This filter is used internally by methods like: - * - `calculateInfo()` and `calculateBalance()` for scanning all UDT cells - * - `completeInputs()` and related methods for finding suitable input cells - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // The filter is used internally, but you can access it if needed - * console.log(`Filter script: ${udt.filter.script?.hash()}`); - * console.log(`Output data range: ${udt.filter.outputDataLenRange}`); - * - * // Manually find cells using the same filter - * for await (const cell of signer.findCells(udt.filter)) { - * console.log(`Found UDT cell with balance: ${ccc.udtBalanceFrom(cell.outputData)}`); - * } - * ``` - */ - public readonly filter: ccc.ClientIndexerSearchKeyFilter; - /** * Constructs a new UDT (User Defined Token) script instance. - * By default it is a SSRI-compliant UDT. This class supports both SSRI-compliant UDTs and legacy sUDT/xUDT standard tokens. - * - * @param code - The script code cell outpoint of the UDT. This points to the cell containing the UDT script code - * @param script - The type script of the UDT that uniquely identifies this token - * @param config - Optional configuration object for advanced settings - * @param config.executor - The SSRI executor instance for advanced UDT operations. If provided, enables SSRI-compliant features - * @param config.filter - Custom search filter for finding UDT cells. If not provided, a default filter will be created + * By default it is a SSRI-compliant UDT. By providing `xudtType`, it is compatible with the legacy xUDT. * + * @param executor - The SSRI executor instance. + * @param code - The script code cell of the UDT. + * @param script - The type script of the UDT. * @example * ```typescript - * // Basic UDT instance - * const udt = new Udt( - * { txHash: "0x...", index: 0 }, // code outpoint - * { codeHash: "0x...", hashType: "type", args: "0x..." } // type script - * ); - * - * // UDT with SSRI executor for advanced features - * const ssriUdt = new Udt( - * codeOutPoint, - * typeScript, - * { executor: ssriExecutor } - * ); - * - * // UDT with custom filter (advanced usage) - * const customUdt = new Udt( - * codeOutPoint, - * typeScript, - * { - * filter: { - * script: typeScript, - * outputDataLenRange: [16, 32], // Only cells with 16-32 bytes output data - * } - * } - * ); + * const udt = new Udt(executor, code, script); * ``` - * - * @remarks - * **Default Filter Behavior:** - * If no custom filter is provided, a default filter is created with: - * - `script`: Set to the provided UDT type script - * - `outputDataLenRange`: [16, "0xffffffff"] to match valid UDT cells - * - * **SSRI Compliance:** - * When an executor is provided, the UDT instance can use SSRI-compliant features like: - * - Advanced transfer operations - * - Metadata queries (name, symbol, decimals, icon) - * - Custom UDT logic execution - * - * **Legacy Support:** - * Even without an executor, the UDT class supports basic operations for legacy sUDT/xUDT tokens. */ constructor( code: ccc.OutPointLike, script: ccc.ScriptLike, - config?: UdtConfigLike | null, + config?: { + executor?: ssri.Executor | null; + } | null, ) { super(code, config?.executor); this.script = ccc.Script.from(script); - this.filter = ccc.ClientIndexerSearchKeyFilter.from( - config?.filter ?? { - script: this.script, - outputDataLenRange: [16, "0xffffffff"], - }, - ); } /** * Retrieves the human-readable name of the User Defined Token. - * This method queries the UDT script to get the token's display name, - * which is typically used in user interfaces and wallets. * - * @param context - Optional script execution context for additional parameters - * @returns A promise resolving to an ExecutorResponse containing the token's name, - * or undefined if the name is not available or the script doesn't support this method - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const nameResponse = await udt.name(); - * if (nameResponse.res) { - * console.log(`Token name: ${nameResponse.res}`); - * } - * ``` + * @returns A promise resolving to the token's name. */ async name( context?: ssri.ContextScript, @@ -357,22 +60,8 @@ export class Udt extends ssri.Trait { } /** - * Retrieves the symbol (ticker) of the User Defined Token. - * The symbol is typically a short abbreviation used to identify the token, - * similar to stock ticker symbols (e.g., "BTC", "ETH", "USDT"). - * - * @param context - Optional script execution context for additional parameters - * @returns A promise resolving to an ExecutorResponse containing the token's symbol, - * or undefined if the symbol is not available or the script doesn't support this method - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const symbolResponse = await udt.symbol(); - * if (symbolResponse.res) { - * console.log(`Token symbol: ${symbolResponse.res}`); - * } - * ``` + * Retrieves the symbol of the UDT. + * @returns The symbol of the UDT. */ async symbol( context?: ssri.ContextScript, @@ -396,24 +85,8 @@ export class Udt extends ssri.Trait { } /** - * Retrieves the number of decimal places for the User Defined Token. - * This value determines how the token amount should be displayed and interpreted. - * For example, if decimals is 8, then a balance of 100000000 represents 1.0 tokens. - * - * @param context - Optional script execution context for additional parameters - * @returns A promise resolving to an ExecutorResponse containing the number of decimals, - * or undefined if decimals are not specified or the script doesn't support this method - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const decimalsResponse = await udt.decimals(); - * if (decimalsResponse.res !== undefined) { - * console.log(`Token decimals: ${decimalsResponse.res}`); - * // Convert raw amount to human-readable format - * const humanReadable = rawAmount / (10 ** Number(decimalsResponse.res)); - * } - * ``` + * Retrieves the decimals of the UDT. + * @returns The decimals of the UDT. */ async decimals( context?: ssri.ContextScript, @@ -437,25 +110,8 @@ export class Udt extends ssri.Trait { } /** - * Retrieves the icon URL or data URI for the User Defined Token. - * This can be used to display a visual representation of the token in user interfaces. - * The returned value may be a URL pointing to an image file or a data URI containing - * the image data directly. - * - * @param context - Optional script execution context for additional parameters - * @returns A promise resolving to an ExecutorResponse containing the icon URL/data, - * or undefined if no icon is available or the script doesn't support this method - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const iconResponse = await udt.icon(); - * if (iconResponse.res) { - * // Use the icon in UI - * const imgElement = document.createElement('img'); - * imgElement.src = iconResponse.res; - * } - * ``` + * Retrieves the icon of the UDT + * @returns The icon of the UDT. */ async icon( context?: ssri.ContextScript, @@ -473,67 +129,13 @@ export class Udt extends ssri.Trait { return ssri.ExecutorResponse.new(undefined); } - /** - * Adds the UDT script code as a cell dependency to the transaction. - * This method ensures that the transaction includes the necessary cell dependency - * for the UDT script code, which is required for any transaction that uses this UDT. - * - * @param txLike - The transaction to add the cell dependency to - * @returns A new transaction with the UDT code cell dependency added - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // Create a basic transaction - * let tx = ccc.Transaction.from({ - * outputs: [{ lock: recipientLock, type: udt.script }], - * outputsData: [ccc.numLeToBytes(100, 16)] - * }); - * - * // Add UDT code dependency - * tx = udt.addCellDeps(tx); - * - * // Now the transaction can be completed and sent - * await tx.completeInputsByCapacity(signer); - * await tx.completeFeeBy(signer); - * ``` - * - * @remarks - * **When to Use:** - * - When manually constructing transactions that involve UDT cells - * - Before sending any transaction that creates or consumes UDT cells - * - This is automatically called by methods like `transfer()` and `mint()` - * - * **Cell Dependency Details:** - * - Adds the UDT script code outpoint as a "code" type dependency - * - This allows the transaction to reference and execute the UDT script - * - Required for script validation during transaction processing - * - * **Note:** Most high-level UDT methods automatically add this dependency, - * so manual usage is typically only needed for custom transaction construction. - */ - addCellDeps(txLike: ccc.TransactionLike): ccc.Transaction { - const tx = ccc.Transaction.from(txLike); - tx.addCellDeps({ - outPoint: this.code, - depType: "code", - }); - return tx; - } - /** * Transfers UDT to specified addresses. - * This method creates a transaction that transfers UDT tokens to one or more recipients. - * It can build upon an existing transaction to achieve combined actions. - * - * @param signer - The signer that will authorize and potentially pay for the transaction - * @param transfers - Array of transfer operations to perform - * @param transfers.to - The lock script of the recipient who will receive the tokens - * @param transfers.amount - The amount of tokens to transfer to this recipient (in smallest unit) - * @param tx - Optional existing transaction to build upon. If not provided, a new transaction will be created - * @returns A promise resolving to an ExecutorResponse containing the transaction with transfer operations - * + * @param tx - Transfer on the basis of an existing transaction to achieve combined actions. If not provided, a new transaction will be created. + * @param transfers - The array of transfers. + * @param transfers.to - The receiver of token. + * @param transfers.amount - The amount of token to the receiver. + * @returns The transaction result. * @tag Mutation - This method represents a mutation of the onchain state and will return a transaction object. * @example * ```typescript @@ -552,12 +154,12 @@ export class Udt extends ssri.Trait { * }, * ); * - * const { res: tx } = await udt.transfer( + * const { res: tx } = await udtTrait.transfer( * signer, * [{ to, amount: 100 }], * ); * - * const completedTx = await udt.completeBy(tx, signer); + * const completedTx = udt.completeUdtBy(tx, signer); * await completedTx.completeInputsByCapacity(signer); * await completedTx.completeFeeBy(signer); * const transferTxHash = await signer.sendTransaction(completedTx); @@ -606,47 +208,21 @@ export class Udt extends ssri.Trait { } resTx = ssri.ExecutorResponse.new(transfer); } - - return resTx.map((tx) => this.addCellDeps(tx)); + resTx.res.addCellDeps({ + outPoint: this.code, + depType: "code", + }); + return resTx; } /** - * Mints new tokens to specified addresses. - * This method creates new UDT tokens and assigns them to the specified recipients. - * The minting operation requires appropriate permissions and may be restricted - * based on the UDT's implementation. - * - * @param signer - The signer that will authorize and potentially pay for the transaction - * @param mints - Array of mint operations to perform - * @param mints.to - The lock script of the recipient who will receive the minted tokens - * @param mints.amount - The amount of tokens to mint for this recipient (in smallest unit) - * @param tx - Optional existing transaction to build upon. If not provided, a new transaction will be created - * @returns A promise resolving to an ExecutorResponse containing the transaction with mint operations - * + * Mints new tokens to specified addresses. See the example in `transfer` as they are similar. + * @param tx - Optional existing transaction to build upon + * @param mints - Array of mints + * @param mints.to - receiver of token + * @param mints.amount - amount to the receiver + * @returns The transaction containing the mint operation * @tag Mutation - This method represents a mutation of the onchain state - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const { script: recipientLock } = await ccc.Address.fromString(recipientAddress, signer.client); - * - * const mintResponse = await udt.mint( - * signer, - * [ - * { to: recipientLock, amount: ccc.fixedPointFrom(1000) }, // Mint 1000 tokens - * { to: anotherLock, amount: ccc.fixedPointFrom(500) } // Mint 500 tokens - * ] - * ); - * - * // Complete the transaction - * const tx = mintResponse.res; - * await tx.completeInputsByCapacity(signer); - * await tx.completeFeeBy(signer, changeLock); - * - * const txHash = await signer.sendTransaction(tx); - * ``` - * - * @throws May throw if the signer doesn't have minting permissions or if the UDT doesn't support minting */ async mint( signer: ccc.Signer, @@ -691,803 +267,40 @@ export class Udt extends ssri.Trait { } resTx = ssri.ExecutorResponse.new(mint); } - - return resTx.map((tx) => this.addCellDeps(tx)); - } - - /** - * Checks if a cell is a valid UDT cell for this token. - * A valid UDT cell must have this UDT's type script and contain at least 16 bytes of output data - * (the minimum required for storing the UDT balance as a 128-bit little-endian integer). - * - * @param cellOutputLike - The cell output to check - * @param outputData - The output data of the cell - * @returns True if the cell is a valid UDT cell for this token, false otherwise - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const cellOutput = { lock: someLock, type: udt.script }; - * const outputData = ccc.numLeToBytes(1000, 16); // 1000 UDT balance - * - * const isValid = udt.isUdt({ cellOutput, outputData }); - * console.log(`Is valid UDT cell: ${isValid}`); // true - * ``` - * - * @remarks - * The method checks two conditions: - * 1. The cell's type script matches this UDT's script - * 2. The output data is at least 16 bytes long (required for UDT balance storage) - */ - isUdt(cell: { cellOutput: ccc.CellOutputLike; outputData: ccc.HexLike }) { - return ( - (ccc.CellOutput.from(cell.cellOutput).type?.eq(this.script) ?? false) && - ccc.bytesFrom(cell.outputData).length >= 16 - ); - } - - /** - * Retrieves comprehensive information about UDT inputs in a transaction. - * This method analyzes all input cells and returns detailed statistics including - * total UDT balance, total capacity occupied, and the number of UDT cells. - * - * @param txLike - The transaction to analyze - * @param client - The client to fetch input cell data - * @returns A promise resolving to an object containing: - * - balance: Total UDT balance from all input cells - * - capacity: Total capacity occupied by all UDT input cells - * - count: Number of UDT input cells - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const tx = ccc.Transaction.from(existingTransaction); - * - * const inputsInfo = await udt.getInputsInfo(tx, client); - * console.log(`UDT inputs: ${inputsInfo.count} cells`); - * console.log(`Total UDT balance: ${inputsInfo.balance}`); - * console.log(`Total capacity: ${inputsInfo.capacity}`); - * ``` - * - * @remarks - * This method provides more comprehensive information than `getInputsBalance`, - * making it useful for transaction analysis, fee calculation, and UI display. - * Only cells with this UDT's type script are included in the statistics. - */ - async getInputsInfo( - txLike: ccc.TransactionLike, - client: ccc.Client, - ): Promise<{ - balance: ccc.Num; - capacity: ccc.Num; - count: number; - }> { - const tx = ccc.Transaction.from(txLike); - const [balance, capacity, count] = await ccc.reduceAsync( - tx.inputs, - async (acc, input) => { - const { cellOutput, outputData } = await input.getCell(client); - if (!this.isUdt({ cellOutput, outputData })) { - return acc; - } - - return [ - acc[0] + ccc.udtBalanceFrom(outputData), - acc[1] + cellOutput.capacity, - acc[2] + 1, - ]; - }, - [ccc.Zero, ccc.Zero, 0], - ); - - return { - balance, - capacity, - count, - }; - } - - /** - * Calculates the total UDT balance from all inputs in a transaction. - * This method examines each input cell and sums up the UDT amounts - * for cells that have this UDT's type script. - * - * @param txLike - The transaction to analyze - * @param client - The client to fetch input cell data - * @returns A promise resolving to the total UDT balance from all inputs - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const tx = ccc.Transaction.from(existingTransaction); - * - * const inputBalance = await udt.getInputsBalance(tx, client); - * console.log(`Total UDT input balance: ${inputBalance}`); - * ``` - * - * @remarks - * This method only counts inputs that have the same type script as this UDT instance. - * Inputs without a type script or with different type scripts are ignored. - */ - async getInputsBalance( - txLike: ccc.TransactionLike, - client: ccc.Client, - ): Promise { - return (await this.getInputsInfo(txLike, client)).balance; - } - - /** - * Retrieves comprehensive information about UDT outputs in a transaction. - * This method analyzes all output cells and returns detailed statistics including - * total UDT balance, total capacity occupied, and the number of UDT cells. - * - * @param txLike - The transaction to analyze - * @param _client - The client parameter (unused for outputs since data is already available) - * @returns A promise resolving to an object containing: - * - balance: Total UDT balance from all output cells - * - capacity: Total capacity occupied by all UDT output cells - * - count: Number of UDT output cells - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const tx = ccc.Transaction.from({ - * outputs: [ - * { lock: recipientLock, type: udt.script }, - * { lock: changeLock, type: udt.script } - * ], - * outputsData: [ - * ccc.numLeToBytes(1000, 16), // 1000 UDT to recipient - * ccc.numLeToBytes(500, 16) // 500 UDT as change - * ] - * }); - * - * const outputsInfo = await udt.getOutputsInfo(tx, client); - * console.log(`UDT outputs: ${outputsInfo.count} cells`); - * console.log(`Total UDT balance: ${outputsInfo.balance}`); // 1500 - * console.log(`Total capacity: ${outputsInfo.capacity}`); - * ``` - * - * @remarks - * This method provides more comprehensive information than `getOutputsBalance`, - * making it useful for transaction validation, analysis, and UI display. - * Only cells with this UDT's type script are included in the statistics. - * This is an async method for consistency with `getInputsInfo`, though it doesn't - * actually need to fetch data since output information is already available. - */ - async getOutputsInfo( - txLike: ccc.TransactionLike, - _client: ccc.Client, - ): Promise<{ - balance: ccc.Num; - capacity: ccc.Num; - count: number; - }> { - const tx = ccc.Transaction.from(txLike); - const [balance, capacity, count] = tx.outputs.reduce( - (acc, output, i) => { - if ( - !this.isUdt({ cellOutput: output, outputData: tx.outputsData[i] }) - ) { - return acc; - } - - return [ - acc[0] + ccc.udtBalanceFrom(tx.outputsData[i]), - acc[1] + output.capacity, - acc[2] + 1, - ]; - }, - [ccc.Zero, ccc.Zero, 0], - ); - - return { - balance, - capacity, - count, - }; - } - - /** - * Calculates the total UDT balance from all outputs in a transaction. - * This method examines each output cell and sums up the UDT amounts - * for cells that have this UDT's type script. - * - * @param txLike - The transaction to analyze - * @param client - The client parameter (passed to getOutputsInfo for consistency) - * @returns A promise resolving to the total UDT balance from all outputs - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const tx = ccc.Transaction.from({ - * outputs: [ - * { lock: recipientLock, type: udt.script }, - * { lock: changeLock, type: udt.script } - * ], - * outputsData: [ - * ccc.numLeToBytes(1000, 16), // 1000 UDT to recipient - * ccc.numLeToBytes(500, 16) // 500 UDT as change - * ] - * }); - * - * const outputBalance = await udt.getOutputsBalance(tx, client); - * console.log(`Total UDT output balance: ${outputBalance}`); // 1500 - * ``` - * - * @remarks - * This method only counts outputs that have the same type script as this UDT instance. - * Outputs without a type script or with different type scripts are ignored. - * This method is a convenience wrapper around `getOutputsInfo` that returns only the balance. - */ - async getOutputsBalance( - txLike: ccc.TransactionLike, - client: ccc.Client, - ): Promise { - return (await this.getOutputsInfo(txLike, client)).balance; - } - - /** - * Calculates the net UDT balance that would be burned (destroyed) in a transaction. - * This is the difference between the total UDT balance in inputs and outputs. - * A positive value indicates UDT tokens are being burned, while a negative value - * indicates more UDT is being created than consumed (which may require minting permissions). - * - * @param txLike - The transaction to analyze - * @param client - The client to fetch input cell data - * @returns A promise resolving to the net UDT balance burned (inputs - outputs) - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * const tx = ccc.Transaction.from(existingTransaction); - * - * const burned = await udt.getBalanceBurned(tx, client); - * if (burned > 0) { - * console.log(`${burned} UDT tokens will be burned`); - * } else if (burned < 0) { - * console.log(`${-burned} UDT tokens will be created`); - * } else { - * console.log('UDT balance is conserved'); - * } - * ``` - * - * @remarks - * This method is useful for: - * - Validating transaction balance conservation - * - Calculating how much UDT is being destroyed in burn operations - * - Detecting minting operations (negative burned balance) - * - Ensuring sufficient UDT inputs are provided for transfers - */ - async getBalanceBurned( - txLike: ccc.TransactionLike, - client: ccc.Client, - ): Promise { - const tx = ccc.Transaction.from(txLike); - return ( - (await this.getInputsBalance(tx, client)) - - (await this.getOutputsBalance(tx, client)) - ); - } - - /** - * Low-level method to complete UDT inputs for a transaction using a custom accumulator function. - * This method provides maximum flexibility for input selection by allowing custom logic - * through the accumulator function. It's primarily used internally by other completion methods. - * - * @template T - The type of the accumulator value - * @param txLike - The transaction to complete with UDT inputs - * @param from - The signer that will provide UDT inputs - * @param accumulator - Function that determines when to stop adding inputs based on accumulated state - * @param init - Initial value for the accumulator - * @returns A promise resolving to an object containing: - * - tx: The transaction with added inputs - * - addedCount: Number of inputs that were added - * - accumulated: Final accumulator value (undefined if target was reached) - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // Custom accumulator to track both balance and capacity - * const result = await udt.completeInputs( - * tx, - * signer, - * ([balanceAcc, capacityAcc], cell) => { - * const balance = ccc.udtBalanceFrom(cell.outputData); - * const newBalance = balanceAcc + balance; - * const newCapacity = capacityAcc + cell.cellOutput.capacity; - * - * // Stop when we have enough balance and capacity - * return newBalance >= requiredBalance && newCapacity >= requiredCapacity - * ? undefined // Stop adding inputs - * : [newBalance, newCapacity]; // Continue with updated accumulator - * }, - * [ccc.Zero, ccc.Zero] // Initial [balance, capacity] - * ); - * ``` - * - * @remarks - * This is a low-level method that most users won't need to call directly. - * Use `completeInputsByBalance` for typical UDT input completion needs. - * The accumulator function should return `undefined` to stop adding inputs, - * or return an updated accumulator value to continue. - */ - async completeInputs( - txLike: ccc.TransactionLike, - from: ccc.Signer, - accumulator: ( - acc: T, - v: ccc.Cell, - i: number, - array: ccc.Cell[], - ) => Promise | T | undefined, - init: T, - ): Promise<{ - tx: ccc.Transaction; - addedCount: number; - accumulated?: T; - }> { - const tx = ccc.Transaction.from(txLike); - const res = await tx.completeInputs(from, this.filter, accumulator, init); - - return { - ...res, - tx, - }; - } - - /** - * Completes UDT inputs for a transaction to satisfy both UDT balance and capacity requirements. - * This method implements intelligent input selection that considers both UDT token balance - * and cell capacity constraints, optimizing for minimal cell usage while meeting all requirements. - * It uses sophisticated balance calculations and early exit optimizations for efficiency. - * - * @param txLike - The transaction to complete with UDT inputs - * @param from - The signer that will provide UDT inputs - * @param balanceTweak - Optional additional UDT balance requirement beyond outputs (default: 0) - * @param capacityTweak - Optional additional CKB capacity requirement beyond outputs (default: 0) - * @returns A promise resolving to an object containing: - * - tx: The modified transaction with added UDT inputs - * - addedCount: Number of UDT input cells that were added - * - * @throws {ErrorUdtInsufficientCoin} When there are insufficient UDT cells to cover the required balance - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // Basic usage: add inputs to cover UDT outputs - * const tx = ccc.Transaction.from({ - * outputs: [{ lock: recipientLock, type: udt.script }], - * outputsData: [ccc.numLeToBytes(1000, 16)] - * }); - * - * const { tx: completedTx, addedCount } = await udt.completeInputsByBalance(tx, signer); - * console.log(`Added ${addedCount} UDT inputs to cover 1000 UDT requirement`); - * - * // Advanced usage: with balance and capacity tweaks - * const { tx: advancedTx, addedCount: advancedCount } = await udt.completeInputsByBalance( - * tx, - * signer, - * ccc.numFrom(100), // Extra 100 UDT balance needed - * ccc.fixedPointFrom(5000) // Extra 5000 capacity needed - * ); - * ``` - * - * @remarks - * This method implements sophisticated dual-constraint input selection with the following logic: - * - * **Balance Calculations:** - * - UDT balance deficit: `inputBalance - outputBalance - balanceTweak` - * - Capacity balance with fee optimization: `min(inputCapacity - outputCapacity, estimatedFee) - capacityTweak` - * - The capacity calculation tries to avoid extra occupation by UDT cells and compress UDT state - * - * **Early Exit Optimization:** - * - Returns immediately with `addedCount: 0` if both balance and capacity constraints are satisfied - * - Avoids unnecessary input addition when existing inputs are sufficient - * - * **Smart Input Selection:** - * - Uses accumulator pattern to track both UDT balance and capacity during selection - * - Continues adding inputs until both constraints are satisfied: `balanceAcc >= 0 && capacityAcc >= 0` - * - Prioritizes providing sufficient capacity through UDT cells to avoid extra non-UDT inputs - * - * **Error Handling:** - * - Throws `ErrorUdtInsufficientCoin` with exact shortfall amount if insufficient UDT balance - * - Only throws error if UDT balance cannot be satisfied (capacity issues don't cause errors) - */ - async completeInputsByBalance( - txLike: ccc.TransactionLike, - from: ccc.Signer, - balanceTweak?: ccc.NumLike, - capacityTweak?: ccc.NumLike, - ): Promise<{ - addedCount: number; - tx: ccc.Transaction; - }> { - const tx = ccc.Transaction.from(txLike); - const { balance: inBalance, capacity: inCapacity } = - await this.getInputsInfo(tx, from.client); - const { balance: outBalance, capacity: outCapacity } = - await this.getOutputsInfo(tx, from.client); - - const balanceBurned = - inBalance - outBalance - ccc.numFrom(balanceTweak ?? 0); - // Try to avoid extra occupation by UDT and also try to compress UDT state - const capacityBurned = - ccc.numMin(inCapacity - outCapacity, await tx.getFee(from.client)) - - ccc.numFrom(capacityTweak ?? 0); - - if (balanceBurned >= ccc.Zero && capacityBurned >= ccc.Zero) { - return { addedCount: 0, tx }; - } - - const { - tx: txRes, - addedCount, - accumulated, - } = await this.completeInputs( - tx, - from, - ([balanceAcc, capacityAcc], { cellOutput: { capacity }, outputData }) => { - const balance = ccc.udtBalanceFrom(outputData); - const balanceBurned = balanceAcc + balance; - const capacityBurned = capacityAcc + capacity; - - // Try to provide enough capacity with UDT cells to avoid extra occupation - return balanceBurned >= ccc.Zero && capacityBurned >= ccc.Zero - ? undefined - : [balanceBurned, capacityBurned]; - }, - [balanceBurned, capacityBurned], - ); - - if (accumulated === undefined || accumulated[0] >= ccc.Zero) { - return { tx: txRes, addedCount }; - } - - throw new ErrorUdtInsufficientCoin({ - amount: -accumulated[0], - type: this.script, + resTx.res.addCellDeps({ + outPoint: this.code, + depType: "code", }); + return resTx; } - /** - * Adds ALL available UDT cells from the signer as inputs to the transaction. - * Unlike `completeInputsByBalance` which adds only the minimum required inputs, - * this method collects every available UDT cell that the signer controls, - * regardless of the transaction's actual UDT requirements. - * - * @param txLike - The transaction to add UDT inputs to - * @param from - The signer that will provide all available UDT inputs - * @returns A promise resolving to an object containing: - * - tx: The transaction with all available UDT inputs added - * - addedCount: Number of UDT input cells that were added - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // Create a transaction (can be empty or have existing outputs) - * const tx = ccc.Transaction.from({ - * outputs: [{ lock: recipientLock, type: udt.script }], - * outputsData: [ccc.numLeToBytes(100, 16)] // Send 100 UDT - * }); - * - * // Add ALL available UDT cells as inputs - * const { tx: completedTx, addedCount } = await udt.completeInputsAll(tx, signer); - * console.log(`Added ${addedCount} UDT cells as inputs`); - * - * // The transaction now contains all UDT cells the signer controls - * const totalInputBalance = await udt.getInputsBalance(completedTx, client); - * console.log(`Total UDT input balance: ${totalInputBalance}`); - * ``` - * - * @remarks - * **Use Cases:** - * - **UDT Consolidation**: Combining multiple small UDT cells into fewer larger ones - * - **Complete Balance Transfer**: Moving all UDT tokens from one address to another - * - **Wallet Cleanup**: Reducing the number of UDT cells for better wallet performance - * - **Batch Operations**: When you need to process all UDT holdings at once - * - * **Important Considerations:** - * - This method will likely create a large excess balance that needs to be handled with change outputs - * - The resulting transaction may be large and expensive due to many inputs - * - Use `completeInputsByBalance` instead if you only need specific amounts - * - Always handle the excess balance with appropriate change outputs after calling this method - * - * **Behavior:** - * - Adds every UDT cell that the signer controls and that isn't already used in the transaction - * - The accumulator tracks total capacity of added cells (used internally for optimization) - * - Does not stop until all available UDT cells are added - * - Skips cells that are already present as inputs in the transaction - */ - async completeInputsAll( - txLike: ccc.TransactionLike, - from: ccc.Signer, - ): Promise<{ - addedCount: number; - tx: ccc.Transaction; - }> { - const tx = ccc.Transaction.from(txLike); - - return this.completeInputs( - tx, - from, - (acc, { cellOutput: { capacity } }) => acc + capacity, - ccc.Zero, - ); - } - - /** - * Completes a UDT transaction by adding inputs and handling change with a custom change function. - * This is a low-level method that provides maximum flexibility for handling UDT transaction completion. - * The change function is called to handle excess UDT balance and can return the capacity cost of the change. - * - * @param txLike - The transaction to complete - * @param signer - The signer that will provide UDT inputs - * @param change - Function to handle excess UDT balance. Called with (tx, balance, shouldModify) - * where shouldModify indicates if the function should actually modify the transaction - * @param options - Optional configuration - * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true - * @returns A promise resolving to the completed transaction - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * const completedTx = await udt.complete( - * tx, - * signer, - * (tx, balance, shouldModify) => { - * if (shouldModify && balance > 0) { - * // Add change output - * const changeData = ccc.numLeToBytes(balance, 16); - * tx.addOutput({ lock: changeLock, type: udt.script }, changeData); - * return ccc.CellOutput.from({ lock: changeLock, type: udt.script }, changeData).capacity; - * } - * return 0; - * } - * ); - * ``` - * - * @remarks - * The change function is called twice: - * 1. First with shouldModify=false to calculate capacity requirements - * 2. Then with shouldModify=true to actually modify the transaction - * This two-phase approach ensures proper input selection considering capacity requirements. - */ - async complete( - txLike: ccc.TransactionLike, - signer: ccc.Signer, - change: ( - tx: ccc.Transaction, - balance: ccc.Num, - shouldModify: boolean, - ) => Promise | ccc.NumLike, - options?: { shouldAddInputs?: boolean }, - ): Promise { - let tx = this.addCellDeps(ccc.Transaction.from(txLike)); - - /* === Figure out the balance to change === */ - if (options?.shouldAddInputs ?? true) { - tx = (await this.completeInputsByBalance(tx, signer)).tx; - } - - const balanceBurned = await this.getBalanceBurned(tx, signer.client); - - if (balanceBurned < ccc.Zero) { - throw new ErrorUdtInsufficientCoin({ - amount: -balanceBurned, - type: this.script, - }); - } else if (balanceBurned === ccc.Zero) { - return tx; - } - /* === Some balance need to change === */ - - if (!(options?.shouldAddInputs ?? true)) { - await Promise.resolve(change(tx, balanceBurned, true)); - return tx; - } - - // Different with `Transaction.completeFee`, we don't need the modified tx to track updated fee - // So one attempt should be enough - const extraCapacity = ccc.numFrom( - await Promise.resolve(change(tx, balanceBurned, false)), - ); // Extra capacity introduced by change cell - tx = ( - await this.completeInputsByBalance(tx, signer, ccc.Zero, extraCapacity) - ).tx; - - const balanceToChange = await this.getBalanceBurned(tx, signer.client); - await Promise.resolve(change(tx, balanceToChange, true)); - - return tx; - } - - /** - * Completes a UDT transaction by adding change to an existing output at the specified index. - * This method modifies an existing UDT output in the transaction to include any excess - * UDT balance as change, rather than creating a new change output. - * - * @param txLike - The transaction to complete - * @param signer - The signer that will provide UDT inputs - * @param indexLike - The index of the output to modify with change balance - * @param options - Optional configuration - * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true - * @returns A promise resolving to the completed transaction - * - * @throws {Error} When the specified output is not a valid UDT cell - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // Create transaction with a UDT output that will receive change - * const tx = ccc.Transaction.from({ - * outputs: [ - * { lock: recipientLock, type: udt.script }, - * { lock: changeLock, type: udt.script } // This will receive change - * ], - * outputsData: [ - * ccc.numLeToBytes(1000, 16), // Send 1000 UDT - * ccc.numLeToBytes(0, 16) // Change output starts with 0 - * ] - * }); - * - * // Complete with change going to output index 1 - * const completedTx = await udt.completeChangeToOutput(tx, signer, 1); - * // Output 1 now contains the excess UDT balance - * ``` - * - * @remarks - * This method is useful when you want to consolidate change into an existing output - * rather than creating a new output, which can save on transaction size and fees. - * The specified output must already be a valid UDT cell with this UDT's type script. - */ - async completeChangeToOutput( + async completeChangeToLock( txLike: ccc.TransactionLike, signer: ccc.Signer, - indexLike: ccc.NumLike, - options?: { shouldAddInputs?: boolean }, + change: ccc.ScriptLike, ) { const tx = ccc.Transaction.from(txLike); - const index = Number(ccc.numFrom(indexLike)); - const outputData = ccc.bytesFrom(tx.outputsData[index]); - if (!this.isUdt({ cellOutput: tx.outputs[index], outputData })) { - throw new Error("Change output must be a UDT cell"); + await tx.completeInputsByUdt(signer, this.script); + const balanceDiff = + (await tx.getInputsUdtBalance(signer.client, this.script)) - + tx.getOutputsUdtBalance(this.script); + if (balanceDiff > ccc.Zero) { + tx.addOutput( + { + lock: change, + type: this.script, + }, + ccc.numLeToBytes(balanceDiff, 16), + ); } - return this.complete( - tx, - signer, - (tx, balance, shouldModify) => { - if (shouldModify) { - const balanceData = ccc.numLeToBytes( - ccc.udtBalanceFrom(outputData) + balance, - 16, - ); - - tx.outputsData[index] = ccc.hexFrom( - ccc.bytesConcatTo([], balanceData, outputData.slice(16)), - ); - } - - return 0; - }, - options, - ); - } - - /** - * Completes a UDT transaction by adding necessary inputs and handling change. - * This method automatically adds UDT inputs to cover the required output amounts - * and creates a change output if there's excess UDT balance. - * - * @param tx - The transaction to complete, containing UDT outputs - * @param signer - The signer that will provide UDT inputs - * @param changeLike - The lock script where any excess UDT balance should be sent as change - * @param options - Optional configuration for the completion process - * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true - * @returns A promise resolving to the completed transaction with inputs and change output added - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // Create a transaction with UDT outputs - * const tx = ccc.Transaction.from({ - * outputs: [ - * { lock: recipientLock, type: udt.script } - * ], - * outputsData: [ccc.numLeToBytes(1000, 16)] // Send 1000 UDT - * }); - * - * // Complete with change going to sender's address - * const { script: changeLock } = await signer.getRecommendedAddressObj(); - * const completedTx = await udt.completeChangeToLock(tx, signer, changeLock); - * - * // The transaction now has: - * // - Sufficient UDT inputs to cover the 1000 UDT output - * // - A change output if there was excess UDT balance - * ``` - * - * @remarks - * This method performs the following operations: - * 1. Adds UDT inputs using `completeInputsByBalance` - * 2. Calculates the difference between input and output UDT balances - * 3. Creates a change output if there's excess UDT balance - */ - async completeChangeToLock( - tx: ccc.TransactionLike, - signer: ccc.Signer, - changeLike: ccc.ScriptLike, - options?: { shouldAddInputs?: boolean }, - ) { - const change = ccc.Script.from(changeLike); - - return this.complete( - tx, - signer, - (tx, balance, shouldModify) => { - const balanceData = ccc.numLeToBytes(balance, 16); - const changeOutput = ccc.CellOutput.from( - { lock: change, type: this.script }, - balanceData, - ); - if (shouldModify) { - tx.addOutput(changeOutput, balanceData); - } - - return changeOutput.capacity; - }, - options, - ); + return tx; } - /** - * Completes a UDT transaction using the signer's recommended address for change. - * This is a convenience method that automatically uses the signer's recommended - * address as the change destination, making it easier to complete UDT transactions - * without manually specifying a change address. - * - * @param tx - The transaction to complete, containing UDT outputs - * @param from - The signer that will provide UDT inputs and receive change - * @param options - Optional configuration for the completion process - * @param options.shouldAddInputs - Whether to automatically add inputs. Defaults to true - * @returns A promise resolving to the completed transaction with inputs and change output added - * - * @example - * ```typescript - * const udt = new Udt(codeOutPoint, scriptConfig); - * - * // Create a transfer transaction - * const transferResponse = await udt.transfer( - * signer, - * [{ to: recipientLock, amount: 1000 }] - * ); - * - * // Complete the transaction (change will go to signer's address) - * const completedTx = await udt.completeBy(transferResponse.res, signer); - * - * // Add capacity inputs and fee - * await completedTx.completeInputsByCapacity(signer); - * await completedTx.completeFeeBy(signer, changeLock); - * - * const txHash = await signer.sendTransaction(completedTx); - * ``` - * - * @see {@link completeChangeToLock} for more control over the change destination - */ - async completeBy( - tx: ccc.TransactionLike, - from: ccc.Signer, - options?: { shouldAddInputs?: boolean }, - ) { + async completeBy(tx: ccc.TransactionLike, from: ccc.Signer) { const { script } = await from.getRecommendedAddressObj(); - return this.completeChangeToLock(tx, from, script, options); + return this.completeChangeToLock(tx, from, script); } } diff --git a/packages/udt/src/udtPausable/index.ts b/packages/udt/src/udtPausable/index.ts index 46fb7dda5..84215cce3 100644 --- a/packages/udt/src/udtPausable/index.ts +++ b/packages/udt/src/udtPausable/index.ts @@ -1,6 +1,6 @@ import { ccc } from "@ckb-ccc/core"; import { ssri } from "@ckb-ccc/ssri"; -import { Udt, UdtConfigLike } from "../udt/index.js"; +import { Udt } from "../udt/index.js"; /** * Represents a UDT (User Defined Token) with pausable functionality. @@ -11,7 +11,9 @@ export class UdtPausable extends Udt { constructor( code: ccc.OutPointLike, script: ccc.ScriptLike, - config: UdtConfigLike & { executor: ssri.Executor }, + config: { + executor: ssri.Executor; + }, ) { super(code, script, config); } diff --git a/packages/udt/vitest.config.ts b/packages/udt/vitest.config.ts deleted file mode 100644 index dc6a58785..000000000 --- a/packages/udt/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["src/**/*.test.ts"], - coverage: { - include: ["src/**/*.ts"], - }, - }, -}); diff --git a/vitest.config.ts b/vitest.config.ts index 35bbdef14..7af3d4a84 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,9 +2,9 @@ import { defineConfig, coverageConfigDefaults } from "vitest/config"; export default defineConfig({ test: { - projects: ["packages/core", "packages/udt"], + projects: ["packages/core"], coverage: { - include: ["packages/core", "packages/udt"], + include: ["packages/core"], exclude: [ "**/dist/**", "**/dist.commonjs/**", From b96b7a6d201c9ac5392dfb329f2eabb37fae986b Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Tue, 9 Sep 2025 18:38:40 +0800 Subject: [PATCH 20/81] Revert "feat: add RGB++ known scripts (RgbppLock, BtcTimeLock)" This reverts commit 507c6e8330eda1ee621d41b533e05b42e3d90356. This information is outdated --- .../client/clientPublicMainnet.advanced.ts | 68 -------- .../client/clientPublicTestnet.advanced.ts | 163 ------------------ packages/core/src/client/knownScript.ts | 5 - 3 files changed, 236 deletions(-) diff --git a/packages/core/src/client/clientPublicMainnet.advanced.ts b/packages/core/src/client/clientPublicMainnet.advanced.ts index 10fb09ada..979b0955c 100644 --- a/packages/core/src/client/clientPublicMainnet.advanced.ts +++ b/packages/core/src/client/clientPublicMainnet.advanced.ts @@ -457,72 +457,4 @@ export const MAINNET_SCRIPTS: Record = }, ], }, - [KnownScript.RgbppLock]: { - codeHash: - "0xbc6c568a1a0d0a09f6844dc9d74ddb4343c32143ff25f727c59edf4fb72d6936", - hashType: "type", - cellDeps: [ - { - cellDep: { - outPoint: { - txHash: - "0x04c5c3e69f1aa6ee27fb9de3d15a81704e387ab3b453965adbe0b6ca343c6f41", - index: 0, - }, - depType: "code", - }, - type: { - codeHash: - "0x00000000000000000000000000000000000000000000000000545950455f4944", - hashType: "type", - args: "0x68ad3d9e0bb9ea841a5d1fcd600137bd3f45401e759e353121f26cd0d981452f", - }, - }, - // Rgbpp lock config cell dep - { - cellDep: { - outPoint: { - txHash: - "0x04c5c3e69f1aa6ee27fb9de3d15a81704e387ab3b453965adbe0b6ca343c6f41", - index: 1, - }, - depType: "code", - }, - }, - ], - }, - [KnownScript.BtcTimeLock]: { - codeHash: - "0x70d64497a075bd651e98ac030455ea200637ee325a12ad08aff03f1a117e5a62", - hashType: "type", - cellDeps: [ - { - cellDep: { - outPoint: { - txHash: - "0x6257bf4297ee75fcebe2654d8c5f8d93bc9fc1b3dc62b8cef54ffe166162e996", - index: 0, - }, - depType: "code", - }, - type: { - codeHash: - "0x00000000000000000000000000000000000000000000000000545950455f4944", - hashType: "type", - args: "0x44b8253ae18e913a2845b0d548eaf6b3ba1099ed26835888932a754194028a8a", - }, - }, - // btc time lock config cell dep - { - cellDep: { - outPoint: { - txHash: - "0x6257bf4297ee75fcebe2654d8c5f8d93bc9fc1b3dc62b8cef54ffe166162e996", - index: 1, - }, - depType: "code", - }, - }, - ], - }, }); diff --git a/packages/core/src/client/clientPublicTestnet.advanced.ts b/packages/core/src/client/clientPublicTestnet.advanced.ts index 6819fc6ed..be811c7eb 100644 --- a/packages/core/src/client/clientPublicTestnet.advanced.ts +++ b/packages/core/src/client/clientPublicTestnet.advanced.ts @@ -469,167 +469,4 @@ export const TESTNET_SCRIPTS: Record = }, ], }, - [KnownScript.RgbppLock]: { - codeHash: - "0x61ca7a4796a4eb19ca4f0d065cb9b10ddcf002f10f7cbb810c706cb6bb5c3248", - hashType: "type", - cellDeps: [ - { - cellDep: { - outPoint: { - txHash: - "0xf1de59e973b85791ec32debbba08dff80c63197e895eb95d67fc1e9f6b413e00", - index: 0, - }, - depType: "code", - }, - type: { - codeHash: - "0x00000000000000000000000000000000000000000000000000545950455f4944", - hashType: "type", - args: "0xa3bc8441df149def76cfe15fec7b1e51d949548bc27fb7a75e9d4b3ef1c12c7f", - }, - }, - // Rgbpp lock config cell dep for Bitcoin Testnet3 - { - cellDep: { - outPoint: { - txHash: - "0xf1de59e973b85791ec32debbba08dff80c63197e895eb95d67fc1e9f6b413e00", - index: 1, - }, - depType: "code", - }, - }, - ], - }, - [KnownScript.BtcTimeLock]: { - codeHash: - "0x00cdf8fab0f8ac638758ebf5ea5e4052b1d71e8a77b9f43139718621f6849326", - hashType: "type", - cellDeps: [ - { - cellDep: { - outPoint: { - txHash: - "0xde0f87878a97500f549418e5d46d2f7704c565a262aa17036c9c1c13ad638529", - index: 0, - }, - depType: "code", - }, - type: { - codeHash: - "0x00000000000000000000000000000000000000000000000000545950455f4944", - hashType: "type", - args: "0xc9828585e6dd2afacb9e6e8ca7deb0975121aabee5c7983178a45509ffaec984", - }, - }, - // btc time lock config cell dep for Bitcoin Testnet3 - { - cellDep: { - outPoint: { - txHash: - "0xde0f87878a97500f549418e5d46d2f7704c565a262aa17036c9c1c13ad638529", - index: 1, - }, - depType: "code", - }, - }, - ], - }, }); - -/** - * Bitcoin Signet specific script overrides for testnet - * - * Contains script configurations that differ when using Bitcoin Signet - * instead of Bitcoin Testnet3. Only RgbppLock and BtcTimeLock are affected. - * - * @example - * ```typescript - * import { ClientPublicTestnet } from "@ckb-ccc/core"; - * import { TESTNET_SCRIPTS, TESTNET_SCRIPTS_BTC_SIGNET_OVERRIDES } from "@ckb-ccc/core/advanced"; - * - * // Use Bitcoin Testnet3 scripts (default) - * const testnet3Client = new ClientPublicTestnet(); - * - * // Use Bitcoin Signet scripts by merging overrides - * const signetClient = new ClientPublicTestnet({ - * scripts: { - * ...TESTNET_SCRIPTS, - * ...TESTNET_SCRIPTS_BTC_SIGNET_OVERRIDES - * } - * }); - */ -export const TESTNET_SCRIPTS_BTC_SIGNET_OVERRIDES: Partial< - Record -> = Object.freeze({ - [KnownScript.RgbppLock]: { - codeHash: - "0xd07598deec7ce7b5665310386b4abd06a6d48843e953c5cc2112ad0d5a220364", - hashType: "type", - cellDeps: [ - { - cellDep: { - outPoint: { - txHash: - "0x61efdeddbaa0bb4132c0eb174b3e8002ff5ec430f61ba46f30768d683c516eec", - index: 0, - }, - depType: "code", - }, - type: { - codeHash: - "0x00000000000000000000000000000000000000000000000000545950455f4944", - hashType: "type", - args: "0xb69fe766ce3b7014a2a78ad1fe688d82f1679325805371d2856c3b8d18ebfa5a", - }, - }, - // Rgbpp lock config cell dep for Bitcoin Signet - { - cellDep: { - outPoint: { - txHash: - "0x61efdeddbaa0bb4132c0eb174b3e8002ff5ec430f61ba46f30768d683c516eec", - index: 1, - }, - depType: "code", - }, - }, - ], - }, - [KnownScript.BtcTimeLock]: { - codeHash: - "0x80a09eca26d77cea1f5a69471c59481be7404febf40ee90f886c36a948385b55", - hashType: "type", - cellDeps: [ - { - cellDep: { - outPoint: { - txHash: - "0x5364b3535965e9eac9a35dd7af8e9e45a61d30a16e115923c032f80b28783e21", - index: 0, - }, - depType: "code", - }, - type: { - codeHash: - "0x00000000000000000000000000000000000000000000000000545950455f4944", - hashType: "type", - args: "0x32fc8c70a6451a1439fd91e214bba093f9cdd9276bc4ab223430dab5940aff92", - }, - }, - // btc time lock config cell dep for Bitcoin Signet - { - cellDep: { - outPoint: { - txHash: - "0x5364b3535965e9eac9a35dd7af8e9e45a61d30a16e115923c032f80b28783e21", - index: 1, - }, - depType: "code", - }, - }, - ], - }, -}); diff --git a/packages/core/src/client/knownScript.ts b/packages/core/src/client/knownScript.ts index f3a185d6c..2171b90b4 100644 --- a/packages/core/src/client/knownScript.ts +++ b/packages/core/src/client/knownScript.ts @@ -25,9 +25,4 @@ export enum KnownScript { TypeBurnLock = "TypeBurnLock", EasyToDiscoverType = "EasyToDiscoverType", TimeLock = "TimeLock", - - // RGB++ related scripts (default using Bitcoin Testnet3) - // For Bitcoin Signet, use TESTNET_SCRIPTS_BTC_SIGNET_OVERRIDES from @ckb-ccc/core/advanced - RgbppLock = "RgbppLock", - BtcTimeLock = "BtcTimeLock", } From f642cc5fd25df53de19c71937767c47143a26f1e Mon Sep 17 00:00:00 2001 From: Phroi <90913182+phroi@users.noreply.github.com> Date: Tue, 16 Sep 2025 04:52:46 +0300 Subject: [PATCH 21/81] feat(core): improve hex utils (#268) * feat(core): `hexFrom` passthru normalized hex and `numToHex` enforce hex normalization * feat(core): improve `isHex` * feat(core): improve `isHex` parameter --- .changeset/shiny-ants-say.md | 5 +++++ packages/core/src/hex/index.ts | 34 ++++++++++++++++++++++++++++++++-- packages/core/src/num/index.ts | 18 +++++++++++++++--- 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 .changeset/shiny-ants-say.md diff --git a/.changeset/shiny-ants-say.md b/.changeset/shiny-ants-say.md new file mode 100644 index 000000000..51df28401 --- /dev/null +++ b/.changeset/shiny-ants-say.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": patch +--- + +`hexFrom` passthru normalized hex and `numToHex` enforce hex normalization \ No newline at end of file diff --git a/packages/core/src/hex/index.ts b/packages/core/src/hex/index.ts index 41df43d76..88a32d489 100644 --- a/packages/core/src/hex/index.ts +++ b/packages/core/src/hex/index.ts @@ -13,8 +13,33 @@ export type Hex = `0x${string}`; export type HexLike = BytesLike; /** - * Converts a HexLike value to a Hex string. - * @public + * Determines whether a given value is a properly formatted hexadecimal string (ccc.Hex). + * + * A valid hexadecimal string: + * - Has at least two characters. + * - Starts with "0x". + * - Has an even length. + * - Contains only characters representing digits (0-9) or lowercase letters (a-f) after the "0x" prefix. + * + * @param v - The value to validate as a hexadecimal (ccc.Hex) string. + * @returns True if the string is a valid hex string, false otherwise. + */ +export function isHex(v: unknown): v is Hex { + if (!(typeof v === "string" && v.length % 2 === 0 && v.startsWith("0x"))) { + return false; + } + + for (let i = 2; i < v.length; i++) { + const c = v.charAt(i); + if (!(("0" <= c && c <= "9") || ("a" <= c && c <= "f"))) { + return false; + } + } + return true; +} + +/** + * Returns the hexadecimal representation of the given value. * * @param hex - The value to convert, which can be a string, Uint8Array, ArrayBuffer, or number array. * @returns A Hex string representing the value. @@ -26,5 +51,10 @@ export type HexLike = BytesLike; * ``` */ export function hexFrom(hex: HexLike): Hex { + // Passthru an already normalized hex. V8 optimization: maintain existing hidden string fields. + if (isHex(hex)) { + return hex; + } + return `0x${bytesTo(bytesFrom(hex), "hex")}`; } diff --git a/packages/core/src/num/index.ts b/packages/core/src/num/index.ts index 8e1905a52..cf2367447 100644 --- a/packages/core/src/num/index.ts +++ b/packages/core/src/num/index.ts @@ -1,4 +1,5 @@ import { Bytes, BytesLike, bytesConcat, bytesFrom } from "../bytes/index.js"; +import { Zero } from "../fixedPoint/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; /** @@ -90,11 +91,16 @@ export function numFrom(val: NumLike): Num { } /** - * Converts a NumLike value to a hexadecimal string. + * Convert a NumLike value into a canonical Hex, so prefixed with `0x` and + * containing an even number of lowercase hex digits (full-byte representation). + * * @public * * @param val - The value to convert, which can be a string, number, bigint, or HexLike. - * @returns A Hex string representing the numeric value. + * @returns A Hex string representing the provided value, prefixed with `0x` and + * containing an even number of lowercase hex digits. + * + * @throws {Error} If the normalized numeric value is negative. * * @example * ```typescript @@ -102,7 +108,13 @@ export function numFrom(val: NumLike): Num { * ``` */ export function numToHex(val: NumLike): Hex { - return `0x${numFrom(val).toString(16)}`; + const v = numFrom(val); + if (v < Zero) { + throw new Error("value must be non-negative"); + } + const h = v.toString(16); + // ensure even length (full bytes) + return h.length % 2 === 0 ? `0x${h}` : `0x0${h}`; } /** From e41333386037e59810e833fc7e6b5d11f1b7cf69 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 18 Sep 2025 22:14:38 +0800 Subject: [PATCH 22/81] fix(core): Invalid Uint64 0x00: with redundant leading zeros. --- .changeset/wise-news-admire.md | 6 ++++++ packages/core/src/num/index.ts | 17 +++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .changeset/wise-news-admire.md diff --git a/.changeset/wise-news-admire.md b/.changeset/wise-news-admire.md new file mode 100644 index 000000000..5607ae979 --- /dev/null +++ b/.changeset/wise-news-admire.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": patch +--- + +fix(core): Invalid Uint64 0x00: with redundant leading zeros. + \ No newline at end of file diff --git a/packages/core/src/num/index.ts b/packages/core/src/num/index.ts index cf2367447..034455c11 100644 --- a/packages/core/src/num/index.ts +++ b/packages/core/src/num/index.ts @@ -91,20 +91,23 @@ export function numFrom(val: NumLike): Num { } /** - * Convert a NumLike value into a canonical Hex, so prefixed with `0x` and - * containing an even number of lowercase hex digits (full-byte representation). + * Converts a {@link NumLike} value into its hexadecimal string representation, prefixed with `0x`. + * + * @remarks + * This function returns the direct hexadecimal representation of the number, which may have an odd number of digits. + * For a full-byte representation (an even number of hex digits), consider using {@link numToBytes}, {@link numLeToBytes}, or {@link numBeToBytes} and then converting the resulting byte array to a hex string. * * @public * * @param val - The value to convert, which can be a string, number, bigint, or HexLike. - * @returns A Hex string representing the provided value, prefixed with `0x` and - * containing an even number of lowercase hex digits. + * @returns A Hex string representing the number. * * @throws {Error} If the normalized numeric value is negative. * * @example * ```typescript - * const hex = numToHex(12345); // Outputs "0x3039" + * const hex = numToHex(4660); // "0x1234" + * const oddLengthHex = numToHex(10); // "0xa" * ``` */ export function numToHex(val: NumLike): Hex { @@ -112,9 +115,7 @@ export function numToHex(val: NumLike): Hex { if (v < Zero) { throw new Error("value must be non-negative"); } - const h = v.toString(16); - // ensure even length (full bytes) - return h.length % 2 === 0 ? `0x${h}` : `0x0${h}`; + return `0x${v.toString(16)}`; } /** From 3aa4425c6f7d756ceb464f58716b1a89620e2d5b Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 16 Oct 2025 19:54:29 +0800 Subject: [PATCH 23/81] chore: search manual proxy lock when signer isn't the proxy provider --- .changeset/tame-swans-hammer.md | 6 ++++++ packages/spore/src/spore/advanced.ts | 31 +++++++++++++++++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 .changeset/tame-swans-hammer.md diff --git a/.changeset/tame-swans-hammer.md b/.changeset/tame-swans-hammer.md new file mode 100644 index 000000000..cfe82c234 --- /dev/null +++ b/.changeset/tame-swans-hammer.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/spore": patch +--- + +Enable searching manual proxy lock for cluster in LockProxy mode + \ No newline at end of file diff --git a/packages/spore/src/spore/advanced.ts b/packages/spore/src/spore/advanced.ts index cc2cad6f9..23aa84aa1 100644 --- a/packages/spore/src/spore/advanced.ts +++ b/packages/spore/src/spore/advanced.ts @@ -29,16 +29,31 @@ export async function prepareCluster( case "lockProxy": { const lock = cluster.cellOutput.lock; - if ((await tx.findInputIndexByLock(lock, signer.client)) === undefined) { - // note: We can only search proxy of signer, if any custom logic is in need, developer should get - // the proxy filled in transaction before invoking `createSpore` - await tx.completeInputsAddOne(signer); - } - if (tx.outputs.every((output) => output.lock !== lock)) { - tx.addOutput({ + // Ensure a lock-proxy input is present, add one if absent + let lockProxyInputIndex = await tx.findInputIndexByLock( + lock, + signer.client, + ); + if (lockProxyInputIndex === undefined) { + await tx.completeInputsAddOne( + new ccc.SignerCkbScriptReadonly(signer.client, lock), + ); + lockProxyInputIndex = await tx.findInputIndexByLock( lock, - }); + signer.client, + ); + if (lockProxyInputIndex === undefined) { + throw new Error("Lock proxy input not found"); + } } + + // Add the output only if there's not already one with proxy lock + if (!tx.outputs.some((output) => output.lock === lock)) { + tx.addOutput( + await tx.getInput(lockProxyInputIndex)!.getCell(signer.client), + ); + } + tx.addCellDeps({ outPoint: cluster.outPoint, depType: "code", From d0af06148666113546c2d269b0c52c75c28e15ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:33:16 +0000 Subject: [PATCH 24/81] chore(release): bump packages version --- .changeset/tame-swans-hammer.md | 6 ------ packages/ccc/CHANGELOG.md | 6 ++++++ packages/ccc/package.json | 2 +- packages/ckb-ccc/CHANGELOG.md | 6 ++++++ packages/ckb-ccc/package.json | 2 +- packages/connector-react/CHANGELOG.md | 6 ++++++ packages/connector-react/package.json | 2 +- packages/connector/CHANGELOG.md | 6 ++++++ packages/connector/package.json | 2 +- packages/shell/CHANGELOG.md | 6 ++++++ packages/shell/package.json | 2 +- packages/spore/CHANGELOG.md | 7 +++++++ packages/spore/package.json | 2 +- 13 files changed, 43 insertions(+), 12 deletions(-) delete mode 100644 .changeset/tame-swans-hammer.md diff --git a/.changeset/tame-swans-hammer.md b/.changeset/tame-swans-hammer.md deleted file mode 100644 index cfe82c234..000000000 --- a/.changeset/tame-swans-hammer.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@ckb-ccc/spore": patch ---- - -Enable searching manual proxy lock for cluster in LockProxy mode - \ No newline at end of file diff --git a/packages/ccc/CHANGELOG.md b/packages/ccc/CHANGELOG.md index 17cd572bf..d94727e03 100644 --- a/packages/ccc/CHANGELOG.md +++ b/packages/ccc/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/ccc +## 1.1.22 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/shell@1.1.22 + ## 1.1.21 ### Patch Changes diff --git a/packages/ccc/package.json b/packages/ccc/package.json index 61219eb9e..6c17e0ef7 100644 --- a/packages/ccc/package.json +++ b/packages/ccc/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/ccc", - "version": "1.1.21", + "version": "1.1.22", "description": "CCC - CKBer's Codebase. Common Chains Connector.", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/ckb-ccc/CHANGELOG.md b/packages/ckb-ccc/CHANGELOG.md index 7461441d1..37628e5fc 100644 --- a/packages/ckb-ccc/CHANGELOG.md +++ b/packages/ckb-ccc/CHANGELOG.md @@ -1,5 +1,11 @@ # ckb-ccc +## 1.0.30 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.22 + ## 1.0.29 ### Patch Changes diff --git a/packages/ckb-ccc/package.json b/packages/ckb-ccc/package.json index 559cd6dd1..8e2a6153c 100644 --- a/packages/ckb-ccc/package.json +++ b/packages/ckb-ccc/package.json @@ -1,6 +1,6 @@ { "name": "ckb-ccc", - "version": "1.0.29", + "version": "1.0.30", "description": "CCC - CKBer's Codebase. Common Chains Connector.", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/connector-react/CHANGELOG.md b/packages/connector-react/CHANGELOG.md index 88c89cab9..a25c16138 100644 --- a/packages/connector-react/CHANGELOG.md +++ b/packages/connector-react/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/connector-react +## 1.0.30 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/connector@1.0.30 + ## 1.0.29 ### Patch Changes diff --git a/packages/connector-react/package.json b/packages/connector-react/package.json index 2e706f02b..6a81f8ca3 100644 --- a/packages/connector-react/package.json +++ b/packages/connector-react/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/connector-react", - "version": "1.0.29", + "version": "1.0.30", "description": "CCC - CKBer's Codebase. Common Chains Connector UI Component for React", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/connector/CHANGELOG.md b/packages/connector/CHANGELOG.md index af6e1b5bc..82fe323cf 100644 --- a/packages/connector/CHANGELOG.md +++ b/packages/connector/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/connector +## 1.0.30 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.22 + ## 1.0.29 ### Patch Changes diff --git a/packages/connector/package.json b/packages/connector/package.json index f48055120..40a31fdd6 100644 --- a/packages/connector/package.json +++ b/packages/connector/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/connector", - "version": "1.0.29", + "version": "1.0.30", "description": "CCC - CKBer's Codebase. Common Chains Connector UI", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/shell/CHANGELOG.md b/packages/shell/CHANGELOG.md index 33f507d34..3a9b85a36 100644 --- a/packages/shell/CHANGELOG.md +++ b/packages/shell/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/shell +## 1.1.22 +### Patch Changes + +- Updated dependencies [[`3aa4425`](https://github.com/ckb-devrel/ccc/commit/3aa4425c6f7d756ceb464f58716b1a89620e2d5b)]: + - @ckb-ccc/spore@1.5.14 + ## 1.1.21 ### Patch Changes diff --git a/packages/shell/package.json b/packages/shell/package.json index 4290d0861..c8f9e8bae 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/shell", - "version": "1.1.21", + "version": "1.1.22", "description": "Backend Shell of CCC - CKBer's Codebase. Common Chains Connector.", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/spore/CHANGELOG.md b/packages/spore/CHANGELOG.md index 4a7d6817b..3fe7b7b44 100644 --- a/packages/spore/CHANGELOG.md +++ b/packages/spore/CHANGELOG.md @@ -1,5 +1,12 @@ # @ckb-ccc/spore +## 1.5.14 +### Patch Changes + + + +- [#324](https://github.com/ckb-devrel/ccc/pull/324) [`3aa4425`](https://github.com/ckb-devrel/ccc/commit/3aa4425c6f7d756ceb464f58716b1a89620e2d5b) Thanks [@ashuralyk](https://github.com/ashuralyk)! - Enable searching manual proxy lock for cluster in LockProxy mode + ## 1.5.13 ### Patch Changes diff --git a/packages/spore/package.json b/packages/spore/package.json index 6c30a46fd..8fbb7d442 100644 --- a/packages/spore/package.json +++ b/packages/spore/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/spore", - "version": "1.5.13", + "version": "1.5.14", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for Spore protocol", "author": "ashuralyk ", "license": "MIT", From 5d4ad4ec5c6b4eede1be6491a2eaa9041a1f6431 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Wed, 12 Nov 2025 04:51:54 +0800 Subject: [PATCH 25/81] feat(app): signature textarea for signing --- .../src/app/connected/(tools)/Sign/page.tsx | 42 ++++++++++++++----- packages/demo/src/components/Textarea.tsx | 6 ++- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/Sign/page.tsx b/packages/demo/src/app/connected/(tools)/Sign/page.tsx index 6ce0f5829..55f36a374 100644 --- a/packages/demo/src/app/connected/(tools)/Sign/page.tsx +++ b/packages/demo/src/app/connected/(tools)/Sign/page.tsx @@ -5,6 +5,7 @@ import { ButtonsPanel } from "@/src/components/ButtonsPanel"; import { Textarea } from "@/src/components/Textarea"; import { useApp } from "@/src/context"; import { ccc } from "@ckb-ccc/connector-react"; +import { CopyIcon } from "lucide-react"; import { useState } from "react"; export default function Sign() { @@ -21,6 +22,24 @@ export default function Sign() { placeholder="Message to sign and verify" state={[messageToSign, setMessageToSign]} /> +