diff --git a/.changeset/calm-doors-poke.md b/.changeset/calm-doors-poke.md new file mode 100644 index 000000000..9809e521f --- /dev/null +++ b/.changeset/calm-doors-poke.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): extract a universal `Codec` from `mol.Codec` + \ No newline at end of file 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/.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/.changeset/crazy-hairs-greet.md b/.changeset/crazy-hairs-greet.md new file mode 100644 index 000000000..5ba3e63e3 --- /dev/null +++ b/.changeset/crazy-hairs-greet.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(Epoch): transform `Epoch` into a class and add utilities diff --git a/.changeset/cuddly-lands-build.md b/.changeset/cuddly-lands-build.md new file mode 100644 index 000000000..b3a48766e --- /dev/null +++ b/.changeset/cuddly-lands-build.md @@ -0,0 +1,13 @@ +--- +"@ckb-ccc/core": minor +"@ckb-ccc/joy-id": patch +"@ckb-ccc/okx": patch +"@ckb-ccc/uni-sat": patch +"@ckb-ccc/utxo-global": patch +"@ckb-ccc/xverse": patch +--- + +feat(core): add BTC PSBT signing support + +- Add `SignerBtc.signPsbt()`, `signAndBroadcastPsbt()`, and `broadcastPsbt()` for signing and broadcasting PSBTs +- Add `SignPsbtOptions` and `InputToSign` for configuring PSBT signing diff --git a/.changeset/curvy-baboons-sip.md b/.changeset/curvy-baboons-sip.md new file mode 100644 index 000000000..9171ff9a1 --- /dev/null +++ b/.changeset/curvy-baboons-sip.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): optional `shouldAddInputs` for `Transaction.completeFee` + \ No newline at end of file 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/.changeset/fair-items-shout.md b/.changeset/fair-items-shout.md new file mode 100644 index 000000000..e0da4d468 --- /dev/null +++ b/.changeset/fair-items-shout.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): `mol.padding` for padding codec + \ No newline at end of file diff --git a/.changeset/fifty-parks-dress.md b/.changeset/fifty-parks-dress.md new file mode 100644 index 000000000..384359158 --- /dev/null +++ b/.changeset/fifty-parks-dress.md @@ -0,0 +1,7 @@ +--- +"@ckb-ccc/shell": minor +"@ckb-ccc/type-id": patch +--- + +feat(type-id): add type-id package for Type ID operations + 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/fruity-drinks-kick.md b/.changeset/fruity-drinks-kick.md new file mode 100644 index 000000000..abb409ec7 --- /dev/null +++ b/.changeset/fruity-drinks-kick.md @@ -0,0 +1,7 @@ +--- +"@ckb-ccc/shell": minor +"@ckb-ccc/did-ckb": patch +--- + +feat(did-ckb): add did-ckb package for basic did operations + \ No newline at end of file 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/.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/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/.changeset/poor-days-guess.md b/.changeset/poor-days-guess.md new file mode 100644 index 000000000..d6269460c --- /dev/null +++ b/.changeset/poor-days-guess.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): add known script did ckb + \ No newline at end of file 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/.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 new file mode 100644 index 000000000..9b7432cfd --- /dev/null +++ b/.changeset/six-steaks-grab.md @@ -0,0 +1,5 @@ +--- +"@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/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/.changeset/weak-adults-rhyme.md b/.changeset/weak-adults-rhyme.md new file mode 100644 index 000000000..54dca531b --- /dev/null +++ b/.changeset/weak-adults-rhyme.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): auto complete cell capacity if it's not enough + \ No newline at end of file diff --git a/.changeset/weak-otters-dance.md b/.changeset/weak-otters-dance.md new file mode 100644 index 000000000..af924ed4f --- /dev/null +++ b/.changeset/weak-otters-dance.md @@ -0,0 +1,6 @@ +--- +"@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/config/prettier.config.mjs b/config/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/config/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/ccc/CHANGELOG.md b/packages/ccc/CHANGELOG.md index 3a11e04b0..ea1376989 100644 --- a/packages/ccc/CHANGELOG.md +++ b/packages/ccc/CHANGELOG.md @@ -1,5 +1,19 @@ # @ckb-ccc/ccc +## 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 diff --git a/packages/ccc/package.json b/packages/ccc/package.json index 24230193a..39d08c63e 100644 --- a/packages/ccc/package.json +++ b/packages/ccc/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/ccc", - "version": "1.1.23", + "version": "1.1.24", "description": "CCC - CKBer's Codebase. Common Chains Connector.", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/ccc/prettier.config.mjs b/packages/ccc/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/ccc/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/ckb-ccc/CHANGELOG.md b/packages/ckb-ccc/CHANGELOG.md index 44b96af02..a11f847a6 100644 --- a/packages/ckb-ccc/CHANGELOG.md +++ b/packages/ckb-ccc/CHANGELOG.md @@ -1,5 +1,11 @@ # ckb-ccc +## 1.0.32 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.24 + ## 1.0.31 ### Patch Changes diff --git a/packages/ckb-ccc/package.json b/packages/ckb-ccc/package.json index e4f3efdad..445f16317 100644 --- a/packages/ckb-ccc/package.json +++ b/packages/ckb-ccc/package.json @@ -1,6 +1,6 @@ { "name": "ckb-ccc", - "version": "1.0.31", + "version": "1.0.32", "description": "CCC - CKBer's Codebase. Common Chains Connector.", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/ckb-ccc/prettier.config.mjs b/packages/ckb-ccc/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/ckb-ccc/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/connector-react/CHANGELOG.md b/packages/connector-react/CHANGELOG.md index 778ccab9f..41b3ee8a9 100644 --- a/packages/connector-react/CHANGELOG.md +++ b/packages/connector-react/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/connector-react +## 1.0.33 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/connector@1.0.32 + ## 1.0.32 ### Patch Changes diff --git a/packages/connector-react/package.json b/packages/connector-react/package.json index f1f639d2f..541131c74 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.32", + "version": "1.0.33", "description": "CCC - CKBer's Codebase. Common Chains Connector UI Component for React", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/connector-react/prettier.config.mjs b/packages/connector-react/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/connector-react/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/connector/CHANGELOG.md b/packages/connector/CHANGELOG.md index 488182c9c..1e0ef5d2a 100644 --- a/packages/connector/CHANGELOG.md +++ b/packages/connector/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/connector +## 1.0.32 +### Patch Changes + +- Updated dependencies []: + - @ckb-ccc/ccc@1.1.24 + ## 1.0.31 ### Patch Changes diff --git a/packages/connector/package.json b/packages/connector/package.json index 53eaa4cca..5ed95676f 100644 --- a/packages/connector/package.json +++ b/packages/connector/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/connector", - "version": "1.0.31", + "version": "1.0.32", "description": "CCC - CKBer's Codebase. Common Chains Connector UI", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/connector/prettier.config.mjs b/packages/connector/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/connector/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index dfac2f057..590342d9e 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,12 @@ # @ckb-ccc/core +## 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 diff --git a/packages/core/package.json b/packages/core/package.json index 5930cc344..56c3b8cd4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/core", - "version": "1.12.3", + "version": "1.12.4", "description": "Core of CCC - CKBer's Codebase", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/core/prettier.config.mjs b/packages/core/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/core/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/core/src/barrel.ts b/packages/core/src/barrel.ts index a92855072..85d942322 100644 --- a/packages/core/src/barrel.ts +++ b/packages/core/src/barrel.ts @@ -2,6 +2,7 @@ export * from "./address/index.js"; export * from "./bytes/index.js"; export * from "./ckb/index.js"; export * from "./client/index.js"; +export * from "./codec/index.js"; export * from "./fixedPoint/index.js"; export * from "./hasher/index.js"; export * from "./hex/index.js"; diff --git a/packages/core/src/ckb/epoch.test.ts b/packages/core/src/ckb/epoch.test.ts new file mode 100644 index 000000000..e969db408 --- /dev/null +++ b/packages/core/src/ckb/epoch.test.ts @@ -0,0 +1,241 @@ +import { describe, expect, it } from "vitest"; +import type { ClientBlockHeader } from "../client/index.js"; +import { Epoch, epochFrom, epochFromHex, epochToHex } from "./epoch"; + +describe("Epoch", () => { + it("constructs from tuple and object via from()", () => { + const a = Epoch.from([1n, 2n, 3n]); + expect(a.integer).toBe(1n); + expect(a.numerator).toBe(2n); + expect(a.denominator).toBe(3n); + + const b = Epoch.from({ integer: 4n, numerator: 5n, denominator: 6n }); + expect(b.integer).toBe(4n); + expect(b.numerator).toBe(5n); + expect(b.denominator).toBe(6n); + + const c = new Epoch(7n, 8n, 9n); + expect(Epoch.from(c)).toBe(c); + }); + + it("packs and unpacks numeric layout (toNum/fromNum) and hex conversion", () => { + const e = new Epoch(0x010203n, 0x0405n, 0x0607n); // use values within bit widths + const packed1 = e.toNum(); + // integer in lower 24 bits, numerator next 16, denominator next 16 + expect(packed1 & 0xffffffn).toBe(0x010203n); + expect((packed1 >> 24n) & 0xffffn).toBe(0x0405n); + expect((packed1 >> 40n) & 0xffffn).toBe(0x0607n); + + const hex = e.toPackedHex(); + expect(typeof hex).toBe("string"); + expect(hex.startsWith("0x")).toBe(true); + + // round-trip + const decoded = Epoch.fromNum(packed1); + expect(decoded.integer).toBe(e.integer); + expect(decoded.numerator).toBe(e.numerator); + expect(decoded.denominator).toBe(e.denominator); + }); + + it("throws when packing negative components with toNum", () => { + const e = new Epoch(-1n, 0n, 1n); + expect(() => e.toNum()).toThrow(); + + const e2 = new Epoch(0n, -1n, 1n); + expect(() => e2.toNum()).toThrow(); + + const e3 = new Epoch(0n, 0n, -1n); + expect(() => e3.toNum()).toThrow(); + }); + + it("throws when packing components too big with toNum", () => { + const e = new Epoch(1n << 24n, 1n, 1n); // integer = 16777215 (24-bit limit + 1) + expect(() => e.toNum()).toThrow(); + + const e2 = new Epoch(1n, 1n << 16n, 1n); // numerator = 65536 (16-bit limit + 1) + expect(() => e2.toNum()).toThrow(); + + const e3 = new Epoch(1n, 1n, 1n << 16n); // denominator = 65536 (16-bit limit + 1) + expect(() => e3.toNum()).toThrow(); + }); + + it("normalizeBase fixes zero or negative denominators", () => { + const a = new Epoch(1n, 2n, 0n).normalizeBase(); + expect(a.denominator).toBe(1n); + expect(a.numerator).toBe(0n); + + const b = new Epoch(1n, 2n, -3n).normalizeBase(); + expect(b.denominator).toBe(3n); + expect(b.numerator).toBe(-2n); + }); + + it("normalizeCanonical reduces fractions and carries/borrows correctly", () => { + // reduction by gcd: 2/4 -> 1/2 + const a = new Epoch(1n, 2n, 4n).normalizeCanonical(); + expect(a.integer).toBe(1n); + expect(a.numerator).toBe(1n); + expect(a.denominator).toBe(2n); + + // carry: 5/2 -> +2 integer, remainder 1/2 + const b = new Epoch(0n, 5n, 2n).normalizeCanonical(); + expect(b.integer).toBe(2n); + expect(b.numerator).toBe(1n); + expect(b.denominator).toBe(2n); + + // borrow when numerator negative + const c = new Epoch(5n, -1n, 2n).normalizeCanonical(); + // -1/2 borrowed: integer 4, numerator becomes 1/2 + expect(c.integer).toBe(4n); + expect(c.numerator).toBe(1n); + expect(c.denominator).toBe(2n); + }); + + it("clone returns a deep copy", () => { + const e = new Epoch(1n, 1n, 1n); + const c = e.clone(); + expect(c).not.toBe(e); + expect(c.integer).toBe(e.integer); + expect(c.numerator).toBe(e.numerator); + expect(c.denominator).toBe(e.denominator); + }); + + it("Genesis and OneNervosDaoCycle helpers", () => { + const g = Epoch.Genesis; + expect(g.integer).toBe(0n); + expect(g.numerator).toBe(0n); + expect(g.denominator).toBe(0n); + + const o = Epoch.OneNervosDaoCycle; + expect(o.integer).toBe(180n); + expect(o.numerator).toBe(0n); + expect(o.denominator).toBe(1n); + }); + + it("comparison operations and compare()", () => { + const a = new Epoch(1n, 0n, 1n); + const b = new Epoch(1n, 1n, 2n); + const c = new Epoch(2n, 0n, 1n); + + expect(a.compare(b)).toBe(-1); + expect(b.compare(a)).toBe(1); + expect(a.compare(a)).toBe(0); + + expect(a.lt(b)).toBe(true); + expect(b.le(b)).toBe(true); + expect(b.eq(new Epoch(1n, 2n, 4n))).toBe(true); // 1 + 1/2 == 1 + 2/4 + expect(c.gt(b)).toBe(true); + expect(c.ge(b)).toBe(true); + }); + + it("add and sub arithmetic with differing denominators", () => { + const a = new Epoch(1n, 1n, 2n); // 1.5 + const b = new Epoch(2n, 1n, 3n); // 2 + 1/3 + const s = a.add(b); + // compute expected: whole = 3, fractional = 1/2 + 1/3 = 5/6 -> 3 + 5/6 + expect(s.integer).toBe(3n); + expect(s.numerator).toBe(5n); + expect(s.denominator).toBe(6n); + + const sub = s.sub(new Epoch(1n, 5n, 6n)); + expect(sub.integer).toBe(2n); + expect(sub.numerator).toBe(0n); + expect(sub.denominator).toBe(1n); + }); + + it("toUnix estimates timestamp using a reference header", () => { + const refEpoch = new Epoch(1n, 0n, 1n); + // Provide a minimal shaped header for toUnix without using `any`. + const refHeader = { + epoch: refEpoch, + timestamp: 1000n, + }; + + // target epoch is 2 + 1/2 + const target = new Epoch(2n, 1n, 2n); + const delta = target.sub(refEpoch); // should be 1 + 1/2 + + // Test default behavior (4 hours) + const expectedDefault = + refHeader.timestamp + + DEFAULT_EPOCH_IN_MILLISECONDS * delta.integer + + (DEFAULT_EPOCH_IN_MILLISECONDS * delta.numerator) / delta.denominator; + + expect(target.toUnix(refHeader)).toBe(expectedDefault); + + // Test custom epoch duration (10 minutes) + const customEpochMs = 10n * 60n * 1000n; + const expectedCustom = + refHeader.timestamp + + customEpochMs * delta.integer + + (customEpochMs * delta.numerator) / delta.denominator; + + expect(target.toUnix(refHeader, customEpochMs)).toBe(expectedCustom); + }); + + it("toUnix accepts full ClientBlockHeader", () => { + const refEpoch = new Epoch(1n, 0n, 1n); + // Simulate a full ClientBlockHeader object + const fullHeader: ClientBlockHeader = { + epoch: refEpoch, + timestamp: 1000n, + compactTarget: 0n, + hash: "0x1234567890abcdef", + number: 100n, + parentHash: "0xabcdef1234567890", + version: 0n, + nonce: 0n, + dao: { c: 0n, ar: 0n, s: 0n, u: 0n }, + extraHash: "0x0000000000000000", + proposalsHash: "0x0000000000000000", + transactionsRoot: "0x0000000000000000", + }; + + const target = new Epoch(2n, 1n, 2n); + const delta = target.sub(fullHeader.epoch); + const expected = + fullHeader.timestamp + + DEFAULT_EPOCH_IN_MILLISECONDS * delta.integer + + (DEFAULT_EPOCH_IN_MILLISECONDS * delta.numerator) / delta.denominator; + + // Full ClientBlockHeader should work due to structural typing + expect(target.toUnix(fullHeader)).toBe(expected); + }); + + it("toUnix accepts object literal with exact required properties", () => { + const target = new Epoch(2n, 1n, 2n); + const minimalRef = { + epoch: new Epoch(1n, 0n, 1n), + timestamp: 1000n, + }; + + const delta = target.sub(minimalRef.epoch); + const expected = + minimalRef.timestamp + + DEFAULT_EPOCH_IN_MILLISECONDS * delta.integer + + (DEFAULT_EPOCH_IN_MILLISECONDS * delta.numerator) / delta.denominator; + + expect(target.toUnix(minimalRef)).toBe(expected); + }); + + it("deprecated helpers epochFrom / epochFromHex / epochToHex", () => { + const e = new Epoch(3n, 4n, 5n); + expect(epochFrom(e)).toBe(e); + + const hex = epochToHex(e); + expect(typeof hex).toBe("string"); + expect(hex.startsWith("0x")).toBe(true); + + const decoded = epochFromHex(hex); + expect(decoded.integer).toBe(e.integer); + expect(decoded.numerator).toBe(e.numerator); + expect(decoded.denominator).toBe(e.denominator); + }); +}); + +/** + * DEFAULT_EPOCH_IN_MILLISECONDS + * + * Constant duration of a single standard ideal epoch expressed in milliseconds. + * Defined as 4 hours = 4 * 60 * 60 * 1000 ms. + */ +const DEFAULT_EPOCH_IN_MILLISECONDS = 4n * 60n * 60n * 1000n; diff --git a/packages/core/src/ckb/epoch.ts b/packages/core/src/ckb/epoch.ts new file mode 100644 index 000000000..8ee254ad5 --- /dev/null +++ b/packages/core/src/ckb/epoch.ts @@ -0,0 +1,489 @@ +import type { ClientBlockHeader } from "../client/clientTypes.js"; +import { codec, codecPadding, codecUint, Entity } from "../codec/index.js"; +import { Zero } from "../fixedPoint/index.js"; +import { type Hex, type HexLike } from "../hex/index.js"; +import { mol } from "../molecule/index.js"; +import { numFrom, NumLike, numToHex, type Num } from "../num/index.js"; +import { gcd } from "../utils/index.js"; + +/** + * EpochLike + * + * Union type that represents any allowed input shapes that can be converted + * into an Epoch instance. + * + * Accepted shapes: + * - Tuple: [integer, numerator, denominator] where each element is NumLike + * - Object: { integer, numerator, denominator } where each field is NumLike + * - Packed numeric form: Num (bigint) or Hex (RPC-style packed hex) + * + * Notes: + * - When constructing an Epoch from a Num or Hex the packed numeric representation + * encodes integer (24 bits), numerator (16 bits) and denominator (16 bits). + * - Use Epoch.from() to convert any EpochLike into an Epoch instance. + * + * @example + * // From tuple + * Epoch.from([1n, 0n, 1n]); + */ +export type EpochLike = + | [NumLike, NumLike, NumLike] + | { + integer: NumLike; + numerator: NumLike; + denominator: NumLike; + } + | Num + | Hex; + +/** + * Epoch + * + * Represents a blockchain epoch consisting of a whole integer part and an + * optional fractional part represented as numerator/denominator. + * + * Behavior highlights: + * - Internally stores values as Num (bigint). + * - Provides normalization routines to canonicalize the fractional part: + * - normalizeBase(): fixes zero/negative denominators + * - normalizeCanonical(): reduces fraction, borrows/carries whole units + * - Supports arithmetic (add/sub), comparison and conversion utilities. + * + * @example + * const e = new Epoch(1n, 1n, 2n); // 1 + 1/2 + * + * @remarks + * This class is primarily a thin value-object; operations return new Epoch instances. + */ +@codec( + mol.struct({ + padding: codecPadding(1), + denominator: codecUint(2), + numerator: codecUint(2), + integer: codecUint(3), + }), +) +export class Epoch extends Entity.Base() { + /** + * Construct a new Epoch instance. + * + * @param integer - Whole epoch units (Num/bigint) + * @param numerator - Fractional numerator (Num). + * @param denominator - Fractional denominator (Num). + */ + public constructor( + public readonly integer: Num, + public readonly numerator: Num, + public readonly denominator: Num, + ) { + super(); + } + + /** + * Normalize simpler base invariants: + * - If denominator === 0, set denominator to 1 and numerator to 0 for arithmetic convenience. + * - If denominator is negative flip signs of numerator and denominator to keep denominator positive. + * + * This is a minimal correction used before arithmetic or canonical normalization. + * + * @returns New Epoch with denominator corrected (but fraction not reduced). + */ + normalizeBase(): Epoch { + if (this.denominator === Zero) { + return new Epoch(this.integer, Zero, numFrom(1)); + } + + if (this.denominator < Zero) { + return new Epoch(this.integer, -this.numerator, -this.denominator); + } + + return this; + } + + /** + * Perform full canonical normalization of the epoch value. + * + * Steps: + * 1. Apply base normalization (normalizeBase). + * 2. If numerator is negative, borrow whole denominator(s) from the integer part + * so numerator becomes non-negative. This ensures 0 <= numerator < denominator whenever possible. + * 3. Reduce numerator/denominator by their greatest common divisor (gcd). + * 4. Carry any whole units from the reduced numerator into the integer part. + * 5. Ensure numerator is the strict remainder (numerator < denominator). + * + * @returns Canonicalized Epoch with a non-negative, reduced fractional part and integer adjusted accordingly. + */ + normalizeCanonical(): Epoch { + let { integer, numerator, denominator } = this.normalizeBase(); + + // If numerator is negative, borrow enough whole denominators from integer so numerator >= 0. + if (numerator < Zero) { + // n is the minimal non-negative integer such that numerator + n * denominator >= 0 + const n = (-numerator + denominator - 1n) / denominator; + integer -= n; + numerator += denominator * n; + } + + // Reduce the fractional part to lowest terms to keep canonical form and avoid unnecessarily large multiples. + const g = gcd(numerator, denominator); + numerator /= g; + denominator /= g; + + // Move any full units contained in the fraction into integer (e.g., 5/2 => +2 integer, remainder 1/2). + integer += numerator / denominator; + + // Remainder numerator after removing whole units; ensures numerator < denominator. + numerator %= denominator; + + return new Epoch(integer, numerator, denominator); + } + + /** + * Backwards-compatible array-style index 0 referencing the whole epoch integer. + * + * @returns integer portion (Num) + * @deprecated Use `.integer` property instead. + */ + get 0(): Num { + return this.integer; + } + + /** + * Backwards-compatible array-style index 1 referencing the epoch fractional numerator. + * + * @returns numerator portion (Num) + * @deprecated Use `.numerator` property instead. + */ + get 1(): Num { + return this.numerator; + } + + /** + * Backwards-compatible array-style index 2 referencing the epoch fractional denominator. + * + * @returns denominator portion (Num) + * @deprecated Use `.denominator` property instead. + */ + get 2(): Num { + return this.denominator; + } + + /** + * Convert this Epoch into its RPC-style packed numeric representation (Num). + * + * Packing layout (little-endian style fields): + * - integer: lower 24 bits + * - numerator: next 16 bits + * - denominator: next 16 bits + * + * Throws if any component is negative since packed representation assumes non-negative components. + * + * @throws {Error} If integer, numerator or denominator are negative. + * @throws {Error} If integer, numerator or denominator overflow the packing limits. + * @returns Packed numeric representation (Num) suitable for RPC packing. + */ + toNum(): Num { + if ( + this.integer < Zero || + this.numerator < Zero || + this.denominator < Zero + ) { + throw Error("Negative values in Epoch to Num conversion"); + } + + if ( + this.integer >= numFrom("0x1000000") || // 24-bit limit + this.numerator >= numFrom("0x10000") || // 16-bit limit + this.denominator >= numFrom("0x10000") // 16-bit limit + ) { + throw Error( + "Integer must be < 2^24, numerator and denominator must be < 2^16", + ); + } + + return ( + this.integer + + (this.numerator << numFrom(24)) + + (this.denominator << numFrom(40)) + ); + } + + /** + * Convert epoch to hex string representation of the RPC-style packed numeric form. + * + * Returns the same representation used by CKB RPC responses where the + * packed numeric bytes may be trimmed of leading zeros, see {@link numToHex} + * + * @returns Hex string corresponding to the packed epoch. + */ + toPackedHex(): Hex { + return numToHex(this.toNum()); + } + + /** + * Construct an Epoch by unpacking a RPC-style packed numeric form. + * + * @param v - NumLike packed epoch (like Num and Hex) + * @returns Epoch whose integer, numerator and denominator are extracted from the packed layout. + */ + static fromNum(v: NumLike): Epoch { + const num = numFrom(v); + + return new Epoch( + num & numFrom("0xffffff"), + (num >> numFrom(24)) & numFrom("0xffff"), + (num >> numFrom(40)) & numFrom("0xffff"), + ); + } + + /** + * Create an Epoch from an EpochLike value. + * + * Accepts: + * - an Epoch instance (returned as-is) + * - an array [integer, numerator, denominator] where each element is NumLike + * - an object { integer, numerator, denominator } where each field is NumLike + * - a packed numeric-like value handled by fromNum + * + * All numeric-like inputs are converted with numFrom() to produce internal Num values. + * + * @param e - Value convertible to Epoch + * @returns Epoch instance + */ + static override from(e: EpochLike): Epoch { + if (e instanceof Epoch) { + return e; + } + + if (Array.isArray(e)) { + return new Epoch(numFrom(e[0]), numFrom(e[1]), numFrom(e[2])); + } + + if (typeof e === "object") { + return new Epoch( + numFrom(e.integer), + numFrom(e.numerator), + numFrom(e.denominator), + ); + } + + return Epoch.fromNum(e); + } + + /** + * Return a deep copy of this Epoch. + * + * @returns New Epoch instance with identical components. + */ + override clone(): Epoch { + return new Epoch(this.integer, this.numerator, this.denominator); + } + + /** + * Return the genesis epoch. + * + * Note: for historical reasons the genesis epoch is represented with all-zero + * fields, no other epoch instance should use a zero denominator. + * + * @returns Epoch with integer = 0, numerator = 0, denominator = 0. + */ + static get Genesis(): Epoch { + return new Epoch(Zero, Zero, Zero); + } + + /** + * Return an Epoch representing one Nervos DAO cycle (180 epochs exactly). + * + * @returns Epoch equal to 180 with denominator set to 1 to represent an exact whole unit. + */ + static get OneNervosDaoCycle(): Epoch { + return new Epoch(numFrom(180), Zero, numFrom(1)); + } + + /** + * Compare this epoch to another EpochLike. + * + * The comparison computes scaled integer values so fractions are compared without precision loss: + * scaled = (integer * denominator + numerator) * other.denominator + * + * Special-case: identical object references return equality immediately. + * + * @param other - Epoch-like value to compare against. + * @returns 1 if this > other, 0 if equal, -1 if this < other. + * + * @example + * epochA.compare(epochB); // -1|0|1 + */ + compare(other: EpochLike): 1 | 0 | -1 { + if (this === other) { + return 0; + } + + const t = this.normalizeBase(); + const o = Epoch.from(other).normalizeBase(); + + // Compute scaled representations to compare fractions without floating-point arithmetic. + const a = (t.integer * t.denominator + t.numerator) * o.denominator; + const b = (o.integer * o.denominator + o.numerator) * t.denominator; + + return a > b ? 1 : a < b ? -1 : 0; + } + + /** + * Check whether this epoch is less than another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this < other. + */ + lt(other: EpochLike): boolean { + return this.compare(other) < 0; + } + + /** + * Check whether this epoch is less than or equal to another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this <= other. + */ + le(other: EpochLike): boolean { + return this.compare(other) <= 0; + } + + /** + * Check whether this epoch equals another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if equal. + */ + eq(other: EpochLike): boolean { + return this.compare(other) === 0; + } + + /** + * Check whether this epoch is greater than or equal to another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this >= other. + */ + ge(other: EpochLike): boolean { + return this.compare(other) >= 0; + } + + /** + * Check whether this epoch is greater than another EpochLike. + * + * @param other - EpochLike to compare against. + * @returns true if this > other. + */ + gt(other: EpochLike): boolean { + return this.compare(other) > 0; + } + + /** + * Add another EpochLike to this epoch and return the normalized result. + * + * Rules and edge-cases: + * - Whole parts are added directly; fractional parts are aligned to a common denominator and added. + * - Final result is canonicalized to reduce the fraction and carry any overflow to the integer part. + * + * @param other - Epoch-like value to add. + * @returns Normalized Epoch representing the sum. + */ + add(other: EpochLike): Epoch { + const t = this.normalizeBase(); + const o = Epoch.from(other).normalizeBase(); + + // Sum whole integer parts. + const integer = t.integer + o.integer; + let numerator: Num; + let denominator: Num; + + // Align denominators if they differ; use multiplication to obtain a common denominator. + if (t.denominator !== o.denominator) { + // Numerators & Denominators are generally small (<= 2000n); multiplication produces a safe common denominator. + numerator = t.numerator * o.denominator + o.numerator * t.denominator; + denominator = t.denominator * o.denominator; + } else { + numerator = t.numerator + o.numerator; + denominator = t.denominator; + } + + // Normalize to reduce fraction and carry whole units into integer. + return new Epoch(integer, numerator, denominator).normalizeCanonical(); + } + + /** + * Subtract an EpochLike from this epoch and return the normalized result. + * + * Implementation notes: + * - Delegates to add by negating the other epoch's integer and numerator while preserving denominator. + * - normalizeCanonical will handle negative numerators by borrowing from integer as necessary. + * + * @param other - Epoch-like value to subtract. + * @returns Normalized Epoch representing this - other. + */ + sub(other: EpochLike): Epoch { + const { integer, numerator, denominator } = Epoch.from(other); + return this.add(new Epoch(-integer, -numerator, denominator)); + } + + /** + * Convert this epoch to an estimated Unix timestamp in milliseconds using a reference header. + * + * Note: This is an estimation that assumes a constant epoch duration. + * + * @param reference - Object providing `epoch` (Epoch) and `timestamp` (Num) fields, such as a ClientBlockHeader. + * @param epochInMilliseconds - Duration of a single epoch in milliseconds. Defaults to 4 hours. + * @returns Estimated Unix timestamp in milliseconds as bigint. + */ + toUnix( + reference: Pick, + epochInMilliseconds: Num = numFrom(4 * 60 * 60 * 1000), + ): bigint { + // Compute relative epoch difference against the reference header. + const { integer, numerator, denominator } = this.sub(reference.epoch); + + // Add whole epoch duration and fractional epoch duration to the reference timestamp. + return ( + reference.timestamp + + epochInMilliseconds * integer + + (epochInMilliseconds * numerator) / denominator + ); + } +} + +/** + * epochFrom + * + * @deprecated prefer using Epoch.from() directly. + * + * @param epochLike - Epoch-like value to convert. + * @returns Epoch instance corresponding to the input. + */ +export function epochFrom(epochLike: EpochLike): Epoch { + return Epoch.from(epochLike); +} + +/** + * epochFromHex + * + * @deprecated use Epoch.fromNum() with numeric input instead. + * + * @param hex - Hex-like or numeric-like value encoding a packed epoch. + * @returns Decoded Epoch instance. + */ +export function epochFromHex(hex: HexLike): Epoch { + return Epoch.fromNum(hex); +} + +/** + * epochToHex + * + * @deprecated use Epoch.from(epochLike).toPackedHex() instead. + * + * @param epochLike - Value convertible to an Epoch (object, tuple or Epoch). + * @returns Hex string representing the packed epoch encoding. + */ +export function epochToHex(epochLike: EpochLike): Hex { + return Epoch.from(epochLike).toPackedHex(); +} diff --git a/packages/core/src/ckb/index.ts b/packages/core/src/ckb/index.ts index 7d20b37d0..198c09299 100644 --- a/packages/core/src/ckb/index.ts +++ b/packages/core/src/ckb/index.ts @@ -1,3 +1,4 @@ +export * from "./epoch.js"; export * from "./hash.js"; export * from "./script.js"; export * from "./transaction.js"; diff --git a/packages/core/src/ckb/script.ts b/packages/core/src/ckb/script.ts index 1de3c7ac0..21405c8a5 100644 --- a/packages/core/src/ckb/script.ts +++ b/packages/core/src/ckb/script.ts @@ -1,6 +1,7 @@ import { Bytes, BytesLike, bytesFrom } from "../bytes/index.js"; import type { Client } from "../client/index.js"; import { KnownScript } from "../client/knownScript.js"; +import { Codec, Entity, codec } from "../codec/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; import { mol } from "../molecule/index.js"; import { @@ -9,7 +10,7 @@ import { NUM_TO_HASH_TYPE, } from "./script.advanced.js"; -export const HashTypeCodec: mol.Codec = mol.Codec.from({ +export const HashTypeCodec: Codec = Codec.from({ byteLength: 1, encode: hashTypeToBytes, decode: hashTypeFromBytes, @@ -108,14 +109,14 @@ export type ScriptLike = { /** * @public */ -@mol.codec( +@codec( mol.table({ codeHash: mol.Byte32, hashType: HashTypeCodec, args: mol.Bytes, }), ) -export class Script extends mol.Entity.Base() { +export class Script extends Entity.Base() { /** * Creates an instance of Script. * diff --git a/packages/core/src/ckb/transaction.test.ts b/packages/core/src/ckb/transaction.test.ts index 6271517d4..c6a3a9d46 100644 --- a/packages/core/src/ckb/transaction.test.ts +++ b/packages/core/src/ckb/transaction.test.ts @@ -385,6 +385,9 @@ describe("Transaction", () => { { previousOutput: mockCapacityCells[0].outPoint, }, + { + previousOutput: mockCapacityCells[1].outPoint, + }, ], outputs: [ { @@ -647,22 +650,13 @@ 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 not modify capacity when data is not provided", () => { const cellOutput = ccc.CellOutput.from({ - capacity: 0n, + capacity: 100n, lock, }); - expect(cellOutput.capacity).toBe(0n); + expect(cellOutput.capacity).toBe(100n); }); it("should calculate capacity automatically when capacity is 0", () => { @@ -679,6 +673,20 @@ describe("Transaction", () => { expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); }); + it("should calculate capacity automatically when capacity is less than min requirement", () => { + const outputData = "0x1234"; // 2 bytes + const cellOutput = ccc.CellOutput.from( + { + capacity: 1000n, + 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( @@ -732,9 +740,9 @@ describe("Transaction", () => { expect(cellOutput.capacity).toBe(ccc.fixedPointFrom(expectedCapacity)); }); - it("should not auto-calculate when capacity is explicitly provided even with outputData", () => { + it("should not auto-calculate when capacity is enough even with outputData", () => { const outputData = "0x1234"; // 2 bytes - const explicitCapacity = 5000n; + const explicitCapacity = ccc.fixedPointFrom(100); const cellOutput = ccc.CellOutput.from( { capacity: explicitCapacity, @@ -745,21 +753,6 @@ describe("Transaction", () => { 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", () => { @@ -798,7 +791,7 @@ describe("Transaction", () => { it("should handle mixed explicit and automatic capacity calculation", () => { const outputsData = ["0x12", "0x3456"]; - const explicitCapacity = 5000n; + const explicitCapacity = ccc.fixedPointFrom(100); const tx = ccc.Transaction.from({ outputs: [ { @@ -951,7 +944,7 @@ describe("Transaction", () => { it("should add output with explicit capacity", () => { const tx = ccc.Transaction.default(); const outputData = "0x12"; - const explicitCapacity = 10000n; + const explicitCapacity = ccc.fixedPointFrom(100); tx.addOutput( { @@ -1142,7 +1135,7 @@ describe("Transaction", () => { it("should calculate capacityFree correctly", () => { const outputData = "0x1234"; - const explicitCapacity = 1000n; + const explicitCapacity = ccc.fixedPointFrom(100); const cell = ccc.Cell.from({ outPoint: { txHash: "0x" + "0".repeat(64), diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 35fc4a727..2cb60e906 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -7,6 +7,7 @@ import { type ClientBlockHeaderLike, } from "../client/index.js"; import { KnownScript } from "../client/knownScript.js"; +import { Codec, Entity, codec } from "../codec/index.js"; import { Zero, fixedPointFrom } from "../fixedPoint/index.js"; import { Hasher, HasherCkb, hashCkb } from "../hasher/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; @@ -16,11 +17,13 @@ import { NumLike, numFrom, numFromBytes, + numMax, numToBytes, numToHex, } from "../num/index.js"; import type { Signer } from "../signer/index.js"; import { apply, reduceAsync } from "../utils/index.js"; +import { Epoch } from "./epoch.js"; import { Script, ScriptLike, ScriptOpt } from "./script.js"; import { DEP_TYPE_TO_NUM, NUM_TO_DEP_TYPE } from "./transaction.advanced.js"; import { @@ -29,7 +32,7 @@ import { } from "./transactionErrors.js"; import type { LumosTransactionSkeletonType } from "./transactionLumos.js"; -export const DepTypeCodec: mol.Codec = mol.Codec.from({ +export const DepTypeCodec: Codec = Codec.from({ byteLength: 1, encode: depTypeToBytes, decode: depTypeFromBytes, @@ -124,13 +127,13 @@ export type OutPointLike = { /** * @public */ -@mol.codec( +@codec( mol.struct({ txHash: mol.Byte32, index: mol.Uint32, }), ) -export class OutPoint extends mol.Entity.Base() { +export class OutPoint extends Entity.Base() { /** * Creates an instance of OutPoint. * @@ -205,14 +208,14 @@ export type CellOutputLike = { /** * @public */ -@mol.codec( +@codec( mol.table({ capacity: mol.Uint64, lock: Script, type: ScriptOpt, }), ) -export class CellOutput extends mol.Entity.Base() { +export class CellOutput extends Entity.Base() { /** * Creates an instance of CellOutput. * @@ -273,9 +276,10 @@ export class CellOutput extends mol.Entity.Base() { ); })(); - if (output.capacity === Zero && outputData != null) { - output.capacity = fixedPointFrom( - output.occupiedSize + bytesFrom(outputData).length, + if (outputData != null) { + output.capacity = numMax( + output.capacity, + fixedPointFrom(output.occupiedSize + bytesFrom(outputData).length), ); } @@ -650,45 +654,6 @@ export class Cell extends CellAny { } } -/** - * @public - */ -export type EpochLike = [NumLike, NumLike, NumLike]; -/** - * @public - */ -export type Epoch = [Num, Num, Num]; -/** - * @public - */ -export function epochFrom(epochLike: EpochLike): Epoch { - return [numFrom(epochLike[0]), numFrom(epochLike[1]), numFrom(epochLike[2])]; -} -/** - * @public - */ -export function epochFromHex(hex: HexLike): Epoch { - const num = numFrom(hexFrom(hex)); - - return [ - num & numFrom("0xffffff"), - (num >> numFrom(24)) & numFrom("0xffff"), - (num >> numFrom(40)) & numFrom("0xffff"), - ]; -} -/** - * @public - */ -export function epochToHex(epochLike: EpochLike): Hex { - const epoch = epochFrom(epochLike); - - return numToHex( - numFrom(epoch[0]) + - (numFrom(epoch[1]) << numFrom(24)) + - (numFrom(epoch[2]) << numFrom(40)), - ); -} - /** * @public */ @@ -702,10 +667,10 @@ export type SinceLike = /** * @public */ -@mol.codec( +@codec( mol.Uint64.mapIn((encodable: SinceLike) => Since.from(encodable).toNum()), ) -export class Since extends mol.Entity.Base() { +export class Since extends Entity.Base() { /** * Creates an instance of Since. * @@ -823,7 +788,7 @@ export type CellInputLike = ( /** * @public */ -@mol.codec( +@codec( mol .struct({ since: Since, @@ -831,7 +796,7 @@ export type CellInputLike = ( }) .mapIn((encodable: CellInputLike) => CellInput.from(encodable)), ) -export class CellInput extends mol.Entity.Base() { +export class CellInput extends Entity.Base() { /** * Creates an instance of CellInput. * @@ -960,13 +925,13 @@ export type CellDepLike = { /** * @public */ -@mol.codec( +@codec( mol.struct({ outPoint: OutPoint, depType: DepTypeCodec, }), ) -export class CellDep extends mol.Entity.Base() { +export class CellDep extends Entity.Base() { /** * Creates an instance of CellDep. * @@ -1034,17 +999,14 @@ export type WitnessArgsLike = { /** * @public */ -@mol.codec( +@codec( mol.table({ lock: mol.BytesOpt, inputType: mol.BytesOpt, outputType: mol.BytesOpt, }), ) -export class WitnessArgs extends mol.Entity.Base< - WitnessArgsLike, - WitnessArgs ->() { +export class WitnessArgs extends Entity.Base() { /** * Creates an instance of WitnessArgs. * @@ -1127,7 +1089,7 @@ export type TransactionLike = { /** * @public */ -@mol.codec( +@codec( mol .table({ raw: RawTransaction, @@ -1142,10 +1104,7 @@ export type TransactionLike = { }) .mapOut((tx) => Transaction.from({ ...tx.raw, witnesses: tx.witnesses })), ) -export class Transaction extends mol.Entity.Base< - TransactionLike, - Transaction ->() { +export class Transaction extends Entity.Base() { /** * Creates an instance of Transaction. * @@ -1890,7 +1849,7 @@ export class Transaction extends mol.Entity.Base< return reduceAsync( this.inputs, async (acc, input) => acc + (await input.getExtraCapacity(client)), - numFrom(0), + Zero, ); } @@ -1906,16 +1865,13 @@ export class Transaction extends mol.Entity.Base< return acc + capacity; }, - numFrom(0), + Zero, )) + (await this.getInputsCapacityExtra(client)) ); } getOutputsCapacity(): Num { - return this.outputs.reduce( - (acc, { capacity }) => acc + capacity, - numFrom(0), - ); + return this.outputs.reduce((acc, { capacity }) => acc + capacity, Zero); } async getInputsUdtBalance(client: Client, type: ScriptLike): Promise { @@ -1929,7 +1885,7 @@ export class Transaction extends mol.Entity.Base< return acc + udtBalanceFrom(outputData); }, - numFrom(0), + Zero, ); } @@ -1940,7 +1896,7 @@ export class Transaction extends mol.Entity.Base< } return acc + udtBalanceFrom(this.outputsData[i]); - }, numFrom(0)); + }, Zero); } async completeInputs( @@ -2062,7 +2018,7 @@ export class Transaction extends mol.Entity.Base< ): Promise { const expectedBalance = this.getOutputsUdtBalance(type) + numFrom(balanceTweak ?? 0); - if (expectedBalance === numFrom(0)) { + if (expectedBalance === Zero) { return 0; } @@ -2076,7 +2032,7 @@ export class Transaction extends mol.Entity.Base< return [balanceAcc + udtBalanceFrom(outputData), countAcc + 1]; }, - [numFrom(0), 0], + [Zero, 0], ); if ( @@ -2515,12 +2471,12 @@ export function calcDaoProfit( * * @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]. + * @returns The epoch when the withdrawal can be claimed, represented as an Epoch instance. * * @example * ```typescript - * const claimEpoch = calcDaoClaimEpoch(depositHeader, withdrawHeader); - * console.log(`Can claim at epoch: ${claimEpoch[0]}, index: ${claimEpoch[1]}, length: ${claimEpoch[2]}`); + * const epoch = calcDaoClaimEpoch(depositHeader, withdrawHeader); + * console.log(`Can claim at epoch: ${epoch.integer}, numerator: ${epoch.numerator}, denominator: ${epoch.denominator}`); * ``` * * @remarks @@ -2534,26 +2490,23 @@ export function calcDaoClaimEpoch( depositHeader: ClientBlockHeaderLike, withdrawHeader: ClientBlockHeaderLike, ): Epoch { - const depositEpoch = ClientBlockHeader.from(depositHeader).epoch; - const withdrawEpoch = ClientBlockHeader.from(withdrawHeader).epoch; - const intDiff = withdrawEpoch[0] - depositEpoch[0]; - // deposit[1] withdraw[1] - // ---------- <= ----------- - // deposit[2] withdraw[2] + const deposit = ClientBlockHeader.from(depositHeader).epoch.normalizeBase(); + const withdraw = ClientBlockHeader.from(withdrawHeader).epoch.normalizeBase(); + + const fullCycle = numFrom(180); + const partialCycle = (withdraw.integer - deposit.integer) % fullCycle; + let withdrawInteger = withdraw.integer; if ( - intDiff % numFrom(180) !== numFrom(0) || - depositEpoch[1] * withdrawEpoch[2] <= depositEpoch[2] * withdrawEpoch[1] + partialCycle !== Zero || + // deposit.numerator withdraw.numerator + // --------------------- <= ---------------------- + // deposit.denominator withdraw.denominator + deposit.numerator * withdraw.denominator <= + withdraw.numerator * deposit.denominator ) { - return [ - depositEpoch[0] + (intDiff / numFrom(180) + numFrom(1)) * numFrom(180), - depositEpoch[1], - depositEpoch[2], - ]; + // Need to wait for the next cycle + withdrawInteger += -partialCycle + fullCycle; } - return [ - depositEpoch[0] + (intDiff / numFrom(180)) * numFrom(180), - depositEpoch[1], - depositEpoch[2], - ]; + return new Epoch(withdrawInteger, deposit.numerator, deposit.denominator); } diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index 8c0cec7a6..70f504324 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, ); @@ -369,7 +369,7 @@ export abstract class Client { async findSingletonCellByType( type: ScriptLike, - withData = false, + withData = true, ): Promise { for await (const cell of this.findCellsByType( type, diff --git a/packages/core/src/client/clientPublicMainnet.advanced.ts b/packages/core/src/client/clientPublicMainnet.advanced.ts index 979b0955c..bdcec4cc1 100644 --- a/packages/core/src/client/clientPublicMainnet.advanced.ts +++ b/packages/core/src/client/clientPublicMainnet.advanced.ts @@ -321,6 +321,29 @@ export const MAINNET_SCRIPTS: Record = }, ], }, + [KnownScript.DidCkb]: { + codeHash: + "0x4a06164dc34dccade5afe3e847a97b6db743e79f5477fa3295acf02849c5984a", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xe2f74c56cdc610d2b9fe898a96a80118845f5278605d7f9ad535dad69ae015bf", + index: 0, + }, + depType: "code", + }, + type: { + codeHash: + "0x00000000000000000000000000000000000000000000000000545950455f4944", + args: "0x55573ef6d78e3ca75170ff476176732309a8b31efe94320a954ded3d75c2cb18", + hashType: "type", + }, + }, + ], + }, [KnownScript.AlwaysSuccess]: { codeHash: "0x3b521cc4b552f109d092d8cc468a8048acb53c5952dbe769d2b2f9cf6e47f7f1", diff --git a/packages/core/src/client/clientPublicTestnet.advanced.ts b/packages/core/src/client/clientPublicTestnet.advanced.ts index be811c7eb..9453d2523 100644 --- a/packages/core/src/client/clientPublicTestnet.advanced.ts +++ b/packages/core/src/client/clientPublicTestnet.advanced.ts @@ -333,6 +333,29 @@ export const TESTNET_SCRIPTS: Record = }, ], }, + [KnownScript.DidCkb]: { + codeHash: + "0x510150477b10d6ab551a509b71265f3164e9fd4137fcb5a4322f49f03092c7c5", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x0e7a830e2d5ebd05cd45a55f93f94559edea0ef1237b7233f49f7facfb3d6a6c", + index: 0, + }, + depType: "code", + }, + type: { + codeHash: + "0x00000000000000000000000000000000000000000000000000545950455f4944", + args: "0x3c27695173b888ed44ddf36f901789014384ad6c05a9137f3db9a0779c141c35", + hashType: "type", + }, + }, + ], + }, [KnownScript.AlwaysSuccess]: { codeHash: "0x3b521cc4b552f109d092d8cc468a8048acb53c5952dbe769d2b2f9cf6e47f7f1", diff --git a/packages/core/src/client/clientTypes.ts b/packages/core/src/client/clientTypes.ts index f7331e8d2..ec57a1a12 100644 --- a/packages/core/src/client/clientTypes.ts +++ b/packages/core/src/client/clientTypes.ts @@ -12,7 +12,6 @@ import { ScriptLike, Transaction, TransactionLike, - epochFrom, hashTypeFrom, } from "../ckb/index.js"; import { Hex, HexLike, hexFrom } from "../hex/index.js"; @@ -398,7 +397,7 @@ export class ClientBlockHeader { s: numFrom(headerLike.dao.s), u: numFrom(headerLike.dao.u), }, - epochFrom(headerLike.epoch), + Epoch.from(headerLike.epoch), hexFrom(headerLike.extraHash), hexFrom(headerLike.hash), numFrom(headerLike.nonce), 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/jsonRpc/transformers.ts b/packages/core/src/client/jsonRpc/transformers.ts index ded06bd80..a571fa412 100644 --- a/packages/core/src/client/jsonRpc/transformers.ts +++ b/packages/core/src/client/jsonRpc/transformers.ts @@ -9,6 +9,7 @@ import { CellOutputLike, DepType, DepTypeLike, + Epoch, HashType, HashTypeLike, OutPoint, @@ -18,7 +19,6 @@ import { Transaction, TransactionLike, depTypeFrom, - epochFromHex, hashTypeFrom, } from "../../ckb/index.js"; import { Hex, HexLike, hexFrom } from "../../hex/index.js"; @@ -217,7 +217,7 @@ export class JsonRpcTransformers { s: numLeFromBytes(dao.slice(16, 24)), u: numLeFromBytes(dao.slice(24, 32)), }, - epoch: epochFromHex(header.epoch), + epoch: Epoch.fromNum(header.epoch), extraHash: header.extra_hash, hash: header.hash, nonce: numFrom(header.nonce), diff --git a/packages/core/src/client/knownScript.ts b/packages/core/src/client/knownScript.ts index 2171b90b4..90a1546fe 100644 --- a/packages/core/src/client/knownScript.ts +++ b/packages/core/src/client/knownScript.ts @@ -15,6 +15,7 @@ export enum KnownScript { OmniLock = "OmniLock", NostrLock = "NostrLock", UniqueType = "UniqueType", + DidCkb = "DidCkb", // ckb-proxy-locks https://github.com/ckb-devrel/ckb-proxy-locks AlwaysSuccess = "AlwaysSuccess", diff --git a/packages/core/src/codec/codec.ts b/packages/core/src/codec/codec.ts new file mode 100644 index 000000000..77a9c4e65 --- /dev/null +++ b/packages/core/src/codec/codec.ts @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Bytes, bytesFrom, BytesLike } from "../bytes/index.js"; + +export type CodecLike = { + readonly encode: (encodable: Encodable) => Bytes; + readonly decode: ( + decodable: BytesLike, + config?: { isExtraFieldIgnored?: boolean }, + ) => Decoded; + readonly byteLength?: number; +}; +export class Codec { + constructor( + public readonly encode: (encodable: Encodable) => Bytes, + public readonly decode: ( + decodable: BytesLike, + config?: { isExtraFieldIgnored?: boolean }, // This is equivalent to "compatible" in the Rust implementation of Molecule. + ) => Decoded, + public readonly byteLength?: number, // if provided, treat codec as fixed length + ) {} + + encodeOr(encodable: Encodable, fallback: T): Bytes | T { + try { + return this.encode(encodable); + } catch (_) { + return fallback; + } + } + + decodeOr( + decodable: BytesLike, + fallback: T, + config?: { isExtraFieldIgnored?: boolean }, // This is equivalent to "compatible" in the Rust implementation of Molecule. + ) { + try { + return this.decode(decodable, config); + } catch (_) { + return fallback; + } + } + + static from({ + encode, + decode, + byteLength, + }: CodecLike): Codec { + return new Codec( + (encodable: Encodable) => { + const encoded = encode(encodable); + if (byteLength !== undefined && encoded.byteLength !== byteLength) { + throw new Error( + `Codec.encode: expected byte length ${byteLength}, got ${encoded.byteLength}`, + ); + } + return encoded; + }, + (decodable, config) => { + const decodableBytes = bytesFrom(decodable); + if ( + byteLength !== undefined && + decodableBytes.byteLength !== byteLength + ) { + throw new Error( + `Codec.decode: expected byte length ${byteLength}, got ${decodableBytes.byteLength}`, + ); + } + return decode(decodable, config); + }, + byteLength, + ); + } + + map({ + inMap, + outMap, + }: { + inMap?: (encodable: NewEncodable) => Encodable; + outMap?: (decoded: Decoded) => NewDecoded; + }): Codec { + return new Codec( + (encodable) => + this.encode((inMap ? inMap(encodable) : encodable) as Encodable), + (buffer, config) => + (outMap + ? outMap(this.decode(buffer, config)) + : this.decode(buffer, config)) as NewDecoded, + this.byteLength, + ); + } + + mapIn( + map: (encodable: NewEncodable) => Encodable, + ): Codec { + return this.map({ inMap: map }); + } + + mapOut( + map: (decoded: Decoded) => NewDecoded, + ): Codec { + return this.map({ outMap: map }); + } +} + +export type EncodableType> = + T extends CodecLike ? Encodable : never; +export type DecodedType> = + T extends CodecLike ? Decoded : never; diff --git a/packages/core/src/molecule/entity.ts b/packages/core/src/codec/entity.ts similarity index 88% rename from packages/core/src/molecule/entity.ts rename to packages/core/src/codec/entity.ts index 629281385..d62a7a539 100644 --- a/packages/core/src/molecule/entity.ts +++ b/packages/core/src/codec/entity.ts @@ -1,6 +1,6 @@ import { Bytes, bytesEq, BytesLike } from "../bytes/index.js"; import { hashCkb } from "../hasher/index.js"; -import { Hex } from "../hex/index.js"; +import { Hex, hexFrom } from "../hex/index.js"; import { Constructor } from "../utils/index.js"; import { Codec } from "./codec.js"; @@ -32,7 +32,7 @@ export abstract class Entity { */ static encode(_: SubTypeLike): Bytes { throw new Error( - "encode not implemented, use @ccc.mol.codec to decorate your type", + "encode not implemented, use @ccc.codec to decorate your type", ); } /** @@ -45,7 +45,7 @@ export abstract class Entity { */ static decode(_: BytesLike): SubType { throw new Error( - "decode not implemented, use @ccc.mol.codec to decorate your type", + "decode not implemented, use @ccc.codec to decorate your type", ); } @@ -59,7 +59,7 @@ export abstract class Entity { */ static fromBytes(_bytes: BytesLike): SubType { throw new Error( - "fromBytes not implemented, use @ccc.mol.codec to decorate your type", + "fromBytes not implemented, use @ccc.codec to decorate your type", ); } @@ -126,6 +126,15 @@ export abstract class Entity { hash(): Hex { return hashCkb(this.toBytes()); } + + /** + * Convert the entity to a full-byte untrimmed Hex representation + * @public + * @returns The entity full-byte untrimmed hexadecimal representation + */ + toHex(): Hex { + return hexFrom(this.toBytes()); + } } /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */ @@ -139,6 +148,7 @@ export abstract class Entity { abstract toBytes(): Bytes; abstract hash(): Hex; + abstract toHex(): Hex; abstract clone(): Entity; } @@ -146,14 +156,14 @@ export abstract class Entity { * A class decorator to add methods implementation on the {@link Entity.Base} class * @example * ```typescript - * @mol.codec( + * @codec( * mol.table({ * codeHash: mol.Byte32, * hashType: HashTypeCodec, * args: mol.Bytes, * }), * ) - * export class Script extends mol.Entity.Base() { + * export class Script extends Entity.Base() { * from(scriptLike: ScriptLike): Script {} * } * ``` @@ -176,7 +186,7 @@ export function codec< Constructor.byteLength = codec.byteLength; if (Constructor.encode === undefined) { Constructor.encode = function (encodable: TypeLike) { - return codec.encode(encodable); + return codec.encode(Constructor.from(encodable)); }; } if (Constructor.decode === undefined) { diff --git a/packages/core/src/codec/index.ts b/packages/core/src/codec/index.ts new file mode 100644 index 000000000..7688d8753 --- /dev/null +++ b/packages/core/src/codec/index.ts @@ -0,0 +1,3 @@ +export * from "./codec.js"; +export * from "./entity.js"; +export * from "./predefined.js"; diff --git a/packages/core/src/codec/predefined.ts b/packages/core/src/codec/predefined.ts new file mode 100644 index 000000000..b14b90dac --- /dev/null +++ b/packages/core/src/codec/predefined.ts @@ -0,0 +1,145 @@ +import { Bytes, bytesFrom, BytesLike } from "../bytes/index.js"; +import { Hex, hexFrom, HexLike } from "../hex/index.js"; +import { + Num, + numBeFromBytes, + numBeToBytes, + numFromBytes, + NumLike, + numToBytes, +} from "../num/index.js"; +import { Codec } from "./codec.js"; + +/** + * Create a codec to deal with fixed LE or BE bytes. + * @param byteLength + * @param littleEndian + */ +export function codecUint( + byteLength: number, + littleEndian = false, +): Codec { + return Codec.from({ + byteLength, + encode: (numLike) => { + if (littleEndian) { + return numToBytes(numLike, byteLength); + } else { + return numBeToBytes(numLike, byteLength); + } + }, + decode: (buffer) => { + if (littleEndian) { + return numFromBytes(buffer); + } else { + return numBeFromBytes(buffer); + } + }, + }); +} + +/** + * Create a codec to deal with fixed LE or BE bytes. + * @param byteLength + * @param littleEndian + */ +export function codecUintNumber( + byteLength: number, + littleEndian = false, +): Codec { + if (byteLength > 4) { + throw new Error("uintNumber: byteLength must be less than or equal to 4"); + } + return codecUint(byteLength, littleEndian).map({ + outMap: (num) => Number(num), + }); +} + +/** + * Create a codec for padding bytes. + * The padding bytes are zero-filled when encoding and ignored when decoding. + * @param byteLength The length of the padding in bytes. + */ +export function codecPadding( + byteLength: number, +): Codec { + return Codec.from({ + byteLength, + encode: () => { + return new Uint8Array(byteLength); + }, + decode: () => {}, + }); +} + +export const CodecRaw: Codec = Codec.from({ + encode: (value) => bytesFrom(value), + decode: (buffer) => bytesFrom(buffer), +}); + +export const CodecBytes: Codec = Codec.from({ + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); + +export const CodecUint8 = codecUintNumber(1, true); + +export const CodecUint16LE = codecUintNumber(2, true); +export const CodecUint16BE = codecUintNumber(2); +export const CodecUint16 = CodecUint16LE; + +export const CodecUint32LE = codecUintNumber(4, true); +export const CodecUint32BE = codecUintNumber(4); +export const CodecUint32 = CodecUint32LE; + +export const CodecUint64LE = codecUint(8, true); +export const CodecUint64BE = codecUint(8); +export const CodecUint64 = CodecUint64LE; + +export const CodecUint128LE = codecUint(16, true); +export const CodecUint128BE = codecUint(16); +export const CodecUint128 = CodecUint128LE; + +export const CodecUint256LE = codecUint(32, true); +export const CodecUint256BE = codecUint(32); +export const CodecUint256 = CodecUint256LE; + +export const CodecUint512LE = codecUint(64, true); +export const CodecUint512BE = codecUint(64); +export const CodecUint512 = CodecUint512LE; + +export const CodecBool: Codec = Codec.from({ + byteLength: 1, + encode: (value) => bytesFrom(value ? [1] : [0]), + decode: (buffer) => bytesFrom(buffer)[0] !== 0, +}); + +export const CodecByte: Codec = Codec.from({ + byteLength: 1, + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); + +export const CodecByte4: Codec = Codec.from({ + byteLength: 4, + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); + +export const CodecByte8: Codec = Codec.from({ + byteLength: 8, + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); + +export const CodecByte16: Codec = Codec.from({ + byteLength: 16, + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); + +export const CodecByte32: Codec = Codec.from({ + byteLength: 32, + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); 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/molecule/barrel.ts b/packages/core/src/molecule/barrel.ts index 7688d8753..98d88a3c3 100644 --- a/packages/core/src/molecule/barrel.ts +++ b/packages/core/src/molecule/barrel.ts @@ -1,3 +1,12 @@ +export { + /** + * @deprecated Use ccc.Entity instead + */ + Entity, + /** + * @deprecated Use ccc.codec instead + */ + codec, +} from "../codec/entity.js"; export * from "./codec.js"; -export * from "./entity.js"; export * from "./predefined.js"; diff --git a/packages/core/src/molecule/codec.ts b/packages/core/src/molecule/codec.ts index 4e80d7a1d..ff09cab9a 100644 --- a/packages/core/src/molecule/codec.ts +++ b/packages/core/src/molecule/codec.ts @@ -1,105 +1,45 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - Bytes, bytesConcat, bytesConcatTo, bytesFrom, BytesLike, } from "../bytes/index.js"; import { - Num, - numBeFromBytes, - numBeToBytes, - numFromBytes, - NumLike, - numToBytes, -} from "../num/index.js"; - -export type CodecLike = { - readonly encode: (encodable: Encodable) => Bytes; - readonly decode: ( - decodable: BytesLike, - config?: { isExtraFieldIgnored?: boolean }, - ) => Decoded; - readonly byteLength?: number; -}; -export class Codec { - constructor( - public readonly encode: (encodable: Encodable) => Bytes, - public readonly decode: ( - decodable: BytesLike, - config?: { isExtraFieldIgnored?: boolean }, // This is equivalent to "compatible" in the Rust implementation of Molecule. - ) => Decoded, - public readonly byteLength?: number, // if provided, treat codec as fixed length - ) {} - - static from({ - encode, - decode, - byteLength, - }: CodecLike): Codec { - return new Codec( - (encodable: Encodable) => { - const encoded = encode(encodable); - if (byteLength !== undefined && encoded.byteLength !== byteLength) { - throw new Error( - `Codec.encode: expected byte length ${byteLength}, got ${encoded.byteLength}`, - ); - } - return encoded; - }, - (decodable, config) => { - const decodableBytes = bytesFrom(decodable); - if ( - byteLength !== undefined && - decodableBytes.byteLength !== byteLength - ) { - throw new Error( - `Codec.decode: expected byte length ${byteLength}, got ${decodableBytes.byteLength}`, - ); - } - return decode(decodable, config); - }, - byteLength, - ); - } - - map({ - inMap, - outMap, - }: { - inMap?: (encodable: NewEncodable) => Encodable; - outMap?: (decoded: Decoded) => NewDecoded; - }): Codec { - return new Codec( - (encodable) => - this.encode((inMap ? inMap(encodable) : encodable) as Encodable), - (buffer, config) => - (outMap - ? outMap(this.decode(buffer, config)) - : this.decode(buffer, config)) as NewDecoded, - this.byteLength, - ); - } - - mapIn( - map: (encodable: NewEncodable) => Encodable, - ): Codec { - return this.map({ inMap: map }); - } - - mapOut( - map: (decoded: Decoded) => NewDecoded, - ): Codec { - return this.map({ outMap: map }); - } -} - -export type EncodableType> = - T extends CodecLike ? Encodable : never; -export type DecodedType> = - T extends CodecLike ? Decoded : never; + Codec, + CodecLike, + DecodedType, + EncodableType, +} from "../codec/index.js"; +import { numFromBytes, NumLike, numToBytes } from "../num/index.js"; + +export { + /** + * @deprecated Use ccc.Codec instead + */ + Codec, + /** + * @deprecated Use ccc.CodecLike instead + */ + CodecLike, + /** + * @deprecated Use ccc.DecodedType instead + */ + DecodedType, + /** + * @deprecated Use ccc.EncodableType instead + */ + EncodableType, + /** + * @deprecated Use ccc.codecUint instead + */ + codecUint as uint, + /** + * @deprecated Use ccc.codecUintNumber instead + */ + codecUintNumber as uintNumber, +} from "../codec/index.js"; function uint32To(numLike: NumLike) { return numToBytes(numLike, 4); @@ -673,48 +613,3 @@ export function array( }, }); } - -/** - * Create a codec to deal with fixed LE or BE bytes. - * @param byteLength - * @param littleEndian - */ -export function uint( - byteLength: number, - littleEndian = false, -): Codec { - return Codec.from({ - byteLength, - encode: (numLike) => { - if (littleEndian) { - return numToBytes(numLike, byteLength); - } else { - return numBeToBytes(numLike, byteLength); - } - }, - decode: (buffer) => { - if (littleEndian) { - return numFromBytes(buffer); - } else { - return numBeFromBytes(buffer); - } - }, - }); -} - -/** - * Create a codec to deal with fixed LE or BE bytes. - * @param byteLength - * @param littleEndian - */ -export function uintNumber( - byteLength: number, - littleEndian = false, -): Codec { - if (byteLength > 4) { - throw new Error("uintNumber: byteLength must be less than or equal to 4"); - } - return uint(byteLength, littleEndian).map({ - outMap: (num) => Number(num), - }); -} diff --git a/packages/core/src/molecule/predefined.ts b/packages/core/src/molecule/predefined.ts index 176da41ca..7e13c3ff8 100644 --- a/packages/core/src/molecule/predefined.ts +++ b/packages/core/src/molecule/predefined.ts @@ -1,91 +1,104 @@ import { bytesFrom, bytesTo } from "../bytes/index.js"; -import { Hex, hexFrom, HexLike } from "../hex/index.js"; -import { byteVec, Codec, option, uint, uintNumber, vector } from "./codec.js"; +import { Codec, CodecBytes } from "../codec/index.js"; +import { Hex, HexLike } from "../hex/index.js"; +import { byteVec, option, vector } from "./codec.js"; + +import { + CodecBool as Bool, + CodecByte as Byte, + CodecByte16 as Byte16, + CodecByte32 as Byte32, + CodecByte4 as Byte4, + CodecByte8 as Byte8, + CodecUint128 as Uint128, + CodecUint128BE as Uint128BE, + CodecUint128LE as Uint128LE, + CodecUint16 as Uint16, + CodecUint16BE as Uint16BE, + CodecUint16LE as Uint16LE, + CodecUint256 as Uint256, + CodecUint256BE as Uint256BE, + CodecUint256LE as Uint256LE, + CodecUint32 as Uint32, + CodecUint32BE as Uint32BE, + CodecUint32LE as Uint32LE, + CodecUint512 as Uint512, + CodecUint512BE as Uint512BE, + CodecUint512LE as Uint512LE, + CodecUint64 as Uint64, + CodecUint64BE as Uint64BE, + CodecUint64LE as Uint64LE, + CodecUint8 as Uint8, +} from "../codec/index.js"; + +export { + Bool, + Byte, + Byte16, + Byte32, + Byte4, + Byte8, + Uint128, + Uint128BE, + Uint128LE, + Uint16, + Uint16BE, + Uint16LE, + Uint256, + Uint256BE, + Uint256LE, + Uint32, + Uint32BE, + Uint32LE, + Uint512, + Uint512BE, + Uint512LE, + Uint64, + Uint64BE, + Uint64LE, + Uint8, +}; -export const Uint8 = uintNumber(1, true); export const Uint8Opt = option(Uint8); export const Uint8Vec = vector(Uint8); -export const Uint16LE = uintNumber(2, true); -export const Uint16BE = uintNumber(2); -export const Uint16 = Uint16LE; export const Uint16Opt = option(Uint16); export const Uint16Vec = vector(Uint16); -export const Uint32LE = uintNumber(4, true); -export const Uint32BE = uintNumber(4); -export const Uint32 = Uint32LE; export const Uint32Opt = option(Uint32); export const Uint32Vec = vector(Uint32); -export const Uint64LE = uint(8, true); -export const Uint64BE = uint(8); -export const Uint64 = Uint64LE; export const Uint64Opt = option(Uint64); export const Uint64Vec = vector(Uint64); -export const Uint128LE = uint(16, true); -export const Uint128BE = uint(16); -export const Uint128 = Uint128LE; export const Uint128Opt = option(Uint128); export const Uint128Vec = vector(Uint128); -export const Uint256LE = uint(32, true); -export const Uint256BE = uint(32); -export const Uint256 = Uint256LE; export const Uint256Opt = option(Uint256); export const Uint256Vec = vector(Uint256); -export const Uint512LE = uint(64, true); -export const Uint512BE = uint(64); -export const Uint512 = Uint512LE; export const Uint512Opt = option(Uint512); export const Uint512Vec = vector(Uint512); -export const Bytes: Codec = byteVec({ - encode: (value) => bytesFrom(value), - decode: (buffer) => hexFrom(buffer), -}); +export const Bytes: Codec = byteVec(CodecBytes); export const BytesOpt = option(Bytes); export const BytesVec = vector(Bytes); -export const Bool: Codec = Codec.from({ - byteLength: 1, - encode: (value) => bytesFrom(value ? [1] : [0]), - decode: (buffer) => bytesFrom(buffer)[0] !== 0, -}); export const BoolOpt = option(Bool); export const BoolVec = vector(Bool); -export const Byte4: Codec = Codec.from({ - byteLength: 4, - encode: (value) => bytesFrom(value), - decode: (buffer) => hexFrom(buffer), -}); +export const ByteOpt = option(Byte); +export const ByteVec = vector(Byte); + export const Byte4Opt = option(Byte4); export const Byte4Vec = vector(Byte4); -export const Byte8: Codec = Codec.from({ - byteLength: 8, - encode: (value) => bytesFrom(value), - decode: (buffer) => hexFrom(buffer), -}); export const Byte8Opt = option(Byte8); export const Byte8Vec = vector(Byte8); -export const Byte16: Codec = Codec.from({ - byteLength: 16, - encode: (value) => bytesFrom(value), - decode: (buffer) => hexFrom(buffer), -}); export const Byte16Opt = option(Byte16); export const Byte16Vec = vector(Byte16); -export const Byte32: Codec = Codec.from({ - byteLength: 32, - encode: (value) => bytesFrom(value), - decode: (buffer) => hexFrom(buffer), -}); export const Byte32Opt = option(Byte32); export const Byte32Vec = vector(Byte32); 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 64112a74a..4c0d389ad 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -2,9 +2,10 @@ import { Address } from "../../address/index.js"; import { bytesConcat, bytesFrom } from "../../bytes/index.js"; import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js"; import { KnownScript } from "../../client/index.js"; -import { HexLike, hexFrom } from "../../hex/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"; /** @@ -22,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. * @@ -123,4 +139,28 @@ export abstract class SignerBtc extends Signer { tx.setWitnessArgsAt(info.position, witness); return tx; } + + /** + * Signs a Partially Signed Bitcoin Transaction (PSBT). + * + * @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: HexLike, + options?: SignPsbtOptionsLike, + ): Promise; + + /** + * Broadcasts a PSBT to the Bitcoin network. + * + * @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 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 50096db7e..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"; /** @@ -70,4 +71,18 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { async getBtcPublicKey(): Promise { return this.publicKey; } + + async signPsbt( + _psbtHex: HexLike, + _options?: SignPsbtOptionsLike, + ): Promise { + throw new Error("Read-only signer does not support signPsbt"); + } + + 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..6abad2eec 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,4 +1,5 @@ -import { NumLike, numFrom, numToHex } from "../num/index.js"; +import { Zero } from "../fixedPoint/index.js"; +import { NumLike, numFrom, numToHex, type Num } from "../num/index.js"; /** * A type safe way to apply a transformer on a value if it's not empty. @@ -99,73 +100,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) { @@ -196,3 +265,21 @@ export function stringify(val: unknown) { return value; }); } + +/** + * Calculate the greatest common divisor (GCD) of two NumLike values using the Euclidean algorithm. + * + * @param a - First operand. + * @param b - Second operand. + * @returns GCD(a, b) as a Num. + */ +export function gcd(a: NumLike, b: NumLike): Num { + a = numFrom(a); + b = numFrom(b); + a = a < Zero ? -a : a; + b = b < Zero ? -b : b; + while (b !== Zero) { + [a, b] = [b, a % b]; + } + return a; +} diff --git a/packages/demo/prettier.config.mjs b/packages/demo/prettier.config.mjs new file mode 100644 index 000000000..edf37446c --- /dev/null +++ b/packages/demo/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"], +}; + +export default config; diff --git a/packages/did-ckb/.npmignore b/packages/did-ckb/.npmignore new file mode 100644 index 000000000..7a88408aa --- /dev/null +++ b/packages/did-ckb/.npmignore @@ -0,0 +1,21 @@ +node_modules/ +misc/ + +*test.js +*test.ts +*test.d.ts +*test.d.ts.map +*spec.js +*spec.ts +*spec.d.ts +*spec.d.ts.map + +tsconfig.json +tsconfig.*.json +eslint.config.mjs +.prettierrc +.prettierignore + +tsconfig.tsbuildinfo +tsconfig.*.tsbuildinfo +.github/ diff --git a/packages/did-ckb/.prettierignore b/packages/did-ckb/.prettierignore new file mode 100644 index 000000000..aef5d239c --- /dev/null +++ b/packages/did-ckb/.prettierignore @@ -0,0 +1,15 @@ +node_modules/ + +dist/ +dist.commonjs/ + +.npmignore +.prettierrc +tsconfig.json +eslint.config.mjs +prettier.config.* + +tsconfig.tsbuildinfo +.github/ + +CHANGELOG.md diff --git a/packages/did-ckb/README.md b/packages/did-ckb/README.md new file mode 100644 index 000000000..1423faaf1 --- /dev/null +++ b/packages/did-ckb/README.md @@ -0,0 +1,41 @@ +

+ + Logo + +

+ +

+ CCC's Support for DID CKB +

+ +

+ NPM Version + GitHub commit activity + GitHub last commit + GitHub branch check runs + Playground + App + Docs +

+ +

+ CCC - CKBers' Codebase is a one-stop solution for your CKB JS/TS ecosystem development. +
+ Empower yourself with CCC to discover the unlimited potential of CKB. +
+ Interoperate with wallets from different chain ecosystems. +
+ Fully enabling CKB's Turing completeness and cryptographic freedom power. +

+ +

+ Read more about CCC on our website or GitHub Repo. +

diff --git a/packages/did-ckb/eslint.config.mjs b/packages/did-ckb/eslint.config.mjs new file mode 100644 index 000000000..b6132c277 --- /dev/null +++ b/packages/did-ckb/eslint.config.mjs @@ -0,0 +1,62 @@ +// @ts-check + +import eslint from "@eslint/js"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import tseslint from "typescript-eslint"; + +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +export default [ + ...tseslint.config({ + files: ["**/*.ts"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }], + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/require-await": "off", + "@typescript-eslint/only-throw-error": [ + "error", + { + allowThrowingAny: true, + allowThrowingUnknown: true, + allowRethrowing: true, + }, + ], + "@typescript-eslint/prefer-promise-reject-errors": [ + "error", + { + allowThrowingAny: true, + allowThrowingUnknown: true, + }, + ], + "no-empty": "off", + "prefer-const": [ + "error", + { ignoreReadBeforeAssign: true, destructuring: "all" }, + ], + }, + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, + }), + eslintPluginPrettierRecommended, +]; diff --git a/packages/did-ckb/misc/basedirs/dist.commonjs/package.json b/packages/did-ckb/misc/basedirs/dist.commonjs/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/packages/did-ckb/misc/basedirs/dist.commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/did-ckb/misc/basedirs/dist/package.json b/packages/did-ckb/misc/basedirs/dist/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/packages/did-ckb/misc/basedirs/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/did-ckb/package.json b/packages/did-ckb/package.json new file mode 100644 index 000000000..e61651208 --- /dev/null +++ b/packages/did-ckb/package.json @@ -0,0 +1,56 @@ +{ + "name": "@ckb-ccc/did-ckb", + "version": "0.0.0", + "description": "CCC - CKBer's Codebase. CCC's support for DID on CKB", + "author": "Hanssen0 ", + "license": "MIT", + "private": false, + "homepage": "https://github.com/ckb-devrel/ccc", + "repository": { + "type": "git", + "url": "git://github.com/ckb-devrel/ccc.git" + }, + "main": "./dist.commonjs/index.js", + "module": "./dist/index.mjs", + "exports": { + ".": { + "require": "./dist.commonjs/index.js", + "import": "./dist/index.mjs" + }, + "./barrel": { + "require": "./dist.commonjs/barrel.js", + "import": "./dist/barrel.mjs" + }, + "./package.json": "./package.json" + }, + "scripts": { + "test": "vitest", + "test:ci": "vitest run", + "build": "tsdown", + "lint": "eslint ./src", + "format": "prettier --write . && eslint --fix ./src" + }, + "devDependencies": { + "@eslint/js": "^9.34.0", + "@types/node": "^24.3.0", + "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.2.0", + "tsdown": "0.19.0-beta.3", + "typescript": "^5.9.2", + "typescript-eslint": "^8.41.0", + "vitest": "^3.2.4" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@ckb-ccc/core": "workspace:*", + "@ckb-ccc/type-id": "workspace:*", + "@ipld/dag-cbor": "^9.2.5" + }, + "packageManager": "pnpm@10.8.1", + "types": "./dist.commonjs/index.d.ts" +} diff --git a/packages/did-ckb/prettier.config.cjs b/packages/did-ckb/prettier.config.cjs new file mode 100644 index 000000000..5e1810363 --- /dev/null +++ b/packages/did-ckb/prettier.config.cjs @@ -0,0 +1,11 @@ +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: [require.resolve("prettier-plugin-organize-imports")], +}; + +module.exports = config; diff --git a/packages/did-ckb/src/barrel.ts b/packages/did-ckb/src/barrel.ts new file mode 100644 index 000000000..6112c76ce --- /dev/null +++ b/packages/did-ckb/src/barrel.ts @@ -0,0 +1,2 @@ +export * from "./codec.js"; +export * from "./didCkb.js"; diff --git a/packages/did-ckb/src/codec.ts b/packages/did-ckb/src/codec.ts new file mode 100644 index 000000000..c89ba8e57 --- /dev/null +++ b/packages/did-ckb/src/codec.ts @@ -0,0 +1,146 @@ +import { ccc } from "@ckb-ccc/core"; +import { decode as cborDecode, encode as cborEncode } from "@ipld/dag-cbor"; + +export type DidCkbDataV1Like = { + document: unknown; + localId?: string | null; +}; +@ccc.codec( + ccc.mol + .table({ + document: ccc.mol.Bytes, + localId: ccc.mol.StringOpt, + }) + .map({ + inMap: (data: DidCkbDataV1Like) => ({ + ...data, + document: ccc.hexFrom(cborEncode(data.document)), + }), + outMap: (data) => ({ + ...data, + document: cborDecode(ccc.bytesFrom(data.document)), + }), + }), +) +export class DidCkbDataV1 extends ccc.Entity.Base< + DidCkbDataV1Like, + DidCkbDataV1 +>() { + constructor( + public document: unknown, + public localId?: string, + ) { + super(); + } + + static from(data: DidCkbDataV1Like): DidCkbDataV1 { + if (data instanceof DidCkbDataV1) { + return data; + } + + return new DidCkbDataV1(data.document, data.localId ?? undefined); + } +} + +export type DidCkbDataLike = { + type?: "v1" | null; + value: DidCkbDataV1Like; +}; +@ccc.codec( + ccc.mol.union({ + v1: DidCkbDataV1, + }), +) +export class DidCkbData extends ccc.Entity.Base() { + constructor( + public type: "v1", + public value: DidCkbDataV1, + ) { + super(); + } + + static from(data: DidCkbDataLike): DidCkbData { + if (data instanceof DidCkbData) { + return data; + } + return new DidCkbData(data.type ?? "v1", DidCkbDataV1.from(data.value)); + } + + static fromV1( + data: DidCkbDataV1Like, + ): DidCkbData & { type: "v1"; value: DidCkbDataV1 } { + return new DidCkbData("v1", DidCkbDataV1.from(data)); + } +} + +export type PlcAuthorizationLike = { + history: object[]; + sig: ccc.HexLike; + rotationKeyIndices: ccc.NumLike[]; +}; +@ccc.codec( + ccc.mol + .table({ + history: ccc.mol.BytesVec, + sig: ccc.mol.Bytes, + rotationKeyIndices: ccc.mol.Uint8Vec, + }) + + .map({ + inMap: (data: PlcAuthorizationLike) => ({ + ...data, + history: data.history.map((h) => ccc.hexFrom(cborEncode(h))), + }), + outMap: (data) => ({ + ...data, + history: data.history.map((h) => cborDecode(ccc.bytesFrom(h))), + }), + }), +) +export class PlcAuthorization extends ccc.Entity.Base< + PlcAuthorizationLike, + PlcAuthorization +>() { + constructor( + public history: object[], + public sig: ccc.Hex, + public rotationKeyIndices: ccc.Num[], + ) { + super(); + } + + static from(data: PlcAuthorizationLike): PlcAuthorization { + if (data instanceof PlcAuthorization) { + return data; + } + return new PlcAuthorization( + data.history, + ccc.hexFrom(data.sig), + data.rotationKeyIndices.map(ccc.numFrom), + ); + } +} + +export type DidCkbWitnessLike = { + localIdAuthorization: PlcAuthorizationLike; +}; +@ccc.codec( + ccc.mol.table({ + localIdAuthorization: PlcAuthorization, + }), +) +export class DidCkbWitness extends ccc.Entity.Base< + DidCkbWitnessLike, + DidCkbWitness +>() { + constructor(public localIdAuthorization: PlcAuthorization) { + super(); + } + + static from(data: DidCkbWitnessLike): DidCkbWitness { + if (data instanceof DidCkbWitness) { + return data; + } + return new DidCkbWitness(PlcAuthorization.from(data.localIdAuthorization)); + } +} diff --git a/packages/did-ckb/src/didCkb.ts b/packages/did-ckb/src/didCkb.ts new file mode 100644 index 000000000..0b3ede189 --- /dev/null +++ b/packages/did-ckb/src/didCkb.ts @@ -0,0 +1,89 @@ +import { ccc } from "@ckb-ccc/core"; +import { typeIdA } from "@ckb-ccc/type-id/advanced"; +import { DidCkbData, DidCkbDataLike } from "./codec"; + +const OPERATIONS = typeIdA.buildTypeIdOperations({ + async getScriptInfo(client: ccc.Client): Promise { + return client.getKnownScript(ccc.KnownScript.DidCkb); + }, + codec: DidCkbData, + async calculateTypeId( + _: ccc.Client, + tx: ccc.Transaction, + ): Promise { + return ccc + .bytesFrom(ccc.hashTypeId(tx.inputs[0], tx.outputs.length)) + .slice(0, 20); + }, +}); + +/** + * Create a DID CKB cell. + * + * @param props The arguments for creating the cell. + * @param props.signer The signer to sign the transaction. + * @param props.receiver The receiver script (optional). + * @param props.data The output data. + * @param props.tx The transaction skeleton (optional). + */ +export function createDidCkb(props: { + signer: ccc.Signer; + data: DidCkbDataLike; + receiver?: ccc.ScriptLike | null; + tx?: ccc.TransactionLike | null; +}): Promise<{ + tx: ccc.Transaction; + id: ccc.Hex; + index: number; +}> { + return OPERATIONS.create(props); +} + +/** + * Transfer a DID CKB cell. + * + * @param props The arguments for transferring the cell. + * @param props.client The client to communicate with CKB. + * @param props.id The Type ID to transfer. + * @param props.receiver The new receiver script. + * @param props.tx The transaction skeleton (optional). + * @param props.data The new output data or a transformer to update the data (optional). + */ +export async function transferDidCkb(props: { + client: ccc.Client; + id: ccc.HexLike; + receiver: ccc.ScriptLike; + tx?: ccc.TransactionLike | null; + data?: + | DidCkbDataLike + | (( + cell: ccc.Cell, + data?: DidCkbData, + ) => DidCkbDataLike | Promise) + | null; +}): Promise<{ + tx: ccc.Transaction; + inIndex: number; + outIndex: number; +}> { + return OPERATIONS.transfer(props); +} + +/** + * Destroy a DID CKB cell. + * + * @param props The arguments for destroying the cell. + * @param props.client The client to communicate with CKB. + * @param props.id The Type ID to destroy. + * @param props.tx The transaction skeleton (optional). + */ +export async function destroyDidCkb(props: { + client: ccc.Client; + id: ccc.HexLike; + tx?: ccc.TransactionLike | null; +}): Promise<{ + tx: ccc.Transaction; + index: number; +}> { + return OPERATIONS.destroy(props); +} diff --git a/packages/did-ckb/src/index.ts b/packages/did-ckb/src/index.ts new file mode 100644 index 000000000..580265645 --- /dev/null +++ b/packages/did-ckb/src/index.ts @@ -0,0 +1,2 @@ +export * from "./barrel.js"; +export * as didCkb from "./barrel.js"; diff --git a/packages/did-ckb/tsconfig.base.json b/packages/did-ckb/tsconfig.base.json new file mode 100644 index 000000000..7e5ac952b --- /dev/null +++ b/packages/did-ckb/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2020", + "incremental": true, + "allowJs": true, + "importHelpers": false, + "declaration": true, + "declarationMap": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/did-ckb/tsconfig.commonjs.json b/packages/did-ckb/tsconfig.commonjs.json new file mode 100644 index 000000000..76a25e98b --- /dev/null +++ b/packages/did-ckb/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/did-ckb/tsconfig.json b/packages/did-ckb/tsconfig.json new file mode 100644 index 000000000..df22faeca --- /dev/null +++ b/packages/did-ckb/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + } +} diff --git a/packages/did-ckb/tsdown.config.mts b/packages/did-ckb/tsdown.config.mts new file mode 100644 index 000000000..d952699a0 --- /dev/null +++ b/packages/did-ckb/tsdown.config.mts @@ -0,0 +1,33 @@ +import { defineConfig } from "tsdown"; + +const common = { + minify: true, + dts: true, + platform: "neutral" as const, + exports: true, +}; + +export default defineConfig( + ( + [ + { + entry: { + index: "src/index.ts", + barrel: "src/barrel.ts", + }, + format: "esm", + copy: "./misc/basedirs/dist/*", + }, + { + entry: { + index: "src/index.ts", + barrel: "src/barrel.ts", + }, + noExternal: ["@ipld/dag-cbor"] as string[], + format: "cjs", + outDir: "dist.commonjs", + copy: "./misc/basedirs/dist.commonjs/*", + }, + ] as const + ).map((c) => ({ ...c, ...common })), +); diff --git a/packages/did-ckb/typedoc.json b/packages/did-ckb/typedoc.json new file mode 100644 index 000000000..5cdb8befe --- /dev/null +++ b/packages/did-ckb/typedoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts"], + "extends": ["../../typedoc.base.json"], + "name": "@ckb-ccc did-ckb" +} diff --git a/packages/did-ckb/vitest.config.mts b/packages/did-ckb/vitest.config.mts new file mode 100644 index 000000000..dc6a58785 --- /dev/null +++ b/packages/did-ckb/vitest.config.mts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + coverage: { + include: ["src/**/*.ts"], + }, + }, +}); diff --git a/packages/docs/docs/code-examples.md b/packages/docs/docs/code-examples.md index 850efdb55..735683820 100644 --- a/packages/docs/docs/code-examples.md +++ b/packages/docs/docs/code-examples.md @@ -28,4 +28,9 @@ That's it! The transaction is sent. - [Use all supported wallets in custom UI.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/customUiWithController.ts) - [Sign and verify any message.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/sign.ts) - [Transfer all native CKB token.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferAll.ts) -- [Transfer UDT token.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferUdt.ts) \ No newline at end of file +- [Transfer UDT token.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferUdt.ts) + + +CCC also supports Bitcoin! You can now build Bitcoin transactions and sign them using supported Bitcoin wallets. + +- [Transfer Bitcoin.](https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/transferBtc.ts) \ No newline at end of file diff --git a/packages/eip6963/CHANGELOG.md b/packages/eip6963/CHANGELOG.md index fcfa2c184..fe1856022 100644 --- a/packages/eip6963/CHANGELOG.md +++ b/packages/eip6963/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/eip6963 +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/eip6963/package.json b/packages/eip6963/package.json index a660f3a1e..a9229617f 100644 --- a/packages/eip6963/package.json +++ b/packages/eip6963/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/eip6963", - "version": "1.0.30", + "version": "1.0.31", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for EIP6963", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/eip6963/prettier.config.mjs b/packages/eip6963/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/eip6963/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json index 756bf2d1d..bac19e45d 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -31,7 +31,9 @@ }, "dependencies": { "@ckb-ccc/ccc": "workspace:*", - "@ckb-ccc/playground": "file:src/playground" + "@ckb-ccc/playground": "file:src/playground", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" }, "packageManager": "pnpm@10.8.1" } diff --git a/packages/examples/prettier.config.mjs b/packages/examples/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/examples/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/examples/src/createDid.ts b/packages/examples/src/createDid.ts new file mode 100644 index 000000000..2d01a6866 --- /dev/null +++ b/packages/examples/src/createDid.ts @@ -0,0 +1,20 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { render, signer } from "@ckb-ccc/playground"; + +// Construct create did tx +const { tx } = await ccc.didCkb.createDidCkb({ + signer, + data: { value: { document: {} } }, +}); + +// Complete missing parts: Fill inputs +await tx.completeInputsByCapacity(signer); +await render(tx); + +// Complete missing parts: Pay fee +await tx.completeFeeBy(signer); +await render(tx); + +// Sign and send the transaction +const txHash = await signer.sendTransaction(tx); +console.log(`Transaction ${txHash} sent`); diff --git a/packages/examples/src/createDidWithLocalId.ts b/packages/examples/src/createDidWithLocalId.ts new file mode 100644 index 000000000..957e4b087 --- /dev/null +++ b/packages/examples/src/createDidWithLocalId.ts @@ -0,0 +1,63 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { render, signer } from "@ckb-ccc/playground"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import { sha256 } from "@noble/hashes/sha2"; + +// From https://github.com/bluesky-social/atproto/blob/main/packages/crypto +function plcSign(key: ccc.BytesLike, msg: ccc.BytesLike): ccc.Bytes { + const msgHash = sha256(ccc.bytesFrom(msg)); + const sig = secp256k1.sign(msgHash, ccc.bytesFrom(key), { lowS: true }); + return sig.toBytes("compact"); +} + +// Construct create did tx +const { tx } = await ccc.didCkb.createDidCkb({ + signer, + data: { + value: { document: {}, localId: "did:plc:yunkr6vorfgzmvzeoofbkhq5" }, + }, +}); + +// Complete missing parts: Fill inputs +await tx.completeInputsByCapacity(signer); +await render(tx); + +// Complete missing parts: Pay fee +await tx.completeFeeBy(signer); +await render(tx); + +// Authorize the transaction with the rotation key +const rotationKey = + "0x806d1925698097c64bc70f629e25b91b48a15eee4e492bb239402cee85356a10"; +const witness = ccc.didCkb.DidCkbWitness.from({ + localIdAuthorization: { + history: [ + { + type: "plc_operation", + verificationMethods: { + atproto: "did:key:zQ3shn3qejTEyEiokszFc4MWEqbdAwyj2XR1oS2AuXKvEBTuN", + }, + rotationKeys: [ + "did:key:zQ3shqtXEdagupBhLzL2vFUACfdVjDEvciip79uY8iHBuu7FD", + "did:key:zDnaefn5fMKvoZ1n4vyxJ9npjWE5P3D8GkM9zNqaGbLqdDrtX", + ], + alsoKnownAs: ["at://alice.example.com"], + services: { + atproto_pds: { + type: "AtprotoPersonalDataServer", + endpoint: "https://example.com", + }, + }, + prev: null, + sig: "2ySrMKwAQ8j_7HlJlNdE9kXFXG6VAGzy0s4P5O12UuMQqUgDHlAe3PQza5zWxIi6TC9K3K8ghmypfhDyJm8LuQ", + }, + ], + rotationKeyIndices: [0n, 0n], + sig: plcSign(rotationKey, tx.hash()), + }, +}); +tx.setWitnessArgsAt(0, ccc.WitnessArgs.from({ outputType: witness.toBytes() })); + +// Sign and send the transaction +const txHash = await signer.sendTransaction(tx); +console.log(`Transaction ${txHash} sent`); diff --git a/packages/examples/src/destroyDid.ts b/packages/examples/src/destroyDid.ts new file mode 100644 index 000000000..55f38b189 --- /dev/null +++ b/packages/examples/src/destroyDid.ts @@ -0,0 +1,31 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { render, signer } from "@ckb-ccc/playground"; + +// === Create a did first === +// Check https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/createDid.ts for the full example +const { tx: createTx, id } = await ccc.didCkb.createDidCkb({ + signer, + data: { + value: { document: {} }, + }, +}); +await createTx.completeFeeBy(signer); +await render(createTx); +const createTxHash = await signer.sendTransaction(createTx); +console.log(`Transaction ${createTxHash} sent`); +// === Create a did first === + +// Construct destroy did tx +const { tx } = await ccc.didCkb.destroyDidCkb({ client: signer.client, id }); + +// Complete missing parts: Fill inputs +await tx.completeInputsByCapacity(signer); +await render(tx); + +// Complete missing parts: Pay fee +await tx.completeFeeBy(signer); +await render(tx); + +// Sign and send the transaction +const txHash = await signer.sendTransaction(tx); +console.log(`Transaction ${txHash} sent`); diff --git a/packages/examples/src/playground/index.d.ts b/packages/examples/src/playground/index.d.ts index fefc0e139..7a7873c29 100644 --- a/packages/examples/src/playground/index.d.ts +++ b/packages/examples/src/playground/index.d.ts @@ -1,5 +1,7 @@ import { ccc } from "@ckb-ccc/ccc"; +import * as bitcoinLib from "bitcoinjs-lib"; export function render(tx: ccc.Transaction): Promise; export const signer: ccc.Signer; export const client: ccc.Client; +export const bitcoin: typeof bitcoinLib; diff --git a/packages/examples/src/transferBtc.ts b/packages/examples/src/transferBtc.ts new file mode 100644 index 000000000..d40af0e60 --- /dev/null +++ b/packages/examples/src/transferBtc.ts @@ -0,0 +1,103 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { bitcoin, signer } from "@ckb-ccc/playground"; + +// Supported wallets: Unisat, JoyID, Xverse +// Check if the current signer is also a Bitcoin signer +if (!(signer instanceof ccc.SignerBtc)) { + throw new Error("Signer is not a Bitcoin signer"); +} + +// Only support testnet for safety +if (signer.client.addressPrefix !== "ckt") { + throw new Error("Only supported on testnet"); +} + +// Xverse has deprecated Testnet3 support, so we default to Signet. Make sure to switch to Signet in Xverse's network settings. +const isXverse = signer instanceof ccc.Xverse.Signer; +const btcTestnetName = isXverse ? "signet" : "testnet"; + +const btcAddress = await signer.getBtcAccount(); +// Fetch UTXOs from mempool.space API +const utxos = (await fetch( + `https://mempool.space/${btcTestnetName}/api/address/${btcAddress}/utxo`, +).then((res) => { + if (!res.ok) { + throw new Error(`Failed to fetch UTXOs: ${res.status} ${res.statusText}`); + } + return res.json(); +})) as { value: number; txid: string; vout: number }[]; + +const DUST_LIMIT = 546; +const FEE_SATS = 200; + +// Select a UTXO above the 546 sat dust threshold +const selectedUtxo = utxos.find((utxo) => utxo.value > DUST_LIMIT + FEE_SATS); +if (!selectedUtxo) { + throw new Error("No UTXO available"); +} + +// Fetch the full transaction to get the scriptpubkey +const btcTx = (await fetch( + `https://mempool.space/${btcTestnetName}/api/tx/${selectedUtxo.txid}`, +).then((res) => { + if (!res.ok) { + throw new Error( + `Failed to fetch transaction: ${res.status} ${res.statusText}`, + ); + } + return res.json(); +})) as { + vout: { + value: number; + scriptpubkey: string; + scriptpubkey_type: string; + }[]; +}; +const vout = btcTx.vout[selectedUtxo.vout]; + +if (!vout || !vout.scriptpubkey) { + throw new Error("Invalid vout data"); +} + +// Build PSBT with the selected UTXO as input +const psbt = new bitcoin.Psbt({ + network: isXverse ? bitcoin.networks.testnet : bitcoin.networks.testnet, +}); +const input: { + hash: string; + index: number; + witnessUtxo: { + script: Uint8Array; + value: bigint; + }; + tapInternalKey?: Uint8Array; +} = { + hash: selectedUtxo.txid, + index: selectedUtxo.vout, + witnessUtxo: { + script: ccc.bytesFrom(vout.scriptpubkey), + value: BigInt(vout.value), + }, +}; + +// Handle Taproot (P2TR) specific input fields +if ( + vout.scriptpubkey_type === "v1_p2tr" || + vout.scriptpubkey_type === "witness_v1_taproot" +) { + input.tapInternalKey = ccc.bytesFrom(await signer.getBtcPublicKey()).slice(1); +} + +psbt.addInput(input); + +// Add a single output back to the same address minus a hardcoded 200 sat fee +psbt.addOutput({ + address: btcAddress, + value: BigInt(vout.value) - BigInt(FEE_SATS), +}); + +// Sign and broadcast the transaction +const txId = await signer.signAndBroadcastPsbt(psbt.toHex()); +console.log( + `View transaction: https://mempool.space/${btcTestnetName}/tx/${txId.slice(2)}`, +); diff --git a/packages/examples/src/transferDid.ts b/packages/examples/src/transferDid.ts new file mode 100644 index 000000000..90af8e3c6 --- /dev/null +++ b/packages/examples/src/transferDid.ts @@ -0,0 +1,47 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { render, signer } from "@ckb-ccc/playground"; + +// === Create a did first === +// Check https://live.ckbccc.com/?src=https://raw.githubusercontent.com/ckb-devrel/ccc/refs/heads/master/packages/examples/src/createDid.ts for the full example +const { tx: createTx, id } = await ccc.didCkb.createDidCkb({ + signer, + data: { + value: { document: {} }, + }, +}); +await createTx.completeFeeBy(signer); +await render(createTx); +const createTxHash = await signer.sendTransaction(createTx); +console.log(`Transaction ${createTxHash} sent`); +// === Create a did first === + +// The receiver is the signer itself on mainnet +const receiver = (await signer.getRecommendedAddressObj()).script; +console.log(receiver); + +// Construct transfer did tx +const { tx } = await ccc.didCkb.transferDidCkb({ + client: signer.client, + id, + receiver, + data: (_, data) => { + if (!data) { + throw Error("Unknown error"); + } + + (data.value.document as Record)["foo"] = "bar"; + return data; + }, +}); + +// Complete missing parts: Fill inputs +await tx.completeInputsByCapacity(signer); +await render(tx); + +// Complete missing parts: Pay fee +await tx.completeFeeBy(signer); +await render(tx); + +// Sign and send the transaction +const txHash = await signer.sendTransaction(tx); +console.log(`Transaction ${txHash} sent`); diff --git a/packages/faucet/prettier.config.mjs b/packages/faucet/prettier.config.mjs new file mode 100644 index 000000000..7be7ba0bc --- /dev/null +++ b/packages/faucet/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; diff --git a/packages/joy-id/CHANGELOG.md b/packages/joy-id/CHANGELOG.md index b06fb2839..6304cec9d 100644 --- a/packages/joy-id/CHANGELOG.md +++ b/packages/joy-id/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/joy-id +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/joy-id/package.json b/packages/joy-id/package.json index c142b7b7e..17448e71d 100644 --- a/packages/joy-id/package.json +++ b/packages/joy-id/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/joy-id", - "version": "1.0.30", + "version": "1.0.31", "description": "Connector's support for JoyID", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/joy-id/prettier.config.mjs b/packages/joy-id/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/joy-id/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index 4a1e92fad..629ea7dc8 100644 --- a/packages/joy-id/src/btc/index.ts +++ b/packages/joy-id/src/btc/index.ts @@ -198,4 +198,80 @@ export class BitcoinSigner extends ccc.SignerBtc { ); return signature; } + + /** + * Signs a PSBT using JoyID wallet. + * + * @param psbtHex - The hex string of PSBT to sign. + * @returns A promise that resolves to the signed PSBT as a Hex string. + */ + async signPsbt( + psbtHex: ccc.HexLike, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + const { address } = await this.assertConnection(); + const formattedOptions = ccc.SignPsbtOptions.from(options); + + const config = this.getConfig(); + const { tx: signedPsbtHex } = await createPopup( + buildJoyIDURL( + { + ...config, + tx: ccc.hexFrom(psbtHex).slice(2), + options: formattedOptions, + signerAddress: address, + autoFinalized: formattedOptions.autoFinalized, + }, + "popup", + "/sign-psbt", + ), + { ...config, type: DappRequestType.SignPsbt }, + ); + + return ccc.hexFrom(signedPsbtHex); + } + + /** + * Broadcasts a PSBT to the Bitcoin network. + * + * @remarks + * JoyID does not support broadcasting a signed PSBT directly. + * It only supports "Sign and Broadcast" as a single atomic operation via `signAndBroadcastPsbt`. + */ + async broadcastPsbt( + _psbtHex: ccc.HexLike, + _options?: ccc.SignPsbtOptionsLike, + ): Promise { + throw new Error( + "JoyID does not support broadcasting signed PSBTs directly. Use signAndBroadcastPsbt instead.", + ); + } + + async signAndBroadcastPsbt( + psbtHex: ccc.HexLike, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + const { address } = await this.assertConnection(); + const formattedOptions = ccc.SignPsbtOptions.from(options); + + const config = this.getConfig(); + // ccc.hexFrom adds 0x prefix, but BTC expects non-0x + const { tx: txid } = await createPopup( + buildJoyIDURL( + { + ...config, + tx: ccc.hexFrom(psbtHex).slice(2), + options: formattedOptions, + signerAddress: address, + autoFinalized: true, // sendPsbt always finalizes + isSend: true, + }, + "popup", + "/sign-psbt", + ), + { ...config, type: DappRequestType.SignPsbt }, // Use SignPsbt type for both operations + ); + + return ccc.hexFrom(txid); + } } diff --git a/packages/joy-id/src/ckb/index.ts b/packages/joy-id/src/ckb/index.ts index 2fa0851e9..289e21d99 100644 --- a/packages/joy-id/src/ckb/index.ts +++ b/packages/joy-id/src/ckb/index.ts @@ -148,6 +148,7 @@ export class CkbSigner extends ccc.Signer { async getIdentity(): Promise { const connection = await this.assertConnection(); return JSON.stringify({ + address: connection.address, keyType: connection.keyType, publicKey: connection.publicKey.slice(2), }); diff --git a/packages/lumos-patches/CHANGELOG.md b/packages/lumos-patches/CHANGELOG.md index 795e19e22..671524a24 100644 --- a/packages/lumos-patches/CHANGELOG.md +++ b/packages/lumos-patches/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/lumos-patches +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/lumos-patches/package.json b/packages/lumos-patches/package.json index 127a8df4a..10de70a18 100644 --- a/packages/lumos-patches/package.json +++ b/packages/lumos-patches/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/lumos-patches", - "version": "1.0.30", + "version": "1.0.31", "description": "Patches for using Lumos with CCC", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/lumos-patches/prettier.config.mjs b/packages/lumos-patches/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/lumos-patches/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/nip07/CHANGELOG.md b/packages/nip07/CHANGELOG.md index 05f9ffe97..ad881d7ad 100644 --- a/packages/nip07/CHANGELOG.md +++ b/packages/nip07/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/nip07 +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/nip07/package.json b/packages/nip07/package.json index 06c668380..2bc0cf79b 100644 --- a/packages/nip07/package.json +++ b/packages/nip07/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/nip07", - "version": "1.0.30", + "version": "1.0.31", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for NIP07", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/nip07/prettier.config.mjs b/packages/nip07/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/nip07/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/okx/CHANGELOG.md b/packages/okx/CHANGELOG.md index 91d339220..df59b7225 100644 --- a/packages/okx/CHANGELOG.md +++ b/packages/okx/CHANGELOG.md @@ -1,5 +1,13 @@ # @ckb-ccc/okx +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + - @ckb-ccc/nip07@1.0.31 + - @ckb-ccc/uni-sat@1.0.31 + ## 1.0.30 ### Patch Changes diff --git a/packages/okx/package.json b/packages/okx/package.json index e4de5c9a4..f3de9b5f1 100644 --- a/packages/okx/package.json +++ b/packages/okx/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/okx", - "version": "1.0.30", + "version": "1.0.31", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for OKX", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/okx/prettier.config.mjs b/packages/okx/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/okx/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/okx/src/advancedBarrel.ts b/packages/okx/src/advancedBarrel.ts index 4704b662f..bdd6b3e04 100644 --- a/packages/okx/src/advancedBarrel.ts +++ b/packages/okx/src/advancedBarrel.ts @@ -2,8 +2,21 @@ import { Nip07A } from "@ckb-ccc/nip07/advanced"; import { UniSatA } from "@ckb-ccc/uni-sat/advanced"; export interface BitcoinProvider - extends Pick, - Partial> { + extends Pick< + UniSatA.Provider, + "on" | "removeListener" | "signMessage" | "signPsbt" | "pushPsbt" + >, + Partial< + Omit< + UniSatA.Provider, + | "on" + | "removeListener" + | "signMessage" + | "signPsbt" + | "pushPsbt" + | "pushTx" + > + > { connect?(): Promise<{ address: string; publicKey: string; diff --git a/packages/okx/src/btc/index.ts b/packages/okx/src/btc/index.ts index c35b9a480..3fe40bed9 100644 --- a/packages/okx/src/btc/index.ts +++ b/packages/okx/src/btc/index.ts @@ -176,4 +176,34 @@ export class BitcoinSigner extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + /** + * Signs a PSBT using OKX wallet. + * + * @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 + */ + async signPsbt( + psbtHex: ccc.HexLike, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + return ccc.hexFrom( + await this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options), + ); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast. + * @returns A promise that resolves to the transaction ID as a Hex string + */ + async broadcastPsbt( + psbtHex: ccc.HexLike, + _options?: ccc.SignPsbtOptionsLike, + ): Promise { + const txid = await this.provider.pushPsbt(ccc.hexFrom(psbtHex).slice(2)); + return ccc.hexFrom(txid); + } } diff --git a/packages/playground/package.json b/packages/playground/package.json index 1959e46a0..6bffe4f4f 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -11,6 +11,7 @@ "format": "prettier --write ./src" }, "dependencies": { + "@bitcoinerlab/secp256k1": "^1.2.0", "@ckb-ccc/ccc": "workspace:*", "@ckb-ccc/connector-react": "workspace:*", "@monaco-editor/react": "^4.7.0", @@ -21,6 +22,7 @@ "@shikijs/monaco": "^3.12.0", "axios": "^1.11.0", "bech32": "^2.0.0", + "bitcoinjs-lib": "^7.0.0", "isomorphic-ws": "^5.0.0", "lucide-react": "^0.542.0", "monaco-editor": "^0.52.2", diff --git a/packages/playground/prettier.config.mjs b/packages/playground/prettier.config.mjs new file mode 100644 index 000000000..edf37446c --- /dev/null +++ b/packages/playground/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"], +}; + +export default config; diff --git a/packages/playground/src/app/components/Editor.tsx b/packages/playground/src/app/components/Editor.tsx index 4bd227fb7..fc1416217 100644 --- a/packages/playground/src/app/components/Editor.tsx +++ b/packages/playground/src/app/components/Editor.tsx @@ -157,7 +157,7 @@ export function Editor({ }); monaco.languages.typescript.typescriptDefaults.addExtraLib( - "import { ccc } from '@ckb-ccc/core'; export function render(...msgs: unknown[]): Promise; export const signer: ccc.Signer; export const client: ccc.Client;", + "import { ccc } from '@ckb-ccc/core'; import * as bitcoin from 'bitcoinjs-lib'; export { bitcoin }; export function render(...msgs: unknown[]): Promise; export const signer: ccc.Signer; export const client: ccc.Client;", "file:///node_modules/@ckb-ccc/playground/index.d.ts", ); monaco.languages.typescript.typescriptDefaults.addExtraLib( diff --git a/packages/playground/src/app/execute/index.tsx b/packages/playground/src/app/execute/index.tsx index cd081999c..f913442eb 100644 --- a/packages/playground/src/app/execute/index.tsx +++ b/packages/playground/src/app/execute/index.tsx @@ -1,5 +1,12 @@ +import * as ecc from "@bitcoinerlab/secp256k1"; import { ccc } from "@ckb-ccc/connector-react"; +import * as bitcoin from "bitcoinjs-lib"; import * as React from "react"; + +// Initialize the ECC library for bitcoinjs-lib to support Schnorr signatures (Taproot). +// This must be done once globally before any PSBT operations. +bitcoin.initEccLib(ecc); + import ts from "typescript"; import { formatTimestamp } from "../utils"; import { vlqDecode } from "./vlq"; @@ -107,6 +114,7 @@ export async function execute( }, signer, client: signer.client, + bitcoin, }; } diff --git a/packages/rei/CHANGELOG.md b/packages/rei/CHANGELOG.md index 0c34da249..751f6c04f 100644 --- a/packages/rei/CHANGELOG.md +++ b/packages/rei/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/rei +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/rei/package.json b/packages/rei/package.json index 9b719bb7d..2b6573093 100644 --- a/packages/rei/package.json +++ b/packages/rei/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/rei", - "version": "1.0.30", + "version": "1.0.31", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for Rei", "license": "MIT", "private": false, diff --git a/packages/rei/prettier.config.mjs b/packages/rei/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/rei/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/shell/CHANGELOG.md b/packages/shell/CHANGELOG.md index 981658cb5..6ae040a14 100644 --- a/packages/shell/CHANGELOG.md +++ b/packages/shell/CHANGELOG.md @@ -1,5 +1,14 @@ # @ckb-ccc/shell +## 1.1.24 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + - @ckb-ccc/spore@1.5.16 + - @ckb-ccc/ssri@0.2.21 + - @ckb-ccc/udt@0.1.23 + ## 1.1.23 ### Patch Changes diff --git a/packages/shell/package.json b/packages/shell/package.json index d5aed9bbb..e23e2f324 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/shell", - "version": "1.1.23", + "version": "1.1.24", "description": "Backend Shell of CCC - CKBer's Codebase. Common Chains Connector.", "author": "Hanssen0 ", "license": "MIT", @@ -57,8 +57,10 @@ }, "dependencies": { "@ckb-ccc/core": "workspace:*", + "@ckb-ccc/did-ckb": "workspace:*", "@ckb-ccc/spore": "workspace:*", "@ckb-ccc/ssri": "workspace:*", + "@ckb-ccc/type-id": "workspace:*", "@ckb-ccc/udt": "workspace:*" }, "packageManager": "pnpm@10.8.1" diff --git a/packages/shell/prettier.config.mjs b/packages/shell/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/shell/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/shell/src/advancedBarrel.ts b/packages/shell/src/advancedBarrel.ts index ec239348c..f2a60e5b7 100644 --- a/packages/shell/src/advancedBarrel.ts +++ b/packages/shell/src/advancedBarrel.ts @@ -1,2 +1,3 @@ export * from "@ckb-ccc/core/advancedBarrel"; export { sporeA } from "@ckb-ccc/spore/advanced"; +export { typeIdA } from "@ckb-ccc/type-id/advanced"; diff --git a/packages/shell/src/barrel.ts b/packages/shell/src/barrel.ts index 9cca15d65..e9d7adb7a 100644 --- a/packages/shell/src/barrel.ts +++ b/packages/shell/src/barrel.ts @@ -1,4 +1,6 @@ export * from "@ckb-ccc/core/barrel"; +export { didCkb } from "@ckb-ccc/did-ckb"; export { spore } from "@ckb-ccc/spore"; export { ssri } from "@ckb-ccc/ssri"; +export { typeId } from "@ckb-ccc/type-id"; export { udt } from "@ckb-ccc/udt"; diff --git a/packages/spore/CHANGELOG.md b/packages/spore/CHANGELOG.md index 044537da4..f66235af1 100644 --- a/packages/spore/CHANGELOG.md +++ b/packages/spore/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/spore +## 1.5.16 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.5.15 ### Patch Changes diff --git a/packages/spore/package.json b/packages/spore/package.json index 974ad76dc..d28818168 100644 --- a/packages/spore/package.json +++ b/packages/spore/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/spore", - "version": "1.5.15", + "version": "1.5.16", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for Spore protocol", "author": "ashuralyk ", "license": "MIT", diff --git a/packages/spore/prettier.config.mjs b/packages/spore/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/spore/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/spore/src/cobuild/index.ts b/packages/spore/src/cobuild/index.ts index c1c5b52b8..4437f56d2 100644 --- a/packages/spore/src/cobuild/index.ts +++ b/packages/spore/src/cobuild/index.ts @@ -1,4 +1,4 @@ -import { ccc, mol } from "@ckb-ccc/core"; +import { ccc } from "@ckb-ccc/core"; import { Action, ActionVec, @@ -11,7 +11,7 @@ export function assembleCreateSporeAction( sporeOutput: ccc.CellOutputLike, sporeData: ccc.BytesLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): mol.EncodableType { +): ccc.EncodableType { if (!sporeOutput.type) { throw new Error("Spore cell must have a type script"); } @@ -39,7 +39,7 @@ export function assembleTransferSporeAction( sporeInput: ccc.CellOutputLike, sporeOutput: ccc.CellOutputLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): mol.EncodableType { +): ccc.EncodableType { if (!sporeInput.type || !sporeOutput.type) { throw new Error("Spore cell must have a type script"); } @@ -70,7 +70,7 @@ export function assembleTransferSporeAction( export function assembleMeltSporeAction( sporeInput: ccc.CellOutputLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): mol.EncodableType { +): ccc.EncodableType { if (!sporeInput.type) { throw new Error("Spore cell must have a type script"); } @@ -97,7 +97,7 @@ export function assembleCreateClusterAction( clusterOutput: ccc.CellOutputLike, clusterData: ccc.BytesLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): mol.EncodableType { +): ccc.EncodableType { if (!clusterOutput.type) { throw new Error("Cluster cell must have a type script"); } @@ -125,7 +125,7 @@ export function assembleTransferClusterAction( clusterInput: ccc.CellOutputLike, clusterOutput: ccc.CellOutputLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): mol.EncodableType { +): ccc.EncodableType { if (!clusterInput.type || !clusterOutput.type) { throw new Error("Cluster cell must have a type script"); } @@ -155,7 +155,7 @@ export function assembleTransferClusterAction( export async function prepareSporeTransaction( signer: ccc.Signer, txLike: ccc.TransactionLike, - actions: mol.EncodableType, + actions: ccc.EncodableType, ): Promise { let tx = ccc.Transaction.from(txLike); @@ -171,7 +171,7 @@ export async function prepareSporeTransaction( export function unpackCommonCobuildProof( data: ccc.HexLike, -): mol.EncodableType | undefined { +): ccc.EncodableType | undefined { try { return WitnessLayout.decode(ccc.bytesFrom(data)); } catch { @@ -181,7 +181,7 @@ export function unpackCommonCobuildProof( export function extractCobuildActionsFromTx( tx: ccc.Transaction, -): mol.EncodableType { +): ccc.EncodableType { if (tx.witnesses.length === 0) { return []; } @@ -202,7 +202,7 @@ export function extractCobuildActionsFromTx( export function injectCobuild( tx: ccc.Transaction, - actions: mol.EncodableType, + actions: ccc.EncodableType, ): void { tx.setWitnessAt( Math.max(tx.witnesses.length, tx.inputs.length), diff --git a/packages/spore/src/codec/cluster.ts b/packages/spore/src/codec/cluster.ts index e8f7e6367..cc9dc7b44 100644 --- a/packages/spore/src/codec/cluster.ts +++ b/packages/spore/src/codec/cluster.ts @@ -5,7 +5,7 @@ export interface ClusterDataV1View { description: string; } -export const ClusterDataV1: mol.Codec = mol.table({ +export const ClusterDataV1: ccc.Codec = mol.table({ name: mol.String, description: mol.String, }); @@ -16,7 +16,7 @@ export interface ClusterDataV2View { mutantId?: ccc.HexLike; } -export const ClusterDataV2: mol.Codec = mol.table({ +export const ClusterDataV2: ccc.Codec = mol.table({ name: mol.String, description: mol.String, mutantId: mol.BytesOpt, diff --git a/packages/spore/src/codec/spore.ts b/packages/spore/src/codec/spore.ts index 4f41555cc..4f1ebef32 100644 --- a/packages/spore/src/codec/spore.ts +++ b/packages/spore/src/codec/spore.ts @@ -6,7 +6,7 @@ export interface SporeDataView { clusterId?: ccc.HexLike; } -export const SporeData: mol.Codec = mol.table({ +export const SporeData: ccc.Codec = mol.table({ contentType: mol.String, content: mol.Bytes, clusterId: mol.BytesOpt, diff --git a/packages/spore/src/spore/advanced.ts b/packages/spore/src/spore/advanced.ts index 23aa84aa1..e83c0dc84 100644 --- a/packages/spore/src/spore/advanced.ts +++ b/packages/spore/src/spore/advanced.ts @@ -1,4 +1,4 @@ -import { ccc, mol } from "@ckb-ccc/core"; +import { ccc } from "@ckb-ccc/core"; import { assembleTransferClusterAction } from "../advanced.js"; import { assertCluster } from "../cluster/index.js"; import { Action, SporeDataView } from "../codec/index.js"; @@ -9,7 +9,7 @@ export async function prepareCluster( data: SporeDataView, clusterMode?: "lockProxy" | "clusterCell" | "skip", scriptInfoHash?: ccc.HexLike, -): Promise | undefined> { +): Promise | undefined> { // skip if the spore is not belong to a cluster if (!data.clusterId || clusterMode === "skip") { return; diff --git a/packages/ssri/CHANGELOG.md b/packages/ssri/CHANGELOG.md index 4d7a96444..33b45f3e6 100644 --- a/packages/ssri/CHANGELOG.md +++ b/packages/ssri/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/ssri +## 0.2.21 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 0.2.20 ### Patch Changes diff --git a/packages/ssri/package.json b/packages/ssri/package.json index 610c37ed1..951cf14d4 100644 --- a/packages/ssri/package.json +++ b/packages/ssri/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/ssri", - "version": "0.2.20", + "version": "0.2.21", "description": "SSRI", "author": "Alive24 ", "license": "MIT", diff --git a/packages/ssri/prettier.config.mjs b/packages/ssri/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/ssri/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/type-id/.npmignore b/packages/type-id/.npmignore new file mode 100644 index 000000000..7a88408aa --- /dev/null +++ b/packages/type-id/.npmignore @@ -0,0 +1,21 @@ +node_modules/ +misc/ + +*test.js +*test.ts +*test.d.ts +*test.d.ts.map +*spec.js +*spec.ts +*spec.d.ts +*spec.d.ts.map + +tsconfig.json +tsconfig.*.json +eslint.config.mjs +.prettierrc +.prettierignore + +tsconfig.tsbuildinfo +tsconfig.*.tsbuildinfo +.github/ diff --git a/packages/type-id/.prettierignore b/packages/type-id/.prettierignore new file mode 100644 index 000000000..aef5d239c --- /dev/null +++ b/packages/type-id/.prettierignore @@ -0,0 +1,15 @@ +node_modules/ + +dist/ +dist.commonjs/ + +.npmignore +.prettierrc +tsconfig.json +eslint.config.mjs +prettier.config.* + +tsconfig.tsbuildinfo +.github/ + +CHANGELOG.md diff --git a/packages/type-id/README.md b/packages/type-id/README.md new file mode 100644 index 000000000..a569c8126 --- /dev/null +++ b/packages/type-id/README.md @@ -0,0 +1,41 @@ +

+ + Logo + +

+ +

+ CCC's Support for Type ID +

+ +

+ NPM Version + GitHub commit activity + GitHub last commit + GitHub branch check runs + Playground + App + Docs +

+ +

+ CCC - CKBers' Codebase is a one-stop solution for your CKB JS/TS ecosystem development. +
+ Empower yourself with CCC to discover the unlimited potential of CKB. +
+ Interoperate with wallets from different chain ecosystems. +
+ Fully enabling CKB's Turing completeness and cryptographic freedom power. +

+ +

+ Read more about CCC on our website or GitHub Repo. +

diff --git a/packages/type-id/eslint.config.mjs b/packages/type-id/eslint.config.mjs new file mode 100644 index 000000000..b6132c277 --- /dev/null +++ b/packages/type-id/eslint.config.mjs @@ -0,0 +1,62 @@ +// @ts-check + +import eslint from "@eslint/js"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import tseslint from "typescript-eslint"; + +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +export default [ + ...tseslint.config({ + files: ["**/*.ts"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }], + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/require-await": "off", + "@typescript-eslint/only-throw-error": [ + "error", + { + allowThrowingAny: true, + allowThrowingUnknown: true, + allowRethrowing: true, + }, + ], + "@typescript-eslint/prefer-promise-reject-errors": [ + "error", + { + allowThrowingAny: true, + allowThrowingUnknown: true, + }, + ], + "no-empty": "off", + "prefer-const": [ + "error", + { ignoreReadBeforeAssign: true, destructuring: "all" }, + ], + }, + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, + }), + eslintPluginPrettierRecommended, +]; diff --git a/packages/type-id/misc/basedirs/dist.commonjs/package.json b/packages/type-id/misc/basedirs/dist.commonjs/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/packages/type-id/misc/basedirs/dist.commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/type-id/misc/basedirs/dist/package.json b/packages/type-id/misc/basedirs/dist/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/packages/type-id/misc/basedirs/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/type-id/package.json b/packages/type-id/package.json new file mode 100644 index 000000000..dcdf82b3b --- /dev/null +++ b/packages/type-id/package.json @@ -0,0 +1,62 @@ +{ + "name": "@ckb-ccc/type-id", + "version": "0.0.0", + "description": "CCC - CKBer's Codebase. CCC's support for Type ID", + "author": "Hanssen0 ", + "license": "MIT", + "private": false, + "homepage": "https://github.com/ckb-devrel/ccc", + "repository": { + "type": "git", + "url": "git://github.com/ckb-devrel/ccc.git" + }, + "main": "./dist.commonjs/index.js", + "module": "./dist/index.mjs", + "exports": { + ".": { + "require": "./dist.commonjs/index.js", + "import": "./dist/index.mjs" + }, + "./advanced": { + "require": "./dist.commonjs/advanced.js", + "import": "./dist/advanced.mjs" + }, + "./advancedBarrel": { + "require": "./dist.commonjs/advancedBarrel.js", + "import": "./dist/advancedBarrel.mjs" + }, + "./barrel": { + "require": "./dist.commonjs/barrel.js", + "import": "./dist/barrel.mjs" + }, + "./package.json": "./package.json" + }, + "scripts": { + "test": "vitest", + "test:ci": "vitest run", + "build": "tsdown", + "lint": "eslint ./src", + "format": "prettier --write . && eslint --fix ./src" + }, + "devDependencies": { + "@eslint/js": "^9.34.0", + "@types/node": "^24.3.0", + "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.2.0", + "tsdown": "0.19.0-beta.3", + "typescript": "^5.9.2", + "typescript-eslint": "^8.41.0", + "vitest": "^3.2.4" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@ckb-ccc/core": "workspace:*" + }, + "packageManager": "pnpm@10.8.1", + "types": "./dist.commonjs/index.d.ts" +} diff --git a/packages/type-id/prettier.config.cjs b/packages/type-id/prettier.config.cjs new file mode 100644 index 000000000..5e1810363 --- /dev/null +++ b/packages/type-id/prettier.config.cjs @@ -0,0 +1,11 @@ +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: [require.resolve("prettier-plugin-organize-imports")], +}; + +module.exports = config; diff --git a/packages/type-id/src/advanced.ts b/packages/type-id/src/advanced.ts new file mode 100644 index 000000000..fa30ed271 --- /dev/null +++ b/packages/type-id/src/advanced.ts @@ -0,0 +1,2 @@ +export * from "./advancedBarrel.js"; +export * as typeIdA from "./advancedBarrel.js"; diff --git a/packages/type-id/src/advancedBarrel.test.ts b/packages/type-id/src/advancedBarrel.test.ts new file mode 100644 index 000000000..d7fad2376 --- /dev/null +++ b/packages/type-id/src/advancedBarrel.test.ts @@ -0,0 +1,493 @@ +import { ccc } from "@ckb-ccc/core"; +import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { buildTypeIdOperations } from "./advancedBarrel.js"; + +describe("type-id", () => { + let client: ccc.Client; + let signer: ccc.Signer; + + const typeIdScript = { + codeHash: + "0x00000000000000000000000000000000000000000000000000545950455f4944", + hashType: "type" as const, + args: "0x", + }; + const typeIdCellDep = { + outPoint: { + txHash: + "0x1111111111111111111111111111111111111111111111111111111111111111", + index: 0, + }, + depType: "code" as const, + }; + + beforeEach(() => { + client = { + getKnownScript: vi.fn(), + getCellDeps: vi.fn(), + findSingletonCellByType: vi.fn(), + } as unknown as ccc.Client; + + signer = { + client, + getRecommendedAddressObj: vi.fn(), + findCells: vi.fn(), + } as unknown as ccc.Signer; + + (client.getKnownScript as Mock).mockResolvedValue({ + ...typeIdScript, + cellDeps: [{ cellDep: typeIdCellDep }], + }); + + (client.getCellDeps as Mock).mockImplementation( + async (deps: ccc.CellDepInfoLike[]) => + deps.map((d) => ccc.CellDep.from(d.cellDep)), + ); + + (signer.getRecommendedAddressObj as Mock).mockResolvedValue({ + script: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x1234", + }), + }); + }); + + describe("buildTypeIdOperations with custom options", () => { + it("should use custom codec", async () => { + const customCodec = { + encode: (data: number) => ccc.numLeToBytes(data, 4), + decode: (bytes: ccc.BytesLike) => Number(ccc.numLeFromBytes(bytes)), + }; + + const { create } = buildTypeIdOperations({ + getScriptInfo: async () => ({ + ...typeIdScript, + cellDeps: [{ cellDep: typeIdCellDep }], + }), + codec: customCodec, + }); + + const inputCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "2".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(1000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + }, + outputData: "0x", + }); + (signer.findCells as Mock).mockImplementation(async function* () { + yield inputCell; + }); + + const { tx } = await create({ + signer, + data: 123456, + }); + + expect(tx.outputsData[0]).toBe(ccc.hexFrom(ccc.numLeToBytes(123456, 4))); + }); + + it("should use custom calculateTypeId", async () => { + const customId = "0x" + "9".repeat(64); + const calculateTypeId = vi.fn().mockResolvedValue(customId); + + const { create } = buildTypeIdOperations({ + getScriptInfo: async () => ({ + ...typeIdScript, + cellDeps: [{ cellDep: typeIdCellDep }], + }), + calculateTypeId, + }); + + const inputCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "2".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(1000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + }, + outputData: "0x", + }); + (signer.findCells as Mock).mockImplementation(async function* () { + yield inputCell; + }); + + const { id, tx } = await create({ + signer, + data: "0x", + }); + + expect(calculateTypeId).toHaveBeenCalled(); + expect(id).toBe(customId); + expect(tx.outputs[0].type?.args).toBe(customId); + }); + + it("should use custom addCellDeps", async () => { + const customDep = ccc.CellDep.from({ + outPoint: { txHash: "0x" + "a".repeat(64), index: 0 }, + depType: "code", + }); + const addCellDeps = vi + .fn() + .mockImplementation(async (_: ccc.Client, tx: ccc.Transaction) => { + tx.addCellDeps(customDep); + return tx; + }); + + const { create } = buildTypeIdOperations({ + getScriptInfo: async () => ({ + ...typeIdScript, + cellDeps: [{ cellDep: typeIdCellDep }], + }), + addCellDeps, + }); + + const inputCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "2".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(1000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + }, + outputData: "0x", + }); + (signer.findCells as Mock).mockImplementation(async function* () { + yield inputCell; + }); + + const { tx } = await create({ + signer, + data: "0x", + }); + + expect(addCellDeps).toHaveBeenCalled(); + expect(tx.cellDeps).toContainEqual(customDep); + }); + }); + + describe("Type ID Operations", () => { + const { create, transfer, destroy } = buildTypeIdOperations({ + getScriptInfo: async () => ({ + ...typeIdScript, + cellDeps: [{ cellDep: typeIdCellDep }], + }), + }); + + describe("create", () => { + it("should create a transaction with correct type id", async () => { + const inputCell = ccc.Cell.from({ + outPoint: { + txHash: "0x" + "2".repeat(64), + index: 0, + }, + cellOutput: { + capacity: ccc.fixedPointFrom(1000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + }, + outputData: "0x", + }); + + (signer.findCells as Mock).mockImplementation(async function* () { + yield inputCell; + }); + + const data = "0x1234"; + const { tx, id, index } = await create({ + signer, + data, + }); + + expect(tx.inputs.length).toBe(1); + expect(tx.inputs[0].previousOutput).toEqual(inputCell.outPoint); + + expect(tx.outputs.length).toBe(1); + expect(index).toBe(0); + + const expectedId = ccc.hashTypeId(tx.inputs[0], 0); + expect(id).toBe(expectedId); + + const output = tx.outputs[0]; + expect(output.type).toBeDefined(); + expect(output.type?.codeHash).toBe(typeIdScript.codeHash); + expect(output.type?.hashType).toBe(typeIdScript.hashType); + expect(output.type?.args).toBe(id); + expect(tx.outputsData[0]).toBe(ccc.hexFrom(data)); + + expect(tx.cellDeps.length).toBeGreaterThan(0); + expect(tx.cellDeps[0].outPoint).toEqual( + ccc.OutPoint.from(typeIdCellDep.outPoint), + ); + }); + + it(" should append to existing tx", async () => { + const existingTx = ccc.Transaction.from({ + headerDeps: ["0x" + "e".repeat(64)], + }); + + const inputCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "2".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(1000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + }, + outputData: "0x", + }); + (signer.findCells as Mock).mockImplementation(async function* () { + yield inputCell; + }); + + const { tx } = await create({ + signer, + data: "0x", + tx: existingTx, + }); + + expect(tx.headerDeps).toContain("0x" + "e".repeat(64)); + }); + }); + + it("should accept explicit receiver", async () => { + const receiver = ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0xffee", + }); + + const inputCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "2".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(1000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + }, + outputData: "0x", + }); + (signer.findCells as Mock).mockImplementation(async function* () { + yield inputCell; + }); + + const { tx } = await create({ + signer, + data: "0x", + receiver, + }); + + expect(tx.outputs[0].lock).toEqual(receiver); + }); + + describe("transferTypeId", () => { + it("should transfer type id cell to new receiver", async () => { + const id = "0x" + "3".repeat(64); + const receiver = ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0xabcd", + }); + + const typeScript = ccc.Script.from({ + ...typeIdScript, + args: id, + }); + + const existingCell = ccc.Cell.from({ + outPoint: { + txHash: "0x" + "4".repeat(64), + index: 0, + }, + cellOutput: { + capacity: ccc.fixedPointFrom(2000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + type: typeScript, + }, + outputData: "0x5678", + }); + + (client.findSingletonCellByType as Mock).mockResolvedValue( + existingCell, + ); + + const newData = "0x9999"; + const { tx, inIndex, outIndex } = await transfer({ + client, + id, + receiver, + data: newData, + }); + + expect(tx.inputs[inIndex].previousOutput).toEqual( + existingCell.outPoint, + ); + + const output = tx.outputs[outIndex]; + expect(output.lock).toEqual(receiver); + expect(output.type).toEqual(typeScript); + expect(tx.outputsData[outIndex]).toBe(ccc.hexFrom(newData)); + }); + + it("transfer should preserve data if not provided", async () => { + const id = "0x" + "3".repeat(64); + const receiver = ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0xabcd", + }); + const typeScript = ccc.Script.from({ ...typeIdScript, args: id }); + const existingCell = ccc.Cell.from({ + outPoint: { txHash: "0x" + "4".repeat(64), index: 0 }, + cellOutput: { + capacity: ccc.fixedPointFrom(2000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + type: typeScript, + }, + outputData: "0x123456", + }); + + (client.findSingletonCellByType as Mock).mockResolvedValue( + existingCell, + ); + + const { tx, outIndex } = await transfer({ + client, + id, + receiver, + }); + + expect(tx.outputsData[outIndex]).toBe("0x123456"); + }); + + it("should transfer type id cell with data transformer", async () => { + const id = "0x" + "3".repeat(64); + const receiver = ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0xabcd", + }); + + const typeScript = ccc.Script.from({ + ...typeIdScript, + args: id, + }); + + const existingCell = ccc.Cell.from({ + outPoint: { + txHash: "0x" + "4".repeat(64), + index: 0, + }, + cellOutput: { + capacity: ccc.fixedPointFrom(2000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + type: typeScript, + }, + outputData: "0x1234", + }); + + (client.findSingletonCellByType as Mock).mockResolvedValue( + existingCell, + ); + + const { tx, outIndex } = await transfer({ + client, + id, + receiver, + data: (c, d) => ccc.bytesConcat(c.outputData, d ?? "0x", "0x5678"), + }); + + const output = tx.outputs[outIndex]; + expect(output.lock).toEqual(receiver); + expect(output.type).toEqual(typeScript); + expect(tx.outputsData[outIndex]).toBe(ccc.hexFrom("0x123412345678")); + }); + + it("should throw error if type id cell not found", async () => { + (client.findSingletonCellByType as Mock).mockResolvedValue(undefined); + + await expect( + transfer({ + client, + id: "0x" + "0".repeat(64), + receiver: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + }), + ).rejects.toThrow("Type ID"); + }); + }); + + describe("destroyTypeId", () => { + it("should consume type id cell without creating new one", async () => { + const id = "0x" + "5".repeat(64); + const typeScript = ccc.Script.from({ + ...typeIdScript, + args: id, + }); + + const existingCell = ccc.Cell.from({ + outPoint: { + txHash: "0x" + "6".repeat(64), + index: 0, + }, + cellOutput: { + capacity: ccc.fixedPointFrom(3000), + lock: ccc.Script.from({ + codeHash: "0x" + "0".repeat(64), + hashType: "type", + args: "0x", + }), + type: typeScript, + }, + outputData: "0x", + }); + + (client.findSingletonCellByType as Mock).mockResolvedValue( + existingCell, + ); + + const { tx, index } = await destroy({ + client, + id, + }); + + expect(tx.inputs[index].previousOutput).toEqual(existingCell.outPoint); + + const hasTypeOutput = tx.outputs.some((o) => o.type?.eq(typeScript)); + expect(hasTypeOutput).toBe(false); + }); + }); + }); +}); diff --git a/packages/type-id/src/advancedBarrel.ts b/packages/type-id/src/advancedBarrel.ts new file mode 100644 index 000000000..29a171d4c --- /dev/null +++ b/packages/type-id/src/advancedBarrel.ts @@ -0,0 +1,229 @@ +import { ccc } from "@ckb-ccc/core"; + +/** + * Build Type ID operations. + * + * @param props The properties to build the operations. + * @param props.getScriptInfo Function to get the script info. + * @param props.calculateTypeId Function to calculate the Type ID. + * @param props.addCellDeps Function to add cell dependencies. + */ + +export function buildTypeIdOperations< + Encodable = ccc.BytesLike, + Decoded = ccc.Bytes, +>(props: { + getScriptInfo: (client: ccc.Client) => Promise; + codec?: ccc.CodecLike | null; + calculateTypeId?: + | ((client: ccc.Client, tx: ccc.Transaction) => Promise) + | null; + addCellDeps?: + | (( + client: ccc.Client, + tx: ccc.Transaction, + scriptInfo: ccc.ScriptInfo, + ) => Promise) + | null; +}) { + async function getScriptInfo(client: ccc.Client): Promise { + return ccc.ScriptInfo.from(await props.getScriptInfo(client)); + } + + const codec = ( + props.codec ? ccc.Codec.from(props.codec) : ccc.CodecRaw + ) as ccc.Codec; + + function getTypeScript(scriptInfo: ccc.ScriptInfo, args: ccc.HexLike) { + return ccc.Script.from({ + ...scriptInfo, + args, + }); + } + + async function addCellDeps( + client: ccc.Client, + tx: ccc.Transaction, + scriptInfo: ccc.ScriptInfo, + ): Promise { + if (props.addCellDeps) { + return ccc.Transaction.from( + await props.addCellDeps(client, tx, scriptInfo), + ); + } + + tx.addCellDeps(...(await client.getCellDeps(scriptInfo.cellDeps))); + return tx; + } + + async function calculateTypeId( + client: ccc.Client, + tx: ccc.Transaction, + ): Promise { + if (props.calculateTypeId) { + return ccc.hexFrom(await props.calculateTypeId(client, tx)); + } + + return ccc.hashTypeId(tx.inputs[0], tx.outputs.length); + } + + return { + /** + * Create a Type ID cell. + * + * @param props The arguments for creating the cell. + * @param props.signer The signer to sign the transaction. + * @param props.receiver The receiver script (optional). + * @param props.data The output data. + * @param props.tx The transaction skeleton (optional). + */ + async create( + this: void, + props: { + signer: ccc.Signer; + data: Encodable; + receiver?: ccc.ScriptLike | null; + tx?: ccc.TransactionLike | null; + }, + ): Promise<{ + tx: ccc.Transaction; + id: ccc.Hex; + index: number; + }> { + const { signer, receiver, data, tx: txLike } = props; + const tx = ccc.Transaction.from(txLike ?? {}); + + await tx.completeInputsAtLeastOne(signer); + const id = await calculateTypeId(signer.client, tx); + + const scriptInfo = await getScriptInfo(signer.client); + const len = tx.addOutput({ + cellOutput: { + type: getTypeScript(scriptInfo, id), + lock: receiver + ? ccc.Script.from(receiver) + : (await signer.getRecommendedAddressObj()).script, + }, + outputData: codec.encode(data), + }); + + return { + tx: await addCellDeps(signer.client, tx, scriptInfo), + id, + index: len - 1, + }; + }, + + /** + * Transfer a Type ID cell. + * + * @param props The arguments for transferring the cell. + * @param props.client The client to communicate with CKB. + * @param props.id The Type ID to transfer. + * @param props.receiver The new receiver script. + * @param props.tx The transaction skeleton (optional). + * @param props.data The new output data or a transformer to update the data (optional). + */ + async transfer( + this: void, + props: { + client: ccc.Client; + id: ccc.HexLike; + receiver: ccc.ScriptLike; + tx?: ccc.TransactionLike | null; + data?: + | Encodable + | ((cell: ccc.Cell, data?: Decoded) => Encodable | Promise) + | null; + }, + ): Promise<{ + tx: ccc.Transaction; + inIndex: number; + outIndex: number; + }> { + const { client, id, receiver, tx: txLike, data } = props; + const tx = ccc.Transaction.from(txLike ?? {}); + + const scriptInfo = await getScriptInfo(client); + const type = getTypeScript(scriptInfo, id); + const inCell = await client.findSingletonCellByType(type); + if (!inCell) { + throw new Error(`Type ID ${ccc.stringify(type)} not found`); + } + + const outputData = await (async () => { + if (!data) { + return inCell.outputData; + } + + if (typeof data === "function") { + return codec.encode( + await ( + data as ( + cell: ccc.Cell, + data?: Decoded, + ) => Encodable | Promise + )(inCell, codec.decodeOr(inCell.outputData, undefined)), + ); + } + + return codec.encode(data); + })(); + + const outCell = ccc.CellAny.from({ + ...inCell, + cellOutput: { + ...inCell.cellOutput, + lock: ccc.Script.from(receiver), + }, + outputData, + }); + + const inLen = tx.addInput(inCell); + const outLen = tx.addOutput(outCell); + + return { + tx: await addCellDeps(client, tx, scriptInfo), + inIndex: inLen - 1, + outIndex: outLen - 1, + }; + }, + + /** + * Destroy a Type ID cell. + * + * @param props The arguments for destroying the cell. + * @param props.client The client to communicate with CKB. + * @param props.id The Type ID to destroy. + * @param props.tx The transaction skeleton (optional). + */ + async destroy( + this: void, + props: { + client: ccc.Client; + id: ccc.HexLike; + tx?: ccc.TransactionLike | null; + }, + ): Promise<{ + tx: ccc.Transaction; + index: number; + }> { + const { client, id, tx: txLike } = props; + const tx = ccc.Transaction.from(txLike ?? {}); + + const scriptInfo = await getScriptInfo(client); + const type = getTypeScript(scriptInfo, id); + const cell = await client.findSingletonCellByType(type); + if (!cell) { + throw new Error(`Type ID ${ccc.stringify(type)} not found`); + } + + const len = tx.addInput(cell); + + return { + tx: await addCellDeps(client, tx, scriptInfo), + index: len - 1, + }; + }, + }; +} diff --git a/packages/type-id/src/barrel.ts b/packages/type-id/src/barrel.ts new file mode 100644 index 000000000..f948bf58e --- /dev/null +++ b/packages/type-id/src/barrel.ts @@ -0,0 +1,12 @@ +import { ccc } from "@ckb-ccc/core"; +import { buildTypeIdOperations } from "./advancedBarrel"; + +export const { + create: createTypeId, + transfer: transferTypeId, + destroy: destroyTypeId, +} = buildTypeIdOperations({ + async getScriptInfo(client: ccc.Client) { + return client.getKnownScript(ccc.KnownScript.TypeId); + }, +}); diff --git a/packages/type-id/src/index.ts b/packages/type-id/src/index.ts new file mode 100644 index 000000000..6a528c1af --- /dev/null +++ b/packages/type-id/src/index.ts @@ -0,0 +1,2 @@ +export * from "./barrel.js"; +export * as typeId from "./barrel.js"; diff --git a/packages/type-id/tsconfig.base.json b/packages/type-id/tsconfig.base.json new file mode 100644 index 000000000..7e5ac952b --- /dev/null +++ b/packages/type-id/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2020", + "incremental": true, + "allowJs": true, + "importHelpers": false, + "declaration": true, + "declarationMap": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/type-id/tsconfig.commonjs.json b/packages/type-id/tsconfig.commonjs.json new file mode 100644 index 000000000..76a25e98b --- /dev/null +++ b/packages/type-id/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/type-id/tsconfig.json b/packages/type-id/tsconfig.json new file mode 100644 index 000000000..df22faeca --- /dev/null +++ b/packages/type-id/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + } +} diff --git a/packages/type-id/tsdown.config.mts b/packages/type-id/tsdown.config.mts new file mode 100644 index 000000000..9f79d88fe --- /dev/null +++ b/packages/type-id/tsdown.config.mts @@ -0,0 +1,36 @@ +import { defineConfig } from "tsdown"; + +const common = { + minify: true, + dts: true, + platform: "neutral" as const, + exports: true, +}; + +export default defineConfig( + ( + [ + { + entry: { + index: "src/index.ts", + barrel: "src/barrel.ts", + advanced: "src/advanced.ts", + advancedBarrel: "src/advancedBarrel.ts", + }, + format: "esm", + copy: "./misc/basedirs/dist/*", + }, + { + entry: { + index: "src/index.ts", + barrel: "src/barrel.ts", + advanced: "src/advanced.ts", + advancedBarrel: "src/advancedBarrel.ts", + }, + format: "cjs", + outDir: "dist.commonjs", + copy: "./misc/basedirs/dist.commonjs/*", + }, + ] as const + ).map((c) => ({ ...c, ...common })), +); diff --git a/packages/type-id/typedoc.json b/packages/type-id/typedoc.json new file mode 100644 index 000000000..2eb611e3e --- /dev/null +++ b/packages/type-id/typedoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts", "./src/advanced.ts"], + "extends": ["../../typedoc.base.json"], + "name": "@ckb-ccc type-id" +} diff --git a/packages/type-id/vitest.config.mts b/packages/type-id/vitest.config.mts new file mode 100644 index 000000000..dc6a58785 --- /dev/null +++ b/packages/type-id/vitest.config.mts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + coverage: { + include: ["src/**/*.ts"], + }, + }, +}); diff --git a/packages/udt/CHANGELOG.md b/packages/udt/CHANGELOG.md index 47b54f837..749c96c2f 100644 --- a/packages/udt/CHANGELOG.md +++ b/packages/udt/CHANGELOG.md @@ -1,5 +1,12 @@ # @ckb-ccc/udt +## 0.1.23 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + - @ckb-ccc/ssri@0.2.21 + ## 0.1.22 ### Patch Changes diff --git a/packages/udt/package.json b/packages/udt/package.json index 5bcfa1b4b..8aa9778da 100644 --- a/packages/udt/package.json +++ b/packages/udt/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/udt", - "version": "0.1.22", + "version": "0.1.23", "description": "UDT", "author": "Alive24 ", "license": "MIT", diff --git a/packages/udt/prettier.config.mjs b/packages/udt/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/udt/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/uni-sat/CHANGELOG.md b/packages/uni-sat/CHANGELOG.md index 440253c5d..593160e1c 100644 --- a/packages/uni-sat/CHANGELOG.md +++ b/packages/uni-sat/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/uni-sat +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/uni-sat/package.json b/packages/uni-sat/package.json index ccece3532..26f73d490 100644 --- a/packages/uni-sat/package.json +++ b/packages/uni-sat/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/uni-sat", - "version": "1.0.30", + "version": "1.0.31", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for UniSat", "author": "Hanssen0 ", "license": "MIT", diff --git a/packages/uni-sat/prettier.config.mjs b/packages/uni-sat/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/uni-sat/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/uni-sat/src/advancedBarrel.ts b/packages/uni-sat/src/advancedBarrel.ts index e6ae56b58..d40e8d860 100644 --- a/packages/uni-sat/src/advancedBarrel.ts +++ b/packages/uni-sat/src/advancedBarrel.ts @@ -1,7 +1,26 @@ +import { ccc } from "@ckb-ccc/core"; + /** * Interface representing a provider for interacting with accounts and signing messages. */ export interface Provider { + /** + * Signs a PSBT using UniSat wallet. + * + * @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 hex string + */ + signPsbt(psbtHex: string, options?: ccc.SignPsbtOptionsLike): Promise; + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of the signed PSBT to broadcast. + * @returns A promise that resolves to the transaction ID. + */ + pushPsbt(psbtHex: string): Promise; + /** * Requests user accounts. * @returns A promise that resolves to an array of account addresses. diff --git a/packages/uni-sat/src/signer.ts b/packages/uni-sat/src/signer.ts index 653bba8ee..938c0b89f 100644 --- a/packages/uni-sat/src/signer.ts +++ b/packages/uni-sat/src/signer.ts @@ -150,4 +150,34 @@ export class Signer extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + /** + * Signs a PSBT using UniSat wallet. + * + * @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 + */ + async signPsbt( + psbtHex: ccc.HexLike, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + return ccc.hexFrom( + await this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options), + ); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast. + * @returns A promise that resolves to the transaction ID as a Hex string + */ + async broadcastPsbt( + psbtHex: ccc.HexLike, + _options?: ccc.SignPsbtOptionsLike, + ): Promise { + const txid = await this.provider.pushPsbt(ccc.hexFrom(psbtHex).slice(2)); + return ccc.hexFrom(txid); + } } diff --git a/packages/utxo-global/CHANGELOG.md b/packages/utxo-global/CHANGELOG.md index ff47c5b69..dda717b97 100644 --- a/packages/utxo-global/CHANGELOG.md +++ b/packages/utxo-global/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/utxo-global +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/utxo-global/package.json b/packages/utxo-global/package.json index 3f7eb39c1..35137300b 100644 --- a/packages/utxo-global/package.json +++ b/packages/utxo-global/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/utxo-global", - "version": "1.0.30", + "version": "1.0.31", "description": "Common Chains Connector's support for UTXO Global", "author": "Trong Dinh ", "license": "MIT", diff --git a/packages/utxo-global/prettier.config.mjs b/packages/utxo-global/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/utxo-global/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/utxo-global/src/btc/index.ts b/packages/utxo-global/src/btc/index.ts index 57e73594d..67771340b 100644 --- a/packages/utxo-global/src/btc/index.ts +++ b/packages/utxo-global/src/btc/index.ts @@ -127,4 +127,32 @@ export class SignerBtc extends ccc.SignerBtc { this.accountCache ?? (await this.getBtcAccount()), ); } + + /** + * Signs a PSBT using UTXO Global wallet. + * + * @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 + */ + async signPsbt( + _psbtHex: ccc.HexLike, + _options?: ccc.SignPsbtOptionsLike, + ): Promise { + throw new Error("UTXO Global PSBT signing not implemented yet"); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast. + * @returns A promise that resolves to the transaction ID as a Hex string + * @todo Implement PSBT broadcasting with UTXO Global + */ + async broadcastPsbt( + _psbtHex: ccc.HexLike, + _options?: ccc.SignPsbtOptionsLike, + ): Promise { + throw new Error("UTXO Global PSBT broadcasting not implemented yet"); + } } diff --git a/packages/xverse/CHANGELOG.md b/packages/xverse/CHANGELOG.md index a0101e509..c059d1d42 100644 --- a/packages/xverse/CHANGELOG.md +++ b/packages/xverse/CHANGELOG.md @@ -1,5 +1,11 @@ # @ckb-ccc/xverse +## 1.0.31 +### Patch Changes + +- Updated dependencies [[`b4aa99f`](https://github.com/ckb-devrel/ccc/commit/b4aa99f1b87c1d14117a15fa1fcac6f9e60b43c1)]: + - @ckb-ccc/core@1.12.4 + ## 1.0.30 ### Patch Changes diff --git a/packages/xverse/package.json b/packages/xverse/package.json index 821cc5db8..360f87354 100644 --- a/packages/xverse/package.json +++ b/packages/xverse/package.json @@ -1,6 +1,6 @@ { "name": "@ckb-ccc/xverse", - "version": "1.0.30", + "version": "1.0.31", "description": "CCC - CKBer's Codebase. Common Chains Connector's support for Xverse", "author": "Hanssen0 ", "license": "MIT", @@ -57,6 +57,7 @@ }, "dependencies": { "@ckb-ccc/core": "workspace:*", + "bitcoinjs-lib": "^7.0.0", "valibot": "^1.1.0" }, "packageManager": "pnpm@10.8.1" diff --git a/packages/xverse/prettier.config.mjs b/packages/xverse/prettier.config.mjs new file mode 100644 index 000000000..af1401cbb --- /dev/null +++ b/packages/xverse/prettier.config.mjs @@ -0,0 +1,13 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: ["prettier-plugin-organize-imports"], +}; + +export default config; \ No newline at end of file diff --git a/packages/xverse/src/signer.ts b/packages/xverse/src/signer.ts index af243a11a..2585ce771 100644 --- a/packages/xverse/src/signer.ts +++ b/packages/xverse/src/signer.ts @@ -1,4 +1,5 @@ import { ccc } from "@ckb-ccc/core"; +import { Psbt } from "bitcoinjs-lib"; import * as v from "valibot"; import { Address, @@ -169,4 +170,160 @@ export class Signer extends ccc.SignerBtc { ) ).signature; } + + /** + * Build default inputsToSign for all unsigned inputs + */ + private buildDefaultinputsToSign( + psbtHex: ccc.Hex, + address: string, + ): ccc.InputToSignLike[] { + const inputsToSign: ccc.InputToSignLike[] = []; + + try { + // Collect all unsigned inputs + const psbt = Psbt.fromHex(psbtHex.slice(2)); + psbt.data.inputs.forEach((input, index) => { + const isSigned = + input.finalScriptSig || + input.finalScriptWitness || + input.tapKeySig || + (input.partialSig && input.partialSig.length > 0) || + (input.tapScriptSig && input.tapScriptSig.length > 0); + + if (!isSigned) { + inputsToSign.push({ index, address }); + } + }); + + // If no unsigned inputs found, the PSBT is already fully signed + // Let the wallet handle this case (likely a no-op or error) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to parse PSBT hex. Please provide inputsToSign explicitly in options. Original error: ${errorMessage}`, + ); + } + + return inputsToSign; + } + + private async prepareSignPsbtParams( + psbtHex: ccc.Hex, + options?: ccc.SignPsbtOptionsLike, + ): Promise<{ + psbtBase64: string; + signInputs: Record; + }> { + let inputsToSign = options?.inputsToSign; + + if (!inputsToSign || !inputsToSign.length) { + const address = await this.getBtcAccount(); + inputsToSign = this.buildDefaultinputsToSign(psbtHex, address); + } + + const psbtBase64 = ccc.bytesTo(psbtHex, "base64"); + + const signInputs = inputsToSign.reduce( + (acc, input) => { + if (!input.address) { + throw new Error( + "Xverse only supports signing with address. Please provide 'address' in inputsToSign.", + ); + } + if (acc[input.address]) { + acc[input.address].push(input.index); + } else { + acc[input.address] = [input.index]; + } + return acc; + }, + {} as Record, + ); + + return { psbtBase64, signInputs }; + } + + /** + * Signs a PSBT using Xverse wallet. + * + * @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 + * + * @remarks + * Xverse accepts: + * - psbt: A string representing the PSBT to sign, encoded in base64 + * - signInputs: A Record where: + * - keys are the addresses to use for signing + * - values are the indexes of the inputs to sign with each address + * + * Xverse returns: + * - psbt: The base64 encoded signed PSBT + * + * @see https://docs.xverse.app/sats-connect/bitcoin-methods/signpsbt + */ + async signPsbt( + psbtHex: ccc.HexLike, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + const { psbtBase64, signInputs } = await this.prepareSignPsbtParams( + ccc.hexFrom(psbtHex), + options, + ); + + const signedPsbtBase64 = ( + await checkResponse( + this.provider.request("signPsbt", { + psbt: psbtBase64, + signInputs, + broadcast: false, + }), + ) + ).psbt; + + return ccc.hexFrom(ccc.bytesFrom(signedPsbtBase64, "base64")); + } + + /** + * Broadcasts a PSBT to the Bitcoin network. + * + * @remarks + * Xverse does not support broadcasting a signed PSBT directly. + * It only supports "Sign and Broadcast" as a single atomic operation via `signAndBroadcastPsbt`. + */ + async broadcastPsbt( + _psbtHex: ccc.HexLike, + _options?: ccc.SignPsbtOptionsLike, + ): Promise { + throw new Error( + "Xverse does not support broadcasting signed PSBTs directly. Use signAndBroadcastPsbt instead.", + ); + } + + async signAndBroadcastPsbt( + psbtHex: ccc.HexLike, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + // ccc.hexFrom adds 0x prefix, but BTC expects non-0x + const { psbtBase64, signInputs } = await this.prepareSignPsbtParams( + ccc.hexFrom(psbtHex), + options, + ); + + const result = await checkResponse( + this.provider.request("signPsbt", { + psbt: psbtBase64, + signInputs, + broadcast: true, + }), + ); + + if (!result.txid) { + throw new Error("Failed to broadcast PSBT"); + } + + return ccc.hexFrom(result.txid); + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d52d483a7..8b8b02927 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -378,7 +378,7 @@ importers: version: 9.34.0(jiti@2.5.1) eslint-config-next: specifier: 16.0.10 - version: 16.0.10(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + version: 16.0.10(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.34.0(jiti@2.5.1)) @@ -404,6 +404,52 @@ importers: specifier: ^5.9.2 version: 5.9.2 + packages/did-ckb: + dependencies: + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + '@ckb-ccc/type-id': + specifier: workspace:* + version: link:../type-id + '@ipld/dag-cbor': + specifier: ^9.2.5 + version: 9.2.5 + devDependencies: + '@eslint/js': + specifier: ^9.34.0 + version: 9.34.0 + '@types/node': + specifier: ^24.3.0 + version: 24.3.0 + eslint: + specifier: ^9.34.0 + version: 9.34.0(jiti@2.5.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-prettier: + specifier: ^5.5.4 + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))(prettier@3.6.2) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-organize-imports: + specifier: ^4.2.0 + version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) + tsdown: + specifier: 0.19.0-beta.3 + version: 0.19.0-beta.3(synckit@0.11.11)(typescript@5.9.2) + typescript: + specifier: ^5.9.2 + version: 5.9.2 + typescript-eslint: + specifier: ^8.41.0 + version: 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + packages/docs: dependencies: '@docusaurus/core': @@ -486,6 +532,12 @@ importers: '@ckb-ccc/playground': specifier: file:src/playground version: playground@file:packages/examples/src/playground + '@noble/curves': + specifier: ^1.9.7 + version: 1.9.7 + '@noble/hashes': + specifier: ^1.8.0 + version: 1.8.0 devDependencies: '@eslint/js': specifier: ^9.34.0 @@ -813,6 +865,9 @@ importers: packages/playground: dependencies: + '@bitcoinerlab/secp256k1': + specifier: ^1.2.0 + version: 1.2.0 '@ckb-ccc/ccc': specifier: workspace:* version: link:../ccc @@ -843,6 +898,9 @@ importers: bech32: specifier: ^2.0.0 version: 2.0.0 + bitcoinjs-lib: + specifier: ^7.0.0 + version: 7.0.0(typescript@5.9.2) isomorphic-ws: specifier: ^5.0.0 version: 5.0.0(ws@8.18.3) @@ -959,12 +1017,18 @@ importers: '@ckb-ccc/core': specifier: workspace:* version: link:../core + '@ckb-ccc/did-ckb': + specifier: workspace:* + version: link:../did-ckb '@ckb-ccc/spore': specifier: workspace:* version: link:../spore '@ckb-ccc/ssri': specifier: workspace:* version: link:../ssri + '@ckb-ccc/type-id': + specifier: workspace:* + version: link:../type-id '@ckb-ccc/udt': specifier: workspace:* version: link:../udt @@ -1122,6 +1186,46 @@ importers: specifier: ^8.41.0 version: 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + packages/type-id: + dependencies: + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@eslint/js': + specifier: ^9.34.0 + version: 9.34.0 + '@types/node': + specifier: ^24.3.0 + version: 24.3.0 + eslint: + specifier: ^9.34.0 + version: 9.34.0(jiti@2.5.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-prettier: + specifier: ^5.5.4 + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))(prettier@3.6.2) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-organize-imports: + specifier: ^4.2.0 + version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) + tsdown: + specifier: 0.19.0-beta.3 + version: 0.19.0-beta.3(synckit@0.11.11)(typescript@5.9.2) + typescript: + specifier: ^5.9.2 + version: 5.9.2 + typescript-eslint: + specifier: ^8.41.0 + version: 8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + packages/udt: dependencies: '@ckb-ccc/core': @@ -1244,6 +1348,9 @@ importers: '@ckb-ccc/core': specifier: workspace:* version: link:../core + bitcoinjs-lib: + specifier: ^7.0.0 + version: 7.0.0(typescript@5.9.2) valibot: specifier: ^1.1.0 version: 1.1.0(typescript@5.9.2) @@ -1408,6 +1515,10 @@ packages: resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -1483,6 +1594,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -1504,6 +1619,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} @@ -2043,6 +2163,10 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2050,6 +2174,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bitcoinerlab/secp256k1@1.2.0': + resolution: {integrity: sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q==} + '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} @@ -2629,12 +2756,18 @@ packages: '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.0.4': resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.25.9': resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} @@ -3187,6 +3320,10 @@ packages: '@types/node': optional: true + '@ipld/dag-cbor@9.2.5': + resolution: {integrity: sha512-84wSr4jv30biui7endhobYhXBQzQE4c/wdoWlFrKcfiwH+ofaPg8fwsM8okX9cOzkkrsAsNdDyH3ou+kiLquwQ==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -3414,6 +3551,9 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nervina-labs/dob-render@0.2.5': resolution: {integrity: sha512-PZ5hcoTYfbdyI51xlHdK1j9VYTd6L1oYxEv9CzpYuu0AtkOqSNmrbn2FkI4noUJl+tqxqWSsJRju7JJiOApTpg==} peerDependencies: @@ -3612,6 +3752,9 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true + '@oxc-project/types@0.106.0': + resolution: {integrity: sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==} + '@paralleldrive/cuid2@2.2.2': resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} @@ -3638,6 +3781,9 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@react-aria/focus@3.21.1': resolution: {integrity: sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==} peerDependencies: @@ -3675,6 +3821,86 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@rolldown/binding-android-arm64@1.0.0-beta.58': + resolution: {integrity: sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.58': + resolution: {integrity: sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.58': + resolution: {integrity: sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.58': + resolution: {integrity: sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58': + resolution: {integrity: sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58': + resolution: {integrity: sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.58': + resolution: {integrity: sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.58': + resolution: {integrity: sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.58': + resolution: {integrity: sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.58': + resolution: {integrity: sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.58': + resolution: {integrity: sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58': + resolution: {integrity: sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.58': + resolution: {integrity: sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.58': + resolution: {integrity: sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==} + '@rollup/rollup-android-arm-eabi@4.49.0': resolution: {integrity: sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==} cpu: [arm] @@ -4065,6 +4291,9 @@ packages: '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4799,6 +5028,10 @@ packages: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -4871,6 +5104,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -4999,6 +5236,17 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bip174@3.0.0: + resolution: {integrity: sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==} + engines: {node: '>=18.0.0'} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + bitcoinjs-lib@7.0.0: + resolution: {integrity: sha512-2W6dGXFd1KG3Bs90Bzb5+ViCeSKNIYkCUWZ4cvUzUgwnneiNNZ6Sk8twGNcjlesmxC0JyLc/958QycfpvXLg7A==} + engines: {node: '>=18.0.0'} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -5148,6 +5396,10 @@ packages: caniuse-lite@1.0.30001737: resolution: {integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==} + cborg@4.3.2: + resolution: {integrity: sha512-l+QzebEAG0vb09YKkaOrMi2zmm80UNjmbvocMIeW5hO7JOXWdrQ/H49yOKfYX0MBgrj/KWgatBnEgRXyNyKD+A==} + hasBin: true + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5721,6 +5973,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -5840,6 +6095,15 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -5882,6 +6146,10 @@ packages: emoticon@4.1.0: resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -6517,6 +6785,9 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + github-slugger@1.5.0: resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} @@ -6707,6 +6978,9 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hookable@6.0.1: + resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -6843,6 +7117,10 @@ packages: engines: {node: '>=8'} hasBin: true + import-without-cache@0.2.5: + resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} + engines: {node: '>=20.19.0'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -8004,6 +8282,9 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true + multiformats@13.4.2: + resolution: {integrity: sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -8161,6 +8442,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -8967,6 +9251,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9267,6 +9554,30 @@ packages: engines: {node: 20 || >=22} hasBin: true + rolldown-plugin-dts@0.20.0: + resolution: {integrity: sha512-cLAY1kN2ilTYMfZcFlGWbXnu6Nb+8uwUBsi+Mjbh4uIx7IN8uMOmJ7RxrrRgPsO4H7eSz3E+JwGoL1gyugiyUA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-beta.57 + typescript: ^5.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-beta.58: + resolution: {integrity: sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.49.0: resolution: {integrity: sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -9837,6 +10148,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -9966,6 +10281,31 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tsdown@0.19.0-beta.3: + resolution: {integrity: sha512-Ud75SBmTap0kDf9hs31yBBlU0iAV17gtZgTJlW6nG/e4J6wXPXwQtUXt/Fck4XSmHXXgSuYRwGrjF6AxTLwk+Q==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@vitejs/devtools': '*' + publint: ^0.3.0 + typescript: ^5.0.0 + unplugin-lightningcss: ^0.4.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-lightningcss: + optional: true + unplugin-unused: + optional: true + tslib@2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -10113,10 +10453,21 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + uint8array-tools@0.0.8: + resolution: {integrity: sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==} + engines: {node: '>=14.0.0'} + + uint8array-tools@0.0.9: + resolution: {integrity: sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==} + engines: {node: '>=14.0.0'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + unconfig-core@7.4.2: + resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -10189,6 +10540,16 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + unrun@0.2.22: + resolution: {integrity: sha512-vlQce4gTLNyCZxGylEQXGG+fSrrEFWiM/L8aghtp+t6j8xXh+lmsBtQJknG7ZSvv7P+/MRgbQtHWHBWk981uTg==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} @@ -10246,6 +10607,14 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + valibot@0.38.0: + resolution: {integrity: sha512-RCJa0fetnzp+h+KN9BdgYOgtsMAG9bfoJ9JSjIhFHobKWVWyzM3jjaeNTdpFK9tQtf3q1sguXeERJ/LcmdFE7w==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + valibot@1.1.0: resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} peerDependencies: @@ -10264,6 +10633,9 @@ packages: value-equal@1.0.1: resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + varuint-bitcoin@2.0.0: + resolution: {integrity: sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -10830,6 +11202,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.2 @@ -10933,6 +11313,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.28.3': @@ -10959,6 +11341,10 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -11626,10 +12012,19 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@0.2.3': {} '@bcoe/v8-coverage@1.0.2': {} + '@bitcoinerlab/secp256k1@1.2.0': + dependencies: + '@noble/curves': 1.9.7 + '@borewit/text-codec@0.1.1': {} '@changesets/apply-release-plan@7.0.12': @@ -12962,6 +13357,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 @@ -12972,6 +13373,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.9': optional: true @@ -13442,6 +13848,11 @@ snapshots: optionalDependencies: '@types/node': 24.3.0 + '@ipld/dag-cbor@9.2.5': + dependencies: + cborg: 4.3.2 + multiformats: 13.4.2 + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -13833,6 +14244,13 @@ snapshots: '@tybys/wasm-util': 0.10.0 optional: true + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nervina-labs/dob-render@0.2.5(satori@0.10.14)': dependencies: satori: 0.10.14 @@ -14035,6 +14453,8 @@ snapshots: dependencies: consola: 3.4.2 + '@oxc-project/types@0.106.0': {} + '@paralleldrive/cuid2@2.2.2': dependencies: '@noble/hashes': 1.8.0 @@ -14058,6 +14478,10 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + '@react-aria/focus@3.21.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@react-aria/interactions': 3.25.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -14107,6 +14531,49 @@ snapshots: dependencies: react: 19.2.3 + '@rolldown/binding-android-arm64@1.0.0-beta.58': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.58': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.58': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.58': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.58': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.58': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.58': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.58': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.58': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.58': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.58': {} + '@rollup/rollup-android-arm-eabi@4.49.0': optional: true @@ -14476,6 +14943,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.3 @@ -15343,6 +15815,8 @@ snapshots: ansis@4.1.0: {} + ansis@4.2.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -15439,6 +15913,11 @@ snapshots: assertion-error@2.0.1: {} + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.28.5 + pathe: 2.0.3 + ast-types-flow@0.0.8: {} ast-v8-to-istanbul@0.3.4: @@ -15594,6 +16073,25 @@ snapshots: binary-extensions@2.3.0: {} + bip174@3.0.0: + dependencies: + uint8array-tools: 0.0.9 + varuint-bitcoin: 2.0.0 + + birpc@4.0.0: {} + + bitcoinjs-lib@7.0.0(typescript@5.9.2): + dependencies: + '@noble/hashes': 1.8.0 + bech32: 2.0.0 + bip174: 3.0.0 + bs58check: 4.0.0(patch_hash=0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26) + uint8array-tools: 0.0.9 + valibot: 0.38.0(typescript@5.9.2) + varuint-bitcoin: 2.0.0 + transitivePeerDependencies: + - typescript + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -15796,6 +16294,8 @@ snapshots: caniuse-lite@1.0.30001737: {} + cborg@4.3.2: {} + ccount@2.0.1: {} chai@5.3.3: @@ -16374,6 +16874,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.4: {} + delayed-stream@1.0.0: {} depd@1.1.2: {} @@ -16388,8 +16890,7 @@ snapshots: detect-libc@2.0.4: {} - detect-libc@2.1.2: - optional: true + detect-libc@2.1.2: {} detect-newline@3.1.0: {} @@ -16486,6 +16987,8 @@ snapshots: dotenv@8.6.0: {} + dts-resolver@2.1.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -16524,6 +17027,8 @@ snapshots: emoticon@4.1.0: {} + empathic@2.0.0: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -16713,28 +17218,8 @@ snapshots: '@next/eslint-plugin-next': 16.0.10 eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.34.0(jiti@2.5.1)) - eslint-plugin-react: 7.37.5(eslint@9.34.0(jiti@2.5.1)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.34.0(jiti@2.5.1)) - globals: 16.4.0 - typescript-eslint: 8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - - eslint-config-next@16.0.10(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2): - dependencies: - '@next/eslint-plugin-next': 16.0.10 - eslint: 9.34.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.34.0(jiti@2.5.1)) @@ -16769,22 +17254,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 - eslint: 9.34.0(jiti@2.5.1) - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.14 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -16792,36 +17262,25 @@ snapshots: get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.2) eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -16832,7 +17291,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.34.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -16850,35 +17309,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.34.0(jiti@2.5.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.34.0(jiti@2.5.1)): dependencies: aria-query: 5.3.2 @@ -17564,6 +17994,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + github-slugger@1.5.0: {} glob-parent@5.1.2: @@ -17840,7 +18274,7 @@ snapshots: history@4.10.1: dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 loose-envify: 1.4.0 resolve-pathname: 3.0.0 tiny-invariant: 1.3.3 @@ -17857,6 +18291,8 @@ snapshots: dependencies: react-is: 16.13.1 + hookable@6.0.1: {} + hosted-git-info@2.8.9: {} hpack.js@2.1.6: @@ -18002,6 +18438,8 @@ snapshots: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 + import-without-cache@0.2.5: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -18780,7 +19218,7 @@ snapshots: lightningcss@1.30.1: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optionalDependencies: lightningcss-darwin-arm64: 1.30.1 lightningcss-darwin-x64: 1.30.1 @@ -19550,6 +19988,8 @@ snapshots: dns-packet: 5.6.1 thunky: 1.1.0 + multiformats@13.4.2: {} + mute-stream@2.0.0: {} nanoassert@2.0.0: {} @@ -19697,6 +20137,8 @@ snapshots: obuf@1.1.2: {} + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -20476,6 +20918,8 @@ snapshots: quansync@0.2.11: {} + quansync@1.0.0: {} + queue-microtask@1.2.3: {} quick-lru@5.1.1: {} @@ -20866,6 +21310,41 @@ snapshots: glob: 11.0.3 package-json-from-dist: 1.0.1 + rolldown-plugin-dts@0.20.0(rolldown@1.0.0-beta.58)(typescript@5.9.2): + dependencies: + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + ast-kit: 2.2.0 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.13.0 + obug: 2.1.1 + rolldown: 1.0.0-beta.58 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0-beta.58: + dependencies: + '@oxc-project/types': 0.106.0 + '@rolldown/pluginutils': 1.0.0-beta.58 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.58 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.58 + '@rolldown/binding-darwin-x64': 1.0.0-beta.58 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.58 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.58 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.58 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.58 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.58 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.58 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.58 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.58 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.58 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.58 + rollup@4.49.0: dependencies: '@types/estree': 1.0.8 @@ -21606,6 +22085,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.14: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -21726,6 +22207,33 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsdown@0.19.0-beta.3(synckit@0.11.11)(typescript@5.9.2): + dependencies: + ansis: 4.2.0 + cac: 6.7.14 + defu: 6.1.4 + empathic: 2.0.0 + hookable: 6.0.1 + import-without-cache: 0.2.5 + obug: 2.1.1 + picomatch: 4.0.3 + rolldown: 1.0.0-beta.58 + rolldown-plugin-dts: 0.20.0(rolldown@1.0.0-beta.58)(typescript@5.9.2) + semver: 7.7.3 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + unconfig-core: 7.4.2 + unrun: 0.2.22(synckit@0.11.11) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - synckit + - vue-tsc + tslib@2.3.1: {} tslib@2.7.0: {} @@ -21735,7 +22243,7 @@ snapshots: tsx@4.20.5: dependencies: esbuild: 0.25.9 - get-tsconfig: 4.10.1 + get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -21875,6 +22383,10 @@ snapshots: uint8array-extras@1.5.0: {} + uint8array-tools@0.0.8: {} + + uint8array-tools@0.0.9: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -21882,6 +22394,11 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + unconfig-core@7.4.2: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + uncrypto@0.1.3: {} undici-types@6.19.8: {} @@ -21977,6 +22494,12 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + unrun@0.2.22(synckit@0.11.11): + dependencies: + rolldown: 1.0.0-beta.58 + optionalDependencies: + synckit: 0.11.11 + untildify@4.0.0: {} update-browserslist-db@1.1.3(browserslist@4.25.3): @@ -22037,6 +22560,10 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@0.38.0(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + valibot@1.1.0(typescript@5.9.2): optionalDependencies: typescript: 5.9.2 @@ -22050,6 +22577,10 @@ snapshots: value-equal@1.0.1: {} + varuint-bitcoin@2.0.0: + dependencies: + uint8array-tools: 0.0.8 + vary@1.1.2: {} vfile-location@5.0.3: diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 515df9321..35f05db43 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -4,9 +4,11 @@ const config = { name: "CCC Docs", entryPoints: [ "packages/core", + "packages/type-id", "packages/ssri", "packages/udt", "packages/spore", + "packages/did-ckb", "packages/shell", "packages/ccc", "packages/connector", diff --git a/vitest.config.mts b/vitest.config.mts index 7af3d4a84..9b64c1358 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,10 +1,12 @@ import { defineConfig, coverageConfigDefaults } from "vitest/config"; +const packages = ["packages/core", "packages/did-ckb", "packages/type-id"]; + export default defineConfig({ test: { - projects: ["packages/core"], + projects: packages, coverage: { - include: ["packages/core"], + include: packages, exclude: [ "**/dist/**", "**/dist.commonjs/**",