diff --git a/.changeset/curvy-baboons-sip.md b/.changeset/curvy-baboons-sip.md deleted file mode 100644 index 9171ff9a1..000000000 --- a/.changeset/curvy-baboons-sip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@ckb-ccc/core": minor ---- - -feat(core): optional `shouldAddInputs` for `Transaction.completeFee` - \ No newline at end of file diff --git a/.changeset/eighty-terms-cry.md b/.changeset/eighty-terms-cry.md new file mode 100644 index 000000000..ba95dd46a --- /dev/null +++ b/.changeset/eighty-terms-cry.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": minor +--- + +Add `bytesLen` and `bytesLenUnsafe` utilities diff --git a/.changeset/fifty-planes-fetch.md b/.changeset/fifty-planes-fetch.md new file mode 100644 index 000000000..bcd19d3f6 --- /dev/null +++ b/.changeset/fifty-planes-fetch.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): `Signer.fromSignature` + \ No newline at end of file diff --git a/.changeset/late-vans-juggle.md b/.changeset/late-vans-juggle.md new file mode 100644 index 000000000..4f88298b8 --- /dev/null +++ b/.changeset/late-vans-juggle.md @@ -0,0 +1,7 @@ +--- +"@ckb-ccc/core": major +"@ckb-ccc/joy-id": minor +--- + +feat(joy-id): address info in identity + \ No newline at end of file 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/.changeset/six-steaks-grab.md b/.changeset/six-steaks-grab.md deleted file mode 100644 index 9b7432cfd..000000000 --- a/.changeset/six-steaks-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@ckb-ccc/core": patch ---- - -Simplify MapLru, while improving Complexity 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/.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/.changeset/weak-otters-dance.md b/.changeset/weak-otters-dance.md deleted file mode 100644 index af924ed4f..000000000 --- a/.changeset/weak-otters-dance.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@ckb-ccc/core": patch ---- - -fix(core): `Transaction.clone` should clone inputs' cache - \ No newline at end of file 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/ccc/CHANGELOG.md b/packages/ccc/CHANGELOG.md index 17cd572bf..38ed5c9a8 100644 --- a/packages/ccc/CHANGELOG.md +++ b/packages/ccc/CHANGELOG.md @@ -1,5 +1,53 @@ # @ckb-ccc/ccc +## 1.1.25 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/eip6963@1.0.32 + - @ckb-ccc/joy-id@1.0.32 + - @ckb-ccc/nip07@1.0.32 + - @ckb-ccc/okx@1.0.32 + - @ckb-ccc/rei@1.0.32 + - @ckb-ccc/shell@1.1.25 + - @ckb-ccc/uni-sat@1.0.32 + - @ckb-ccc/utxo-global@1.0.32 + - @ckb-ccc/xverse@1.0.32 + +## 1.1.24 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/eip6963@1.0.31 + - @ckb-ccc/joy-id@1.0.31 + - @ckb-ccc/nip07@1.0.31 + - @ckb-ccc/okx@1.0.31 + - @ckb-ccc/rei@1.0.31 + - @ckb-ccc/shell@1.1.24 + - @ckb-ccc/uni-sat@1.0.31 + - @ckb-ccc/utxo-global@1.0.31 + - @ckb-ccc/xverse@1.0.31 + +## 1.1.23 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/eip6963@1.0.30 + - @ckb-ccc/joy-id@1.0.30 + - @ckb-ccc/nip07@1.0.30 + - @ckb-ccc/okx@1.0.30 + - @ckb-ccc/rei@1.0.30 + - @ckb-ccc/shell@1.1.23 + - @ckb-ccc/uni-sat@1.0.30 + - @ckb-ccc/utxo-global@1.0.30 + - @ckb-ccc/xverse@1.0.30 + +## 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..05efb0489 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.25", "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..ee3bd1101 100644 --- a/packages/ckb-ccc/CHANGELOG.md +++ b/packages/ckb-ccc/CHANGELOG.md @@ -1,5 +1,29 @@ # ckb-ccc +## 1.0.33 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.25 + +## 1.0.32 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.24 + +## 1.0.31 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.23 + +## 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..71d74d547 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.33", "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..c30202f04 100644 --- a/packages/connector-react/CHANGELOG.md +++ b/packages/connector-react/CHANGELOG.md @@ -1,5 +1,36 @@ # @ckb-ccc/connector-react +## 1.0.34 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/connector@1.0.33 + +## 1.0.33 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/connector@1.0.32 + +## 1.0.32 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/connector@1.0.31 + +## 1.0.31 +### Patch Changes + + + +- [#335](https://github.com/ckb-devrel/ccc/pull/335) [`ea7e626`](https://github.com/ckb-devrel/ccc/commit/ea7e626a81ad4fb78142f0d948843de84478debf) Thanks [@Hanssen0](https://github.com/Hanssen0)! - chore: bump version of 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..7af39736a 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.34", "description": "CCC - CKBer's Codebase. Common Chains Connector UI Component for React", "author": "Hanssen0 ", "license": "MIT", @@ -25,7 +25,7 @@ }, "devDependencies": { "@eslint/js": "^9.34.0", - "@types/react": "^19.1.12", + "@types/react": "^19.2.7", "eslint": "^9.34.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", diff --git a/packages/connector/CHANGELOG.md b/packages/connector/CHANGELOG.md index af6e1b5bc..de88116f2 100644 --- a/packages/connector/CHANGELOG.md +++ b/packages/connector/CHANGELOG.md @@ -1,5 +1,29 @@ # @ckb-ccc/connector +## 1.0.33 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.25 + +## 1.0.32 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.24 + +## 1.0.31 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.23 + +## 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..577b11e7b 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.33", "description": "CCC - CKBer's Codebase. Common Chains Connector UI", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/connector/src/signers/index.ts b/packages/connector/src/signers/index.ts index 157d319bf..6b943be9a 100644 --- a/packages/connector/src/signers/index.ts +++ b/packages/connector/src/signers/index.ts @@ -45,7 +45,7 @@ export class SignersController { hostConnected(): void { void this.refresh(); // Wait for plugins to be loaded - setTimeout(() => this.refresh(), 500); + setTimeout(() => void this.refresh(), 500); } hostDisconnected(): void { diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 548886218..d8b38dca3 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,26 @@ # @ckb-ccc/core +## 1.12.5 +### Patch Changes + + + +- [#354](https://github.com/ckb-devrel/ccc/pull/354) [`a96dec6`](https://github.com/ckb-devrel/ccc/commit/a96dec6d0517113391b0edc510f1af821a45d5a8) Thanks [@RetricSu](https://github.com/RetricSu)! - chore(core): bump nostr-lock mainnet cell deps + +## 1.12.4 +### Patch Changes + + + +- [#350](https://github.com/ckb-devrel/ccc/pull/350) [`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1) Thanks [@Hanssen0](https://github.com/Hanssen0)! - fix(core): circular dependency due to btc.verify + +## 1.12.3 +### Patch Changes + + + +- [#344](https://github.com/ckb-devrel/ccc/pull/344) [`6a3be47`](https://github.com/ckb-devrel/ccc/commit/6a3be477b40870dc40d491ce51e667f61f70965e) Thanks [@Hanssen0](https://github.com/Hanssen0)! - fix: wrong capacity completion while deserializing transaction + ## 1.12.2 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 83fa70411..40640b81d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/core", - "version": "1.12.2", + "version": "1.12.5", "description": "Core of CCC - CKBer's Codebase", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/core/src/ckb/transaction.test.ts b/packages/core/src/ckb/transaction.test.ts index 6271943ae..6271517d4 100644 --- a/packages/core/src/ckb/transaction.test.ts +++ b/packages/core/src/ckb/transaction.test.ts @@ -656,6 +656,15 @@ describe("Transaction", () => { expect(cellOutput.capacity).toBe(1000n); }); + it("should not modify capacity when data is not provided", () => { + const cellOutput = ccc.CellOutput.from({ + capacity: 0n, + lock, + }); + + expect(cellOutput.capacity).toBe(0n); + }); + it("should calculate capacity automatically when capacity is 0", () => { const outputData = "0x1234"; // 2 bytes const cellOutput = ccc.CellOutput.from( @@ -813,6 +822,30 @@ describe("Transaction", () => { ); }); + it("should automatically fill capacity considering outputData while deserialization", () => { + const outputsData = ["0x1234"]; + const calculatedTx = ccc.Transaction.from({ + outputs: [ + { + lock, + }, + ], + outputsData, + }); + calculatedTx.outputs[0].capacity = 0n; + const data = calculatedTx.toBytes(); + expect(ccc.hexFrom(data)).toBe( + "0xb30000000c000000af000000a30000001c0000002000000024000000280000002c00000095000000000000000000000000000000000000006900000008000000610000001000000018000000610000000000000000000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8011400000036053c1bbc237e164137c03664fe8384b2cf9b260e0000000800000002000000123404000000", + ); + const tx = ccc.Transaction.fromBytes(data); + + // Should use outputData for calculation + const expectedCapacity = 8 + lock.occupiedSize + 2; + expect(tx.outputs[0].capacity).toBe( + ccc.fixedPointFrom(expectedCapacity), + ); + }); + it("should handle empty outputsData array", () => { const tx = ccc.Transaction.from({ outputs: [ diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 9a5920bd9..35fc4a727 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -235,7 +235,7 @@ 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. + * This method supports automatic capacity calculation when outputData is provided and capacity is 0 or omitted. * * @param cellOutput - A CellOutputLike object or an instance of CellOutput. * @param outputData - Optional output data used for automatic capacity calculation. @@ -262,19 +262,20 @@ export class CellOutput extends mol.Entity.Base() { cellOutput: CellOutputLike, outputData?: HexLike | null, ): CellOutput { - if (cellOutput instanceof CellOutput) { - return cellOutput; - } - - const output = new CellOutput( - numFrom(cellOutput.capacity ?? 0), - Script.from(cellOutput.lock), - apply(Script.from, cellOutput.type), - ); + const output = (() => { + if (cellOutput instanceof CellOutput) { + return cellOutput; + } + return new CellOutput( + numFrom(cellOutput.capacity ?? 0), + Script.from(cellOutput.lock), + apply(Script.from, cellOutput.type), + ); + })(); - if (output.capacity === Zero) { + if (output.capacity === Zero && outputData != null) { output.capacity = fixedPointFrom( - output.occupiedSize + bytesFrom(outputData ?? "0x").length, + output.occupiedSize + bytesFrom(outputData).length, ); } @@ -365,9 +366,11 @@ export class CellAny { return cell; } + const outputData = hexFrom(cell.outputData ?? "0x"); + return new CellAny( - CellOutput.from(cell.cellOutput, cell.outputData), - hexFrom(cell.outputData ?? "0x"), + CellOutput.from(cell.cellOutput, outputData), + outputData, apply(OutPoint.from, cell.outPoint ?? cell.previousOutput), ); } diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index 8c0cec7a6..7f178786d 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/clientPublicMainnet.advanced.ts b/packages/core/src/client/clientPublicMainnet.advanced.ts index 10fb09ada..e8f1d6909 100644 --- a/packages/core/src/client/clientPublicMainnet.advanced.ts +++ b/packages/core/src/client/clientPublicMainnet.advanced.ts @@ -290,7 +290,7 @@ export const MAINNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0x1911208b136957d5f7c1708a8835edfe8ae1d02700d5cb2c3a6aacf4d5906306", + "0x99b116dd1e4f1fa903b70112ae672c18bb34241d3d03a9ad555cd2a611be7327", index: 0, }, depType: "code", @@ -466,7 +466,7 @@ export const MAINNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0x04c5c3e69f1aa6ee27fb9de3d15a81704e387ab3b453965adbe0b6ca343c6f41", + "0xcb4d9f9726e66306bfda6359d39d3bea8b4e5345d0f95f26a3e51626ebe82a63", index: 0, }, depType: "code", @@ -483,7 +483,7 @@ export const MAINNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0x04c5c3e69f1aa6ee27fb9de3d15a81704e387ab3b453965adbe0b6ca343c6f41", + "0xcb4d9f9726e66306bfda6359d39d3bea8b4e5345d0f95f26a3e51626ebe82a63", index: 1, }, depType: "code", @@ -500,7 +500,7 @@ export const MAINNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0x6257bf4297ee75fcebe2654d8c5f8d93bc9fc1b3dc62b8cef54ffe166162e996", + "0x3d1c26b966504b09253ad84173bf3baa7b8135c5ff520c32cf70b631c1d08b9b", index: 0, }, depType: "code", @@ -517,7 +517,7 @@ export const MAINNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0x6257bf4297ee75fcebe2654d8c5f8d93bc9fc1b3dc62b8cef54ffe166162e996", + "0x3d1c26b966504b09253ad84173bf3baa7b8135c5ff520c32cf70b631c1d08b9b", index: 1, }, depType: "code", diff --git a/packages/core/src/client/clientPublicTestnet.advanced.ts b/packages/core/src/client/clientPublicTestnet.advanced.ts index 6819fc6ed..c38074b0b 100644 --- a/packages/core/src/client/clientPublicTestnet.advanced.ts +++ b/packages/core/src/client/clientPublicTestnet.advanced.ts @@ -478,7 +478,7 @@ export const TESTNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0xf1de59e973b85791ec32debbba08dff80c63197e895eb95d67fc1e9f6b413e00", + "0x0d1567da0979f78b297d5311442669fbd1bd853c8be324c5ab6da41e7a1ed6e5", index: 0, }, depType: "code", @@ -495,7 +495,7 @@ export const TESTNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0xf1de59e973b85791ec32debbba08dff80c63197e895eb95d67fc1e9f6b413e00", + "0x0d1567da0979f78b297d5311442669fbd1bd853c8be324c5ab6da41e7a1ed6e5", index: 1, }, depType: "code", @@ -512,7 +512,7 @@ export const TESTNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0xde0f87878a97500f549418e5d46d2f7704c565a262aa17036c9c1c13ad638529", + "0x8fb747ff0416a43e135c583b028f98c7b81d3770551b196eb7ba1062dd9acc94", index: 0, }, depType: "code", @@ -529,7 +529,7 @@ export const TESTNET_SCRIPTS: Record = cellDep: { outPoint: { txHash: - "0xde0f87878a97500f549418e5d46d2f7704c565a262aa17036c9c1c13ad638529", + "0x8fb747ff0416a43e135c583b028f98c7b81d3770551b196eb7ba1062dd9acc94", index: 1, }, depType: "code", @@ -538,98 +538,3 @@ export const TESTNET_SCRIPTS: Record = ], }, }); - -/** - * 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/jsonRpc/client.ts b/packages/core/src/client/jsonRpc/client.ts index 55790e919..3b1d837b4 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"]; diff --git a/packages/core/src/client/knownScript.ts b/packages/core/src/client/knownScript.ts index f3a185d6c..c65259c61 100644 --- a/packages/core/src/client/knownScript.ts +++ b/packages/core/src/client/knownScript.ts @@ -27,7 +27,6 @@ export enum KnownScript { 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", } diff --git a/packages/core/src/hex/index.ts b/packages/core/src/hex/index.ts index 41df43d76..e34add49f 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 (odd-length hex is considered non-standard). + * - 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,66 @@ 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")}`; } + +/** + * Return the number of bytes occupied by `hexLike`. + * + * This function efficiently calculates the byte length of hex-like values. + * For valid Hex strings, it uses a fast-path helper. For other types, it + * converts to bytes first. + * + * @param hexLike - Hex-like value (Hex string, Uint8Array, ArrayBuffer, or iterable of numbers). + * @returns Byte length of `hexLike`. + * + * @example + * ```typescript + * bytesLen("0x48656c6c6f") // 5 + * bytesLen(new Uint8Array([1, 2, 3])) // 3 + * bytesLen(new ArrayBuffer(4)) // 4 + * bytesLen([1, 2]) // 2 + * ``` + * + * @throws May throw if `hexLike` contains invalid byte values when passed to `bytesFrom`. + * @see bytesLenUnsafe - Fast version for already-validated Hex strings + * + * @note Prefer direct `.length`/`.byteLength` access on Uint8Array/ArrayBuffer when you already have bytes. + * Use `bytesLen()` only when you need length without performing additional operations. + * @see bytesFrom - Convert values to Bytes (Uint8Array) + */ +export function bytesLen(hexLike: HexLike): number { + if (isHex(hexLike)) { + return bytesLenUnsafe(hexLike); + } + + return bytesFrom(hexLike).length; +} + +/** + * Fast byte length for Hex strings. + * + * This function efficiently calculates the byte length of Hex values: + * - Skips isHex validation (caller must ensure input is valid Hex) + * - Handles odd-digit hex by rounding up, matching bytesFrom's padding behavior. + * + * @param hex - A valid Hex string (with "0x" prefix). + * @returns Byte length of the hex string. + * + * @example + * ```typescript + * bytesLenUnsafe("0x48656c6c6f") // 5 + * bytesLenUnsafe("0x123") // 2 (odd digits round up via padding) + * ``` + * + * @see bytesLen - Validated version for untrusted input + */ +export function bytesLenUnsafe(hex: Hex): number { + // Equivalent to Math.ceil((hex.length - 2) / 2), rounds up for odd-digit hex. + return (hex.length - 1) >> 1; +} diff --git a/packages/core/src/molecule/entity.ts b/packages/core/src/molecule/entity.ts index 5e1771e8c..629281385 100644 --- a/packages/core/src/molecule/entity.ts +++ b/packages/core/src/molecule/entity.ts @@ -128,6 +128,12 @@ export abstract class Entity { } } + /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ + Impl.encode = undefined as any; + Impl.decode = undefined as any; + Impl.fromBytes = undefined as any; + /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ + return Impl; } @@ -168,15 +174,21 @@ export function codec< }, >(Constructor: ConstructorType, ..._: unknown[]) { Constructor.byteLength = codec.byteLength; - Constructor.encode = function (encodable: TypeLike) { - return codec.encode(encodable); - }; - Constructor.decode = function (bytesLike: BytesLike) { - return Constructor.from(codec.decode(bytesLike)); - }; - Constructor.fromBytes = function (bytes: BytesLike) { - return Constructor.from(codec.decode(bytes)); - }; + if (Constructor.encode === undefined) { + Constructor.encode = function (encodable: TypeLike) { + return codec.encode(encodable); + }; + } + if (Constructor.decode === undefined) { + Constructor.decode = function (bytesLike: BytesLike) { + return Constructor.from(codec.decode(bytesLike)); + }; + } + if (Constructor.fromBytes === undefined) { + Constructor.fromBytes = function (bytes: BytesLike) { + return Constructor.from(codec.decode(bytes)); + }; + } return Constructor; }; diff --git a/packages/core/src/num/index.ts b/packages/core/src/num/index.ts index 8e1905a52..034455c11 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,19 +91,31 @@ export function numFrom(val: NumLike): Num { } /** - * Converts a NumLike value to a hexadecimal string. + * 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 numeric value. + * @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 { - return `0x${numFrom(val).toString(16)}`; + const v = numFrom(val); + if (v < Zero) { + throw new Error("value must be non-negative"); + } + return `0x${v.toString(16)}`; } /** diff --git a/packages/core/src/signer/btc/index.ts b/packages/core/src/signer/btc/index.ts index d0aa15884..694ff7250 100644 --- a/packages/core/src/signer/btc/index.ts +++ b/packages/core/src/signer/btc/index.ts @@ -1,3 +1,4 @@ +export * from "./psbt.js"; export * from "./signerBtc.js"; export * from "./signerBtcPublicKeyReadonly.js"; export * from "./verify.js"; diff --git a/packages/core/src/signer/btc/psbt.ts b/packages/core/src/signer/btc/psbt.ts new file mode 100644 index 000000000..abbc47cd0 --- /dev/null +++ b/packages/core/src/signer/btc/psbt.ts @@ -0,0 +1,98 @@ +import { Hex, HexLike, hexFrom } from "../../hex/index.js"; + +/** + * Options for signing a PSBT (Partially Signed Bitcoin Transaction) + */ +export type SignPsbtOptionsLike = { + /** + * Whether to finalize the PSBT after signing. + * Default is true. + */ + autoFinalized?: boolean; + /** + * Array of inputs to sign + */ + inputsToSign?: InputToSignLike[]; +}; + +export class SignPsbtOptions { + constructor( + public autoFinalized: boolean, + public inputsToSign: InputToSign[], + ) {} + + static from(options?: SignPsbtOptionsLike): SignPsbtOptions { + if (options instanceof SignPsbtOptions) { + return options; + } + return new SignPsbtOptions( + options?.autoFinalized ?? true, + options?.inputsToSign?.map((i) => InputToSign.from(i)) ?? [], + ); + } +} + +/** + * Specification for an input to sign in a PSBT. + * Must specify at least one of: address or pubkey. + */ +export type InputToSignLike = { + /** + * Which input to sign (index in the PSBT inputs array) + */ + index: number; + /** + * (Optional) Sighash types to use for signing. + */ + sighashTypes?: number[]; + /** + * (Optional) When signing and unlocking Taproot addresses, the tweakSigner is used by default + * for signature generation. Setting this to true allows for signing with the original private key. + * Default value is false. + */ + disableTweakSigner?: boolean; +} & ( + | { + /** + * The address whose corresponding private key to use for signing. + */ + address: string; + /** + * The public key whose corresponding private key to use for signing. + */ + publicKey?: HexLike; + } + | { + /** + * The address whose corresponding private key to use for signing. + */ + address?: string; + /** + * The public key whose corresponding private key to use for signing. + */ + publicKey: HexLike; + } +); + +export class InputToSign { + constructor( + public index: number, + public sighashTypes?: number[], + public disableTweakSigner?: boolean, + public address?: string, + public publicKey?: Hex, + ) {} + + static from(input: InputToSignLike): InputToSign { + if (input instanceof InputToSign) { + return input; + } + return new InputToSign( + input.index, + input.sighashTypes, + input.disableTweakSigner, + input.address, + input.publicKey ? hexFrom(input.publicKey) : undefined, + ); + } +} diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index 7d5620c39..4c0d389ad 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -1,10 +1,11 @@ import { Address } from "../../address/index.js"; import { bytesConcat, bytesFrom } from "../../bytes/index.js"; import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js"; -import { Client, KnownScript } from "../../client/index.js"; -import { HexLike, hexFrom } from "../../hex/index.js"; +import { KnownScript } from "../../client/index.js"; +import { Hex, HexLike, hexFrom } from "../../hex/index.js"; import { numToBytes } from "../../num/index.js"; import { Signer, SignerSignType, SignerType } from "../signer/index.js"; +import { SignPsbtOptionsLike } from "./psbt.js"; import { btcEcdsaPublicKeyHash } from "./verify.js"; /** @@ -14,10 +15,6 @@ import { btcEcdsaPublicKeyHash } from "./verify.js"; * @public */ export abstract class SignerBtc extends Signer { - constructor(client: Client) { - super(client); - } - get type(): SignerType { return SignerType.BTC; } @@ -26,6 +23,21 @@ export abstract class SignerBtc extends Signer { return SignerSignType.BtcEcdsa; } + /** + * Sign and broadcast a PSBT. + * + * @param psbtHex - The hex string of PSBT to sign and broadcast. + * @param options - Options for signing the PSBT. + * @returns A promise that resolves to the transaction ID as a Hex string. + */ + async signAndBroadcastPsbt( + psbtHex: HexLike, + options?: SignPsbtOptionsLike, + ): Promise { + const signedPsbt = await this.signPsbt(psbtHex, options); + return this.broadcastPsbt(signedPsbt, options); + } + /** * Gets the Bitcoin account associated with the signer. * @@ -131,17 +143,24 @@ export abstract class SignerBtc extends Signer { /** * Signs a Partially Signed Bitcoin Transaction (PSBT). * - * @param psbtHex - The hex string of PSBT to sign - * @returns A promise that resolves to the signed PSBT hex string - * @todo Add support for Taproot signing options (useTweakedSigner, etc.) + * @param psbtHex - The hex string of PSBT to sign. + * @param options - Options for signing the PSBT + * @returns A promise that resolves to the signed PSBT as a Hex string. */ - abstract signPsbt(psbtHex: string): Promise; + abstract signPsbt( + psbtHex: HexLike, + options?: SignPsbtOptionsLike, + ): Promise; /** - * Broadcasts a signed PSBT to the Bitcoin network. + * Broadcasts a PSBT to the Bitcoin network. * - * @param psbtHex - The hex string of signed PSBT to broadcast - * @returns A promise that resolves to the transaction ID + * @param psbtHex - The hex string of the PSBT to broadcast. + * @param options - Options for broadcasting the PSBT. + * @returns A promise that resolves to the transaction ID as a Hex string. */ - abstract pushPsbt(psbtHex: string): Promise; + abstract broadcastPsbt( + psbtHex: HexLike, + options?: SignPsbtOptionsLike, + ): Promise; } diff --git a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts index ba7e93228..25af50b6f 100644 --- a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts +++ b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts @@ -1,5 +1,6 @@ import { Client } from "../../client/index.js"; import { Hex, HexLike, hexFrom } from "../../hex/index.js"; +import { SignPsbtOptionsLike } from "./psbt.js"; import { SignerBtc } from "./signerBtc.js"; /** @@ -71,11 +72,17 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { return this.publicKey; } - async signPsbt(_: string): Promise { + async signPsbt( + _psbtHex: HexLike, + _options?: SignPsbtOptionsLike, + ): Promise { throw new Error("Read-only signer does not support signPsbt"); } - async pushPsbt(_: string): Promise { - throw new Error("Read-only signer does not support pushPsbt"); + async broadcastPsbt( + _psbtHex: HexLike, + _options?: SignPsbtOptionsLike, + ): Promise { + throw new Error("Read-only signer does not support broadcastPsbt"); } } diff --git a/packages/core/src/signer/ckb/verifyJoyId.ts b/packages/core/src/signer/ckb/verifyJoyId.ts index eb9089946..44eece343 100644 --- a/packages/core/src/signer/ckb/verifyJoyId.ts +++ b/packages/core/src/signer/ckb/verifyJoyId.ts @@ -1,27 +1,55 @@ -import { verifySignature } from "@joyid/ckb"; +import { + CredentialKeyType, + SigningAlg, + verifyCredential, + verifySignature, +} from "@joyid/ckb"; import { BytesLike } from "../../bytes/index.js"; import { hexFrom } from "../../hex/index.js"; /** * @public */ -export function verifyMessageJoyId( +export async function verifyMessageJoyId( message: string | BytesLike, signature: string, identity: string, ): Promise { const challenge = typeof message === "string" ? message : hexFrom(message).slice(2); - const { publicKey, keyType } = JSON.parse(identity) as { + const { address, publicKey, keyType } = JSON.parse(identity) as { + address: string; publicKey: string; - keyType: string; + keyType: CredentialKeyType; }; + const signatureObj = JSON.parse(signature) as { + alg: SigningAlg; + signature: string; + message: string; + }; + + if ( + !(await verifySignature({ + challenge, + pubkey: publicKey, + keyType, + ...signatureObj, + })) + ) { + return false; + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return verifySignature({ - challenge, - pubkey: publicKey, - keyType, - ...JSON.parse(signature), - }); + // I sincerely hope one day we can get rid of the centralized registry + const registry = address.startsWith("ckb") + ? "https://api.joy.id/api/v1/" + : "https://api.testnet.joyid.dev/api/v1/"; + return verifyCredential( + { + pubkey: publicKey, + address, + keyType, + alg: signatureObj.alg, + }, + registry, + ); } diff --git a/packages/core/src/signer/index.ts b/packages/core/src/signer/index.ts index 350b487e8..052248f15 100644 --- a/packages/core/src/signer/index.ts +++ b/packages/core/src/signer/index.ts @@ -5,3 +5,20 @@ export * from "./dummy/index.js"; export * from "./evm/index.js"; export * from "./nostr/index.js"; export * from "./signer/index.js"; +export * from "./signerFromSignature.js"; + +import { BytesLike } from "../bytes/index.js"; +import { Client } from "../client/index.js"; +import { Signer as BaseSigner, Signature } from "./signer/index.js"; +import { signerFromSignature } from "./signerFromSignature.js"; + +export abstract class Signer extends BaseSigner { + static fromSignature( + client: Client, + signature: Signature, + message?: string | BytesLike | null, + ...addresses: (string | string[])[] + ): Promise { + return signerFromSignature(client, signature, message, ...addresses); + } +} diff --git a/packages/core/src/signer/signer/index.ts b/packages/core/src/signer/signer/index.ts index 1522b3353..04dd84642 100644 --- a/packages/core/src/signer/signer/index.ts +++ b/packages/core/src/signer/signer/index.ts @@ -10,7 +10,7 @@ import { } from "../../client/index.js"; import { Hex } from "../../hex/index.js"; import { Num } from "../../num/index.js"; -import { verifyMessageBtcEcdsa } from "../btc/index.js"; +import { verifyMessageBtcEcdsa } from "../btc/verify.js"; import { verifyMessageCkbSecp256k1 } from "../ckb/verifyCkbSecp256k1.js"; import { verifyMessageJoyId } from "../ckb/verifyJoyId.js"; import { verifyMessageDogeEcdsa } from "../doge/verify.js"; @@ -157,6 +157,17 @@ export abstract class Signer { } } + static async fromSignature( + _client: Client, + _signature: Signature, + _message?: string | BytesLike | null, + ..._addresses: (string | string[])[] + ): Promise { + throw Error( + "Signer.fromSignature should be override to avoid circular references", + ); + } + /** * Connects to the signer. * diff --git a/packages/core/src/signer/signerFromSignature.ts b/packages/core/src/signer/signerFromSignature.ts new file mode 100644 index 000000000..7f296f827 --- /dev/null +++ b/packages/core/src/signer/signerFromSignature.ts @@ -0,0 +1,65 @@ +import { Address } from "../address/index.js"; +import { BytesLike } from "../bytes/index.js"; +import { Client } from "../client/index.js"; +import { SignerBtcPublicKeyReadonly } from "./btc/index.js"; +import { SignerCkbPublicKey, SignerCkbScriptReadonly } from "./ckb/index.js"; +import { SignerDogeAddressReadonly } from "./doge/index.js"; +import { SignerEvmAddressReadonly } from "./evm/index.js"; +import { SignerNostrPublicKeyReadonly } from "./nostr/index.js"; +import { Signature, Signer, SignerSignType } from "./signer/index.js"; + +/** + * Creates a signer from a signature. + * + * @param client - The client instance. + * @param signature - The signature to create the signer from. + * @param message - The message that was signed. + * @param addresses - The addresses to check against the signer. + * @returns The signer if the signature is valid and the addresses match, otherwise undefined. + * @throws Error if the signature sign type is unknown. + */ +export async function signerFromSignature( + client: Client, + signature: Signature, + message?: string | BytesLike | null, + ...addresses: (string | string[])[] +): Promise { + if ( + message != undefined && + !(await Signer.verifyMessage(message, signature)) + ) { + return; + } + + const signer = await (async () => { + switch (signature.signType) { + case SignerSignType.EvmPersonal: + return new SignerEvmAddressReadonly(client, signature.identity); + case SignerSignType.BtcEcdsa: + return new SignerBtcPublicKeyReadonly(client, "", signature.identity); + case SignerSignType.JoyId: { + const { address } = JSON.parse(signature.identity) as { + address: string; + }; + return new SignerCkbScriptReadonly( + client, + (await Address.fromString(address, client)).script, + ); + } + case SignerSignType.NostrEvent: + return new SignerNostrPublicKeyReadonly(client, signature.identity); + case SignerSignType.CkbSecp256k1: + return new SignerCkbPublicKey(client, signature.identity); + case SignerSignType.DogeEcdsa: + return new SignerDogeAddressReadonly(client, signature.identity); + case SignerSignType.Unknown: + throw new Error("Unknown signer sign type"); + } + })(); + const signerAddresses = await signer.getAddresses(); + if (!addresses.flat().every((addr) => signerAddresses.includes(addr))) { + return; + } + + return signer; +} 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) { diff --git a/packages/demo/README.md b/packages/demo/README.md index c4033664f..2bd252ea8 100644 --- a/packages/demo/README.md +++ b/packages/demo/README.md @@ -2,6 +2,33 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next ## Getting Started +### Environment Variables + +Before running the development server, you need to configure environment variables for RGB++ features. + +Create a `.env.local` file in the `packages/demo` directory with the following variables: + +```bash +# BTC Assets API Configuration +# Required: BTC Assets API URL (must be an absolute URL starting with http:// or https://) +NEXT_PUBLIC_BTC_ASSETS_API_URL=https://api-testnet.rgbpp.com + +# Optional: BTC Assets API Token (required for mainnet) +NEXT_PUBLIC_BTC_ASSETS_API_TOKEN= + +# Optional: BTC Assets API Origin (required for mainnet) +# Your application's origin domain (e.g., localhost:3000, app.example.com) +NEXT_PUBLIC_BTC_ASSETS_API_ORIGIN= +``` + +**Note:** +- The URL must be an absolute URL (starting with `http://` or `https://`), not a relative path +- For testnet, you can use `https://api-testnet.rgbpp.com` +- For mainnet, you'll need to provide a valid token and origin +- After modifying environment variables, restart the development server + +### Run the Development Server + First, run the development server: ```bash diff --git a/packages/demo/env.example b/packages/demo/env.example new file mode 100644 index 000000000..e31e0c6b5 --- /dev/null +++ b/packages/demo/env.example @@ -0,0 +1,22 @@ +# BTC Assets API Configuration +# Copy this file to .env.local and fill in your actual values +# +# To use this configuration: +# 1. Copy this file: cp env.example .env.local +# 2. Edit .env.local and fill in your actual values +# 3. Restart the development server + +# BTC Assets API URL (required) +# Must be an absolute URL starting with http:// or https:// +# Testnet: https://api-testnet.rgbpp.com +# Mainnet: https://api.rgbpp.com (if available) +NEXT_PUBLIC_BTC_ASSETS_API_URL=https://api-testnet.rgbpp.com + +# BTC Assets API Token (optional, required for mainnet) +# Get your token from the BTC Assets API service provider +NEXT_PUBLIC_BTC_ASSETS_API_TOKEN= + +# BTC Assets API Origin (optional, required for mainnet) +# Your application's origin domain (e.g., localhost:3000, app.example.com) +NEXT_PUBLIC_BTC_ASSETS_API_ORIGIN= + diff --git a/packages/demo/eslint.config.mjs b/packages/demo/eslint.config.mjs index aacfd2aeb..48fe8d310 100644 --- a/packages/demo/eslint.config.mjs +++ b/packages/demo/eslint.config.mjs @@ -2,6 +2,8 @@ import { dirname } from "path"; import { fileURLToPath } from "url"; import { FlatCompat } from "@eslint/eslintrc"; import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -11,7 +13,8 @@ const compat = new FlatCompat({ }); export default [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + ...nextVitals, + ...nextTs, { ignores: [ "node_modules/**", @@ -36,4 +39,4 @@ export default [ }, }, eslintPluginPrettierRecommended, -]; \ No newline at end of file +]; diff --git a/packages/demo/package.json b/packages/demo/package.json index 6c7abbc14..92439c9ec 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -17,12 +17,12 @@ }, "dependencies": { "@lit/react": "^1.0.8", - "@next/third-parties": "^15.5.2", + "@next/third-parties": "^16.0.10", "@uiw/react-json-view": "2.0.0-alpha.37", "lucide-react": "^0.542.0", - "next": "15.5.2", - "react": "^19.1.1", - "react-dom": "^19.1.1" + "next": "16.0.10", + "react": "^19.2.3", + "react-dom": "^19.2.3" }, "devDependencies": { "@ckb-ccc/connector-react": "workspace:*", @@ -41,10 +41,10 @@ "@scure/bip39": "^2.0.0", "@tailwindcss/postcss": "^4.1.12", "@types/node": "^24.3.0", - "@types/react": "^19.1.12", - "@types/react-dom": "^19.1.8", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", "eslint": "^9.34.0", - "eslint-config-next": "15.5.2", + "eslint-config-next": "16.0.10", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "postcss": "^8.5.6", diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 12b1c06a6..9202c3e68 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -11,16 +11,18 @@ import { BtcApiUtxo, BtcAssetApiConfig, buildNetworkConfig, - CkbRgbppUnlockSinger, + CkbRgbppUnlockSigner, + ClientScriptProvider, createBrowserRgbppBtcWallet, getSupportedWallets, isMainnet, NetworkConfig, PredefinedNetwork, + RgbppScriptName, RgbppUdtClient, UtxoSeal, } from "@ckb-ccc/rgbpp"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; const issuanceAmount = BigInt(21000000); const xudtToken = { @@ -47,6 +49,11 @@ export default function IssueRGBPPXUdt() { [createSender], ); const { error, log } = sender; + // Use ref to store error function to avoid re-running effects when it changes + const errorRef = useRef(error); + useEffect(() => { + errorRef.current = error; + }, [error]); const { explorerTransaction } = useGetExplorerLink(); const [rgbppBtcTxId, setRgbppBtcTxId] = useState(""); @@ -99,7 +106,9 @@ export default function IssueRGBPPXUdt() { // * use Testnet3 as default network = PredefinedNetwork.BitcoinTestnet3; } else { - error(`Unsupported network prefix: ${signer.client.addressPrefix}`); + errorRef.current( + `Unsupported network prefix: ${signer.client.addressPrefix}`, + ); return; } @@ -111,7 +120,8 @@ export default function IssueRGBPPXUdt() { : new ccc.ClientPublicTestnet(); setCkbClient(client); - const udtClient = new RgbppUdtClient(config, client); + const scriptProvider = new ClientScriptProvider(client); + const udtClient = new RgbppUdtClient(config, client, scriptProvider); setRgbppUdtClient(udtClient); }, [signer]); @@ -122,8 +132,9 @@ export default function IssueRGBPPXUdt() { const config: BtcAssetApiConfig = { url: process.env.NEXT_PUBLIC_BTC_ASSETS_API_URL!, - token: process.env.NEXT_PUBLIC_BTC_ASSETS_API_TOKEN!, - origin: process.env.NEXT_PUBLIC_BTC_ASSETS_API_ORIGIN!, + token: process.env.NEXT_PUBLIC_BTC_ASSETS_API_TOKEN, + origin: process.env.NEXT_PUBLIC_BTC_ASSETS_API_ORIGIN, + isMainnet: networkConfig.isMainnet, }; return createBrowserRgbppBtcWallet(signer, networkConfig, config); @@ -136,7 +147,7 @@ export default function IssueRGBPPXUdt() { networkConfig && !rgbppBtcWallet ) { - error( + errorRef.current( `Unsupported wallet type: ${signer.constructor.name}. Supported wallets: ${getSupportedWallets().join(", ")}`, ); } @@ -168,33 +179,33 @@ export default function IssueRGBPPXUdt() { setIsLoadingUtxos(false); }) .catch((err) => { - error("Failed to get UTXOs:", String(err)); + errorRef.current("Failed to get UTXOs:", String(err)); setUtxos([]); setSelectedUtxo(""); setIsLoadingUtxos(false); }); }, [rgbppBtcWallet]); - const [ckbRgbppUnlockSinger, setCkbRgbppUnlockSinger] = - useState(); + const [ckbRgbppUnlockSigner, setCkbRgbppUnlockSigner] = + useState(); useEffect(() => { if (!ckbClient || !rgbppBtcWallet || !rgbppUdtClient) { - setCkbRgbppUnlockSinger(undefined); + setCkbRgbppUnlockSigner(undefined); return; } let mounted = true; - rgbppBtcWallet.getAddress().then((address: string) => { + rgbppBtcWallet.getAddress().then(async (address: string) => { if (mounted) { - setCkbRgbppUnlockSinger( - new CkbRgbppUnlockSinger( + const scriptInfos = await rgbppUdtClient.getRgbppScriptInfos(); + setCkbRgbppUnlockSigner( + new CkbRgbppUnlockSigner({ ckbClient, - address, - rgbppBtcWallet, - rgbppBtcWallet, - rgbppUdtClient.getRgbppScriptInfos(), - ), + rgbppBtcAddress: address, + btcDataSource: rgbppBtcWallet, + scriptInfos: scriptInfos as Record, + }), ); } }); @@ -208,7 +219,7 @@ export default function IssueRGBPPXUdt() { !signer || !(signer instanceof SignerBtc) || !rgbppBtcWallet || - !ckbRgbppUnlockSinger || + !ckbRgbppUnlockSigner || !rgbppUdtClient || !selectedUtxo ) { @@ -228,7 +239,8 @@ export default function IssueRGBPPXUdt() { txId, index: parseInt(indexStr), }; - const rgbppLockScript = rgbppUdtClient.buildRgbppLockScript(utxoSeal); + const rgbppLockScript = + await rgbppUdtClient.buildRgbppLockScript(utxoSeal); const rgbppCellsGen = await signer.client.findCellsByLock(rgbppLockScript); @@ -270,16 +282,7 @@ export default function IssueRGBPPXUdt() { token: xudtToken, amount: issuanceAmount, rgbppLiveCells: rgbppIssuanceCells, - udtScriptInfo: { - name: ccc.KnownScript.XUdt, - script: await ccc.Script.fromKnownScript( - signer.client, - ccc.KnownScript.XUdt, - "", - ), - cellDep: (await signer.client.getKnownScript(ccc.KnownScript.XUdt)) - .cellDeps[0].cellDep, - }, + udtScriptInfo: await signer.client.getKnownScript(ccc.KnownScript.XUdt), }); setCurrentStep("signing-btc"); @@ -292,7 +295,7 @@ export default function IssueRGBPPXUdt() { rgbppUdtClient, btcChangeAddress: btcAccount, receiverBtcAddresses: [btcAccount], - feeRate: 10, + feeRate: 5, }); const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); @@ -307,7 +310,7 @@ export default function IssueRGBPPXUdt() { btcTxId, ); const rgbppSignedCkbTx = - await ckbRgbppUnlockSinger.signTransaction(ckbPartialTxInjected); + await ckbRgbppUnlockSigner.signTransaction(ckbPartialTxInjected); await rgbppSignedCkbTx.completeFeeBy(signer); setCurrentStep("waiting-ckb"); @@ -325,16 +328,15 @@ export default function IssueRGBPPXUdt() { } catch (err) { setCurrentStep("idle"); setStepMessage(""); - error("Transaction failed:", String(err)); + errorRef.current("Transaction failed:", String(err)); } }, [ signer, selectedUtxo, rgbppBtcWallet, rgbppUdtClient, - ckbRgbppUnlockSinger, + ckbRgbppUnlockSigner, log, - error, explorerTransaction, getBtcExplorerLink, ]); @@ -401,7 +403,7 @@ export default function IssueRGBPPXUdt() { } setIsLoadingUtxos(false); } catch (err) { - error("Failed to refresh UTXOs:", String(err)); + errorRef.current("Failed to refresh UTXOs:", String(err)); setIsLoadingUtxos(false); } }} 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]} /> +