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/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/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/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/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-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/package.json b/package.json index b615afd34..04295ecf4 100644 --- a/package.json +++ b/package.json @@ -48,4 +48,4 @@ ] }, "packageManager": "pnpm@10.8.1" -} +} \ No newline at end of file diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index f8d8d5e2a..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, ); diff --git a/packages/core/src/client/clientPublicMainnet.advanced.ts b/packages/core/src/client/clientPublicMainnet.advanced.ts index bdcec4cc1..65775872a 100644 --- a/packages/core/src/client/clientPublicMainnet.advanced.ts +++ b/packages/core/src/client/clientPublicMainnet.advanced.ts @@ -480,4 +480,72 @@ export const MAINNET_SCRIPTS: Record = }, ], }, + [KnownScript.RgbppLock]: { + codeHash: + "0xbc6c568a1a0d0a09f6844dc9d74ddb4343c32143ff25f727c59edf4fb72d6936", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xcb4d9f9726e66306bfda6359d39d3bea8b4e5345d0f95f26a3e51626ebe82a63", + index: 0, + }, + depType: "code", + }, + type: { + codeHash: + "0x00000000000000000000000000000000000000000000000000545950455f4944", + hashType: "type", + args: "0x68ad3d9e0bb9ea841a5d1fcd600137bd3f45401e759e353121f26cd0d981452f", + }, + }, + // Rgbpp lock config cell dep + { + cellDep: { + outPoint: { + txHash: + "0xcb4d9f9726e66306bfda6359d39d3bea8b4e5345d0f95f26a3e51626ebe82a63", + index: 1, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.BtcTimeLock]: { + codeHash: + "0x70d64497a075bd651e98ac030455ea200637ee325a12ad08aff03f1a117e5a62", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x3d1c26b966504b09253ad84173bf3baa7b8135c5ff520c32cf70b631c1d08b9b", + index: 0, + }, + depType: "code", + }, + type: { + codeHash: + "0x00000000000000000000000000000000000000000000000000545950455f4944", + hashType: "type", + args: "0x44b8253ae18e913a2845b0d548eaf6b3ba1099ed26835888932a754194028a8a", + }, + }, + // btc time lock config cell dep + { + cellDep: { + outPoint: { + txHash: + "0x3d1c26b966504b09253ad84173bf3baa7b8135c5ff520c32cf70b631c1d08b9b", + index: 1, + }, + depType: "code", + }, + }, + ], + }, }); diff --git a/packages/core/src/client/clientPublicTestnet.advanced.ts b/packages/core/src/client/clientPublicTestnet.advanced.ts index 9453d2523..47868e347 100644 --- a/packages/core/src/client/clientPublicTestnet.advanced.ts +++ b/packages/core/src/client/clientPublicTestnet.advanced.ts @@ -492,4 +492,72 @@ export const TESTNET_SCRIPTS: Record = }, ], }, + [KnownScript.RgbppLock]: { + codeHash: + "0x61ca7a4796a4eb19ca4f0d065cb9b10ddcf002f10f7cbb810c706cb6bb5c3248", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x0d1567da0979f78b297d5311442669fbd1bd853c8be324c5ab6da41e7a1ed6e5", + index: 0, + }, + depType: "code", + }, + type: { + codeHash: + "0x00000000000000000000000000000000000000000000000000545950455f4944", + hashType: "type", + args: "0xa3bc8441df149def76cfe15fec7b1e51d949548bc27fb7a75e9d4b3ef1c12c7f", + }, + }, + // Rgbpp lock config cell dep for Bitcoin Testnet3 + { + cellDep: { + outPoint: { + txHash: + "0x0d1567da0979f78b297d5311442669fbd1bd853c8be324c5ab6da41e7a1ed6e5", + index: 1, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.BtcTimeLock]: { + codeHash: + "0x00cdf8fab0f8ac638758ebf5ea5e4052b1d71e8a77b9f43139718621f6849326", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x8fb747ff0416a43e135c583b028f98c7b81d3770551b196eb7ba1062dd9acc94", + index: 0, + }, + depType: "code", + }, + type: { + codeHash: + "0x00000000000000000000000000000000000000000000000000545950455f4944", + hashType: "type", + args: "0xc9828585e6dd2afacb9e6e8ca7deb0975121aabee5c7983178a45509ffaec984", + }, + }, + // btc time lock config cell dep for Bitcoin Testnet3 + { + cellDep: { + outPoint: { + txHash: + "0x8fb747ff0416a43e135c583b028f98c7b81d3770551b196eb7ba1062dd9acc94", + index: 1, + }, + depType: "code", + }, + }, + ], + }, }); diff --git a/packages/core/src/client/jsonRpc/client.ts b/packages/core/src/client/jsonRpc/client.ts index 55790e919..3b1d837b4 100644 --- a/packages/core/src/client/jsonRpc/client.ts +++ b/packages/core/src/client/jsonRpc/client.ts @@ -131,9 +131,9 @@ export abstract class ClientJsonRpc extends Client { getFeeRateStatistics = this.buildSender( "get_fee_rate_statistics", [(n: NumLike) => apply(numFrom, n)], - ({ mean, median }: { mean: NumLike; median: NumLike }) => ({ - mean: numFrom(mean), - median: numFrom(median), + (res: { mean: NumLike; median: NumLike } | null | undefined) => ({ + mean: apply(numFrom, res?.mean), + median: apply(numFrom, res?.median), }), ) as Client["getFeeRateStatistics"]; diff --git a/packages/core/src/client/knownScript.ts b/packages/core/src/client/knownScript.ts index 90a1546fe..b32e8f55c 100644 --- a/packages/core/src/client/knownScript.ts +++ b/packages/core/src/client/knownScript.ts @@ -26,4 +26,8 @@ export enum KnownScript { TypeBurnLock = "TypeBurnLock", EasyToDiscoverType = "EasyToDiscoverType", TimeLock = "TimeLock", + + // RGB++ related scripts + RgbppLock = "RgbppLock", + BtcTimeLock = "BtcTimeLock", } diff --git a/packages/core/src/hex/index.ts b/packages/core/src/hex/index.ts index 41df43d76..88a32d489 100644 --- a/packages/core/src/hex/index.ts +++ b/packages/core/src/hex/index.ts @@ -13,8 +13,33 @@ export type Hex = `0x${string}`; export type HexLike = BytesLike; /** - * Converts a HexLike value to a Hex string. - * @public + * Determines whether a given value is a properly formatted hexadecimal string (ccc.Hex). + * + * A valid hexadecimal string: + * - Has at least two characters. + * - Starts with "0x". + * - Has an even length. + * - Contains only characters representing digits (0-9) or lowercase letters (a-f) after the "0x" prefix. + * + * @param v - The value to validate as a hexadecimal (ccc.Hex) string. + * @returns True if the string is a valid hex string, false otherwise. + */ +export function isHex(v: unknown): v is Hex { + if (!(typeof v === "string" && v.length % 2 === 0 && v.startsWith("0x"))) { + return false; + } + + for (let i = 2; i < v.length; i++) { + const c = v.charAt(i); + if (!(("0" <= c && c <= "9") || ("a" <= c && c <= "f"))) { + return false; + } + } + return true; +} + +/** + * Returns the hexadecimal representation of the given value. * * @param hex - The value to convert, which can be a string, Uint8Array, ArrayBuffer, or number array. * @returns A Hex string representing the value. @@ -26,5 +51,10 @@ export type HexLike = BytesLike; * ``` */ export function hexFrom(hex: HexLike): Hex { + // Passthru an already normalized hex. V8 optimization: maintain existing hidden string fields. + if (isHex(hex)) { + return hex; + } + return `0x${bytesTo(bytesFrom(hex), "hex")}`; } diff --git a/packages/core/src/num/index.ts b/packages/core/src/num/index.ts index 8e1905a52..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/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 f75bdb8ab..6abad2eec 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -100,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) { diff --git a/packages/demo/package.json b/packages/demo/package.json index b96bf67a2..96ca3cf04 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -54,4 +54,4 @@ "typescript": "^5.9.2" }, "packageManager": "pnpm@10.8.1" -} +} \ No newline at end of file diff --git a/packages/playground/package.json b/packages/playground/package.json index 6bffe4f4f..258c4c6fb 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -22,7 +22,7 @@ "@shikijs/monaco": "^3.12.0", "axios": "^1.11.0", "bech32": "^2.0.0", - "bitcoinjs-lib": "^7.0.0", + "bitcoinjs-lib": "^7.0.1", "isomorphic-ws": "^5.0.0", "lucide-react": "^0.542.0", "monaco-editor": "^0.52.2", @@ -51,4 +51,4 @@ "tailwindcss": "^4.1.12" }, "packageManager": "pnpm@10.8.1" -} +} \ No newline at end of file diff --git a/packages/rgbpp/.prettierignore b/packages/rgbpp/.prettierignore new file mode 100644 index 000000000..611e372bf --- /dev/null +++ b/packages/rgbpp/.prettierignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Testing +coverage +__snapshots__ + +# Production +build +dist +lib + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.idea +.vscode + +# Cache +.cache +.eslintcache diff --git a/packages/rgbpp/eslint.config.mjs b/packages/rgbpp/eslint.config.mjs new file mode 100644 index 000000000..b6132c277 --- /dev/null +++ b/packages/rgbpp/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/rgbpp/package.json b/packages/rgbpp/package.json new file mode 100644 index 000000000..13e6f1e09 --- /dev/null +++ b/packages/rgbpp/package.json @@ -0,0 +1,57 @@ +{ + "name": "@ckb-ccc/rgbpp", + "version": "1.0.0", + "description": "RGB++ for CKB", + "types": "dist/index.d.ts", + "source": "src/index.ts", + "type": "module", + "main": "dist.commonjs/index.js", + "module": "dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist.commonjs/index.js", + "default": "./dist.commonjs/index.js" + } + }, + "scripts": { + "test": "jest", + "build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json", + "lint": "eslint ./src", + "format": "prettier --write . && eslint --fix ./src" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@ckb-ccc/udt": "workspace:*", + "@eslint/js": "^9.34.0", + "@exact-realty/multipart-parser": "^1.0.13", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.14", + "@types/node": "^22.10.6", + "copyfiles": "^2.4.1", + "dotenv": "^16.4.7", + "eslint": "^9.34.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.2.0", + "rimraf": "^6.0.1", + "ts-jest": "^29.2.5", + "tsx": "^4.20.6", + "typescript": "^5.7.3", + "typescript-eslint": "^8.41.0" + }, + "dependencies": { + "@bitcoinerlab/secp256k1": "1.1.1", + "@noble/hashes": "^1.4.0", + "@ckb-ccc/core": "workspace:*", + "@ckb-ccc/spore": "workspace:*", + "bip32": "4.0.0", + "bitcoinjs-lib": "7.0.1", + "ecpair": "3.0.1", + "lodash": "^4.17.21" + } +} diff --git a/packages/rgbpp/prettier.config.cjs b/packages/rgbpp/prettier.config.cjs new file mode 100644 index 000000000..5e1810363 --- /dev/null +++ b/packages/rgbpp/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/rgbpp/src/barrel.ts b/packages/rgbpp/src/barrel.ts new file mode 100644 index 000000000..b8bd65f5c --- /dev/null +++ b/packages/rgbpp/src/barrel.ts @@ -0,0 +1,7 @@ +export * from "./bitcoin/index.js"; +export * from "./data-source/index.js"; +export * from "./error.js"; +export * from "./script/index.js"; +export * from "./signer/index.js"; +export * from "./udt/index.js"; +export * from "./utils/index.js"; diff --git a/packages/rgbpp/src/bitcoin/account.ts b/packages/rgbpp/src/bitcoin/account.ts new file mode 100644 index 000000000..d72d26aba --- /dev/null +++ b/packages/rgbpp/src/bitcoin/account.ts @@ -0,0 +1,119 @@ +import * as ecc from "@bitcoinerlab/secp256k1"; +import * as bitcoin from "bitcoinjs-lib"; +import { ECPairFactory } from "ecpair"; + +import { ccc } from "@ckb-ccc/core"; + +import { ErrorBtcUnsupportedAddressType } from "../error.js"; +import { removeHexPrefix } from "../utils/index.js"; +import { + AddressType, + isSupportedAddressType, + SUPPORTED_ADDRESS_TYPES, +} from "./address.js"; +import { toBtcNetwork } from "./network.js"; +import { toXOnly } from "./public-key.js"; + +bitcoin.initEccLib(ecc); +const ECPair = ECPairFactory(ecc); + +export interface BtcAccount { + from: string; + fromPubkey?: string; + keyPair: bitcoin.Signer; + payment: bitcoin.Payment; + addressType: AddressType; + networkType: string; +} + +export function createBtcAccount( + privateKey: string, + addressType: AddressType, + networkType: string, +): BtcAccount { + if (!isSupportedAddressType(addressType)) { + throw new ErrorBtcUnsupportedAddressType( + addressType, + SUPPORTED_ADDRESS_TYPES, + ); + } + + const network = toBtcNetwork(networkType); + const key = ccc.bytesFrom(removeHexPrefix(privateKey)); + const keyPair = ECPair.fromPrivateKey(key, { network }); + + if (addressType === AddressType.P2WPKH) { + const p2wpkh = bitcoin.payments.p2wpkh({ + pubkey: keyPair.publicKey, + network, + }); + return { + from: p2wpkh.address!, + payment: p2wpkh, + keyPair, + addressType, + networkType, + }; + } else if (addressType === AddressType.P2TR) { + const p2tr = bitcoin.payments.p2tr({ + internalPubkey: toXOnly(keyPair.publicKey), + network, + }); + return { + from: p2tr.address!, + fromPubkey: ccc.bytesTo(keyPair.publicKey, "hex"), + payment: p2tr, + keyPair, + addressType, + networkType, + }; + } + + throw new ErrorBtcUnsupportedAddressType( + addressType, + SUPPORTED_ADDRESS_TYPES, + ); +} + +interface TweakableSigner extends bitcoin.Signer { + privateKey?: Uint8Array; +} + +export function tweakSigner( + signer: T, + options?: { + network?: bitcoin.Network; + tweakHash?: Uint8Array; + }, +): bitcoin.Signer { + if (!signer.privateKey) { + throw new Error("Private key is required for tweaking signer!"); + } + + let privateKey: Uint8Array = signer.privateKey; + if (signer.publicKey[0] === 3) { + privateKey = ecc.privateNegate(privateKey); + } + + const tweakedPrivateKey = ecc.privateAdd( + privateKey, + tapTweakHash(toXOnly(signer.publicKey), options?.tweakHash), + ); + if (!tweakedPrivateKey) { + throw new Error("Invalid tweaked private key!"); + } + + return ECPair.fromPrivateKey(tweakedPrivateKey, { + network: options?.network, + }); +} + +function tapTweakHash( + publicKey: Uint8Array, + hash: Uint8Array | undefined, +): Uint8Array { + return bitcoin.crypto.taggedHash( + "TapTweak", + hash ? new Uint8Array([...publicKey, ...hash]) : publicKey, + ); +} diff --git a/packages/rgbpp/src/bitcoin/address.ts b/packages/rgbpp/src/bitcoin/address.ts new file mode 100644 index 000000000..444eb79d1 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/address.ts @@ -0,0 +1,134 @@ +import * as bitcoin from "bitcoinjs-lib"; + +import { ccc } from "@ckb-ccc/core"; + +import { ErrorBtcInvalidAddress } from "../error.js"; +import { toBtcNetwork } from "./network.js"; + +// Read more about the available address types: +// - P2WPKH: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh +// - P2TR: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki +export enum AddressType { + P2PKH = "P2PKH", + P2WPKH = "P2WPKH", + P2TR = "P2TR", + P2SH_P2WPKH = "P2SH_P2WPKH", + P2WSH = "P2WSH", + P2SH = "P2SH", + UNKNOWN = "UNKNOWN", +} + +export const SUPPORTED_ADDRESS_TYPES = [ + AddressType.P2WPKH, + AddressType.P2TR, +] as const; + +export function isSupportedAddressType(at: AddressType): boolean { + return (SUPPORTED_ADDRESS_TYPES as readonly AddressType[]).includes(at); +} + +export function addressToScriptPublicKeyHex( + address: string, + networkType: string, +): string { + const network = toBtcNetwork(networkType); + const script = bitcoin.address.toOutputScript(address, network); + if (!script) { + throw new Error("Invalid address!"); + } + return ccc.bytesTo(script, "hex"); +} + +export function decodeAddress(address: string): { + network: bitcoin.Network; + addressType: AddressType; +} { + const mainnet = bitcoin.networks.bitcoin; + const testnet = bitcoin.networks.testnet; + const regtest = bitcoin.networks.regtest; + let decodeBase58: bitcoin.address.Base58CheckResult; + let decodeBech32: bitcoin.address.Bech32Result; + let network: bitcoin.Network | undefined; + let addressType: AddressType | undefined; + if ( + address.startsWith("bc1") || + address.startsWith("tb1") || + address.startsWith("bcrt1") + ) { + try { + decodeBech32 = bitcoin.address.fromBech32(address); + if (decodeBech32.prefix === mainnet.bech32) { + network = mainnet; + } else if (decodeBech32.prefix === testnet.bech32) { + network = testnet; + } else if (decodeBech32.prefix === regtest.bech32) { + network = regtest; + } + if (decodeBech32.version === 0) { + if (decodeBech32.data.length === 20) { + addressType = AddressType.P2WPKH; + } else if (decodeBech32.data.length === 32) { + addressType = AddressType.P2WSH; + } + } else if (decodeBech32.version === 1) { + if (decodeBech32.data.length === 32) { + addressType = AddressType.P2TR; + } + } + if (network !== undefined && addressType !== undefined) { + return { + network, + addressType, + }; + } + } catch (_e) { + // Do nothing (no need to throw here) + } + } else { + try { + decodeBase58 = bitcoin.address.fromBase58Check(address); + if (decodeBase58.version === mainnet.pubKeyHash) { + network = mainnet; + addressType = AddressType.P2PKH; + } else if (decodeBase58.version === testnet.pubKeyHash) { + network = testnet; + addressType = AddressType.P2PKH; + } else if (decodeBase58.version === mainnet.scriptHash) { + network = mainnet; + addressType = AddressType.P2SH_P2WPKH; + } else if (decodeBase58.version === testnet.scriptHash) { + network = testnet; + addressType = AddressType.P2SH_P2WPKH; + } + + if (network !== undefined && addressType !== undefined) { + return { + network, + addressType, + }; + } + } catch (_e) { + // Do nothing (no need to throw here) + } + } + + throw new ErrorBtcInvalidAddress(address); +} + +export function getAddressType(address: string): AddressType { + return decodeAddress(address).addressType; +} + +export function parseAddressType( + addressType: AddressType | string, +): AddressType { + if (typeof addressType === "string") { + const type = AddressType[addressType as keyof typeof AddressType]; + if (!type) { + throw new Error(`Invalid address type: ${addressType}`); + } + return type; + } + + return addressType; +} diff --git a/packages/rgbpp/src/bitcoin/index.ts b/packages/rgbpp/src/bitcoin/index.ts new file mode 100644 index 000000000..ee71a84f4 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/index.ts @@ -0,0 +1,6 @@ +export * from "./account.js"; +export * from "./address.js"; +export * from "./network.js"; +export * from "./public-key.js"; +export * from "./transaction/index.js"; +export * from "./wallet.js"; diff --git a/packages/rgbpp/src/bitcoin/network.ts b/packages/rgbpp/src/bitcoin/network.ts new file mode 100644 index 000000000..9d0eb36bd --- /dev/null +++ b/packages/rgbpp/src/bitcoin/network.ts @@ -0,0 +1,86 @@ +import * as bitcoin from "bitcoinjs-lib"; + +import { + BTC_DEFAULT_DUST_LIMIT, + BTC_DEFAULT_FEE_RATE, +} from "./transaction/index.js"; + +export enum PredefinedNetwork { + BitcoinTestnet3 = "BitcoinTestnet3", + BitcoinMainnet = "BitcoinMainnet", +} + +export interface NetworkConfig { + name: string; + isMainnet: boolean; + btcDustLimit: number; + btcFeeRate: number; +} + +export interface NetworkConfigOverrides { + btcDustLimit?: number; + btcFeeRate?: number; +} + +export type Network = PredefinedNetwork | string; + +export function toBtcNetwork(network: string): bitcoin.Network { + return network === (PredefinedNetwork.BitcoinMainnet as string) + ? bitcoin.networks.bitcoin + : bitcoin.networks.testnet; +} + +export function buildNetworkConfig( + network: Network, + overrides?: NetworkConfigOverrides, +): NetworkConfig { + let config: NetworkConfig; + + switch (network) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + case PredefinedNetwork.BitcoinTestnet3: + config = { + name: PredefinedNetwork.BitcoinTestnet3, + isMainnet: false, + btcDustLimit: overrides?.btcDustLimit || BTC_DEFAULT_DUST_LIMIT, + btcFeeRate: overrides?.btcFeeRate || BTC_DEFAULT_FEE_RATE, + }; + break; + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + case PredefinedNetwork.BitcoinMainnet: + config = { + name: PredefinedNetwork.BitcoinMainnet, + isMainnet: true, + btcDustLimit: overrides?.btcDustLimit || BTC_DEFAULT_DUST_LIMIT, + btcFeeRate: overrides?.btcFeeRate || BTC_DEFAULT_FEE_RATE, + }; + break; + default: + config = { + name: network, + isMainnet: false, + btcDustLimit: overrides?.btcDustLimit || BTC_DEFAULT_DUST_LIMIT, + btcFeeRate: overrides?.btcFeeRate || BTC_DEFAULT_FEE_RATE, + }; + break; + } + + return overrides ? mergeNetworkConfigs(config, overrides) : config; +} + +function mergeNetworkConfigs( + base: NetworkConfig, + overrides: NetworkConfigOverrides, +): NetworkConfig { + return { + name: base.name, + isMainnet: base.isMainnet, + btcDustLimit: overrides?.btcDustLimit || base.btcDustLimit, + btcFeeRate: overrides?.btcFeeRate || base.btcFeeRate, + }; +} + +export function isMainnet(network: Network): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + return network === PredefinedNetwork.BitcoinMainnet; +} diff --git a/packages/rgbpp/src/bitcoin/public-key.ts b/packages/rgbpp/src/bitcoin/public-key.ts new file mode 100644 index 000000000..47e8b461d --- /dev/null +++ b/packages/rgbpp/src/bitcoin/public-key.ts @@ -0,0 +1,130 @@ +import { AddressType } from "./address.js"; +import { RgbppBtcWallet } from "./wallet.js"; + +/** + * Public key provider interface + * Used to lookup public keys for addresses when building P2TR PSBT inputs + * + * @public + */ +export interface PublicKeyProvider { + /** + * Get the public key for a given address + * + * @param address - Bitcoin address + * @param addressType - Address type (e.g., P2TR, P2WPKH) + * @returns Public key in hex format (33-byte compressed or 32-byte x-only format), or undefined if not found + */ + getPublicKey( + address: string, + addressType: AddressType, + ): Promise; +} + +/** + * Public key provider that retrieves public keys from a wallet + * + * @public + */ +export class WalletPublicKeyProvider implements PublicKeyProvider { + constructor(private wallet: RgbppBtcWallet) {} + + async getPublicKey( + address: string, + _addressType: AddressType, + ): Promise { + // If it's the current wallet address, return its public key + const currentAddress = await this.wallet.getAddress(); + if (address === currentAddress) { + return await this.wallet.getPublicKey(); + } + return undefined; + } +} + +/** + * Public key provider that stores address-to-publickey mappings in memory + * + * @public + */ +export class CachedPublicKeyProvider implements PublicKeyProvider { + private cache = new Map(); + + /** + * Add an address-to-publickey mapping to the cache + * + * @param address - Bitcoin address + * @param publicKey - Public key in hex format + */ + addMapping(address: string, publicKey: string): void { + this.cache.set(address, publicKey); + } + + /** + * Remove an address-to-publickey mapping from the cache + * + * @param address - Bitcoin address + */ + removeMapping(address: string): void { + this.cache.delete(address); + } + + /** + * Clear all cached mappings + */ + clear(): void { + this.cache.clear(); + } + + async getPublicKey(address: string): Promise { + return this.cache.get(address); + } +} + +/** + * Composite public key provider that tries multiple providers in sequence + * + * @public + */ +export class CompositePublicKeyProvider implements PublicKeyProvider { + constructor(private providers: PublicKeyProvider[]) {} + + /** + * Add a provider to the end of the provider chain + * + * @param provider - The provider to add + */ + addProvider(provider: PublicKeyProvider): void { + this.providers.push(provider); + } + + /** + * Remove a provider from the provider chain + * + * @param provider - The provider to remove + */ + removeProvider(provider: PublicKeyProvider): void { + const index = this.providers.indexOf(provider); + if (index > -1) { + this.providers.splice(index, 1); + } + } + + async getPublicKey( + address: string, + addressType: AddressType, + ): Promise { + // Try each provider in sequence until one returns a result + for (const provider of this.providers) { + const pubkey = await provider.getPublicKey(address, addressType); + if (pubkey) { + return pubkey; + } + } + return undefined; + } +} + +export function toXOnly(pubKey: Uint8Array): Uint8Array { + return pubKey.length === 32 ? pubKey : pubKey.subarray(1, 33); +} diff --git a/packages/rgbpp/src/bitcoin/transaction/fee-estimator.ts b/packages/rgbpp/src/bitcoin/transaction/fee-estimator.ts new file mode 100644 index 000000000..8df714abf --- /dev/null +++ b/packages/rgbpp/src/bitcoin/transaction/fee-estimator.ts @@ -0,0 +1,127 @@ +import { AddressType, getAddressType } from "../address.js"; +import { TxInputData, TxOutput } from "./transaction.js"; + +export const DEFAULT_VIRTUAL_SIZE_BUFFER = 20; + +/** + * Stateless fee estimator for Bitcoin transactions. + * Estimates virtual size based on input/output address types + * without requiring actual signing. + */ +export class BtcFeeEstimator { + /** + * Estimate virtual size of a transaction + * Based on Bitcoin transaction structure and different address types + */ + estimateVirtualSize(inputs: TxInputData[], outputs: TxOutput[]): number { + // Base transaction size (version + locktime + input count + output count) + let baseSize = + 4 + 4 + getVarIntSize(inputs.length) + getVarIntSize(outputs.length); + + // Calculate input sizes + let witnessSize = 0; + for (const input of inputs) { + // Each input: txid (32) + vout (4) + scriptSig length + scriptSig + sequence (4) + baseSize += 32 + 4 + 4; // txid + vout + sequence + + // Determine address type from the input + const addressType = getInputAddressType(input); + + switch (addressType) { + case "P2WPKH": + // P2WPKH: scriptSig is empty, witness has 2 items (signature + pubkey) + baseSize += 1; // empty scriptSig + witnessSize += 1 + 1 + 72 + 1 + 33; // witness stack count + sig length + sig + pubkey length + pubkey + break; + case "P2TR": + // P2TR: scriptSig is empty, witness has 1 item (signature) + baseSize += 1; // empty scriptSig + witnessSize += 1 + 1 + 64; // witness stack count + sig length + sig + break; + case "P2PKH": + // P2PKH: scriptSig has signature + pubkey, no witness + baseSize += 1 + 72 + 33; // scriptSig length + sig + pubkey + break; + default: + // Default estimation for unknown types + baseSize += 1 + 107; // average scriptSig size + break; + } + } + + // Calculate output sizes + for (const output of outputs) { + // Each output: value (8) + scriptPubKey length + scriptPubKey + baseSize += 8; // value + + if ("address" in output && output.address) { + const addressType = getAddressType(output.address); + switch (addressType) { + case AddressType.P2WPKH: + baseSize += 1 + 22; // length + scriptPubKey + break; + case AddressType.P2TR: + baseSize += 1 + 34; // length + scriptPubKey + break; + case AddressType.P2PKH: + baseSize += 1 + 25; // length + scriptPubKey + break; + default: + baseSize += 1 + 25; // default size + break; + } + } else if ("script" in output && output.script) { + // For script outputs, use the actual script length + baseSize += getVarIntSize(output.script.length) + output.script.length; + } else { + // Default for unknown output types + baseSize += 1 + 25; + } + } + + // Add witness header if there are witness inputs + if (witnessSize > 0) { + witnessSize += 2; // witness marker + flag + } + + // Calculate weight: base_size * 4 + witness_size + const weight = baseSize * 4 + witnessSize; + + // Virtual size is weight / 4, rounded up + return Math.ceil(weight / 4); + } +} + +/** + * Get the size of a variable integer encoding + */ +export function getVarIntSize(value: number): number { + if (value < 0xfd) return 1; + if (value <= 0xffff) return 3; + if (value <= 0xffffffff) return 5; + return 9; +} + +/** + * Determine address type from PSBT input data + */ +export function getInputAddressType(input: TxInputData): string { + // Check if it's a Taproot input + if (input.tapInternalKey) { + return "P2TR"; + } + + // Check if it has witness data (P2WPKH or P2WSH) + if (input.witnessUtxo) { + const script = input.witnessUtxo.script; + if (script.length === 22 && script[0] === 0x00 && script[1] === 0x14) { + return "P2WPKH"; + } + if (script.length === 34 && script[0] === 0x00 && script[1] === 0x20) { + return "P2WSH"; + } + } + + // Default to P2PKH for legacy inputs + return "P2PKH"; +} diff --git a/packages/rgbpp/src/bitcoin/transaction/index.ts b/packages/rgbpp/src/bitcoin/transaction/index.ts new file mode 100644 index 000000000..ba229faec --- /dev/null +++ b/packages/rgbpp/src/bitcoin/transaction/index.ts @@ -0,0 +1,5 @@ +export * from "./fee-estimator.js"; +export * from "./script.js"; +export * from "./transaction-builder.js"; +export * from "./transaction.js"; +export * from "./utxo.js"; diff --git a/packages/rgbpp/src/bitcoin/transaction/script.ts b/packages/rgbpp/src/bitcoin/transaction/script.ts new file mode 100644 index 000000000..51dd56b90 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/transaction/script.ts @@ -0,0 +1,44 @@ +import * as bitcoin from "bitcoinjs-lib"; + +/** + * Check if a script pubkey is an OP_RETURN script. + * + * A valid OP_RETURN script should have the following structure: + * - + * - + * + * @example + * // + * isOpReturnScriptPubkey(ccc.bytesFrom('6a0401020304')); // true + * // + * isOpReturnScriptPubkey(ccc.bytesFrom('6a4c0f746573742d636f6d6d69746d656e74')); // true + * // + * isOpReturnScriptPubkey(ccc.bytesFrom('6a4c')); // false + * // + * isOpReturnScriptPubkey(ccc.bytesFrom('6a01')); // false + * // ... (not an OP_RETURN script) + * isOpReturnScriptPubkey(ccc.bytesFrom('76a914a802fc56c704ce87c42d7c92eb75e7896bdc41e788ac')); // false + */ +export function isOpReturnScriptPubkey(script: Uint8Array): boolean { + const scripts = bitcoin.script.decompile(script); + if (!scripts || scripts.length !== 2) { + return false; + } + + const [op, data] = scripts; + // OP_RETURN opcode is 0x6a in hex or 106 in integer + if (op !== bitcoin.opcodes.OP_RETURN) { + return false; + } + // Standard OP_RETURN data size is up to 80 bytes + if ( + !(data instanceof Uint8Array) || + data.byteLength < 1 || + data.byteLength > 80 + ) { + return false; + } + + // No false condition matched, it's an OP_RETURN script + return true; +} diff --git a/packages/rgbpp/src/bitcoin/transaction/transaction-builder.ts b/packages/rgbpp/src/bitcoin/transaction/transaction-builder.ts new file mode 100644 index 000000000..cfb796c81 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/transaction/transaction-builder.ts @@ -0,0 +1,309 @@ +import { ccc } from "@ckb-ccc/core"; + +import { + BtcDataProvider, + BtcUtxo, + BtcUtxoParams, +} from "../../data-source/index.js"; +import { + ErrorBtcInsufficientFunds, + ErrorBtcOpReturnUtxo, + ErrorBtcTransactionNotFound, + ErrorBtcUtxoNotFound, +} from "../../error.js"; +import { + Logger, + mapWithConcurrency, + RetryOptions, + retryWithBackoff, +} from "../../utils/index.js"; +import { AddressType, getAddressType } from "../address.js"; +import { NetworkConfig } from "../network.js"; +import { PublicKeyProvider } from "../public-key.js"; +import { + BtcFeeEstimator, + DEFAULT_VIRTUAL_SIZE_BUFFER, +} from "./fee-estimator.js"; +import { isOpReturnScriptPubkey } from "./script.js"; +import { TxInputData, TxOutput } from "./transaction.js"; +import { + deduplicateUtxoSeals, + Utxo, + UtxoSeal, + utxoToInputData, +} from "./utxo.js"; + +export interface BtcTransactionBuilderOptions { + concurrency?: number; + retryOptions?: RetryOptions; + logger?: Logger; +} + +/** + * Handles BTC transaction building: input construction, UTXO collection, + * fee estimation, and input/output balancing. + * + * Stateless with respect to wallet identity — receives dependencies via constructor. + */ +export class BtcTransactionBuilder { + private feeEstimator: BtcFeeEstimator; + private options: BtcTransactionBuilderOptions & { + concurrency: number; + retryOptions: RetryOptions; + }; + + constructor( + private dataSource: BtcDataProvider, + private networkConfig: NetworkConfig, + private publicKeyProvider: PublicKeyProvider, + private getAddress: () => Promise, + options?: BtcTransactionBuilderOptions, + ) { + this.feeEstimator = new BtcFeeEstimator(); + this.options = { + concurrency: options?.concurrency ?? 5, + retryOptions: options?.retryOptions ?? { maxRetries: 3, initialDelay: 1 }, + logger: options?.logger, + }; + } + + async buildInputs(utxoSeals: UtxoSeal[]): Promise { + const uniqueSeals = deduplicateUtxoSeals(utxoSeals); + + if (uniqueSeals.length < utxoSeals.length) { + this.options.logger?.warn?.( + `[BtcTransactionBuilder] Removed ${utxoSeals.length - uniqueSeals.length} duplicate UTXO(s) from inputs`, + ); + } + + const inputs = await mapWithConcurrency( + uniqueSeals, + this.options.concurrency, + async (utxoSeal) => { + const tx = await retryWithBackoff( + () => this.dataSource.getTransaction(utxoSeal.txid), + this.options.retryOptions, + ); + if (!tx) { + throw new ErrorBtcTransactionNotFound(utxoSeal.txid); + } + const vout = tx.vout[utxoSeal.vout]; + if (!vout) { + throw new ErrorBtcUtxoNotFound( + utxoSeal.txid, + utxoSeal.vout, + tx.vout.length, + ); + } + + const scriptBuffer = ccc.bytesFrom(vout.scriptpubkey); + if (isOpReturnScriptPubkey(scriptBuffer)) { + throw new ErrorBtcOpReturnUtxo(utxoSeal.txid, utxoSeal.vout); + } + + return utxoToInputData( + { + txid: utxoSeal.txid, + vout: utxoSeal.vout, + value: vout.value, + scriptPk: vout.scriptpubkey, + address: vout.scriptpubkey_address, + addressType: getAddressType(vout.scriptpubkey_address), + } as Utxo, + this.publicKeyProvider, + ); + }, + ); + return inputs; + } + + async balanceInputsOutputs( + inputs: TxInputData[], + outputs: TxOutput[], + btcUtxoParams?: BtcUtxoParams, + feeRate?: number, + ): Promise<{ + balancedInputs: TxInputData[]; + balancedOutputs: TxOutput[]; + }> { + let ins = inputs.slice(); + + let fulfilled = false; + let changeValue = 0; + const outsValue = outputs.reduce((acc, output) => acc + output.value, 0); + + while (!fulfilled) { + const insValue = ins.reduce( + (acc, input) => acc + input.witnessUtxo.value, + 0, + ); + + // Estimate fee assuming we will need a dummy change output + const dummyChangeOutput = { + address: await this.getAddress(), + value: 0, + }; + + const requiredFeeWithChange = await this.estimateFee( + ins, + [...outputs, dummyChangeOutput], + feeRate, + ); + const requiredFeeWithoutChange = await this.estimateFee( + ins, + outputs, + feeRate, + ); + + if ( + insValue > + outsValue + requiredFeeWithChange + this.networkConfig.btcDustLimit + ) { + // We have enough to create a change output + changeValue = insValue - outsValue - requiredFeeWithChange; + fulfilled = true; + } else if (insValue >= outsValue + requiredFeeWithoutChange) { + // We have enough to cover the fee without change output, but not enough to create a change output + // The remaining value will just go to miners as fee + changeValue = 0; + fulfilled = true; + } else { + const { inputs: extraInputs } = await this.collectUtxos( + outsValue + + requiredFeeWithChange + + this.networkConfig.btcDustLimit - + insValue, + btcUtxoParams ?? { + only_non_rgbpp_utxos: true, + }, + ins, + ); + ins = [...ins, ...extraInputs]; + } + } + + if (changeValue >= this.networkConfig.btcDustLimit) { + outputs.push({ + address: await this.getAddress(), + value: changeValue, + }); + } + + return { + balancedInputs: ins, + balancedOutputs: outputs, + }; + } + + async collectUtxos( + requiredValue: number, + params?: BtcUtxoParams, + knownInputs?: TxInputData[], + ): Promise<{ inputs: TxInputData[]; changeValue: number }> { + const utxos = await this.dataSource.getUtxos( + await this.getAddress(), + params, + ); + + let filteredUtxos = utxos; + if (knownInputs) { + filteredUtxos = utxos.filter((utxo: BtcUtxo) => { + return !knownInputs.some( + (input) => input.hash === utxo.txid && input.index === utxo.vout, + ); + }); + } + + if (filteredUtxos.length === 0) { + throw new ErrorBtcInsufficientFunds(requiredValue, 0); + } + + const selectedUtxos: BtcUtxo[] = []; + let totalValue = 0; + + for (const utxo of filteredUtxos) { + selectedUtxos.push(utxo); + totalValue += utxo.value; + + if (totalValue >= requiredValue) { + break; + } + } + + if (totalValue < requiredValue) { + throw new ErrorBtcInsufficientFunds(requiredValue, totalValue); + } + + return { + inputs: await this.buildInputs( + selectedUtxos.map((utxo) => ({ + txid: utxo.txid, + vout: utxo.vout, + })), + ), + changeValue: totalValue - requiredValue, + }; + } + + /** + * Estimate transaction fee without requiring actual signing. + * This avoids triggering wallet confirmation dialogs for fee estimation. + */ + async estimateFee( + inputs: TxInputData[], + outputs: TxOutput[], + feeRate?: number, + ) { + const totalInputValue = inputs.reduce( + (acc, input) => acc + input.witnessUtxo.value, + 0, + ); + const totalOutputValue = outputs.reduce( + (acc, output) => acc + output.value, + 0, + ); + + let balancedInputs = [...inputs]; + if (totalInputValue < totalOutputValue) { + const address = await this.getAddress(); + const addressType = getAddressType(address); + const dummyInput = { + witnessUtxo: { value: 0, script: new Uint8Array(0) }, + } as unknown as TxInputData; + + if (addressType === AddressType.P2TR) { + dummyInput.tapInternalKey = new Uint8Array(32); + } else if (addressType === AddressType.P2WPKH) { + const script = new Uint8Array(22); + script[0] = 0x00; + script[1] = 0x14; + dummyInput.witnessUtxo.script = script; + } else if (addressType === AddressType.P2WSH) { + const script = new Uint8Array(34); + script[0] = 0x00; + script[1] = 0x20; + dummyInput.witnessUtxo.script = script; + } + balancedInputs = [...inputs, dummyInput]; + } + + const virtualSize = this.feeEstimator.estimateVirtualSize( + balancedInputs, + outputs, + ); + const bufferedVirtualSize = virtualSize + DEFAULT_VIRTUAL_SIZE_BUFFER; + + if (!feeRate) { + try { + feeRate = (await this.dataSource.getRecommendedFee()).fastestFee; + } catch (error) { + feeRate = this.networkConfig.btcFeeRate; + this.options.logger?.warn?.( + `Failed to get recommended fee rate: ${String(error)}, using default fee rate ${this.networkConfig.btcFeeRate}`, + ); + } + } + + return Math.ceil(bufferedVirtualSize * feeRate); + } +} diff --git a/packages/rgbpp/src/bitcoin/transaction/transaction.ts b/packages/rgbpp/src/bitcoin/transaction/transaction.ts new file mode 100644 index 000000000..c2959e6a2 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/transaction/transaction.ts @@ -0,0 +1,113 @@ +import * as bitcoin from "bitcoinjs-lib"; +import lodash from "lodash"; + +import { ccc } from "@ckb-ccc/core"; + +import { isOpReturnScriptPubkey } from "./script.js"; + +export const BTC_DEFAULT_DUST_LIMIT = 546; + +export const BTC_DEFAULT_FEE_RATE = 1; + +export const BTC_DEFAULT_CONFIRMATIONS = 6; + +export interface TxInputData { + hash: string; + index: number; + witnessUtxo: { value: number; script: Uint8Array }; + tapInternalKey?: Uint8Array; +} + +export type TxOutput = TxAddressOutput | TxScriptOutput; + +export interface TxBaseOutput { + value: number; + fixed?: boolean; + protected?: boolean; + minUtxoSatoshi?: number; +} + +export interface TxAddressOutput extends TxBaseOutput { + address: string; +} + +export interface TxScriptOutput extends TxBaseOutput { + script: Uint8Array; +} + +export type InitOutput = TxAddressOutput | TxDataOutput | TxScriptOutput; + +export interface TxDataOutput extends TxBaseOutput { + data: Uint8Array | string; +} + +export function convertToOutput(output: InitOutput): TxOutput { + let result: TxOutput | undefined; + + if ("data" in output) { + result = { + script: dataToOpReturnScriptPubkey(output.data), + value: output.value, + fixed: output.fixed, + protected: output.protected, + minUtxoSatoshi: output.minUtxoSatoshi, + }; + } else if ("address" in output || "script" in output) { + result = lodash.cloneDeep(output); + } + if (!result) { + throw new Error("Unsupported output"); + } + + const minUtxoSatoshi = result.minUtxoSatoshi ?? BTC_DEFAULT_DUST_LIMIT; + const isOpReturnOutput = + "script" in result && isOpReturnScriptPubkey(result.script); + if (!isOpReturnOutput && result.value < minUtxoSatoshi) { + throw new Error(`value is less than minUtxoSatoshi (${minUtxoSatoshi})`); + } + + return result; +} + +/** + * Convert data to OP_RETURN script pubkey. + * The data size should be ranged in 1 to 80 bytes. + * + * @example + * const data = ccc.bytesFrom('01020304'); + * const scriptPk = dataToOpReturnScriptPubkey(data); // Uint8Array [0x6a, 0x04, 0x01, 0x02, 0x03, 0x04] + * const scriptPkHex = ccc.bytesTo(scriptPk, 'hex'); // 6a0401020304 + */ +export function dataToOpReturnScriptPubkey( + data: Uint8Array | string, +): Uint8Array { + if (typeof data === "string") { + data = ccc.bytesFrom(data); + } + + const payment = bitcoin.payments.embed({ data: [data] }); + if (!payment.output) { + throw new Error("Failed to create OP_RETURN script. Data cannot be empty."); + } + return payment.output; +} + +/** + * Convert a bitcoin.Transaction to hex string. + * Note if using for RGBPP proof, shouldn't set the "withWitness" param to "true". + * + * @param tx - The Bitcoin transaction object + * @param withWitness - Whether to include witness data (default: false) + * @returns Hex string of the transaction + */ +export function transactionToHex( + tx: bitcoin.Transaction, + withWitness: boolean = false, +): string { + if (!withWitness) { + const _tx = tx.clone(); + _tx.stripWitnesses(); + return _tx.toHex(); + } + return tx.toHex(); +} diff --git a/packages/rgbpp/src/bitcoin/transaction/utxo.ts b/packages/rgbpp/src/bitcoin/transaction/utxo.ts new file mode 100644 index 000000000..3a4c36e96 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/transaction/utxo.ts @@ -0,0 +1,117 @@ +import { ccc } from "@ckb-ccc/core"; + +import { + ErrorBtcMissingPublicKey, + ErrorBtcUnsupportedAddressType, +} from "../../error.js"; +import { + AddressType, + isSupportedAddressType, + SUPPORTED_ADDRESS_TYPES, +} from "../address.js"; +import { PublicKeyProvider, toXOnly } from "../public-key.js"; +import { TxInputData } from "./transaction.js"; + +interface OutPoint { + txid: string; + vout: number; +} + +export type UtxoSeal = OutPoint; + +/** + * Deduplicate UTXO seals based on txid and vout + */ +export function deduplicateUtxoSeals(utxoSeals: UtxoSeal[]): UtxoSeal[] { + if (!utxoSeals || utxoSeals.length === 0) { + return []; + } + + const seen = new Map(); + + for (const seal of utxoSeals) { + const normalizedTxId = seal.txid?.toLowerCase() ?? ""; + const key = `${normalizedTxId}:${seal.vout}`; + + if (!seen.has(key)) { + seen.set(key, seal); + } + } + + return Array.from(seen.values()); +} + +export interface Output extends OutPoint { + value: number; + scriptPk: string; +} + +export interface Utxo extends Output { + addressType: AddressType; + address: string; + pubkey?: string; +} + +/** + * Convert UTXO to PSBT input data + * + * @param utxo - The UTXO to convert + * @param publicKeyProvider - Optional provider to lookup public keys for P2TR addresses + * @returns PSBT input data + * + * @throws Error if address type is unsupported or public key is missing for P2TR + */ +export async function utxoToInputData( + utxo: Utxo, + publicKeyProvider?: PublicKeyProvider, +): Promise { + if (!isSupportedAddressType(utxo.addressType)) { + throw new ErrorBtcUnsupportedAddressType( + utxo.addressType, + SUPPORTED_ADDRESS_TYPES, + ); + } + + if (utxo.addressType === AddressType.P2WPKH) { + const data = { + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + value: utxo.value, + script: ccc.bytesFrom(utxo.scriptPk), + }, + }; + return data; + } + + if (utxo.addressType === AddressType.P2TR) { + let pubkey = utxo.pubkey; + + if (!pubkey && publicKeyProvider) { + pubkey = await publicKeyProvider.getPublicKey( + utxo.address, + utxo.addressType, + ); + } + + if (!pubkey) { + throw new ErrorBtcMissingPublicKey(utxo.address); + } + + const data = { + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + value: utxo.value, + script: ccc.bytesFrom(utxo.scriptPk), + }, + tapInternalKey: toXOnly(ccc.bytesFrom(pubkey)), + }; + return data; + } + + throw new ErrorBtcUnsupportedAddressType( + utxo.addressType, + SUPPORTED_ADDRESS_TYPES, + ); +} diff --git a/packages/rgbpp/src/bitcoin/wallet.ts b/packages/rgbpp/src/bitcoin/wallet.ts new file mode 100644 index 000000000..117cd83e1 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/wallet.ts @@ -0,0 +1,806 @@ +import { sha256 } from "@noble/hashes/sha2"; +import * as bitcoin from "bitcoinjs-lib"; + +import { ccc } from "@ckb-ccc/core"; + +import { + ErrorRgbppInvalidCellLock, + ErrorRgbppMaxCellExceeded, + ErrorRgbppNoTypedOutput, +} from "../error.js"; +import { Logger, RetryOptions, retryWithBackoff } from "../utils/index.js"; + +import { + RGBPP_BTC_BLANK_TX_ID, + RGBPP_BTC_TX_ID_PLACEHOLDER, + RGBPP_BTC_TX_PSEUDO_INDEX, + isSameScriptTemplate, + parseUtxoSealFromRgbppLockArgs, +} from "../script/index.js"; + +import { + BtcTransactionBuilder, + BtcTransactionBuilderOptions, + InitOutput, + TxInputData, + TxOutput, + convertToOutput, + transactionToHex, +} from "./transaction/index.js"; + +import { + BtcDataProvider, + BtcTransaction, + BtcUtxoParams, +} from "../data-source/index.js"; +import { + btcTxIdInReverseByteOrder, + isUsingOneOfScripts, + pseudoRgbppLockArgs, + pseudoRgbppLockArgsForCommitment, +} from "../script/index.js"; +import { RgbppUdtClient } from "../udt/index.js"; +import { removeHexPrefix } from "../utils/index.js"; +import { BtcAccount, createBtcAccount, tweakSigner } from "./account.js"; +import { AddressType, addressToScriptPublicKeyHex } from "./address.js"; +import { NetworkConfig, toBtcNetwork } from "./network.js"; +import { + CachedPublicKeyProvider, + CompositePublicKeyProvider, + PublicKeyProvider, + WalletPublicKeyProvider, + toXOnly, +} from "./public-key.js"; +/** Default polling interval in milliseconds for waiting transaction confirmation */ +export const BTC_DEFAULT_CONFIRMATION_POLL_INTERVAL = 30_000; + +/** Options for waiting for a BTC transaction to be confirmed */ +export interface WaitForConfirmationOptions { + /** Polling interval in milliseconds (default: 30000, minimum: 5000) */ + pollInterval?: number; + /** Maximum time to wait in milliseconds (default: unlimited) */ + timeout?: number; + /** AbortSignal to cancel the wait */ + signal?: AbortSignal; + /** Retry options for initial transaction indexing (default: maxRetries=10, initialDelay=5) */ + retryOptions?: RetryOptions; +} + +/** Parameters for building a seal UTXO PSBT */ +export interface UtxoSealParams { + /** Target UTXO value in satoshis (default: btcDustLimit) */ + targetValue?: number; + /** Fee rate for the transaction */ + feeRate?: number; + /** UTXO selection parameters */ + btcUtxoParams?: BtcUtxoParams; +} + +/** Result of building a seal UTXO PSBT */ +export interface UtxoSealPsbt { + /** The constructed PSBT, ready to be signed */ + psbt: bitcoin.Psbt; + /** The output index of the seal UTXO in the transaction */ + sealOutputIndex: number; +} + +export interface RgbppBtcTxParams { + ckbPartialTx: ccc.Transaction; + ckbClient: ccc.Client; + rgbppUdtClient: RgbppUdtClient; + receiverBtcAddresses: string[]; + btcChangeAddress: string; + btcUtxoParams?: BtcUtxoParams; + feeRate?: number; +} + +export interface RgbppBtcWalletOptions { + btcTxBuilderOptions?: BtcTransactionBuilderOptions; + logger?: Logger; +} + +export abstract class RgbppBtcWallet { + protected publicKeyProvider: PublicKeyProvider; + protected txBuilder: BtcTransactionBuilder; + private cachedPubKeyProvider: CachedPublicKeyProvider; + + constructor( + protected networkConfig: NetworkConfig, + protected dataSource: BtcDataProvider, + protected options?: RgbppBtcWalletOptions, + ) { + // Initialize public key providers + this.cachedPubKeyProvider = new CachedPublicKeyProvider(); + this.publicKeyProvider = new CompositePublicKeyProvider([ + this.cachedPubKeyProvider, + new WalletPublicKeyProvider(this), + ]); + + this.txBuilder = new BtcTransactionBuilder( + this.dataSource, + this.networkConfig, + this.publicKeyProvider, + () => this.getAddress(), + { + ...this.options?.btcTxBuilderOptions, + logger: this.options?.logger, + }, + ); + } + + get btcDataSource(): BtcDataProvider { + return this.dataSource; + } + + /** + * Get the current wallet address + */ + abstract getAddress(): Promise; + + /** + * Get the public key for the current wallet address + * @returns Public key in hex format (33-byte compressed format) + */ + abstract getPublicKey(): Promise; + + /** + * Register a public key for a specific address + * This is useful when you need to spend UTXOs from addresses other than the current wallet + * + * @param address - Bitcoin address + * @param publicKey - Public key in hex format (33-byte compressed or 32-byte x-only format) + * + * @example + * ```typescript + * // Register a public key for a service address + * wallet.registerPublicKey( + * "bc1p_service_address_xxx", + * "02abc123..." // 33-byte compressed public key + * ); + * ``` + */ + registerPublicKey(address: string, publicKey: string): void { + this.cachedPubKeyProvider.addMapping(address, publicKey); + } + + /** + * Set a custom public key provider + * This will replace the default composite provider + * + * @param provider - The public key provider to use + */ + setPublicKeyProvider(provider: PublicKeyProvider): void { + this.publicKeyProvider = provider; + } + + private async buildBtcRgbppOutputs( + ckbPartialTx: ccc.Transaction, + btcChangeAddress: string, + receiverBtcAddresses: string[], + btcDustLimit: number, + rgbppUdtClient: RgbppUdtClient, + ): Promise { + const rgbppLockScriptTemplate = + await rgbppUdtClient.rgbppLockScriptTemplate(); + const btcTimeLockScriptTemplate = + await rgbppUdtClient.btcTimeLockScriptTemplate(); + + const outputs: InitOutput[] = []; + let lastCkbTypedOutputIndex = -1; + ckbPartialTx.outputs.forEach((output, index) => { + // If output.type is not null, then the output.lock must be RGB++ Lock or BTC Time Lock + if (output.type) { + if ( + !isUsingOneOfScripts(output.lock, [ + rgbppLockScriptTemplate, + btcTimeLockScriptTemplate, + ]) + ) { + throw new ErrorRgbppInvalidCellLock( + ["RgbppLock", "BtcTimeLock"], + output.lock.codeHash, + ); + } + lastCkbTypedOutputIndex = index; + } + + // If output.lock is RGB++ Lock, generate a corresponding output in outputs + if (isSameScriptTemplate(output.lock, rgbppLockScriptTemplate)) { + outputs.push({ + fixed: true, + // Out-of-range index indicates this is a RGB++ change output returning to the BTC address + address: receiverBtcAddresses[index] ?? btcChangeAddress, + value: btcDustLimit, + minUtxoSatoshi: btcDustLimit, + }); + } + }); + + if (lastCkbTypedOutputIndex < 0) { + throw new ErrorRgbppNoTypedOutput(); + } + + const rgbppPartialTx = ccc.Transaction.from({ + inputs: ckbPartialTx.inputs, + outputs: ckbPartialTx.outputs.slice(0, lastCkbTypedOutputIndex + 1), + outputsData: ckbPartialTx.outputsData.slice( + 0, + lastCkbTypedOutputIndex + 1, + ), + }); + + const commitment = calculateCommitment(rgbppPartialTx); + + // place the commitment as the first output + outputs.unshift({ + data: commitment, + value: 0, + fixed: true, + }); + + return outputs.map((output) => convertToOutput(output)); + } + + /** + * Assemble a PSBT from balanced inputs and outputs. + * Shared helper used by buildPsbt and buildSealPsbt. + */ + private assemblePsbt( + balancedInputs: TxInputData[], + balancedOutputs: TxOutput[], + ): bitcoin.Psbt { + const psbt = new bitcoin.Psbt({ + network: toBtcNetwork(this.networkConfig.name), + }); + balancedInputs.forEach((input) => { + psbt.data.addInput({ + ...input, + witnessUtxo: { + ...input.witnessUtxo, + value: BigInt(input.witnessUtxo.value), + }, + }); + }); + balancedOutputs.forEach((output) => { + if ("address" in output) { + psbt.addOutput({ + address: output.address, + value: BigInt(output.value), + }); + } else { + psbt.addOutput({ script: output.script, value: BigInt(output.value) }); + } + }); + return psbt; + } + + async buildPsbt( + params: RgbppBtcTxParams, + ): Promise<{ psbt: bitcoin.Psbt; indexedCkbPartialTx: ccc.Transaction }> { + const { + ckbPartialTx, + ckbClient, + rgbppUdtClient, + btcChangeAddress, + receiverBtcAddresses, + feeRate, + btcUtxoParams, + } = params; + + const commitmentTx = ckbPartialTx.clone(); + const indexedTx = ckbPartialTx.clone(); + + const utxoSeals = await Promise.all( + ckbPartialTx.inputs.map(async (input) => { + await input.completeExtraInfos(ckbClient); + return parseUtxoSealFromRgbppLockArgs(input.cellOutput!.lock.args); + }), + ); + + const inputs = await this.txBuilder.buildInputs(utxoSeals); + + // Pre-fetch script templates for comparison + const rgbppLockTemplate = await rgbppUdtClient.rgbppLockScriptTemplate(); + const btcTimeLockTemplate = + await rgbppUdtClient.btcTimeLockScriptTemplate(); + + // adjust index in rgbpp lock args of outputs + let rgbppIndex = 0; + const commitmentOutputs: ccc.CellOutput[] = []; + const indexedOutputs: ccc.CellOutput[] = []; + for (const output of ckbPartialTx.outputs) { + if (isSameScriptTemplate(output.lock, rgbppLockTemplate)) { + indexedOutputs.push( + ccc.CellOutput.from({ + ...output, + lock: { + ...output.lock, + args: output.lock.args.replace( + ccc + .hexFrom(ccc.numLeToBytes(RGBPP_BTC_TX_PSEUDO_INDEX, 4)) + .slice(2), + ccc.hexFrom(ccc.numLeToBytes(rgbppIndex + 1, 4)).slice(2), + ), + }, + }), + ); + commitmentOutputs.push( + ccc.CellOutput.from({ + ...output, + lock: { + ...output.lock, + args: output.lock.args.replace( + pseudoRgbppLockArgs(), + pseudoRgbppLockArgsForCommitment(rgbppIndex + 1), + ), + }, + }), + ); + rgbppIndex++; + } else if (isSameScriptTemplate(output.lock, btcTimeLockTemplate)) { + indexedOutputs.push(output); + commitmentOutputs.push( + ccc.CellOutput.from({ + ...output, + lock: { + ...output.lock, + args: output.lock.args.replace( + btcTxIdInReverseByteOrder(RGBPP_BTC_TX_ID_PLACEHOLDER), + btcTxIdInReverseByteOrder(RGBPP_BTC_BLANK_TX_ID), + ), + }, + }), + ); + } else { + indexedOutputs.push(output); + commitmentOutputs.push(output); + } + } + commitmentTx.outputs = commitmentOutputs; + indexedTx.outputs = indexedOutputs; + + const rgbppOutputs = await this.buildBtcRgbppOutputs( + commitmentTx, + btcChangeAddress, + receiverBtcAddresses, + this.networkConfig.btcDustLimit, + rgbppUdtClient, + ); + + const { balancedInputs, balancedOutputs } = + await this.txBuilder.balanceInputsOutputs( + inputs, + rgbppOutputs, + btcUtxoParams, + feeRate, + ); + + const psbt = this.assemblePsbt(balancedInputs, balancedOutputs); + + return { psbt, indexedCkbPartialTx: indexedTx }; + } + + abstract signAndBroadcast(psbt: bitcoin.Psbt): Promise; + + rawTxHex(tx: bitcoin.Transaction, withWitness: boolean = false): string { + return transactionToHex(tx, withWitness); + } + + isCommitmentMatched( + commitment: string, + ckbPartialTx: ccc.Transaction, + lastCkbTypedOutputIndex: number, + ): boolean { + return ( + commitment === + calculateCommitment( + ccc.Transaction.from({ + inputs: ckbPartialTx.inputs, + outputs: ckbPartialTx.outputs.slice(0, lastCkbTypedOutputIndex + 1), + outputsData: ckbPartialTx.outputsData.slice( + 0, + lastCkbTypedOutputIndex + 1, + ), + }), + ) + ); + } + + /** + * Build a PSBT that creates a dust UTXO for use as an RGB++ seal. + * + * This only constructs the PSBT without signing or broadcasting. + * Use this when you need custom signing flows. + * + * @returns The PSBT and the output index of the seal UTXO. + */ + async buildSealPsbt(options?: UtxoSealParams): Promise { + const targetValue = options?.targetValue ?? this.networkConfig.btcDustLimit; + const feeRate = options?.feeRate ?? this.networkConfig.btcFeeRate; + const btcUtxoParams = options?.btcUtxoParams ?? { + only_non_rgbpp_utxos: true, + }; + + // The seal output is always the first output + const sealOutputIndex = 0; + const outputs = [ + { + address: await this.getAddress(), + value: targetValue, + }, + ]; + + const { inputs } = await this.txBuilder.collectUtxos( + targetValue, + btcUtxoParams, + ); + + const { balancedInputs, balancedOutputs } = + await this.txBuilder.balanceInputsOutputs( + inputs, + outputs, + btcUtxoParams, + feeRate, + ); + + const psbt = this.assemblePsbt(balancedInputs, balancedOutputs); + + return { psbt, sealOutputIndex }; + } + + /** + * Wait for a BTC transaction to be confirmed. + * + * Supports bounded polling with optional timeout and AbortSignal cancellation. + * + * @param txId - The transaction ID to wait for. + * @param options - Polling, timeout, and cancellation options. + * @returns The confirmed BTC transaction. + * + * @throws Error if timeout is reached or signal is aborted. + */ + async waitForConfirmation( + txId: string, + options?: WaitForConfirmationOptions, + ): Promise { + const pollInterval = Math.max( + options?.pollInterval ?? BTC_DEFAULT_CONFIRMATION_POLL_INTERVAL, + 5_000, + ); + const deadline = options?.timeout + ? Date.now() + options.timeout + : undefined; + const logger = this.options?.logger; + + // Initial fetch with retry (transaction may not be indexed yet) + let btcTx = await retryWithBackoff( + () => this.dataSource.getTransaction(txId), + { + maxRetries: options?.retryOptions?.maxRetries ?? 10, + initialDelay: options?.retryOptions?.initialDelay ?? 5, + }, + ); + + while (!btcTx.status.confirmed) { + if (options?.signal?.aborted) { + throw new Error( + `[waitForConfirmation] Aborted while waiting for ${txId}`, + ); + } + if (deadline && Date.now() >= deadline) { + throw new Error( + `[waitForConfirmation] Timeout waiting for ${txId} confirmation after ${options!.timeout}ms`, + ); + } + + logger?.info?.( + `[waitForConfirmation] Transaction ${txId} not confirmed, waiting ${pollInterval / 1000}s...`, + ); + + await ccc.sleep(pollInterval); + + try { + btcTx = await this.dataSource.getTransaction(txId); + } catch (error) { + logger?.warn?.( + `[waitForConfirmation] Failed to get transaction ${txId}: ${String(error)}. Retrying...`, + ); + } + } + + return btcTx; + } +} + +export class RgbppPrivateKeyBtcWallet extends RgbppBtcWallet { + private account: BtcAccount; + + constructor( + privateKey: string, + addressType: AddressType, + networkConfig: NetworkConfig, + dataSource: BtcDataProvider, + options?: RgbppBtcWalletOptions, + ) { + super(networkConfig, dataSource, options); + this.account = createBtcAccount( + privateKey, + addressType, + networkConfig.name, + ); + } + + async getAddress(): Promise { + return this.account.from; + } + + async getPublicKey(): Promise { + return ccc.bytesTo(this.account.keyPair.publicKey, "hex"); + } + + /** + * Format SignPsbtOptions to actual inputs that need to be signed + * @private + */ + private formatOptionsToSignInputs( + psbt: bitcoin.Psbt, + options?: ccc.SignPsbtOptionsLike, + ): Array<{ index: number; disableTweakSigner?: boolean }> { + const account = this.account; + const accountScript = addressToScriptPublicKeyHex( + account.from, + account.networkType, + ); + + // If options are provided, validate and use them + if (options?.inputsToSign && options.inputsToSign.length > 0) { + return options.inputsToSign.map((input: ccc.InputToSignLike) => { + const index = Number(input.index); + if (isNaN(index)) { + throw new Error("Invalid index in toSignInput"); + } + + // Validate address if provided + if (input.address && input.address !== account.from) { + throw new Error( + `Invalid address in toSignInput. Expected ${account.from}, got ${input.address}`, + ); + } + + // Validate pubkey if provided + if (input.publicKey) { + const fullPubkey = ccc.bytesTo(account.keyPair.publicKey, "hex"); + const xOnlyPubkey = + account.addressType === AddressType.P2TR + ? ccc.bytesTo(toXOnly(account.keyPair.publicKey), "hex") + : fullPubkey; + const inputPubkeyStr = ccc + .hexFrom(input.publicKey) + .replace(/^0x/i, ""); + + // Accept both full pubkey and x-only pubkey for Taproot + if (inputPubkeyStr !== fullPubkey && inputPubkeyStr !== xOnlyPubkey) { + throw new Error( + `Invalid public key in toSignInput. Expected ${fullPubkey} or ${xOnlyPubkey}, got ${inputPubkeyStr}`, + ); + } + } + + return { + index, + disableTweakSigner: input.disableTweakSigner, + }; + }); + } + + // If no options, auto-detect inputs that match this account + const toSignInputs: Array<{ index: number; disableTweakSigner?: boolean }> = + []; + + psbt.data.inputs.forEach((input, index) => { + let script: Uint8Array | null = null; + + // Get script from witnessUtxo or nonWitnessUtxo + if (input.witnessUtxo) { + script = input.witnessUtxo.script; + } else if (input.nonWitnessUtxo) { + const tx = bitcoin.Transaction.fromBuffer(input.nonWitnessUtxo); + const outputIndex = psbt.txInputs[index].index; + if (outputIndex >= tx.outs.length) { + throw new Error( + `Invalid PSBT: input ${index} references output ${outputIndex}, but transaction only has ${tx.outs.length} outputs`, + ); + } + const output = tx.outs[outputIndex]; + script = output.script; + } + + // Check if already signed + const isSigned = + input.finalScriptSig || + input.finalScriptWitness || + input.tapKeySig || + (input.partialSig && input.partialSig.length > 0) || + (input.tapScriptSig && input.tapScriptSig.length > 0); + + // Only sign if script matches and not already signed + if (script && !isSigned) { + const inputScriptHex = ccc.bytesTo(script, "hex"); + if (inputScriptHex === accountScript) { + toSignInputs.push({ index }); + } + } + }); + + // If no matching inputs found, do NOT sign any inputs + // This prevents accidentally signing inputs that don't belong to this account + if (toSignInputs.length === 0) { + // Don't throw error, just return empty array + // This allows the PSBT to pass through without signing + } + + return toSignInputs; + } + + async signPsbt( + psbt: bitcoin.Psbt, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + const account = this.account; + const tweaked = tweakSigner(account.keyPair, { + network: account.payment.network, + }); + + // Get inputs to sign based on options + const toSignInputs = this.formatOptionsToSignInputs(psbt, options); + + // Sign each input + for (const { index, disableTweakSigner } of toSignInputs) { + const input = psbt.data.inputs[index]; + if (!input) { + throw new Error(`Input at index ${index} not found`); + } + + // Determine which signer to use + let signer: bitcoin.Signer; + if (account.addressType === AddressType.P2TR) { + // For Taproot, use tweaked signer unless disabled + signer = disableTweakSigner ? account.keyPair : tweaked; + } else { + // For other address types (P2WPKH, etc), use regular keyPair + signer = account.keyPair; + } + + // Sign the input + try { + psbt.signInput(index, signer); + } catch (error) { + throw new Error( + `Failed to sign input ${index}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + // Finalize if autoFinalized is not explicitly false + const autoFinalize = options?.autoFinalized ?? true; + if (autoFinalize) { + psbt.finalizeAllInputs(); + return psbt.extractTransaction(true); + } else { + // For multi-sig scenarios, return a partial transaction + // Note: This will throw if you try to broadcast it + try { + return psbt.extractTransaction(false); + } catch { + throw new Error("Cannot extract transaction from incomplete PSBT. "); + } + } + } + + async sendTx(tx: bitcoin.Transaction): Promise { + const txHex = tx.toHex(); + return this.dataSource.sendTransaction(txHex); + } + + async signAndBroadcast( + psbt: bitcoin.Psbt, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + // Always finalize for signAndBroadcast + const finalOptions: ccc.SignPsbtOptionsLike = { + ...options, + autoFinalized: true, // Force finalization + inputsToSign: options?.inputsToSign ?? [], + }; + + const tx = await this.signPsbt(psbt, finalOptions); + return this.sendTx(tx); + } +} + +// TODO: add default btc asset api URL +export class RgbppBrowserBtcWallet extends RgbppBtcWallet { + constructor( + protected signer: ccc.SignerBtc, + networkConfig: NetworkConfig, + dataSource: BtcDataProvider, + options?: RgbppBtcWalletOptions, + ) { + super(networkConfig, dataSource, options); + } + + async getAddress(): Promise { + return this.signer.getBtcAccount(); + } + + async getPublicKey(): Promise { + const pubkey = await this.signer.getBtcPublicKey(); + const hexString: string = + typeof pubkey === "string" + ? pubkey + : (pubkey as { toString(): string }).toString(); + return hexString.startsWith("0x") ? hexString.slice(2) : hexString; + } + + async signAndBroadcast( + psbt: bitcoin.Psbt, + options?: ccc.SignPsbtOptionsLike, + ): Promise { + return this.signer + .signAndBroadcastPsbt(psbt.toHex(), options) + .then((hex) => removeHexPrefix(hex)); + } +} + +/** + * Create a RgbppBrowserBtcWallet from a SignerBtc. + * + * TODO: Wallet capability validation (e.g. whether the signer fully implements + * signAndBroadcastPsbt) should be enforced at the ccc.SignerBtc. + */ +export function createRgbppBrowserBtcWallet( + signer: ccc.SignerBtc, + networkConfig: NetworkConfig, + dataSource: BtcDataProvider, +): RgbppBrowserBtcWallet { + return new RgbppBrowserBtcWallet(signer, networkConfig, dataSource); +} + +export const RGBPP_CKB_MAX_CELL = 255; + +// The maximum length of inputs and outputs is 255, and the field type representing the length in the RGB++ protocol is Uint8 +// refer to https://github.com/ckb-cell/rgbpp/blob/0c090b039e8d026aad4336395b908af283a70ebf/contracts/rgbpp-lock/src/main.rs#L173-L211 +export const calculateCommitment = (ckbPartialTx: ccc.Transaction): string => { + const hash = sha256.create(); + hash.update(ccc.bytesFrom("RGB++", "utf8")); + const version = new Uint8Array([0, 0]); + hash.update(version); + + const { inputs, outputs, outputsData } = ckbPartialTx; + + if ( + inputs.length > RGBPP_CKB_MAX_CELL || + outputs.length > RGBPP_CKB_MAX_CELL + ) { + throw new ErrorRgbppMaxCellExceeded( + Math.max(inputs.length, outputs.length), + RGBPP_CKB_MAX_CELL, + ); + } + hash.update(new Uint8Array([inputs.length, outputs.length])); + + for (const input of inputs) { + hash.update(input.previousOutput.toBytes()); + } + for (let index = 0; index < outputs.length; index++) { + const outputData = outputsData[index]; + hash.update(outputs[index].toBytes()); + + const od = ccc.bytesFrom(outputData); + const outputDataLen = ccc.numLeToBytes(od.length, 4); + hash.update(outputDataLen); + hash.update(od); + } + // double sha256 + return ccc.bytesTo(sha256(hash.digest()), "hex"); +}; diff --git a/packages/rgbpp/src/data-source/btc-assets-api.ts b/packages/rgbpp/src/data-source/btc-assets-api.ts new file mode 100644 index 000000000..6a1be760d --- /dev/null +++ b/packages/rgbpp/src/data-source/btc-assets-api.ts @@ -0,0 +1,388 @@ +import lodash from "lodash"; + +import { ccc } from "@ckb-ccc/core"; + +import { isDomain } from "../utils/index.js"; +import { + BtcBalance, + BtcBalanceParams, + BtcRecommendedFeeRates, + BtcSentTransaction, + BtcTransaction, + BtcTransactionHex, + BtcUtxo, + BtcUtxoParams, + RgbppDataSource, +} from "./data-source.js"; + +export interface RgbppApiSpvProof { + proof: string; + spv_client: { + tx_hash: string; + index: string; + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Json = Record; + +export interface BaseApis { + request(route: string, options?: BaseApiRequestOptions): Promise; + post(route: string, options?: BaseApiRequestOptions): Promise; +} + +export interface BaseApiRequestOptions extends RequestInit { + params?: Json; + method?: "GET" | "POST"; + requireToken?: boolean; + allow404?: boolean; +} + +export interface BtcAssetsApiToken { + token: string; +} + +export interface BtcAssetsApiContext { + request: { + url: string; + body?: Json; + params?: Json; + }; + response: { + status: number; + data?: Json | string; + }; +} + +export class BtcAssetsApiBase implements BaseApis { + public url: string; + public app?: string; + public domain?: string; + public origin?: string; + private token?: string; + private isMainnet: boolean; + + constructor(config: BtcAssetApiConfig) { + this.url = config.url; + this.app = config.app; + this.domain = config.domain; + this.origin = config.origin; + this.token = config.token; + this.isMainnet = config.isMainnet ?? true; + + // Validation + if (this.domain && !isDomain(this.domain, true)) { + throw BtcAssetsApiError.withComment( + ErrorCodes.ASSETS_API_INVALID_PARAM, + `Invalid domain format: "${this.domain}". Please provide a valid domain (e.g., "example.com")`, + ); + } + } + + async request(route: string, options?: BaseApiRequestOptions): Promise { + const { + requireToken = this.isMainnet, + allow404 = false, + method = "GET", + headers, + params, + ...otherOptions + } = options ?? {}; + + if (requireToken && (!this.token || !this.origin)) { + throw BtcAssetsApiError.withComment( + ErrorCodes.ASSETS_API_INVALID_PARAM, + "Missing required parameters: both token and origin are required", + ); + } + + const pickedParams = lodash.pickBy(params, (val) => val !== undefined); + const packedParams = params + ? "?" + new URLSearchParams(pickedParams).toString() + : ""; + const url = `${this.url}${route}${packedParams}`; + + const authHeaders: Record = {}; + if (requireToken) { + authHeaders.authorization = `Bearer ${this.token}`; + authHeaders.origin = this.origin!; + } + + const res = await fetch(url, { + method, + headers: { + ...authHeaders, + ...(headers || {}), + }, + ...otherOptions, + } as RequestInit); + + let text: string | undefined; + let json: Json | undefined; + let ok: boolean = false; + try { + text = await res.text(); + if (text) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + json = JSON.parse(text); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ok = json?.ok ?? res.ok ?? false; + } else { + ok = res.ok; + } + } catch { + // JSON.parse failed on non-empty text + // We'll handle this decode error below + } + + let comment: string | undefined; + const status = res.status; + const context: BtcAssetsApiContext = { + request: { + url, + params, + body: tryParseBody(otherOptions.body), + }, + response: { + status, + data: json ?? text, + }, + }; + + if (!json) { + comment = text ? `(${status}) ${text}` : `${status}`; + } + if (json && !ok) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const code = + json.code ?? json.statusCode ?? json.error?.error?.code ?? res.status; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const message = + json.message ?? + (typeof json.error === "string" + ? json.error + : json.error?.error?.message); + if (message) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + comment = code ? `(${code}) ${message}` : message; + } else { + comment = JSON.stringify(json); + } + } + + if (status === 200) { + if (text && !json) { + // 200 OK, but we had body text that failed JSON parsing + throw BtcAssetsApiError.withComment( + ErrorCodes.ASSETS_API_RESPONSE_DECODE_ERROR, + "Failed to decode JSON response", + context, + ); + } + if (!text) { + return "" as unknown as T; + } + return (json ?? text) as unknown as T; + } + if (status === 401) { + throw BtcAssetsApiError.withComment( + ErrorCodes.ASSETS_API_UNAUTHORIZED, + comment, + context, + ); + } + if (status === 404 && !allow404) { + throw BtcAssetsApiError.withComment( + ErrorCodes.ASSETS_API_RESOURCE_NOT_FOUND, + comment, + context, + ); + } + if (status !== 200 && status !== 404 && !allow404) { + throw BtcAssetsApiError.withComment( + ErrorCodes.ASSETS_API_RESPONSE_ERROR, + comment, + context, + ); + } + if (status === 404 && allow404) { + return undefined as T; + } + + return json! as T; + } + + async post(route: string, options?: BaseApiRequestOptions): Promise { + return this.request(route, { + method: "POST", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + } as BaseApiRequestOptions); + } +} + +function tryParseBody(body: unknown): Record | undefined { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return typeof body === "string" ? JSON.parse(body) : undefined; + } catch { + return undefined; + } +} + +export enum ErrorCodes { + UNKNOWN, + + ASSETS_API_RESPONSE_ERROR, + ASSETS_API_UNAUTHORIZED, + ASSETS_API_INVALID_PARAM, + ASSETS_API_RESOURCE_NOT_FOUND, + ASSETS_API_RESPONSE_DECODE_ERROR, + + OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE, +} + +export const ErrorMessages = { + [ErrorCodes.UNKNOWN]: "Unknown error", + + [ErrorCodes.ASSETS_API_UNAUTHORIZED]: + "BtcAssetsAPI unauthorized, please check your token/origin", + [ErrorCodes.ASSETS_API_INVALID_PARAM]: + "Invalid param(s) was provided to the BtcAssetsAPI", + [ErrorCodes.ASSETS_API_RESPONSE_ERROR]: "BtcAssetsAPI returned an error", + [ErrorCodes.ASSETS_API_RESOURCE_NOT_FOUND]: + "Resource not found on the BtcAssetsAPI", + [ErrorCodes.ASSETS_API_RESPONSE_DECODE_ERROR]: + "Failed to decode the response of BtcAssetsAPI", + + [ErrorCodes.OFFLINE_DATA_SOURCE_METHOD_NOT_AVAILABLE]: + "Method not available for offline data source", +}; + +export class BtcAssetsApiError extends Error { + public code = ErrorCodes.UNKNOWN; + public message: string; + public context?: BtcAssetsApiContext; + + constructor(payload: { + code: ErrorCodes; + message?: string; + context?: BtcAssetsApiContext; + }) { + const message = + payload.message ?? + ErrorMessages[payload.code] ?? + ErrorMessages[ErrorCodes.UNKNOWN]; + + super(message); + this.message = message; + this.code = payload.code; + this.context = payload.context; + Object.setPrototypeOf(this, BtcAssetsApiError.prototype); + } + + static withComment( + code: ErrorCodes, + comment?: string, + context?: BtcAssetsApiContext, + ): BtcAssetsApiError { + const prefixMessage = + ErrorMessages[code] ?? ErrorMessages[ErrorCodes.UNKNOWN]; + const message = comment ? `${prefixMessage}: ${comment}` : undefined; + return new BtcAssetsApiError({ code, message, context }); + } +} + +export interface BtcAssetApiConfig { + url: string; + app?: string; + domain?: string; + origin?: string; + token?: string; + isMainnet?: boolean; +} + +/** + * Typed API client for Bitcoin and RGBPP endpoints. + * + * Encapsulates all endpoint URLs and response types in one place. + * Consumers use typed methods instead of raw `request(url)` calls. + */ +export class BtcAssetsApi implements RgbppDataSource { + private api: BtcAssetsApiBase; + + constructor(config: BtcAssetApiConfig) { + this.api = new BtcAssetsApiBase(config); + } + + getTransaction(txId: string) { + return this.api.request(`/bitcoin/v1/transaction/${txId}`); + } + + async getTransactionHex(txId: string) { + const { hex } = await this.api.request( + `/bitcoin/v1/transaction/${txId}/hex`, + ); + return hex; + } + + getUtxos(address: string, params?: BtcUtxoParams) { + return this.api.request( + `/bitcoin/v1/address/${address}/unspent`, + { params }, + ); + } + + getBalance(address: string, params?: BtcBalanceParams) { + return this.api.request( + `/bitcoin/v1/address/${address}/balance`, + { params }, + ); + } + + getRecommendedFee() { + return this.api.request( + `/bitcoin/v1/fees/recommended`, + ); + } + + async sendTransaction(txHex: string): Promise { + const { txid: txId } = await this.api.post( + "/bitcoin/v1/transaction", + { + body: JSON.stringify({ txhex: txHex }), + }, + ); + return txId; + } + + async getRgbppSpvProof(btcTxId: string, confirmations: number) { + const spvProof: RgbppApiSpvProof | null = + await this.api.request("/rgbpp/v1/btc-spv/proof", { + params: { + btc_txid: btcTxId, + confirmations, + }, + }); + + return spvProof + ? { + proof: spvProof.proof as ccc.Hex, + spvClientOutpoint: ccc.OutPoint.from({ + txHash: spvProof.spv_client.tx_hash, + index: spvProof.spv_client.index, + }), + } + : null; + } + + async getRgbppCellOutputs(btcAddress: string) { + const res = await this.api.request<{ cellOutput: ccc.CellOutput }[]>( + `/rgbpp/v1/address/${btcAddress}/assets`, + ); + return res.map((item: { cellOutput: ccc.CellOutput }) => item.cellOutput); + } +} diff --git a/packages/rgbpp/src/data-source/data-source.ts b/packages/rgbpp/src/data-source/data-source.ts new file mode 100644 index 000000000..1b4b28694 --- /dev/null +++ b/packages/rgbpp/src/data-source/data-source.ts @@ -0,0 +1,109 @@ +import { ccc } from "@ckb-ccc/core"; + +import { RgbppSpvProofProvider } from "./spv.js"; + +export interface BtcBalance { + address: string; + total_satoshi: number; + pending_satoshi: number; + available_satoshi: number; + dust_satoshi: number; + rgbpp_satoshi: number; + utxo_count: number; +} + +export interface BtcRecommendedFeeRates { + fastestFee: number; + halfHourFee: number; + hourFee: number; + economyFee: number; + minimumFee: number; +} + +export interface BtcTransaction { + txid: string; + version: number; + locktime: number; + vin: { + txid: string; + vout: number; + prevout: { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + }; + scriptsig: string; + scriptsig_asm: string; + witness: string[]; + is_coinbase: boolean; + sequence: number; + }[]; + vout: { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + }[]; + weight: number; + size: number; + fee: number; + status: { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; + }; +} + +export interface BtcTransactionHex { + hex: string; +} + +export interface BtcUtxo { + txid: string; + vout: number; + value: number; + status: { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; + }; +} + +export interface BtcSentTransaction { + txid: string; +} + +export interface BtcUtxoParams { + only_non_rgbpp_utxos?: boolean; + only_confirmed?: boolean; + min_satoshi?: number; + no_cache?: boolean; +} + +export interface BtcBalanceParams { + min_satoshi?: number; + no_cache?: boolean; +} + +export interface BtcDataProvider { + getTransactionHex(txId: string): Promise; + getTransaction(txId: string): Promise; + getUtxos(address: string, params?: BtcUtxoParams): Promise; + getBalance(address: string, params?: BtcBalanceParams): Promise; + getRecommendedFee(): Promise; + sendTransaction(txHex: string): Promise; +} + +export interface RgbppCkbCellProvider { + getRgbppCellOutputs(btcAddress: string): Promise; +} + +export interface RgbppDataSource + extends BtcDataProvider, + RgbppCkbCellProvider, + RgbppSpvProofProvider {} diff --git a/packages/rgbpp/src/data-source/index.ts b/packages/rgbpp/src/data-source/index.ts new file mode 100644 index 000000000..354c32ed3 --- /dev/null +++ b/packages/rgbpp/src/data-source/index.ts @@ -0,0 +1,3 @@ +export * from "./btc-assets-api.js"; +export * from "./data-source.js"; +export * from "./spv.js"; diff --git a/packages/rgbpp/src/data-source/spv.ts b/packages/rgbpp/src/data-source/spv.ts new file mode 100644 index 000000000..d22d733d3 --- /dev/null +++ b/packages/rgbpp/src/data-source/spv.ts @@ -0,0 +1,56 @@ +import { ccc } from "@ckb-ccc/core"; + +export interface RgbppSpvProof { + proof: ccc.Hex; + spvClientOutpoint: ccc.OutPoint; +} + +export interface RgbppSpvProofProvider { + getRgbppSpvProof( + btcTxId: string, + confirmations: number, + ): Promise; +} + +/** Default polling interval in milliseconds for SPV proof polling */ +export const DEFAULT_SPV_POLL_INTERVAL = 30_000; + +/** + * Poll for an SPV proof until it becomes available. + * + * @param provider - The SPV proof provider to query + * @param btcTxId - The Bitcoin transaction ID to get the proof for + * @param confirmations - The number of confirmations required + * @param pollInterval - The polling interval in milliseconds + * @returns The SPV proof once available + */ +export async function pollForSpvProof( + spvProofProvider: RgbppSpvProofProvider, + btcTxId: string, + confirmations: number = 0, + intervalMs?: number, +): Promise { + const interval = intervalMs ?? DEFAULT_SPV_POLL_INTERVAL; + + while (true) { + try { + console.log(`[SPV] Polling for BTC tx ${btcTxId}`); + const proof = await spvProofProvider.getRgbppSpvProof( + btcTxId, + confirmations, + ); + + if (proof) { + return proof; + } + } catch (e) { + console.info( + `[SPV] Error polling for BTC tx ${btcTxId}:`, + e instanceof Error ? e.message : String(e), + ); + // Continue polling on error + } + + await ccc.sleep(interval); + } +} diff --git a/packages/rgbpp/src/error.ts b/packages/rgbpp/src/error.ts new file mode 100644 index 000000000..fb0de0938 --- /dev/null +++ b/packages/rgbpp/src/error.ts @@ -0,0 +1,149 @@ +// ============================================================================= +// Bitcoin errors +// ============================================================================= + +export class ErrorBtcUnsupportedAddressType extends Error { + constructor( + public readonly addressType: string, + public readonly supported: readonly string[], + ) { + super( + `Unsupported address type: ${addressType}, only ${supported.join(", ")} supported`, + ); + } +} + +export class ErrorBtcInvalidAddress extends Error { + constructor(public readonly address: string) { + super(`Unable to decode address: "${address}". Unrecognized format.`); + } +} + +export class ErrorBtcInsufficientFunds extends Error { + public readonly required: number; + public readonly available: number; + + constructor(required: number, available: number) { + super( + `Insufficient BTC funds: needed ${required} sats, but only ${available} available`, + ); + this.required = required; + this.available = available; + } +} + +export class ErrorBtcTransactionNotFound extends Error { + constructor(public readonly txid: string) { + super( + `Transaction ${txid} not found. The referenced UTXO may not exist or the API may be unavailable.`, + ); + } +} + +export class ErrorBtcUtxoNotFound extends Error { + constructor( + public readonly txid: string, + public readonly vout: number, + public readonly txVoutCount: number, + ) { + super( + `Output index ${vout} not found in transaction ${txid} (tx has ${txVoutCount} outputs).`, + ); + } +} + +export class ErrorBtcOpReturnUtxo extends Error { + constructor( + public readonly txid: string, + public readonly vout: number, + ) { + super( + `Output ${vout} of transaction ${txid} is an OP_RETURN output and cannot be spent. ` + + `RGB++ lock args should not reference OP_RETURN outputs.`, + ); + } +} + +export class ErrorBtcMissingPublicKey extends Error { + constructor(public readonly address: string) { + super( + `Missing public key for P2TR address ${address}. ` + + `Provide a PublicKeyProvider or include pubkey in UTXO data.`, + ); + } +} + +// ============================================================================= +// RGB++ script errors +// ============================================================================= + +export class ErrorRgbppInvalidLockArgs extends Error { + constructor( + public readonly argsLength: number, + public readonly minLength: number, + ) { + super( + `Invalid RGB++ lock args: got ${argsLength} bytes, need at least ${minLength}`, + ); + } +} + +export class ErrorRgbppScriptNotFound extends Error { + constructor(public readonly scriptName: string) { + super(`Required RGB++ script not found: ${scriptName}`); + } +} + +export class ErrorRgbppInvalidCellLock extends Error { + constructor( + public readonly expected: string[], + public readonly actual: string, + ) { + super( + `Invalid cell lock: expected one of [${expected.join(", ")}], got ${actual}`, + ); + } +} + +// ============================================================================= +// RGB++ signer errors +// ============================================================================= + +export class ErrorRgbppOutputNotFound extends Error { + constructor() { + super("No output with RGB++ lock or BTC time lock found in transaction"); + } +} + +export class ErrorRgbppInvalidInputLock extends Error { + constructor(public readonly codeHash: string) { + super( + `All inputs must use RGB++ lock, but found input with lock codeHash: ${codeHash}`, + ); + } +} + +export class ErrorRgbppMaxCellExceeded extends Error { + constructor( + public readonly count: number, + public readonly max: number, + ) { + super( + `RGB++ CKB virtual tx exceeds cell limit: ${count} cells, max ${max}`, + ); + } +} + +export class ErrorRgbppNoTypedOutput extends Error { + constructor() { + super( + "RGB++ transaction has no CKB outputs with a type script. At least one typed output is required.", + ); + } +} + +export class ErrorRgbppCellNotFound extends Error { + constructor(public readonly txHash: string) { + super(`RGB++ cell not found after issuance in transaction ${txHash}`); + } +} diff --git a/packages/rgbpp/src/examples/env/env.ts b/packages/rgbpp/src/examples/env/env.ts new file mode 100644 index 000000000..7dc1078d6 --- /dev/null +++ b/packages/rgbpp/src/examples/env/env.ts @@ -0,0 +1,82 @@ +import { ccc } from "@ckb-ccc/core"; + +import { + buildNetworkConfig, + isMainnet, + NetworkConfig, + parseAddressType, + PredefinedNetwork, + RgbppPrivateKeyBtcWallet, +} from "../../bitcoin/index.js"; + +import { BtcAssetsApi } from "../../data-source/index.js"; +import { ClientScriptProvider } from "../../script/index.js"; +import { CkbRgbppUnlockSigner } from "../../signer/index.js"; +import { RgbppUdtClient } from "../../udt/index.js"; + +const utxoBasedChainName = process.env.UTXO_BASED_CHAIN_NAME!; +const ckbPrivateKey = process.env.CKB_SECP256K1_PRIVATE_KEY!; +const utxoBasedChainPrivateKey = process.env.UTXO_BASED_CHAIN_PRIVATE_KEY!; +const utxoBasedChainAddressType = process.env.UTXO_BASED_CHAIN_ADDRESS_TYPE!; +const btcAssetsApiUrl = process.env.BTC_ASSETS_API_URL!; +const btcAssetsApiToken = process.env.BTC_ASSETS_API_TOKEN!; +const btcAssetsApiOrigin = process.env.BTC_ASSETS_API_ORIGIN!; + +export async function initializeRgbppEnv(): Promise<{ + ckbClient: ccc.Client; + ckbSigner: ccc.SignerCkbPrivateKey; + networkConfig: NetworkConfig; + utxoBasedAccountAddress: string; + rgbppUdtClient: RgbppUdtClient; + rgbppBtcWallet: RgbppPrivateKeyBtcWallet; + ckbRgbppUnlockSigner: CkbRgbppUnlockSigner; +}> { + const ckbClient = isMainnet(utxoBasedChainName) + ? new ccc.ClientPublicMainnet() + : new ccc.ClientPublicTestnet(); + + const ckbSigner = new ccc.SignerCkbPrivateKey(ckbClient, ckbPrivateKey); + + const addressType = parseAddressType(utxoBasedChainAddressType); + + const networkConfig = buildNetworkConfig( + utxoBasedChainName as PredefinedNetwork, + ); + + const rgbppUdtClient = new RgbppUdtClient( + ckbClient, + new ClientScriptProvider(ckbClient), + ); + + const rgbppDataSource = new BtcAssetsApi({ + url: btcAssetsApiUrl, + token: btcAssetsApiToken, + origin: btcAssetsApiOrigin, + isMainnet: networkConfig.isMainnet, + }); + + const rgbppBtcWallet = new RgbppPrivateKeyBtcWallet( + utxoBasedChainPrivateKey, + addressType, + networkConfig, + rgbppDataSource, + { + logger: console, + }, + ); + + return { + ckbClient, + ckbSigner, + networkConfig, + utxoBasedAccountAddress: await rgbppBtcWallet.getAddress(), + rgbppUdtClient, + rgbppBtcWallet, + ckbRgbppUnlockSigner: new CkbRgbppUnlockSigner({ + ckbClient, + rgbppBtcAddress: await rgbppBtcWallet.getAddress(), + rgbppDataSource, + scriptInfos: await rgbppUdtClient.scriptManager.getRgbppScriptInfos(), + }), + }; +} diff --git a/packages/rgbpp/src/examples/env/load-env.ts b/packages/rgbpp/src/examples/env/load-env.ts new file mode 100644 index 000000000..3d4191ddd --- /dev/null +++ b/packages/rgbpp/src/examples/env/load-env.ts @@ -0,0 +1,5 @@ +import dotenv from "dotenv"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +dotenv.config({ path: dirname(fileURLToPath(import.meta.url)) + "/.env" }); diff --git a/packages/rgbpp/src/examples/spore/rgbpp-spore-leap-to-ckb.ts b/packages/rgbpp/src/examples/spore/rgbpp-spore-leap-to-ckb.ts new file mode 100644 index 000000000..f85d35960 --- /dev/null +++ b/packages/rgbpp/src/examples/spore/rgbpp-spore-leap-to-ckb.ts @@ -0,0 +1,71 @@ +import { spore } from "@ckb-ccc/spore"; + +import "../env/load-env.js"; + +import { initializeRgbppEnv } from "../env/env.js"; + +const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSigner, + ckbClient, + ckbSigner, +} = await initializeRgbppEnv(); + +async function btcSporeToCkb({ + ckbAddress, + sporeTypeArgs, +}: { + ckbAddress: string; + sporeTypeArgs: string; +}) { + const { tx: ckbPartialTx } = await spore.transferSpore({ + signer: ckbSigner, + id: sporeTypeArgs, + to: await rgbppUdtClient.buildBtcTimeLockScript(ckbAddress), + }); + + const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ + ckbPartialTx, + ckbClient, + rgbppUdtClient, + btcChangeAddress: utxoBasedAccountAddress, + receiverBtcAddresses: [], + }); + + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); + console.log("btcTxId:", btcTxId); + + const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( + indexedCkbPartialTx, + btcTxId, + ); + const rgbppSignedCkbTx = + await ckbRgbppUnlockSigner.signTransaction(ckbPartialTxInjected); + + await rgbppSignedCkbTx.completeFeeBy(ckbSigner); + const ckbFinalTx = await ckbSigner.signTransaction(rgbppSignedCkbTx); + const txHash = await ckbSigner.client.sendTransaction(ckbFinalTx); + await ckbRgbppUnlockSigner.client.waitTransaction(txHash); + console.log("ckbTxId:", txHash); +} + +btcSporeToCkb({ + ckbAddress: + "ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqfpu7pwavwf3yang8khrsklumayj6nyxhqpmh7fq", + sporeTypeArgs: + "0x8ce8307ac273c6e5548bd1a5dbf6596aab5dd5e75259a092b5461d3dba1c34bf", +}) + .then(() => { + process.exit(0); + }) + .catch((e) => { + const error = e instanceof Error ? e : new Error(String(e)); + console.error(error); + process.exit(1); + }); + +/* +pnpm tsx packages/rgbpp/src/examples/spore/rgbpp-spore-leap-to-ckb.ts +*/ diff --git a/packages/rgbpp/src/examples/spore/rgbpp-spore-transfer-on-btc.ts b/packages/rgbpp/src/examples/spore/rgbpp-spore-transfer-on-btc.ts new file mode 100644 index 000000000..f5f2cc242 --- /dev/null +++ b/packages/rgbpp/src/examples/spore/rgbpp-spore-transfer-on-btc.ts @@ -0,0 +1,84 @@ +import { ccc } from "@ckb-ccc/core"; +import { spore } from "@ckb-ccc/spore"; + +import "../env/load-env.js"; + +import { initializeRgbppEnv } from "../env/env.js"; + +const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSigner, + ckbClient, + ckbSigner, +} = await initializeRgbppEnv(); + +async function transferSpore( + transfers: Array<{ + btcAddress: string; + sporeTypeArgs: string; + }>, +) { + const pseudoRgbppLock = await rgbppUdtClient.buildPseudoRgbppLockScript(); + + let ckbPartialTx = ccc.Transaction.from({}); + for (const { sporeTypeArgs } of transfers) { + const { tx: _ckbPartialTx } = await spore.transferSpore({ + signer: ckbSigner, + id: sporeTypeArgs, + to: pseudoRgbppLock, + tx: ckbPartialTx, + }); + ckbPartialTx = _ckbPartialTx; + } + + const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ + ckbPartialTx, + ckbClient, + rgbppUdtClient, + btcChangeAddress: utxoBasedAccountAddress, + receiverBtcAddresses: transfers.map((t) => t.btcAddress), + }); + + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); + console.log("btcTxId:", btcTxId); + + const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( + indexedCkbPartialTx, + btcTxId, + ); + const rgbppSignedCkbTx = + await ckbRgbppUnlockSigner.signTransaction(ckbPartialTxInjected); + + await rgbppSignedCkbTx.completeFeeBy(ckbSigner); + const ckbFinalTx = await ckbSigner.signTransaction(rgbppSignedCkbTx); + const txHash = await ckbSigner.client.sendTransaction(ckbFinalTx); + await ckbRgbppUnlockSigner.client.waitTransaction(txHash); + console.log("ckbTxId:", txHash); +} + +transferSpore([ + { + btcAddress: "tb1q4vkt8486w7syqyvz3a4la0f3re5vvj9zw4henw", + sporeTypeArgs: + "0x8d814f7306d31bdfa40ddec0d3c9391c5505a7e9c0917596a8535e2a81ef3ab2", + }, + { + btcAddress: "tb1q4vkt8486w7syqyvz3a4la0f3re5vvj9zw4henw", + sporeTypeArgs: + "0x01eb873a190a200cdf3a21ee823663e3f2d5d220b0dee6033fd06a67c43cb733", + }, +]) + .then(() => { + process.exit(0); + }) + .catch((e) => { + const error = e instanceof Error ? e : new Error(String(e)); + console.error(error); + process.exit(1); + }); + +/* +pnpm tsx packages/rgbpp/src/examples/spore/rgbpp-spore-transfer-on-btc.ts +*/ diff --git a/packages/rgbpp/src/examples/spore/spore-leap-to-btc.ts b/packages/rgbpp/src/examples/spore/spore-leap-to-btc.ts new file mode 100644 index 000000000..318be8e5e --- /dev/null +++ b/packages/rgbpp/src/examples/spore/spore-leap-to-btc.ts @@ -0,0 +1,56 @@ +import { spore } from "@ckb-ccc/spore"; + +import { UtxoSeal } from "../../bitcoin/index.js"; + +import "../env/load-env.js"; + +import { initializeRgbppEnv } from "../env/env.js"; + +const { rgbppBtcWallet, rgbppUdtClient, ckbSigner } = + await initializeRgbppEnv(); + +async function ckbSporeToBtc({ + utxoSeal, + sporeTypeArgs, +}: { + utxoSeal?: UtxoSeal; + sporeTypeArgs: string; +}) { + if (!utxoSeal) { + const { psbt, sealOutputIndex } = await rgbppBtcWallet.buildSealPsbt(); + const txId = await rgbppBtcWallet.signAndBroadcast(psbt); + await rgbppBtcWallet.waitForConfirmation(txId); + utxoSeal = { txid: txId, vout: sealOutputIndex }; + } + + const rgbppLock = await rgbppUdtClient.buildRgbppLockScript(utxoSeal); + + const { tx } = await spore.transferSpore({ + signer: ckbSigner, + id: sporeTypeArgs, + to: rgbppLock, + }); + + await tx.completeFeeBy(ckbSigner); + const signedTx = await ckbSigner.signTransaction(tx); + const txHash = await ckbSigner.client.sendTransaction(signedTx); + await ckbSigner.client.waitTransaction(txHash); + console.log("ckbTxId:", txHash); +} + +ckbSporeToBtc({ + sporeTypeArgs: + "0xb1bf3620fa9caf55bd5e6ca05a99013cb48ba5cbf522efc34cc098da4a1cb1fe", +}) + .then(() => { + process.exit(0); + }) + .catch((e) => { + const error = e instanceof Error ? e : new Error(String(e)); + console.error(error); + process.exit(1); + }); + +/* +pnpm tsx packages/rgbpp/src/examples/spore/spore-leap-to-btc.ts +*/ diff --git a/packages/rgbpp/src/examples/udt/rgbpp-udt-leap-to-ckb.ts b/packages/rgbpp/src/examples/udt/rgbpp-udt-leap-to-ckb.ts new file mode 100644 index 000000000..22aae6cf2 --- /dev/null +++ b/packages/rgbpp/src/examples/udt/rgbpp-udt-leap-to-ckb.ts @@ -0,0 +1,106 @@ +import { ccc } from "@ckb-ccc/core"; +import { Udt } from "@ckb-ccc/udt"; + +import "../env/load-env.js"; + +import { initializeRgbppEnv } from "../env/env.js"; + +const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSigner, + ckbClient, + ckbSigner, +} = await initializeRgbppEnv(); + +async function btcUdtToCkb({ + udtScriptArgs, + customUdtScriptInfo, + receivers, +}: { + udtScriptArgs: ccc.Hex; + customUdtScriptInfo?: ccc.ScriptInfo; + receivers: { address: string; amount: bigint }[]; +}) { + const scriptInfo = + customUdtScriptInfo ?? + (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)); + const udtInstance = new Udt( + scriptInfo.cellDeps[0].cellDep.outPoint, + ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: udtScriptArgs, + }), + ); + + const { res: tx } = await udtInstance.transfer( + ckbSigner, + await Promise.all( + receivers.map(async (receiver) => ({ + to: await rgbppUdtClient.buildBtcTimeLockScript(receiver.address), + amount: ccc.fixedPointFrom(receiver.amount), + })), + ), + ); + + const pseudoRgbppLock = await rgbppUdtClient.buildPseudoRgbppLockScript(); + const txWithInputs = await udtInstance.completeChangeToLock( + tx, + ckbRgbppUnlockSigner, + // merge multiple inputs to a single change output + pseudoRgbppLock, + ); + + const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ + ckbPartialTx: txWithInputs, + ckbClient, + rgbppUdtClient, + btcChangeAddress: utxoBasedAccountAddress, + receiverBtcAddresses: [], + }); + + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); + console.log("BTC tx broadcast (RGB++ UDT leap to CKB):", btcTxId); + + const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( + indexedCkbPartialTx, + btcTxId, + ); + + const rgbppSignedCkbTx = + await ckbRgbppUnlockSigner.signTransaction(ckbPartialTxInjected); + await rgbppSignedCkbTx.completeFeeBy(ckbSigner); + const ckbFinalTx = await ckbSigner.signTransaction(rgbppSignedCkbTx); + const txHash = await ckbSigner.client.sendTransaction(ckbFinalTx); + await ckbRgbppUnlockSigner.client.waitTransaction(txHash); + console.log("CKB tx confirmed (RGB++ UDT leap to CKB):", txHash); +} + +btcUdtToCkb({ + udtScriptArgs: + "0xe6fa637f763fd63732146015b0964fe88f16996846b3d0a164bf15c069ff008b", + receivers: [ + { + address: await ckbSigner.getRecommendedAddress(), + amount: ccc.fixedPointFrom(1), + }, + { + address: await ckbSigner.getRecommendedAddress(), + amount: ccc.fixedPointFrom(10), + }, + ], +}) + .then(() => { + process.exit(0); + }) + .catch((e) => { + const error = e instanceof Error ? e : new Error(String(e)); + console.error(error); + process.exit(1); + }); + +/* +pnpm tsx packages/rgbpp/src/examples/udt/rgbpp-udt-leap-to-ckb.ts +*/ diff --git a/packages/rgbpp/src/examples/udt/rgbpp-udt-transfer-on-btc.ts b/packages/rgbpp/src/examples/udt/rgbpp-udt-transfer-on-btc.ts new file mode 100644 index 000000000..a765a1b58 --- /dev/null +++ b/packages/rgbpp/src/examples/udt/rgbpp-udt-transfer-on-btc.ts @@ -0,0 +1,122 @@ +import { ccc } from "@ckb-ccc/core"; +import { Udt } from "@ckb-ccc/udt"; + +import "../env/load-env.js"; + +import { initializeRgbppEnv } from "../env/env.js"; + +export interface RgbppBtcReceiver { + address: string; + amount: bigint; +} + +const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSigner, + ckbClient, + ckbSigner, +} = await initializeRgbppEnv(); + +async function transferUdt({ + udtScriptArgs, + customUdtScriptInfo, + receivers, +}: { + udtScriptArgs: ccc.Hex; + customUdtScriptInfo?: ccc.ScriptInfo; + receivers: RgbppBtcReceiver[]; +}) { + const scriptInfo = + customUdtScriptInfo ?? + (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)); + const udtInstance = new Udt( + scriptInfo.cellDeps[0].cellDep.outPoint, + ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: udtScriptArgs, + }), + ); + + const pseudoRgbppLock = await rgbppUdtClient.buildPseudoRgbppLockScript(); + + const { res: tx } = await udtInstance.transfer( + ckbSigner, + receivers.map((receiver) => ({ + to: pseudoRgbppLock, + amount: ccc.fixedPointFrom(receiver.amount), + })), + ); + + // * collect udt inputs using ccc + const txWithInputs = await udtInstance.completeChangeToLock( + tx, + ckbRgbppUnlockSigner, + pseudoRgbppLock, + ); + + const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ + ckbPartialTx: txWithInputs, + ckbClient, + rgbppUdtClient, + btcChangeAddress: utxoBasedAccountAddress, + receiverBtcAddresses: receivers.map((receiver) => receiver.address), + }); + + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); + console.log("BTC tx broadcast (RGB++ UDT transfer on BTC):", btcTxId); + + const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( + indexedCkbPartialTx, + btcTxId, + ); + + const rgbppSignedCkbTx = + await ckbRgbppUnlockSigner.signTransaction(ckbPartialTxInjected); + await rgbppSignedCkbTx.completeFeeBy(ckbSigner); + const ckbFinalTx = await ckbSigner.signTransaction(rgbppSignedCkbTx); + const txHash = await ckbSigner.client.sendTransaction(ckbFinalTx); + await ckbRgbppUnlockSigner.client.waitTransaction(txHash); + console.log("CKB tx confirmed (RGB++ UDT transfer on BTC):", txHash); +} + +transferUdt({ + udtScriptArgs: + "0xe6fa637f763fd63732146015b0964fe88f16996846b3d0a164bf15c069ff008b", + receivers: [ + { + address: "tb1qgsdzelnw8dvajqgl9mqrahatqe06u7dn2gkz9u", + amount: ccc.fixedPointFrom(1), + }, + { + address: "tb1qgsdzelnw8dvajqgl9mqrahatqe06u7dn2gkz9u", + amount: ccc.fixedPointFrom(2), + }, + { + address: "tb1qgsdzelnw8dvajqgl9mqrahatqe06u7dn2gkz9u", + amount: ccc.fixedPointFrom(3), + }, + { + address: "tb1qgsdzelnw8dvajqgl9mqrahatqe06u7dn2gkz9u", + amount: ccc.fixedPointFrom(4), + }, + { + address: "tb1qgsdzelnw8dvajqgl9mqrahatqe06u7dn2gkz9u", + amount: ccc.fixedPointFrom(5), + }, + ], +}) + .then(() => { + process.exit(0); + }) + .catch((e) => { + const error = e instanceof Error ? e : new Error(String(e)); + console.log(error); + process.exit(1); + }); + +/* +pnpm tsx packages/rgbpp/src/examples/udt/rgbpp-udt-transfer-on-btc.ts +*/ diff --git a/packages/rgbpp/src/examples/udt/udt-leap-to-btc.ts b/packages/rgbpp/src/examples/udt/udt-leap-to-btc.ts new file mode 100644 index 000000000..ed7535bf9 --- /dev/null +++ b/packages/rgbpp/src/examples/udt/udt-leap-to-btc.ts @@ -0,0 +1,76 @@ +import { ccc } from "@ckb-ccc/core"; +import { Udt } from "@ckb-ccc/udt"; + +import { UtxoSeal } from "../../bitcoin/index.js"; + +import "../env/load-env.js"; + +import { initializeRgbppEnv } from "../env/env.js"; + +const { rgbppBtcWallet, rgbppUdtClient, ckbClient, ckbSigner } = + await initializeRgbppEnv(); + +async function ckbUdtToBtc({ + utxoSeal, + udtScriptArgs, + customUdtScriptInfo, + amount, +}: { + utxoSeal?: UtxoSeal; + udtScriptArgs: ccc.Hex; + customUdtScriptInfo?: ccc.ScriptInfo; + amount: bigint; +}) { + if (!utxoSeal) { + const { psbt, sealOutputIndex } = await rgbppBtcWallet.buildSealPsbt(); + const txId = await rgbppBtcWallet.signAndBroadcast(psbt); + await rgbppBtcWallet.waitForConfirmation(txId); + utxoSeal = { txid: txId, vout: sealOutputIndex }; + } + + const scriptInfo = + customUdtScriptInfo ?? + (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)); + const udtInstance = new Udt( + scriptInfo.cellDeps[0].cellDep.outPoint, + ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: udtScriptArgs, + }), + ); + + const rgbppLock = await rgbppUdtClient.buildRgbppLockScript(utxoSeal); + + const { res: tx } = await udtInstance.transfer(ckbSigner, [ + { + to: rgbppLock, + amount: ccc.fixedPointFrom(amount), + }, + ]); + + const txWithInputs = await udtInstance.completeBy(tx, ckbSigner); + await txWithInputs.completeFeeBy(ckbSigner); + const signedTx = await ckbSigner.signTransaction(txWithInputs); + const txHash = await ckbSigner.client.sendTransaction(signedTx); + await ckbSigner.client.waitTransaction(txHash); + console.log("CKB tx confirmed (UDT leap to BTC):", txHash); +} + +ckbUdtToBtc({ + udtScriptArgs: + "0xe6fa637f763fd63732146015b0964fe88f16996846b3d0a164bf15c069ff008b", + amount: ccc.fixedPointFrom(1000), +}) + .then(() => { + process.exit(0); + }) + .catch((e) => { + const error = e instanceof Error ? e : new Error(String(e)); + console.error(error); + process.exit(1); + }); + +/* +pnpm tsx packages/rgbpp/src/examples/udt/udt-leap-to-btc.ts +*/ diff --git a/packages/rgbpp/src/index.ts b/packages/rgbpp/src/index.ts new file mode 100644 index 000000000..8c3c6187a --- /dev/null +++ b/packages/rgbpp/src/index.ts @@ -0,0 +1,2 @@ +export * from "./barrel.js"; +export * as rgbpp from "./barrel.js"; diff --git a/packages/rgbpp/src/script/index.ts b/packages/rgbpp/src/script/index.ts new file mode 100644 index 000000000..02e36bafb --- /dev/null +++ b/packages/rgbpp/src/script/index.ts @@ -0,0 +1,3 @@ +export * from "./manager.js"; +export * from "./provider.js"; +export * from "./script.js"; diff --git a/packages/rgbpp/src/script/manager.ts b/packages/rgbpp/src/script/manager.ts new file mode 100644 index 000000000..663c6d1eb --- /dev/null +++ b/packages/rgbpp/src/script/manager.ts @@ -0,0 +1,166 @@ +import { ccc } from "@ckb-ccc/core"; + +import { UtxoSeal } from "../bitcoin/transaction/index.js"; +import { IScriptProvider } from "./provider.js"; +import { + buildBtcTimeLockArgs, + buildRgbppLockArgs, + buildUniqueTypeArgs, + pseudoRgbppLockArgs, + RGBPP_BTC_TX_DEFAULT_CONFIRMATIONS, + RgbppScriptName, +} from "./script.js"; + +/** + * ScriptManager - Manages and builds RGB++ related scripts + * + * Uses IScriptProvider for flexible script source configuration. + * Supports multiple script sources through provider composition. + * + * @example + * ```typescript + * import { ScriptManager, ClientScriptProvider } from "@ckb-ccc/rgbpp"; + * + * // Basic usage with client + * const manager = new ScriptManager(new ClientScriptProvider(client)); + * + * // With custom scripts + * const manager = new ScriptManager( + * createScriptProvider(client, customScripts) + * ); + * ``` + */ +export class ScriptManager { + constructor(private provider: IScriptProvider) {} + + /** + * Get script info by name using the configured provider + * + * @param name - Known script name from ccc.KnownScript + * @returns ccc.ScriptInfo containing code hash, hash type, and cell dependencies + */ + async getKnownScriptInfo(name: ccc.KnownScript): Promise { + return this.provider.getScriptInfo(name); + } + + /** + * Get all required RGBPP script infos in one call + * This is a convenience method for initializing CkbRgbppUnlockSigner + * + * @returns Record containing RgbppLock, BtcTimeLock, and UniqueType script infos + * @example + * ```typescript + * const scriptInfos = await scriptManager.getRgbppScriptInfos(); + * const signer = new CkbRgbppUnlockSigner({ + * ckbClient, + * rgbppBtcAddress, + * btcDataSource, + * scriptInfos, + * }); + * ``` + */ + async getRgbppScriptInfos(): Promise< + Record + > { + const [rgbppLock, btcTimeLock, uniqueType] = await Promise.all([ + this.getKnownScriptInfo(ccc.KnownScript.RgbppLock), + this.getKnownScriptInfo(ccc.KnownScript.BtcTimeLock), + this.getKnownScriptInfo(ccc.KnownScript.UniqueType), + ]); + + return { + [ccc.KnownScript.RgbppLock]: rgbppLock, + [ccc.KnownScript.BtcTimeLock]: btcTimeLock, + [ccc.KnownScript.UniqueType]: uniqueType, + }; + } + + async buildPseudoRgbppLockScript(): Promise { + const scriptInfo = await this.getKnownScriptInfo(ccc.KnownScript.RgbppLock); + return ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: pseudoRgbppLockArgs(), + }); + } + + async buildRgbppLockScript(utxoSeal: UtxoSeal): Promise { + const scriptInfo = await this.getKnownScriptInfo(ccc.KnownScript.RgbppLock); + return ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: buildRgbppLockArgs({ + txid: utxoSeal.txid, + vout: utxoSeal.vout, // index in btc tx output + }), + }); + } + + async buildBtcTimeLockScript( + receiverLock: ccc.Script, + btcTxId: string, + confirmations = RGBPP_BTC_TX_DEFAULT_CONFIRMATIONS, + ): Promise { + const scriptInfo = await this.getKnownScriptInfo( + ccc.KnownScript.BtcTimeLock, + ); + return ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: buildBtcTimeLockArgs(receiverLock, btcTxId, confirmations), + }); + } + + /** + * Build unique type script by firstInput and outputIndex + * + * @see https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md#type-id + * + * There are two ways to create a new cell with a specific type id. + * + * 1. Create a transaction which uses any out point as tx.inputs[0] and has a output cell whose type script is Type ID. The output cell's type script args is the hash of tx.inputs[0] and its output index. Because any out point can only be used once as an input, tx.inputs[0] and thus the new type id must be different in each creation transaction. + * 2. Destroy an old cell with a specific type id and create a new cell with the same type id in the same transaction. + * + * @param firstInput - The first input of the transaction + * @param outputIndex - The index of the output cell + */ + async buildUniqueTypeScript( + firstInput: ccc.CellInput, + outputIndex: number, + ): Promise { + const scriptInfo = await this.getKnownScriptInfo( + ccc.KnownScript.UniqueType, + ); + return ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: buildUniqueTypeArgs(firstInput, outputIndex), + }); + } + + /** + * Get RGB++ lock script template (without args) + */ + async rgbppLockScriptTemplate(): Promise { + const scriptInfo = await this.getKnownScriptInfo(ccc.KnownScript.RgbppLock); + return ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: "", + }); + } + + /** + * Get BTC time lock script template (without args) + */ + async btcTimeLockScriptTemplate(): Promise { + const scriptInfo = await this.getKnownScriptInfo( + ccc.KnownScript.BtcTimeLock, + ); + return ccc.Script.from({ + codeHash: scriptInfo.codeHash, + hashType: scriptInfo.hashType, + args: "", + }); + } +} diff --git a/packages/rgbpp/src/script/provider.ts b/packages/rgbpp/src/script/provider.ts new file mode 100644 index 000000000..e917e39b9 --- /dev/null +++ b/packages/rgbpp/src/script/provider.ts @@ -0,0 +1,235 @@ +import { ccc } from "@ckb-ccc/core"; + +import { ErrorRgbppScriptNotFound } from "../error.js"; + +/** + * Script provider interface + * Implement this interface to provide custom script sources + * + * Returns ccc.ScriptInfo which contains the script's code hash, hash type, and cell dependencies. + * + * @public + */ +export interface IScriptProvider { + getScriptInfo(name: ccc.KnownScript): Promise; +} + +/** + * ClientScriptProvider - Fetches script info from CKB client with caching + * + * @example + * ```typescript + * const provider = new ClientScriptProvider(client); + * const scriptInfo = await provider.getScriptInfo(ccc.KnownScript.RgbppLock); + * ``` + */ +export class ClientScriptProvider implements IScriptProvider { + private cache = new Map>(); + + constructor(private client: ccc.Client) {} + + async getScriptInfo(name: ccc.KnownScript): Promise { + let promise = this.cache.get(name); + if (!promise) { + promise = this.client.getKnownScript(name); + this.cache.set(name, promise); + } + return promise; + } + + /** + * Clear the internal cache + */ + clearCache(): void { + this.cache.clear(); + } +} + +/** + * StaticScriptProvider - Uses predefined script configurations + * + * @example + * ```typescript + * const provider = new StaticScriptProvider({ + * [ccc.KnownScript.RgbppLock]: ccc.ScriptInfo.from({ + * codeHash: "0x...", + * hashType: "type", + * cellDeps: [{ outPoint: { txHash: "0x...", index: 0 }, depType: "code" }] + * }) + * }); + * ``` + */ +export class StaticScriptProvider implements IScriptProvider { + private scripts: Map; + + constructor(scripts: Partial>) { + this.scripts = new Map(Object.entries(scripts)) as Map< + ccc.KnownScript, + ccc.ScriptInfo + >; + } + + async getScriptInfo(name: ccc.KnownScript): Promise { + const info = this.scripts.get(name); + if (!info) { + throw new ErrorRgbppScriptNotFound(name); + } + return info; + } +} + +/** + * CompositeScriptProvider - Tries multiple providers in order until one succeeds + * + * **Priority**: Providers are tried in array order - the first provider has the highest priority. + * If a provider throws an error, the next provider is tried. This enables fallback strategies. + * + * Common use cases: + * - **Custom scripts with fallback**: Try user-defined scripts first, then fall back to client + * - **Multi-source**: Support multiple script sources with priority + * - **Override**: Override specific scripts while keeping others from the default source + * + * @example + * ```typescript + * // Priority: Custom scripts (highest) → Client (fallback) + * const provider = new CompositeScriptProvider([ + * new StaticScriptProvider(customScripts), // Tried first + * new ClientScriptProvider(client), // Tried if custom fails + * ]); + * + * // Priority: TestNet → MainNet (for testing with mainnet fallback) + * const provider = new CompositeScriptProvider([ + * new ClientScriptProvider(testnetClient), // Tried first + * new ClientScriptProvider(mainnetClient), // Fallback + * ]); + * ``` + */ +export class CompositeScriptProvider implements IScriptProvider { + /** + * @param providers - Array of providers in priority order (first = highest priority) + */ + constructor(private providers: IScriptProvider[]) { + if (providers.length === 0) { + throw new Error("CompositeScriptProvider requires at least one provider"); + } + } + + async getScriptInfo(name: ccc.KnownScript): Promise { + const errors: Error[] = []; + + for (const provider of this.providers) { + try { + return await provider.getScriptInfo(name); + } catch (error) { + errors.push(error as Error); + // Continue to next provider + } + } + + // All providers failed + throw new ErrorRgbppScriptNotFound( + `${name} (all ${errors.length} providers failed)`, + ); + } + + /** + * Add a high-priority provider (inserted at the beginning) + * Useful for adding override scripts without recreating the provider chain + * + * @example + * ```typescript + * const baseProvider = createScriptProvider(client); + * const withOverrides = baseProvider.withHighPriority( + * new StaticScriptProvider(overrideScripts) + * ); + * ``` + */ + withHighPriority(provider: IScriptProvider): CompositeScriptProvider { + return new CompositeScriptProvider([provider, ...this.providers]); + } + + /** + * Add a low-priority provider (appended at the end) + * Useful for adding fallback scripts + * + * @example + * ```typescript + * const provider = createScriptProvider(client); + * const withFallback = provider.withFallback( + * new StaticScriptProvider(fallbackScripts) + * ); + * ``` + */ + withFallback(provider: IScriptProvider): CompositeScriptProvider { + return new CompositeScriptProvider([...this.providers, provider]); + } + + /** + * Create a composite provider with custom scripts having highest priority + * + * @example + * ```typescript + * const provider = CompositeScriptProvider.withCustomScripts( + * client, + * customScripts + * ); + * ``` + */ + static withCustomScripts( + client: ccc.Client, + customScripts: Partial>, + ): CompositeScriptProvider { + return new CompositeScriptProvider([ + new StaticScriptProvider(customScripts), + new ClientScriptProvider(client), + ]); + } +} + +/** + * Factory function to create a script provider with common patterns + * + * **Priority when custom scripts provided:** + * 1. Custom scripts (highest priority) - user-defined configurations + * 2. Client scripts (fallback) - fetched from CKB node + * + * This ensures your custom scripts always take precedence, with automatic fallback + * to the client for any scripts not defined in your custom configuration. + * + * @param client - CKB client instance + * @param customScripts - Optional custom script configurations (highest priority) + * @returns A script provider (composite if custom scripts provided, otherwise client-based) + * + * @example + * ```typescript + * // Without custom scripts - uses client only + * const provider = createScriptProvider(client); + * + * // With custom scripts - custom scripts have highest priority + * const provider = createScriptProvider(client, { + * [ccc.KnownScript.RgbppLock]: myCustomRgbppScript, // This will be used + * }); + * // When requesting RgbppLock: returns myCustomRgbppScript + * // When requesting other scripts: falls back to client + * + * // Override specific scripts for testing + * const testProvider = createScriptProvider(client, { + * [ccc.KnownScript.RgbppLock]: testRgbppScript, // Override for testing + * // Other scripts will use client defaults + * }); + * ``` + */ +export function createScriptProvider( + client: ccc.Client, + customScripts?: Partial>, +): IScriptProvider { + if (!customScripts || Object.keys(customScripts).length === 0) { + return new ClientScriptProvider(client); + } + + // Priority: Custom scripts (1st) → Client scripts (2nd) + return new CompositeScriptProvider([ + new StaticScriptProvider(customScripts), + new ClientScriptProvider(client), + ]); +} diff --git a/packages/rgbpp/src/script/script.ts b/packages/rgbpp/src/script/script.ts new file mode 100644 index 000000000..bdb125940 --- /dev/null +++ b/packages/rgbpp/src/script/script.ts @@ -0,0 +1,394 @@ +import { sha256 } from "@noble/hashes/sha2"; + +import { ccc, mol } from "@ckb-ccc/core"; + +import { UtxoSeal } from "../bitcoin/transaction/index.js"; +import { ErrorRgbppInvalidLockArgs } from "../error.js"; + +/** + * Required RGBPP scripts that must be provided + * @public + */ +export const RGBPP_REQUIRED_SCRIPTS = [ + ccc.KnownScript.RgbppLock, + ccc.KnownScript.BtcTimeLock, + ccc.KnownScript.UniqueType, +] as const; + +/** + * Type representing the required RGBPP script names + * @public + */ +export type RgbppScriptName = (typeof RGBPP_REQUIRED_SCRIPTS)[number]; + +// struct ExtraCommitmentData { +// input_len: byte, +// output_len: byte, +// } + +/** + * @public + */ +export type ExtraCommitmentDataLike = { + inputLen: ccc.NumLike; + outputLen: ccc.NumLike; +}; + +@mol.codec( + mol.struct({ + inputLen: mol.Uint8, + outputLen: mol.Uint8, + }), +) +export class ExtraCommitmentData extends mol.Entity.Base< + ExtraCommitmentDataLike, + ExtraCommitmentData +>() { + constructor( + public inputLen: ccc.Num, + public outputLen: ccc.Num, + ) { + super(); + } + + static from(ec: ExtraCommitmentDataLike): ExtraCommitmentData { + return new ExtraCommitmentData( + ccc.numFrom(ec.inputLen), + ccc.numFrom(ec.outputLen), + ); + } +} + +// table RGBPPUnlock { +// version: Uint16, +// extra_data: ExtraCommitmentData, +// btc_tx: Bytes, +// btc_tx_proof: Bytes, +// } + +/** + * @public + */ +export type RgbppUnlockLike = { + version: ccc.NumLike; + extraData: ExtraCommitmentDataLike; + btcTx: ccc.HexLike; + btcTxProof: ccc.HexLike; +}; +/** + * @public + */ +@mol.codec( + mol.table({ + version: mol.Uint16, + extraData: ExtraCommitmentData, + btcTx: mol.Bytes, + btcTxProof: mol.Bytes, + }), +) +export class RgbppUnlock extends mol.Entity.Base< + RgbppUnlockLike, + RgbppUnlock +>() { + constructor( + public version: ccc.Num, + public extraData: ExtraCommitmentData, + public btcTx: ccc.Hex, + public btcTxProof: ccc.Hex, + ) { + super(); + } + + static from(ru: RgbppUnlockLike): RgbppUnlock { + return new RgbppUnlock( + ccc.numFrom(ru.version), + ExtraCommitmentData.from(ru.extraData), + ccc.hexFrom(ru.btcTx), + ccc.hexFrom(ru.btcTxProof), + ); + } +} + +// table BTCTimeLock { +// lock_script: Script, +// after: Uint32, +// btc_txid: Byte32, +// } + +/** + * @public + */ +export type BtcTimeLockLike = { + lockScript: ccc.Script; + after: ccc.NumLike; + btcTxid: ccc.HexLike; +}; +/** + * @public + */ +@mol.codec( + mol.table({ + lockScript: ccc.Script, + after: mol.Uint32, + btcTxid: mol.Byte32, + }), +) +export class BtcTimeLock extends mol.Entity.Base< + BtcTimeLockLike, + BtcTimeLock +>() { + constructor( + public lockScript: ccc.Script, + public after: ccc.Num, + public btcTxid: ccc.Hex, + ) { + super(); + } + + static from(btl: BtcTimeLockLike): BtcTimeLock { + return new BtcTimeLock( + ccc.Script.from(btl.lockScript), + ccc.numFrom(btl.after), + ccc.hexFrom(btl.btcTxid), + ); + } +} + +// table BTCTimeUnlock { +// btc_tx_proof: Bytes, +// } + +/** + * @public + */ +export type BtcTimeUnlockLike = { + btcTxProof: ccc.HexLike; +}; +/** + * @public + */ +@mol.codec( + mol.table({ + btcTxProof: mol.Bytes, + }), +) +export class BtcTimeUnlock extends mol.Entity.Base< + BtcTimeUnlockLike, + BtcTimeUnlock +>() { + constructor(public btcTxProof: ccc.Hex) { + super(); + } + + static from(btul: BtcTimeUnlockLike): BtcTimeUnlock { + return new BtcTimeUnlock(ccc.hexFrom(btul.btcTxProof)); + } +} + +// https://github.com/RGBPlusPlus/rgbpp/blob/main/contracts/rgbpp-lock/src/main.rs#L228 +export const RGBPP_BTC_BLANK_TX_ID = + "0000000000000000000000000000000000000000000000000000000000000000"; + +export const RGBPP_BTC_TX_ID_PLACEHOLDER_PRE_IMAGE = + "sha256 this for easy replacement in spore co-build witness"; +export const RGBPP_BTC_TX_ID_PLACEHOLDER = ccc.bytesTo( + sha256(ccc.bytesFrom(RGBPP_BTC_TX_ID_PLACEHOLDER_PRE_IMAGE, "utf8")), + "hex", +); + +export const RGBPP_BTC_TX_PSEUDO_INDEX = 0xffffffff; // 4,294,967,295 (max u32) + +export const RGBPP_BTC_TX_DEFAULT_CONFIRMATIONS = 6; + +export const RGBPP_UNIQUE_TYPE_OUTPUT_INDEX = 1; + +export const RGBPP_CONFIG_CELL_INDEX = 1; + +export const deadLock = ccc.Script.from({ + codeHash: + "0x0000000000000000000000000000000000000000000000000000000000000000", + hashType: "data", + args: "0x", +}); + +/** + * https://learnmeabitcoin.com/technical/general/byte-order/ + * Whenever you're working with transaction/block hashes internally (e.g. inside raw bitcoin data), you use the natural byte order. + * Whenever you're displaying or searching for transaction/block hashes, you use the reverse byte order. + */ +export const buildRgbppLockArgs = (utxoSeal: UtxoSeal): ccc.Hex => { + return ccc.hexFrom( + ccc.bytesConcat( + ccc.numLeToBytes(utxoSeal.vout, 4), + ccc.bytesFrom(utxoSeal.txid).reverse(), + ), + ); +}; + +export function pseudoRgbppLockArgs(): ccc.Hex { + return buildRgbppLockArgs({ + txid: RGBPP_BTC_TX_ID_PLACEHOLDER, + vout: RGBPP_BTC_TX_PSEUDO_INDEX, + }); +} + +export function pseudoRgbppLockArgsForCommitment(index: number): ccc.Hex { + return buildRgbppLockArgs({ + txid: RGBPP_BTC_BLANK_TX_ID, + vout: index, + }); +} + +export const buildBtcTimeLockArgs = ( + receiverLock: ccc.Script, + btcTxId: string, + confirmations = RGBPP_BTC_TX_DEFAULT_CONFIRMATIONS, +): ccc.Hex => { + return ccc.hexFrom( + BtcTimeLock.encode({ + lockScript: receiverLock, + after: confirmations, + btcTxid: ccc.hexFrom(ccc.bytesFrom(btcTxId).reverse()), + }), + ); +}; + +export const buildUniqueTypeArgs = ( + firstInput: ccc.CellInput, + firstOutputIndex: number, +) => { + const input = ccc.bytesFrom(firstInput.toBytes()); + const s = new ccc.HasherCkb(); + s.update(input); + s.update(ccc.numLeToBytes(firstOutputIndex, 8)); + return s.digest().slice(0, 42); +}; + +export const buildRgbppUnlock = ( + btcLikeTxBytes: string, + btcLikeTxProof: ccc.Hex, + inputLen: number, + outputLen: number, +) => { + return ccc.hexFrom( + RgbppUnlock.encode({ + version: 0, + extraData: { + inputLen, + outputLen, + }, + btcTx: ccc.hexFrom(btcLikeTxBytes), + btcTxProof: ccc.hexFrom(btcLikeTxProof), + }), + ); +}; + +export const isSameScriptTemplate = ( + lock1: ccc.Script, + lock2: ccc.Script, +): boolean => { + return lock1.codeHash === lock2.codeHash && lock1.hashType === lock2.hashType; +}; + +export const isUsingOneOfScripts = ( + script: ccc.Script, + scripts: ccc.Script[], +): boolean => { + return ( + scripts.length > 0 && scripts.some((s) => isSameScriptTemplate(s, script)) + ); +}; + +export const updateScriptArgsWithTxId = ( + args: ccc.Hex, + txId: string, +): string => { + const argsBytes = ccc.bytesFrom(args); + if (argsBytes.length < 32) { + throw new ErrorRgbppInvalidLockArgs(argsBytes.length, 32); + } + const txIdBytes = ccc.bytesFrom(txId).reverse(); + const newArgs = ccc.bytesConcat( + argsBytes.subarray(0, argsBytes.length - 32), + txIdBytes, + ); + return ccc.hexFrom(newArgs); +}; + +export function getTxIdFromRgbppLockArgs(args: ccc.Hex): string { + const argsBytes = ccc.bytesFrom(args); + if (argsBytes.length < 32) { + throw new ErrorRgbppInvalidLockArgs(argsBytes.length, 32); + } + + return ccc.bytesTo( + argsBytes.subarray(argsBytes.length - 32).reverse(), + "hex", + ); +} + +export function getTxIndexFromRgbppLockArgs(args: ccc.Hex): number { + const argsBytes = ccc.bytesFrom(args); + if (argsBytes.length < 32) { + throw new ErrorRgbppInvalidLockArgs(argsBytes.length, 32); + } + + return Number(ccc.numLeFromBytes(argsBytes.subarray(0, 4))); +} + +export function parseUtxoSealFromRgbppLockArgs(args: ccc.Hex): UtxoSeal { + return { + txid: getTxIdFromRgbppLockArgs(args), + vout: getTxIndexFromRgbppLockArgs(args), + }; +} + +export function deduplicateByOutPoint( + items: T[], +): T[] { + const seen = new Set(); + return items.filter((item) => { + const key = `${item.outPoint.txHash}-${item.outPoint.index.toString()}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +export const parseBtcTimeLockArgs = ( + args: string, +): { + lock: ccc.Script; + confirmations: number; + btcTxId: string; +} => { + const { + lockScript, + after: confirmations, + btcTxid: btcTxId, + } = BtcTimeLock.decode(ccc.hexFrom(args)); + + return { + lock: lockScript, + confirmations: Number(confirmations), + btcTxId: btcTxIdInReverseByteOrder(btcTxId), + }; +}; + +export const buildBtcTimeUnlockWitness = (btcTxProof: string): ccc.Hex => { + const btcTimeUnlock = BtcTimeUnlock.encode({ + btcTxProof: ccc.hexFrom(btcTxProof), + }); + + return ccc.hexFrom( + ccc.WitnessArgs.from({ + lock: ccc.hexFrom(btcTimeUnlock), + inputType: "", + outputType: "", + }).toBytes(), + ); +}; + +export function btcTxIdInReverseByteOrder(btcTxId: string): string { + return ccc.bytesTo(ccc.bytesFrom(btcTxId).reverse(), "hex"); +} diff --git a/packages/rgbpp/src/signer/index.ts b/packages/rgbpp/src/signer/index.ts new file mode 100644 index 000000000..f91cc1943 --- /dev/null +++ b/packages/rgbpp/src/signer/index.ts @@ -0,0 +1,479 @@ +import * as bitcoin from "bitcoinjs-lib"; + +import { + ccc, + SignerSignType, + SignerType, + Transaction, + TransactionLike, +} from "@ckb-ccc/core"; +import { spore } from "@ckb-ccc/spore"; + +import { transactionToHex } from "../bitcoin/transaction/index.js"; +import { pollForSpvProof, RgbppSpvProof } from "../data-source/index.js"; +import { + ErrorRgbppInvalidInputLock, + ErrorRgbppOutputNotFound, +} from "../error.js"; +import { + btcTxIdInReverseByteOrder, + buildRgbppUnlock, + deduplicateByOutPoint, + getTxIdFromRgbppLockArgs, + isSameScriptTemplate, + isUsingOneOfScripts, + pseudoRgbppLockArgs, + RGBPP_BTC_TX_ID_PLACEHOLDER, + RGBPP_CONFIG_CELL_INDEX, + RgbppScriptName, +} from "../script/index.js"; +import { removeHexPrefix } from "../utils/index.js"; + +import { + DEFAULT_SPV_POLL_INTERVAL, + RgbppDataSource, +} from "../data-source/index.js"; + +export interface CkbRgbppUnlockSignerParams { + ckbClient: ccc.Client; + rgbppBtcAddress: string; + rgbppDataSource: RgbppDataSource; + scriptInfos: Record; + /** Polling interval in milliseconds for SPV proof polling (default: 30000, minimum: 5000) */ + spvPollIntervalMs?: number; + /** SPV proof cache expiry time in milliseconds (default: 600000 = 10 minutes) */ + cacheExpiryMs?: number; +} + +export class CkbRgbppUnlockSigner extends ccc.Signer { + // map of script code hash to script name + private readonly scriptMap: Record; + private readonly rgbppScriptInfos: { + [ccc.KnownScript.RgbppLock]: { + script: ccc.Script; + cellDep: ccc.CellDep; + }; + [ccc.KnownScript.BtcTimeLock]: { + script: ccc.Script; + cellDep: ccc.CellDep; + }; + }; + + private spvProofCache = new Map>(); + private readonly cacheExpiryTime: number; + private readonly spvPollIntervalMs: number; + private readonly rgbppBtcAddress: string; + private readonly rgbppDataSource: RgbppDataSource; + + constructor({ + ckbClient, + rgbppBtcAddress, + rgbppDataSource, + scriptInfos, + spvPollIntervalMs, + cacheExpiryMs, + }: CkbRgbppUnlockSignerParams) { + super(ckbClient); + this.rgbppBtcAddress = rgbppBtcAddress; + this.rgbppDataSource = rgbppDataSource; + + // Validate required script infos + const requiredScripts = [ + ccc.KnownScript.RgbppLock, + ccc.KnownScript.BtcTimeLock, + ] as const; + for (const name of requiredScripts) { + const info = scriptInfos[name]; + if (!info || !info.cellDeps?.[0]?.cellDep) { + throw new Error( + `Missing or invalid ScriptInfo for ${name}. ` + + `scriptInfos must include both RgbppLock and BtcTimeLock with valid cellDeps.`, + ); + } + } + + this.scriptMap = Object.fromEntries( + Object.entries(scriptInfos).map(([key, value]) => [ + value.codeHash, + key as ccc.KnownScript, + ]), + ); + + // Convert ccc.ScriptInfo to internal format + const convertScriptInfo = (info: ccc.ScriptInfo) => ({ + script: ccc.Script.from({ + codeHash: info.codeHash, + hashType: info.hashType, + args: "", + }), + cellDep: info.cellDeps[0].cellDep, + }); + + this.rgbppScriptInfos = { + [ccc.KnownScript.RgbppLock]: convertScriptInfo( + scriptInfos[ccc.KnownScript.RgbppLock], + ), + [ccc.KnownScript.BtcTimeLock]: convertScriptInfo( + scriptInfos[ccc.KnownScript.BtcTimeLock], + ), + }; + this.spvPollIntervalMs = Math.max( + spvPollIntervalMs ?? DEFAULT_SPV_POLL_INTERVAL, + 5_000, + ); + this.cacheExpiryTime = cacheExpiryMs ?? 600_000; + } + + get type(): SignerType { + return SignerType.CKB; + } + + get signType(): SignerSignType { + return SignerSignType.Unknown; + } + + getScriptName(script?: ccc.Script): ccc.KnownScript | undefined { + return script ? this.scriptMap[script.codeHash] : undefined; + } + + async collectCellDeps(tx: Transaction): Promise { + const scriptNames = new Set( + [ + ...( + await Promise.all( + tx.inputs.map(async (input) => { + await input.completeExtraInfos(this.client); + return input.cellOutput + ? [ + this.getScriptName(input.cellOutput.lock), + this.getScriptName(input.cellOutput.type), + ] + : []; + }), + ) + ).flat(), + ...tx.outputs.map((output) => this.getScriptName(output.type)), + ].filter((name): name is ccc.KnownScript => !!name), + ); + + const cellDeps = Array.from(scriptNames).flatMap((name) => { + if ( + name === ccc.KnownScript.RgbppLock || + name === ccc.KnownScript.BtcTimeLock + ) { + return [ + this.rgbppScriptInfos[name].cellDep, + ccc.CellDep.from({ + outPoint: { + ...this.rgbppScriptInfos[name].cellDep.outPoint, + index: RGBPP_CONFIG_CELL_INDEX, + }, + depType: this.rgbppScriptInfos[name].cellDep.depType, + }), + ]; + } + return []; + }); + + const clusterCellDeps = await this.collectClusterCellDeps(tx); + + return deduplicateByOutPoint([ + ...cellDeps, + ...clusterCellDeps, + ...tx.cellDeps, + ]); + } + + private async collectClusterCellDeps( + tx: Transaction, + ): Promise { + const clusterScriptInfos = Object.values( + spore.getClusterScriptInfos(this.client), + ); + + const clusterIndicesInInputs: number[] = []; + const clusterIndicesInOutputs: number[] = []; + + tx.inputs.forEach((input, index) => { + if (input.cellOutput?.type) { + clusterScriptInfos.forEach((si) => { + if (si && si.codeHash === input.cellOutput?.type?.codeHash) { + clusterIndicesInInputs.push(index); + } + }); + } + }); + + tx.outputs.forEach((output, index) => { + clusterScriptInfos.forEach((si) => { + if (si && si.codeHash === output.type?.codeHash) { + clusterIndicesInOutputs.push(index); + } + }); + }); + + if ( + clusterIndicesInInputs.length === 0 || + clusterIndicesInOutputs.length === 0 + ) { + return []; + } + + if ( + clusterIndicesInInputs.length !== 1 || + clusterIndicesInOutputs.length !== 1 + ) { + throw new Error("Invalid cluster indices"); + } + + const inputCluster = tx.inputs[clusterIndicesInInputs[0]]; + await inputCluster.completeExtraInfos(this.client); + const inputClusterId = inputCluster.cellOutput!.type!.args; + const { cell: inputClusterCell } = await spore.assertCluster( + this.client, + inputClusterId, + ); + + return [ + ccc.CellDep.from({ + outPoint: inputClusterCell.outPoint, + depType: "code", + }), + ]; + } + + async prepareTransaction(txLike: TransactionLike): Promise { + const tx = ccc.Transaction.from(txLike); + + tx.cellDeps = await this.collectCellDeps(tx); + + const btcTxId = this.parseBtcTxIdFromScriptArgs(tx); + const spvProof = await this.getRgbppSpvProof(btcTxId); + tx.cellDeps.push( + ccc.CellDep.from({ + outPoint: spvProof.spvClientOutpoint, + depType: "code", + }), + ); + + return tx; + } + + async signOnlyTransaction(txLike: TransactionLike): Promise { + const tx = ccc.Transaction.from(txLike); + + const btcTxId = this.parseBtcTxIdFromScriptArgs(tx); + const spvProof = await this.getRgbppSpvProof(btcTxId); + + const rawBtcTxHex = await this.getRawBtcTxHex(btcTxId); + return Promise.resolve(this.insertWitnesses(tx, rawBtcTxHex, spvProof)); + } + + private async getRgbppSpvProof(btcTxId: string): Promise { + const spvProof = this.spvProofCache.get(btcTxId); + + if (spvProof) { + return spvProof; + } + + const proofPromise = pollForSpvProof( + this.rgbppDataSource, + btcTxId, + 0, + this.spvPollIntervalMs, + ); + // Store the promise in cache so concurrent requests can share it + this.spvProofCache.set(btcTxId, proofPromise); + try { + const proof = await proofPromise; + + const timer = setTimeout(() => { + if (this.spvProofCache.get(btcTxId) === proofPromise) { + this.spvProofCache.delete(btcTxId); + } + }, this.cacheExpiryTime); + if ( + typeof timer === "object" && + timer !== null && + "unref" in timer && + typeof timer.unref === "function" + ) { + timer.unref(); + } + + return proof; + } catch (error) { + if (this.spvProofCache.get(btcTxId) === proofPromise) { + this.spvProofCache.delete(btcTxId); + } + throw error; + } + } + + private async getRawBtcTxHex(txId: string): Promise { + const hex = await this.rgbppDataSource.getTransactionHex(txId); + return transactionToHex(bitcoin.Transaction.fromHex(hex), false); + } + + parseBtcTxIdFromScriptArgs(tx: ccc.Transaction): string { + const outputs = tx.outputs.filter((output) => output.lock); + const rgbppOutput = outputs.find((output) => + isUsingOneOfScripts(output.lock, [ + this.rgbppScriptInfos[ccc.KnownScript.RgbppLock].script, + this.rgbppScriptInfos[ccc.KnownScript.BtcTimeLock].script, + ]), + ); + if (!rgbppOutput) { + throw new ErrorRgbppOutputNotFound(); + } + return getTxIdFromRgbppLockArgs(rgbppOutput.lock.args); + } + + async insertWitnesses( + partialTx: ccc.Transaction, + btcLikeTxBytes: string, + spvClient: RgbppSpvProof, + ): Promise { + const tx = partialTx.clone(); + + const rgbppUnlock = buildRgbppUnlock( + btcLikeTxBytes, + spvClient.proof, + tx.inputs.length, + tx.outputs.length, + ); + + const rgbppWitness = ccc.WitnessArgs.from({ + lock: rgbppUnlock, + }).toBytes(); + + // Validate all inputs use RGBPP lock — fail fast if not + for (const input of tx.inputs) { + await input.completeExtraInfos(this.client); + const scriptName = input.cellOutput + ? this.getScriptName(input.cellOutput.lock) + : undefined; + if (scriptName !== ccc.KnownScript.RgbppLock) { + throw new ErrorRgbppInvalidInputLock( + input.cellOutput?.lock.codeHash ?? "unknown", + ); + } + } + + tx.inputs.forEach((_, index) => { + tx.setWitnessAt(index, rgbppWitness); + }); + + await this.handleSporeWitness(tx); + + return tx; + } + + async handleSporeWitness(tx: ccc.Transaction): Promise { + if (tx.witnesses.length === tx.inputs.length) { + return; + } + + const pseudoCobuild = tx.witnesses[tx.witnesses.length - 1]; + if (!pseudoCobuild) { + throw new Error( + "Expected a cobuild witness at the end of the witnesses array, but found none.", + ); + } + tx.witnesses = tx.witnesses.slice(0, tx.inputs.length); + + let btcTxId: string | undefined; + const rgbppLockArgs: ccc.Hex[] = []; + for (const output of tx.outputs) { + if ( + isSameScriptTemplate( + output.lock, + this.rgbppScriptInfos[ccc.KnownScript.RgbppLock].script, + ) + ) { + btcTxId = getTxIdFromRgbppLockArgs(output.lock.args); + rgbppLockArgs.push(output.lock.args); + } else if ( + isSameScriptTemplate( + output.lock, + this.rgbppScriptInfos[ccc.KnownScript.BtcTimeLock].script, + ) + ) { + btcTxId = getTxIdFromRgbppLockArgs(output.lock.args); + } + } + + if (!btcTxId) { + throw new Error("Invalid transaction"); + } + + let cobuildWitness: string = pseudoCobuild; + if (rgbppLockArgs.length > 0) { + let currentCobuild: string = pseudoCobuild; + const pseudoArg = removeHexPrefix(pseudoRgbppLockArgs()); + let lastIndex = 0; + + for (const lockArg of rgbppLockArgs) { + const index = currentCobuild.indexOf(pseudoArg, lastIndex); + if (index === -1) { + break; + } + + currentCobuild = + currentCobuild.substring(0, index) + + removeHexPrefix(lockArg) + + currentCobuild.substring(index + pseudoArg.length); + lastIndex = index + removeHexPrefix(lockArg).length; + } + cobuildWitness = currentCobuild; + } + + const txIdPlaceholder = btcTxIdInReverseByteOrder( + RGBPP_BTC_TX_ID_PLACEHOLDER, + ); + const txIdReplacement = btcTxIdInReverseByteOrder(btcTxId); + const finalCobuild = cobuildWitness.replace( + new RegExp(txIdPlaceholder, "g"), + txIdReplacement, + ) as ccc.Hex; + + tx.witnesses.push(finalCobuild); + } + + async connect(): Promise {} + + async isConnected(): Promise { + return true; + } + + async getInternalAddress(): Promise { + return this.getRecommendedAddress(); + } + + async getAddressObjs(): Promise { + const rgbppCellOutputs = await this.rgbppDataSource.getRgbppCellOutputs( + this.rgbppBtcAddress, + ); + + // output.type in each cell output must be present except for issuance + // if (rgbppCellOutputs.some((output) => !output.type)) { + // throw new Error("Rgbpp cell output type not found"); + // } + + const ckbAddresses = rgbppCellOutputs.map((output) => { + return ccc.Address.from({ + script: output.lock, + prefix: this.client.addressPrefix, + }); + }); + + return ckbAddresses; + } + + async getAddressObj(): Promise { + return await ccc.Address.fromString( + await this.getInternalAddress(), + this.client, + ); + } +} diff --git a/packages/rgbpp/src/udt/client.ts b/packages/rgbpp/src/udt/client.ts new file mode 100644 index 000000000..ee2315333 --- /dev/null +++ b/packages/rgbpp/src/udt/client.ts @@ -0,0 +1,135 @@ +import { ccc } from "@ckb-ccc/core"; + +import { UtxoSeal } from "../bitcoin/transaction/index.js"; +import { ErrorRgbppCellNotFound, ErrorRgbppInvalidCellLock } from "../error.js"; +import { + IScriptProvider, + isUsingOneOfScripts, + RGBPP_BTC_TX_ID_PLACEHOLDER, + RgbppScriptName, + ScriptManager, + updateScriptArgsWithTxId, +} from "../script/index.js"; + +export class RgbppUdtClient { + public scriptManager: ScriptManager; + + constructor( + private ckbClient: ccc.Client, + scriptProvider: IScriptProvider, + ) { + this.scriptManager = new ScriptManager(scriptProvider); + } + + async rgbppLockScriptTemplate(): Promise { + return this.scriptManager.rgbppLockScriptTemplate(); + } + + async btcTimeLockScriptTemplate(): Promise { + return this.scriptManager.btcTimeLockScriptTemplate(); + } + + async buildRgbppLockScript(utxoSeal: UtxoSeal): Promise { + return this.scriptManager.buildRgbppLockScript(utxoSeal); + } + + async buildPseudoRgbppLockScript(): Promise { + return this.scriptManager.buildPseudoRgbppLockScript(); + } + + async buildBtcTimeLockScript( + ckbAddress: string, + confirmations?: number, + ): Promise { + const receiverLock = ( + await ccc.Address.fromString(ckbAddress, this.ckbClient) + ).script; + + return this.scriptManager.buildBtcTimeLockScript( + receiverLock, + RGBPP_BTC_TX_ID_PLACEHOLDER, + confirmations, + ); + } + + async getRgbppScriptInfos(): Promise< + Record + > { + return this.scriptManager.getRgbppScriptInfos(); + } + + // * It's assumed that all the tx.outputs are rgbpp/btc time lock scripts. + injectTxIdToRgbppCkbTx = async ( + tx: ccc.Transaction, + txId: string, + ): Promise => { + const rgbppLockTemplate = await this.rgbppLockScriptTemplate(); + const btcTimeLockTemplate = await this.btcTimeLockScriptTemplate(); + + const outputs = tx.outputs.map((output, _index) => { + if ( + !isUsingOneOfScripts(output.lock, [ + rgbppLockTemplate, + btcTimeLockTemplate, + ]) + ) { + throw new ErrorRgbppInvalidCellLock( + ["RgbppLock", "BtcTimeLock"], + output.lock.codeHash, + ); + } + + return ccc.CellOutput.from({ + ...output, + lock: { + ...output.lock, + args: updateScriptArgsWithTxId(output.lock.args, txId), + }, + }); + }); + + return ccc.Transaction.from({ + ...tx, + outputs, + }); + }; + + async createRgbppUdtIssuanceCells( + signer: ccc.Signer, + utxoSeal: UtxoSeal, + ): Promise { + const rgbppLockScript = await this.buildRgbppLockScript(utxoSeal); + + const rgbppCellsGen = signer.client.findCellsByLock(rgbppLockScript); + const rgbppCells: ccc.Cell[] = []; + for await (const cell of rgbppCellsGen) { + rgbppCells.push(cell); + } + + if (rgbppCells.length !== 0) { + return rgbppCells; + } + + const tx = ccc.Transaction.default(); + + // If additional capacity is required when used as an input in a transaction, it can always be supplemented in `completeInputsByCapacity`. + tx.addOutput({ + lock: rgbppLockScript, + }); + + await tx.completeInputsByCapacity(signer); + await tx.completeFeeBy(signer); + const txHash = await signer.sendTransaction(tx); + await signer.client.waitTransaction(txHash); + + const cell = await signer.client.getCellLive({ + txHash, + index: 0, + }); + if (!cell) { + throw new ErrorRgbppCellNotFound(txHash); + } + + return [cell]; + } +} diff --git a/packages/rgbpp/src/udt/index.ts b/packages/rgbpp/src/udt/index.ts new file mode 100644 index 000000000..fcd2f4c90 --- /dev/null +++ b/packages/rgbpp/src/udt/index.ts @@ -0,0 +1 @@ +export * from "./client.js"; diff --git a/packages/rgbpp/src/utils/concurrency.ts b/packages/rgbpp/src/utils/concurrency.ts new file mode 100644 index 000000000..8e26a418d --- /dev/null +++ b/packages/rgbpp/src/utils/concurrency.ts @@ -0,0 +1,44 @@ +/** + * Iterate through an array and run an async function on each item with concurrency control. + * + * @param items - The items to iterate through + * @param concurrency - The maximum number of concurrent executions + * @param fn - The async function to run on each item + * @returns An array of results in the same order as the inputs + */ +export async function mapWithConcurrency( + items: T[], + concurrency: number, + fn: (item: T) => Promise, +): Promise { + if (!Number.isInteger(concurrency) || concurrency <= 0) { + throw new Error( + `Concurrency must be a positive integer, got: ${concurrency}`, + ); + } + + const results: R[] = new Array(items.length); + let nextIndex = 0; + let hasError = false; + + async function worker() { + while (nextIndex < items.length && !hasError) { + const i = nextIndex++; + try { + results[i] = await fn(items[i]); + } catch (err) { + hasError = true; + throw err; + } + } + } + + // Launch workers + const workers = Array.from( + { length: Math.min(concurrency, items.length) }, + () => worker(), + ); + await Promise.all(workers); + + return results; +} diff --git a/packages/rgbpp/src/utils/index.ts b/packages/rgbpp/src/utils/index.ts new file mode 100644 index 000000000..17109f942 --- /dev/null +++ b/packages/rgbpp/src/utils/index.ts @@ -0,0 +1,24 @@ +export function removeHexPrefix(hex: string): string { + return hex.startsWith("0x") ? hex.slice(2) : hex; +} + +// Domain validation utility +/** + * Check if target string is a valid domain. + * @exmaple + * isDomain('google.com') // => true + * isDomain('https://google.com') // => false + * isDomain('localhost') // => false + * isDomain('localhost', true) // => true + */ +export function isDomain(domain: string, allowLocalhost?: boolean): boolean { + if (allowLocalhost && domain === "localhost") { + return true; + } + const regex = /^(?:[-A-Za-z0-9]+\.)+[A-Za-z]{2,}$/; + return regex.test(domain); +} + +export * from "./concurrency.js"; +export * from "./logger.js"; +export * from "./retry.js"; diff --git a/packages/rgbpp/src/utils/logger.ts b/packages/rgbpp/src/utils/logger.ts new file mode 100644 index 000000000..c876ea6cb --- /dev/null +++ b/packages/rgbpp/src/utils/logger.ts @@ -0,0 +1,10 @@ +/** + * Standard logger interface compatible with console and major logging libraries + */ +export interface Logger { + log?: (message: string, ...args: unknown[]) => void; + info?: (message: string, ...args: unknown[]) => void; + warn?: (message: string, ...args: unknown[]) => void; + error?: (message: string, ...args: unknown[]) => void; + debug?: (message: string, ...args: unknown[]) => void; +} diff --git a/packages/rgbpp/src/utils/retry.ts b/packages/rgbpp/src/utils/retry.ts new file mode 100644 index 000000000..72470293f --- /dev/null +++ b/packages/rgbpp/src/utils/retry.ts @@ -0,0 +1,103 @@ +/** + * Retry utility with exponential backoff and jitter + */ + +export interface RetryOptions { + /** Maximum number of retry attempts (default: 10) */ + maxRetries?: number; + /** Initial delay in seconds (default: 5) */ + initialDelay?: number; + /** Backoff multiplier (default: 1.5) */ + backoffMultiplier?: number; + /** Jitter factor as percentage (default: 0.1 for ±10%) */ + jitterFactor?: number; + /** Enable verbose logging (default: false) */ + verbose?: boolean; + /** Custom logger function (default: console.log) */ + logger?: (message: string) => void; +} + +export async function retryWithBackoff( + operation: () => Promise, + options: RetryOptions = {}, +): Promise { + const { + maxRetries = 10, + initialDelay = 5, + backoffMultiplier = 1.5, + jitterFactor = 0.1, + verbose = false, + logger = console.log, + } = options; + + // Parameter validation + if (maxRetries < 0 || !Number.isInteger(maxRetries)) { + throw new Error( + `maxRetries must be a non-negative integer, got: ${maxRetries}`, + ); + } + if (initialDelay < 0 || !Number.isFinite(initialDelay)) { + throw new Error( + `initialDelay must be a non-negative number, got: ${initialDelay}`, + ); + } + if (backoffMultiplier <= 0 || !Number.isFinite(backoffMultiplier)) { + throw new Error( + `backoffMultiplier must be a positive number, got: ${backoffMultiplier}`, + ); + } + if (jitterFactor < 0 || jitterFactor > 1 || !Number.isFinite(jitterFactor)) { + throw new Error( + `jitterFactor must be between 0 and 1, got: ${jitterFactor}`, + ); + } + + let lastError: unknown; + const totalAttempts = maxRetries + 1; // Include the initial attempt + + for (let attempt = 0; attempt < totalAttempts; attempt++) { + if (attempt > 0) { + // Calculate delay with exponential backoff and bidirectional jitter + const baseDelay = initialDelay * Math.pow(backoffMultiplier, attempt - 1); + const jitterRange = jitterFactor * baseDelay; + const jitter = (Math.random() - 0.5) * 2 * jitterRange; // ±jitterRange + const delay = Math.max(0, baseDelay + jitter); // Ensure non-negative + + if (verbose) { + logger( + `Retrying in ${delay.toFixed(1)} seconds (attempt ${attempt + 1}/${totalAttempts})...`, + ); + } + await new Promise((resolve) => + setTimeout(resolve, Math.floor(delay * 1000)), + ); + } + + try { + const result = await operation(); + if (verbose && attempt > 0) { + logger(`Operation succeeded after ${attempt + 1} attempts`); + } + return result; + } catch (error) { + lastError = error; + + if (verbose && attempt < totalAttempts - 1) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logger(`Attempt ${attempt + 1} failed: ${errorMessage}`); + } + + // Don't throw on the last attempt, let the loop complete + if (attempt === totalAttempts - 1) { + if (verbose) { + logger(`All ${totalAttempts} attempts failed`); + } + throw error; + } + } + } + + // This should never be reached due to the logic above, but TypeScript needs it + throw lastError; +} diff --git a/packages/rgbpp/tsconfig.base.json b/packages/rgbpp/tsconfig.base.json new file mode 100644 index 000000000..7e5ac952b --- /dev/null +++ b/packages/rgbpp/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/rgbpp/tsconfig.commonjs.json b/packages/rgbpp/tsconfig.commonjs.json new file mode 100644 index 000000000..76a25e98b --- /dev/null +++ b/packages/rgbpp/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/rgbpp/tsconfig.json b/packages/rgbpp/tsconfig.json new file mode 100644 index 000000000..16f78d28a --- /dev/null +++ b/packages/rgbpp/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist" + } +} diff --git a/packages/xverse/package.json b/packages/xverse/package.json index 360f87354..0eb0fec46 100644 --- a/packages/xverse/package.json +++ b/packages/xverse/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "@ckb-ccc/core": "workspace:*", - "bitcoinjs-lib": "^7.0.0", + "bitcoinjs-lib": "^7.0.1", "valibot": "^1.1.0" }, "packageManager": "pnpm@10.8.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b8b02927..7d04027f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: version: 30.0.0 '@vitest/coverage-v8': specifier: 3.2.4 - version: 3.2.4(vitest@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)) + version: 3.2.4(vitest@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.21.0)(yaml@2.8.1)) jest: specifier: 30.1.1 version: 30.1.1(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)) @@ -48,7 +48,7 @@ importers: version: 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) + 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.21.0)(yaml@2.8.1) packages/ccc: dependencies: @@ -296,7 +296,7 @@ importers: 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) + 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.21.0)(yaml@2.8.1) packages/demo: dependencies: @@ -448,7 +448,7 @@ importers: 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) + 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.21.0)(yaml@2.8.1) packages/docs: dependencies: @@ -899,8 +899,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 bitcoinjs-lib: - specifier: ^7.0.0 - version: 7.0.0(typescript@5.9.2) + specifier: ^7.0.1 + version: 7.0.1(typescript@5.9.2) isomorphic-ws: specifier: ^5.0.0 version: 5.0.0(ws@8.18.3) @@ -1012,6 +1012,91 @@ importers: specifier: ^8.41.0 version: 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + packages/rgbpp: + dependencies: + '@bitcoinerlab/secp256k1': + specifier: 1.1.1 + version: 1.1.1 + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + '@ckb-ccc/spore': + specifier: workspace:* + version: link:../spore + '@noble/hashes': + specifier: ^1.4.0 + version: 1.8.0 + bip32: + specifier: 4.0.0 + version: 4.0.0 + bitcoinjs-lib: + specifier: 7.0.1 + version: 7.0.1(typescript@5.9.2) + ecpair: + specifier: 3.0.1 + version: 3.0.1(typescript@5.9.2) + lodash: + specifier: ^4.17.21 + version: 4.17.21 + devDependencies: + '@ckb-ccc/udt': + specifier: workspace:* + version: link:../udt + '@eslint/js': + specifier: ^9.34.0 + version: 9.34.0 + '@exact-realty/multipart-parser': + specifier: ^1.0.13 + version: 1.0.14 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/lodash': + specifier: ^4.17.14 + version: 4.17.20 + '@types/node': + specifier: ^22.10.6 + version: 22.19.17 + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + 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) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.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) + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + ts-jest: + specifier: ^29.2.5 + version: 29.4.1(@babel/core@7.28.3)(@jest/transform@30.1.1)(@jest/types@30.0.5)(babel-jest@30.1.1(@babel/core@7.28.3))(jest-util@30.0.5)(jest@29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)))(typescript@5.9.2) + tsx: + specifier: ^4.20.6 + version: 4.21.0 + typescript: + specifier: ^5.7.3 + 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) + packages/shell: dependencies: '@ckb-ccc/core': @@ -1111,7 +1196,7 @@ importers: 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) + 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.21.0)(yaml@2.8.1) packages/ssri: dependencies: @@ -1224,7 +1309,7 @@ importers: 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) + 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.21.0)(yaml@2.8.1) packages/udt: dependencies: @@ -1349,8 +1434,8 @@ importers: specifier: workspace:* version: link:../core bitcoinjs-lib: - specifier: ^7.0.0 - version: 7.0.0(typescript@5.9.2) + specifier: ^7.0.1 + version: 7.0.1(typescript@5.9.2) valibot: specifier: ^1.1.0 version: 1.1.0(typescript@5.9.2) @@ -2174,6 +2259,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bitcoinerlab/secp256k1@1.1.1': + resolution: {integrity: sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==} + '@bitcoinerlab/secp256k1@1.2.0': resolution: {integrity: sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q==} @@ -2774,156 +2862,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.9': resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.9': resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.9': resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.9': resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.9': resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.9': resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.9': resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.9': resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.9': resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.9': resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.9': resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.9': resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.9': resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.9': resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.9': resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.9': resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.9': resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.9': resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.9': resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.9': resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.9': resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.9': resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2973,6 +3217,11 @@ packages: '@euberdeveloper/eslint-plugin@2.7.0': resolution: {integrity: sha512-IyZfysHjYCSU6Ty86imkCZmZ5diTAOKn7DlEmEO1yHhFjvVk+xFr0ApjTD7TyIQolnBPixZNw477V7mq86llcw==} + '@exact-realty/multipart-parser@1.0.14': + resolution: {integrity: sha512-ln1+s1XOvRY9NRof3lpjWJZfSoV5XOWdVgK7sYeE3TAt0brtB+pIPJpSbxaol/490re2rcMEo24VZXeffKm8Aw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + deprecated: Package has moved to @apeleghq/multipart-parser + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -3348,10 +3597,23 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/console@30.1.1': resolution: {integrity: sha512-f7TGqR1k4GtN5pyFrKmq+ZVndesiwLU33yDpJIGMS9aW+j6hKjue7ljeAdznBsH9kAnxUWe2Y+Y3fLV/FJt3gA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/core@30.1.1': resolution: {integrity: sha512-3ncU9peZ3D2VdgRkdZtUceTrDgX5yiDRwAFjtxNfU22IiZrpVWlv/FogzDLYSJQptQGfFo3PcHK86a2oG6WUGg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3365,18 +3627,34 @@ packages: resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/environment@30.1.1': resolution: {integrity: sha512-yWHbU+3j7ehQE+NRpnxRvHvpUhoohIjMePBbIr8lfe0cWVb0WeTf80DNux1GPJa18CDHiIU5DtksGUfxcDE+Rw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect-utils@30.1.1': resolution: {integrity: sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/expect@30.1.1': resolution: {integrity: sha512-3vHIHsF+qd3D8FU2c7U5l3rg1fhDwAYcGyHyZAi94YIlTwcJ+boNhRyJf373cl4wxbOX+0Q7dF40RTrTFTSuig==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/fake-timers@30.1.1': resolution: {integrity: sha512-fK/25dNgBNYPw3eLi2CRs57g1H04qBAFNMsUY3IRzkfx/m4THe0E1zF+yGQBOMKKc2XQVdc9EYbJ4hEm7/2UtA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3385,6 +3663,10 @@ packages: resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/globals@30.1.1': resolution: {integrity: sha512-NNUUkHT2TU/xztZl6r1UXvJL+zvCwmZsQDmK69fVHHcB9fBtlu3FInnzOve/ZoyKnWY8JXWJNT+Lkmu1+ubXUA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3393,6 +3675,15 @@ packages: resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/reporters@30.1.1': resolution: {integrity: sha512-Hb2Bq80kahOC6Sv2waEaH1rEU6VdFcM6WHaRBWQF9tf30+nJHxhl/Upbgo9+25f0mOgbphxvbwSMjSgy9gW/FA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3414,18 +3705,34 @@ packages: resolution: {integrity: sha512-TkVBc9wuN22TT8hESRFmjjg/xIMu7z0J3UDYtIRydzCqlLPTB7jK1DDBKdnTUZ4zL3z3rnPpzV6rL1Uzh87sXg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@30.0.1': resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-result@30.1.1': resolution: {integrity: sha512-bMdj7fNu8iZuBPSnbVir5ezvWmVo4jrw7xDE+A33Yb3ENCoiJK9XgOLgal+rJ9XSKjsL7aPUMIo87zhN7I5o2w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/test-sequencer@30.1.1': resolution: {integrity: sha512-yruRdLXSA3HYD/MTNykgJ6VYEacNcXDFRMqKVAwlYegmxICUiT/B++CNuhJnYJzKYks61iYnjVsMwbUqmmAYJg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/transform@30.1.1': resolution: {integrity: sha512-PHIA2AbAASBfk6evkNifvmx9lkOSkmvaQoO6VSpuL8+kQqDMHeDoJ7RU3YP1wWAMD7AyQn9UL5iheuFYCC4lqQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3731,6 +4038,9 @@ packages: resolution: {integrity: sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw==} engines: {node: '>= 20.19.0'} + '@noble/secp256k1@1.7.2': + resolution: {integrity: sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4004,6 +4314,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@scure/base@2.0.0': resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} @@ -4068,6 +4381,9 @@ packages: '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} @@ -4360,6 +4676,9 @@ packages: '@types/express@5.0.3': resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/gtag.js@0.0.12': resolution: {integrity: sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==} @@ -4390,6 +4709,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/jest@30.0.0': resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} @@ -4432,6 +4754,9 @@ packages: '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} @@ -5150,6 +5475,12 @@ packages: b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + babel-jest@30.1.1: resolution: {integrity: sha512-1bZfC/V03qBCzASvZpNFhx3Ouj6LgOd4KFJm4br/fYOS+tSSvVCE61QmcAVbMTwq/GoB7KN4pzGMoyr9cMxSvQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5166,10 +5497,18 @@ packages: babel-plugin-dynamic-import-node@2.3.3: resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + babel-plugin-istanbul@7.0.0: resolution: {integrity: sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==} engines: {node: '>=12'} + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-jest-hoist@30.0.1: resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5194,6 +5533,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + babel-preset-jest@30.0.1: resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5206,6 +5551,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base-x@3.0.11: + resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} + base-x@4.0.1: resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} @@ -5240,11 +5588,15 @@ packages: resolution: {integrity: sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==} engines: {node: '>=18.0.0'} + bip32@4.0.0: + resolution: {integrity: sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==} + engines: {node: '>=6.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==} + bitcoinjs-lib@7.0.1: + resolution: {integrity: sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww==} engines: {node: '>=18.0.0'} bl@4.1.0: @@ -5303,12 +5655,18 @@ packages: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + bs58@5.0.0: resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} bs58@6.0.0: resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + bs58check@2.1.2: + resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} + bs58check@4.0.0: resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} @@ -5473,6 +5831,13 @@ packages: resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} engines: {node: '>=8'} + cipher-base@1.0.7: + resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} + engines: {node: '>= 0.10'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.1.0: resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} @@ -5711,6 +6076,14 @@ packages: typescript: optional: true + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -6026,6 +6399,10 @@ packages: dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -6114,6 +6491,10 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecpair@3.0.1: + resolution: {integrity: sha512-uz8wMFvtdr58TLrXnAesBsoMEyY8UudLOfApcyg40XfZjP+gt1xO4cuZSIkZ8hTMTQ8+ETgt7xSIV4eM7M6VNw==} + engines: {node: '>=20.0.0'} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -6226,6 +6607,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -6510,10 +6896,18 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect@30.1.1: resolution: {integrity: sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -6921,6 +7315,10 @@ packages: resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hash-base@3.1.2: + resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} + engines: {node: '>= 0.8'} + hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -7431,6 +7829,10 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + istanbul-lib-instrument@6.0.3: resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} @@ -7439,6 +7841,10 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.6: resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} @@ -7462,14 +7868,32 @@ packages: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-changed-files@30.0.5: resolution: {integrity: sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-circus@30.1.1: resolution: {integrity: sha512-M3Vd4x5wD7eSJspuTvRF55AkOOBndRxgW3gqQBDlFvbH3X+ASdi8jc+EqXEeAFd/UHulVYIlC4XKJABOhLw6UA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jest-cli@30.1.1: resolution: {integrity: sha512-xm9llxuh5OoI5KZaYzlMhklryHBwg9LZy/gEaaMlXlxb+cZekGNzukU0iblbDo3XOBuN6N0CgK4ykgNRYSEb6g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7480,53 +7904,105 @@ packages: node-notifier: optional: true - jest-config@30.1.1: - resolution: {integrity: sha512-xuPGUGDw+9fPPnGmddnLnHS/mhKUiJOW7K65vErYmglEPKq65NKwSRchkQ7iv6gqjs2l+YNEsAtbsplxozdOWg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' - esbuild-register: '>=3.4.0' ts-node: '>=9.0.0' peerDependenciesMeta: '@types/node': optional: true - esbuild-register: - optional: true ts-node: optional: true + jest-config@30.1.1: + resolution: {integrity: sha512-xuPGUGDw+9fPPnGmddnLnHS/mhKUiJOW7K65vErYmglEPKq65NKwSRchkQ7iv6gqjs2l+YNEsAtbsplxozdOWg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@30.1.1: resolution: {integrity: sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-docblock@30.0.1: resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-each@30.1.0: resolution: {integrity: sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-environment-node@30.1.1: resolution: {integrity: sha512-IaMoaA6saxnJimqCppUDqKck+LKM0Jg+OxyMUIvs1yGd2neiC22o8zXo90k04+tO+49OmgMR4jTgM5e4B0S62Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-haste-map@30.1.0: resolution: {integrity: sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-leak-detector@30.1.0: resolution: {integrity: sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@30.1.1: resolution: {integrity: sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-message-util@30.1.0: resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-mock@30.0.5: resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7540,26 +8016,50 @@ packages: jest-resolve: optional: true + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-regex-util@30.0.1: resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve-dependencies@30.1.1: resolution: {integrity: sha512-tRtaaoH8Ws1Gn1o/9pedt19dvVgr81WwdmvJSP9Ow3amOUOP2nN9j94u5jC9XlIfa2Q1FQKIWWQwL4ajqsjCGQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-resolve@30.1.0: resolution: {integrity: sha512-hASe7D/wRtZw8Cm607NrlF7fi3HWC5wmA5jCVc2QjQAB2pTwP9eVZILGEi6OeSLNUtE1zb04sXRowsdh5CUjwA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runner@30.1.1: resolution: {integrity: sha512-ATe6372SOfJvCRExtCAr06I4rGujwFdKg44b6i7/aOgFnULwjxzugJ0Y4AnG+jeSeQi8dU7R6oqLGmsxRUbErQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-runtime@30.1.1: resolution: {integrity: sha512-7sOyR0Oekw4OesQqqBHuYJRB52QtXiq0NNgLRzVogiMSxKCMiliUd6RrXHCnG5f12Age/ggidCBiQftzcA9XKw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-snapshot@30.1.1: resolution: {integrity: sha512-7/iBEzoJqEt2TjkQY+mPLHP8cbPhLReZVkkxjTMzIzoTC4cZufg7HzKo/n9cIkXKj2LG0x3mmBHsZto+7TOmFg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7572,10 +8072,18 @@ packages: resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-validate@30.1.0: resolution: {integrity: sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-watcher@30.1.1: resolution: {integrity: sha512-CrAQ73LlaS6KGQQw6NBi71g7qvP7scy+4+2c0jKX6+CWaYg85lZiig5nQQVTsS5a5sffNPL3uxXnaE9d7v9eQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7592,6 +8100,16 @@ packages: resolution: {integrity: sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jest@30.1.1: resolution: {integrity: sha512-yC3JvpP/ZcAZX5rYCtXO/g9k6VTCQz0VFE2v1FpxytWzUqfDtu0XL/pwnNvptzYItvGwomh1ehomRNMOyhCJKw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7932,6 +8450,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -9182,6 +9703,10 @@ packages: pretty-error@4.0.0: resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.0.5: resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -9237,6 +9762,9 @@ packages: resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} engines: {node: '>=12.20'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} @@ -9519,6 +10047,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -9554,6 +10086,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + ripemd160@2.0.3: + resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} + engines: {node: '>= 0.8'} + rolldown-plugin-dts@0.20.0: resolution: {integrity: sha512-cLAY1kN2ilTYMfZcFlGWbXnu6Nb+8uwUBsi+Mjbh4uIx7IN8uMOmJ7RxrrRgPsO4H7eSz3E+JwGoL1gyugiyUA==} engines: {node: '>=20.19.0'} @@ -9727,6 +10263,11 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} @@ -10175,6 +10716,10 @@ packages: tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -10320,6 +10865,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -10413,6 +10963,9 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x + typeforce@1.18.0: + resolution: {integrity: sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==} + typescript-eslint@8.41.0: resolution: {integrity: sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -10474,6 +11027,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} @@ -10607,16 +11163,16 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} - valibot@0.38.0: - resolution: {integrity: sha512-RCJa0fetnzp+h+KN9BdgYOgtsMAG9bfoJ9JSjIhFHobKWVWyzM3jjaeNTdpFK9tQtf3q1sguXeERJ/LcmdFE7w==} + valibot@1.1.0: + resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} peerDependencies: typescript: '>=5' peerDependenciesMeta: typescript: optional: true - valibot@1.1.0: - resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} + valibot@1.3.1: + resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -10851,6 +11407,12 @@ packages: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} + wif@2.0.6: + resolution: {integrity: sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==} + + wif@5.0.0: + resolution: {integrity: sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==} + wildcard@2.0.1: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} @@ -10879,6 +11441,10 @@ packages: write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -12021,6 +12587,11 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bitcoinerlab/secp256k1@1.1.1': + dependencies: + '@noble/hashes': 1.8.0 + '@noble/secp256k1': 1.7.2 + '@bitcoinerlab/secp256k1@1.2.0': dependencies: '@noble/curves': 1.9.7 @@ -12586,7 +13157,7 @@ snapshots: '@babel/preset-env': 7.28.3(@babel/core@7.28.3) '@babel/preset-react': 7.27.1(@babel/core@7.28.3) '@babel/preset-typescript': 7.27.1(@babel/core@7.28.3) - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 '@babel/runtime-corejs3': 7.28.3 '@babel/traverse': 7.28.3 '@docusaurus/logger': 3.9.2 @@ -13381,81 +13952,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.9': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.25.9': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.25.9': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.25.9': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.25.9': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.25.9': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.25.9': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.25.9': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.25.9': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.25.9': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.25.9': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.25.9': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.25.9': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.25.9': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.25.9': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.25.9': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.25.9': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + '@esbuild/netbsd-arm64@0.25.9': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.25.9': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + '@esbuild/openbsd-arm64@0.25.9': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.25.9': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + '@esbuild/openharmony-arm64@0.25.9': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.25.9': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.25.9': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.25.9': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.25.9': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -13537,6 +14186,8 @@ snapshots: - supports-color - typescript + '@exact-realty/multipart-parser@1.0.14': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -13882,6 +14533,15 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + '@jest/console@30.1.1': dependencies: '@jest/types': 30.0.5 @@ -13891,6 +14551,41 @@ snapshots: jest-util: 30.0.5 slash: 3.0.0 + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@30.1.1(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2))': dependencies: '@jest/console': 30.1.1 @@ -13929,6 +14624,13 @@ snapshots: '@jest/diff-sequences@30.0.1': {} + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + jest-mock: 29.7.0 + '@jest/environment@30.1.1': dependencies: '@jest/fake-timers': 30.1.1 @@ -13936,10 +14638,21 @@ snapshots: '@types/node': 24.3.0 jest-mock: 30.0.5 + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + '@jest/expect-utils@30.1.1': dependencies: '@jest/get-type': 30.1.0 + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + '@jest/expect@30.1.1': dependencies: expect: 30.1.1 @@ -13947,6 +14660,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.19.17 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + '@jest/fake-timers@30.1.1': dependencies: '@jest/types': 30.0.5 @@ -13958,6 +14680,15 @@ snapshots: '@jest/get-type@30.1.0': {} + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + '@jest/globals@30.1.1': dependencies: '@jest/environment': 30.1.1 @@ -13972,6 +14703,35 @@ snapshots: '@types/node': 24.3.0 jest-regex-util: 30.0.1 + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.30 + '@types/node': 22.19.17 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/reporters@30.1.1': dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -14015,12 +14775,25 @@ snapshots: graceful-fs: 4.2.11 natural-compare: 1.4.0 + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.30 + callsites: 3.1.0 + graceful-fs: 4.2.11 + '@jest/source-map@30.0.1': dependencies: '@jridgewell/trace-mapping': 0.3.30 callsites: 3.1.0 graceful-fs: 4.2.11 + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + '@jest/test-result@30.1.1': dependencies: '@jest/console': 30.1.1 @@ -14028,6 +14801,13 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.2 + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + '@jest/test-sequencer@30.1.1': dependencies: '@jest/test-result': 30.1.1 @@ -14035,6 +14815,26 @@ snapshots: jest-haste-map: 30.1.0 slash: 3.0.0 + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.28.3 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.30 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + '@jest/transform@30.1.1': dependencies: '@babel/core': 7.28.3 @@ -14060,7 +14860,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.3.0 + '@types/node': 22.19.17 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -14435,6 +15235,8 @@ snapshots: '@noble/hashes@2.0.0': {} + '@noble/secp256k1@1.7.2': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14636,6 +15438,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@scure/base@1.2.6': {} + '@scure/base@2.0.0': {} '@scure/bip32@2.0.0': @@ -14713,6 +15517,10 @@ snapshots: dependencies: type-detect: 4.0.8 + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@13.0.5': dependencies: '@sinonjs/commons': 3.0.1 @@ -15046,6 +15854,10 @@ snapshots: '@types/express-serve-static-core': 5.0.7 '@types/serve-static': 1.15.8 + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.19.17 + '@types/gtag.js@0.0.12': {} '@types/hast@3.0.4': @@ -15074,6 +15886,11 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + '@types/jest@30.0.0': dependencies: expect: 30.1.1 @@ -15111,6 +15928,10 @@ snapshots: '@types/node@17.0.45': {} + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + '@types/node@22.7.5': dependencies: undici-types: 6.19.8 @@ -15550,7 +16371,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/coverage-v8@3.2.4(vitest@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))': + '@vitest/coverage-v8@3.2.4(vitest@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.21.0)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -15565,7 +16386,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 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) + vitest: 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.21.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -15577,13 +16398,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.3(@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))': + '@vitest/mocker@3.2.4(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.18 optionalDependencies: - vite: 7.1.3(@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) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -15960,6 +16781,19 @@ snapshots: b4a@1.6.7: {} + babel-jest@29.7.0(@babel/core@7.28.3): + dependencies: + '@babel/core': 7.28.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@30.1.1(@babel/core@7.28.3): dependencies: '@babel/core': 7.28.3 @@ -15984,17 +16818,34 @@ snapshots: dependencies: object.assign: 4.1.7 - babel-plugin-istanbul@7.0.0: + babel-plugin-istanbul@6.1.1: dependencies: '@babel/helper-plugin-utils': 7.27.1 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 6.0.3 + istanbul-lib-instrument: 5.2.1 test-exclude: 6.0.0 transitivePeerDependencies: - supports-color - babel-plugin-jest-hoist@30.0.1: + babel-plugin-istanbul@7.0.0: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-plugin-jest-hoist@30.0.1: dependencies: '@babel/template': 7.27.2 '@babel/types': 7.28.2 @@ -16043,6 +16894,12 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.3) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.3) + babel-preset-jest@29.6.3(@babel/core@7.28.3): + dependencies: + '@babel/core': 7.28.3 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) + babel-preset-jest@30.0.1(@babel/core@7.28.3): dependencies: '@babel/core': 7.28.3 @@ -16053,6 +16910,10 @@ snapshots: balanced-match@1.0.2: {} + base-x@3.0.11: + dependencies: + safe-buffer: 5.2.1 + base-x@4.0.1: {} base-x@5.0.1: {} @@ -16078,16 +16939,23 @@ snapshots: uint8array-tools: 0.0.9 varuint-bitcoin: 2.0.0 + bip32@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + typeforce: 1.18.0 + wif: 2.0.6 + birpc@4.0.0: {} - bitcoinjs-lib@7.0.0(typescript@5.9.2): + bitcoinjs-lib@7.0.1(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) + valibot: 1.3.1(typescript@5.9.2) varuint-bitcoin: 2.0.0 transitivePeerDependencies: - typescript @@ -16196,6 +17064,10 @@ snapshots: dependencies: fast-json-stable-stringify: 2.1.0 + bs58@4.0.1: + dependencies: + base-x: 3.0.11 + bs58@5.0.0: dependencies: base-x: 4.0.1 @@ -16204,6 +17076,12 @@ snapshots: dependencies: base-x: 5.0.1 + bs58check@2.1.2: + dependencies: + bs58: 4.0.1 + create-hash: 1.2.0 + safe-buffer: 5.2.1 + bs58check@4.0.0(patch_hash=0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26): dependencies: '@noble/hashes': 1.8.0 @@ -16376,6 +17254,14 @@ snapshots: ci-info@4.3.0: {} + cipher-base@1.0.7: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.1.0: {} class-transformer@0.5.1: {} @@ -16609,6 +17495,29 @@ snapshots: optionalDependencies: typescript: 5.9.2 + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.7 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.3 + sha.js: 2.4.12 + + create-jest@29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-require@1.1.1: {} cross-fetch@3.2.0: @@ -16912,6 +17821,8 @@ snapshots: asap: 2.0.6 wrappy: 1.0.2 + diff-sequences@29.6.3: {} + diff@4.0.2: {} dir-glob@3.0.1: @@ -16999,6 +17910,14 @@ snapshots: eastasianwidth@0.2.0: {} + ecpair@3.0.1(typescript@5.9.2): + dependencies: + uint8array-tools: 0.0.8 + valibot: 1.3.1(typescript@5.9.2) + wif: 5.0.0 + transitivePeerDependencies: + - typescript + ee-first@1.1.1: {} electron-to-chromium@1.5.211: {} @@ -17199,6 +18118,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-goat@4.0.0: {} @@ -17626,8 +18574,18 @@ snapshots: exit-x@0.2.2: {} + exit@0.1.2: {} + expect-type@1.2.2: {} + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + expect@30.1.1: dependencies: '@jest/expect-utils': 30.1.1 @@ -18145,6 +19103,13 @@ snapshots: has-yarn@3.0.0: {} + hash-base@3.1.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + hash.js@1.1.7: dependencies: inherits: 2.0.4 @@ -18694,6 +19659,16 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.3 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.28.3 @@ -18710,6 +19685,14 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.30 @@ -18744,12 +19727,44 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + jest-changed-files@30.0.5: dependencies: execa: 5.1.1 jest-util: 30.0.5 p-limit: 3.1.0 + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.6.0 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-circus@30.1.1: dependencies: '@jest/environment': 30.1.1 @@ -18776,6 +19791,25 @@ snapshots: - babel-plugin-macros - supports-color + jest-cli@29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@30.1.1(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)): dependencies: '@jest/core': 30.1.1(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)) @@ -18795,6 +19829,37 @@ snapshots: - supports-color - ts-node + jest-config@29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)): + dependencies: + '@babel/core': 7.28.3 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.3) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.17 + ts-node: 10.9.2(@types/node@22.19.17)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@30.1.1(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)): dependencies: '@babel/core': 7.28.3 @@ -18828,6 +19893,13 @@ snapshots: - babel-plugin-macros - supports-color + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + jest-diff@30.1.1: dependencies: '@jest/diff-sequences': 30.0.1 @@ -18835,10 +19907,22 @@ snapshots: chalk: 4.1.2 pretty-format: 30.0.5 + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + jest-docblock@30.0.1: dependencies: detect-newline: 3.1.0 + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + jest-each@30.1.0: dependencies: '@jest/get-type': 30.1.0 @@ -18847,6 +19931,15 @@ snapshots: jest-util: 30.0.5 pretty-format: 30.0.5 + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jest-environment-node@30.1.1: dependencies: '@jest/environment': 30.1.1 @@ -18857,6 +19950,24 @@ snapshots: jest-util: 30.0.5 jest-validate: 30.1.0 + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.19.17 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + jest-haste-map@30.1.0: dependencies: '@jest/types': 30.0.5 @@ -18872,11 +19983,23 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + jest-leak-detector@30.1.0: dependencies: '@jest/get-type': 30.1.0 pretty-format: 30.0.5 + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + jest-matcher-utils@30.1.1: dependencies: '@jest/get-type': 30.1.0 @@ -18884,6 +20007,18 @@ snapshots: jest-diff: 30.1.1 pretty-format: 30.0.5 + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-message-util@30.1.0: dependencies: '@babel/code-frame': 7.27.1 @@ -18896,18 +20031,37 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + jest-util: 29.7.0 + jest-mock@30.0.5: dependencies: '@jest/types': 30.0.5 '@types/node': 24.3.0 jest-util: 30.0.5 + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + jest-pnp-resolver@1.2.3(jest-resolve@30.1.0): optionalDependencies: jest-resolve: 30.1.0 + jest-regex-util@29.6.3: {} + jest-regex-util@30.0.1: {} + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + jest-resolve-dependencies@30.1.1: dependencies: jest-regex-util: 30.0.1 @@ -18915,6 +20069,18 @@ snapshots: transitivePeerDependencies: - supports-color + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + jest-resolve@30.1.0: dependencies: chalk: 4.1.2 @@ -18926,6 +20092,32 @@ snapshots: slash: 3.0.0 unrs-resolver: 1.11.1 + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + jest-runner@30.1.1: dependencies: '@jest/console': 30.1.1 @@ -18953,6 +20145,33 @@ snapshots: transitivePeerDependencies: - supports-color + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + jest-runtime@30.1.1: dependencies: '@jest/environment': 30.1.1 @@ -18980,6 +20199,31 @@ snapshots: transitivePeerDependencies: - supports-color + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.28.3 + '@babel/generator': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) + '@babel/types': 7.28.5 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + jest-snapshot@30.1.1: dependencies: '@babel/core': 7.28.3 @@ -19009,7 +20253,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.3.0 + '@types/node': 22.19.17 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -19024,6 +20268,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 4.0.3 + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + jest-validate@30.1.0: dependencies: '@jest/get-type': 30.1.0 @@ -19033,6 +20286,17 @@ snapshots: leven: 3.1.0 pretty-format: 30.0.5 + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.17 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + jest-watcher@30.1.1: dependencies: '@jest/test-result': 30.1.1 @@ -19065,6 +20329,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@30.1.1(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)): dependencies: '@jest/core': 30.1.1(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)) @@ -19370,6 +20646,12 @@ snapshots: math-intrinsics@1.1.0: {} + md5.js@1.3.5: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + safe-buffer: 5.2.1 + mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -20856,6 +22138,12 @@ snapshots: lodash: 4.17.21 renderkid: 3.0.0 + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + pretty-format@30.0.5: dependencies: '@jest/schemas': 30.0.5 @@ -20906,6 +22194,8 @@ snapshots: dependencies: escape-goat: 4.0.0 + pure-rand@6.1.0: {} + pure-rand@7.0.1: {} qs@6.13.0: @@ -20978,19 +22268,19 @@ snapshots: react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.3))(webpack@5.101.3): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.3)' webpack: 5.101.3 react-router-config@5.1.1(react-router@5.3.4(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 react: 19.2.3 react-router: 5.3.4(react@19.2.3) react-router-dom@5.3.4(react@19.2.3): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -21001,7 +22291,7 @@ snapshots: react-router@5.3.4(react@19.2.3): dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 history: 4.10.1 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -21276,6 +22566,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -21310,6 +22602,11 @@ snapshots: glob: 11.0.3 package-json-from-dist: 1.0.1 + ripemd160@2.0.3: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + rolldown-plugin-dts@0.20.0(rolldown@1.0.0-beta.58)(typescript@5.9.2): dependencies: '@babel/generator': 7.28.5 @@ -21589,6 +22886,12 @@ snapshots: setprototypeof@1.2.0: {} + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + shallow-clone@3.0.1: dependencies: kind-of: 6.0.3 @@ -22105,6 +23408,12 @@ snapshots: tmpl@1.0.5: {} + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -22139,6 +23448,26 @@ snapshots: dependencies: typescript: 5.9.2 + ts-jest@29.4.1(@babel/core@7.28.3)(@jest/transform@30.1.1)(@jest/types@30.0.5)(babel-jest@30.1.1(@babel/core@7.28.3))(jest-util@30.0.5)(jest@29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)))(typescript@5.9.2): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.17)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.9.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.3 + '@jest/transform': 30.1.1 + '@jest/types': 30.0.5 + babel-jest: 30.1.1(@babel/core@7.28.3) + jest-util: 30.0.5 + ts-jest@29.4.1(@babel/core@7.28.3)(@jest/transform@30.1.1)(@jest/types@30.0.5)(babel-jest@30.1.1(@babel/core@7.28.3))(jest-util@30.0.5)(jest@30.1.1(@types/node@24.3.0)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)))(typescript@5.9.2): dependencies: bs-logger: 0.2.6 @@ -22169,6 +23498,25 @@ snapshots: typescript: 5.9.2 webpack: 5.100.2 + ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.17 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -22247,6 +23595,13 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -22346,6 +23701,8 @@ snapshots: typescript: 5.9.2 yaml: 2.8.1 + typeforce@1.18.0: {} + typescript-eslint@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2): dependencies: '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.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) @@ -22403,6 +23760,8 @@ snapshots: undici-types@6.19.8: {} + undici-types@6.21.0: {} + undici-types@7.10.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -22560,11 +23919,11 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - valibot@0.38.0(typescript@5.9.2): + valibot@1.1.0(typescript@5.9.2): optionalDependencies: typescript: 5.9.2 - valibot@1.1.0(typescript@5.9.2): + valibot@1.3.1(typescript@5.9.2): optionalDependencies: typescript: 5.9.2 @@ -22598,13 +23957,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@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): + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.3(@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) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -22619,7 +23978,7 @@ snapshots: - tsx - yaml - vite@7.1.3(@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): + vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -22633,14 +23992,14 @@ snapshots: jiti: 2.5.1 lightningcss: 1.30.1 terser: 5.43.1 - tsx: 4.20.5 + tsx: 4.21.0 yaml: 2.8.1 - vitest@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): + vitest@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.21.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.3(@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)) + '@vitest/mocker': 3.2.4(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -22658,8 +24017,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.3(@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) - vite-node: 3.2.4(@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) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -22925,6 +24284,14 @@ snapshots: dependencies: string-width: 5.1.2 + wif@2.0.6: + dependencies: + bs58check: 2.1.2 + + wif@5.0.0: + dependencies: + bs58check: 4.0.0(patch_hash=0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26) + wildcard@2.0.1: {} word-wrap@1.2.5: {} @@ -22958,6 +24325,11 @@ snapshots: signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + write-file-atomic@5.0.1: dependencies: imurmurhash: 0.1.4