diff --git a/.changeset/dirty-horses-visit.md b/.changeset/dirty-horses-visit.md new file mode 100644 index 00000000..7feb8b62 --- /dev/null +++ b/.changeset/dirty-horses-visit.md @@ -0,0 +1,7 @@ +--- +"@ckb-ccc/core": patch +"@ckb-ccc/spore": patch +--- + +feat: molecule codec +feat: spore searcher diff --git a/packages/core/src/barrel.ts b/packages/core/src/barrel.ts index 8d95d37b..b1d6c989 100644 --- a/packages/core/src/barrel.ts +++ b/packages/core/src/barrel.ts @@ -6,6 +6,7 @@ export * from "./fixedPoint/index.js"; export * from "./hasher/index.js"; export * from "./hex/index.js"; export * from "./keystore/index.js"; +export * as mol from "./molecule/index.js"; export * from "./num/index.js"; export * from "./signer/index.js"; export * from "./utils/index.js"; diff --git a/packages/core/src/molecule/codec.ts b/packages/core/src/molecule/codec.ts new file mode 100644 index 00000000..e120b6cd --- /dev/null +++ b/packages/core/src/molecule/codec.ts @@ -0,0 +1,610 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Bytes, bytesConcat, bytesFrom, BytesLike } from "../bytes/index.js"; +import { + Num, + numBeFromBytes, + numBeToBytes, + numFromBytes, + NumLike, + numToBytes, +} from "../num/index.js"; + +export type CodecLike = { + readonly encode: (encodable: Encodable) => Bytes; + readonly decode: (decodable: BytesLike) => Decoded; + readonly byteLength?: number; +}; +export class Codec { + constructor( + public readonly encode: (encodable: Encodable) => Bytes, + public readonly decode: (decodable: BytesLike) => Decoded, + public readonly byteLength?: number, // if provided, treat codec as fixed length + ) {} + + static from({ + encode, + decode, + byteLength, + }: CodecLike): Codec { + return new Codec(encode, decode, byteLength); + } + + map({ + inMap, + outMap, + }: { + inMap?: (encodable: NewEncodable) => Encodable; + outMap?: (decoded: Decoded) => NewDecoded; + }): Codec { + return Codec.from({ + byteLength: this.byteLength, + encode: (encodable) => + this.encode((inMap ? inMap(encodable) : encodable) as Encodable), + decode: (buffer) => + (outMap + ? outMap(this.decode(buffer)) + : this.decode(buffer)) as NewDecoded, + }); + } +} + +export type EncodableType> = + T extends CodecLike ? Encodable : never; +export type DecodedType> = + T extends CodecLike ? Decoded : never; + +function uint32To(numLike: NumLike) { + return numToBytes(numLike, 4); +} + +function uint32From(bytesLike: BytesLike) { + return Number(numFromBytes(bytesLike)); +} + +/** + * Vector with fixed size item codec + * @param itemCodec fixed-size vector item codec + */ +export function fixedItemVec( + itemCodec: CodecLike, +): Codec, Array> { + const itemByteLength = itemCodec.byteLength; + if (itemByteLength === undefined) { + throw new Error("fixedItemVec: itemCodec requires a byte length"); + } + + return Codec.from({ + encode(userDefinedItems) { + try { + return userDefinedItems.reduce( + (concatted, item) => bytesConcat(concatted, itemCodec.encode(item)), + uint32To(userDefinedItems.length), + ); + } catch (e: unknown) { + throw new Error(`fixedItemVec(${e?.toString()})`); + } + }, + decode(buffer) { + const value = bytesFrom(buffer); + if (value.byteLength < 4) { + throw new Error( + `fixedItemVec: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`, + ); + } + const itemCount = uint32From(value.slice(0, 4)); + const byteLength = 4 + itemCount * itemByteLength; + if (value.byteLength !== byteLength) { + throw new Error( + `fixedItemVec: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`, + ); + } + + try { + const decodedArray: Array = []; + for (let offset = 0; offset < byteLength; offset += itemByteLength) { + decodedArray.push( + itemCodec.decode(value.slice(offset, offset + itemByteLength)), + ); + } + return decodedArray; + } catch (e) { + throw new Error(`fixedItemVec(${e?.toString()})`); + } + }, + }); +} + +/** + * Vector with dynamic size item codec, you can create a recursive vector with this function + * @param itemCodec the vector item codec. It can be fixed-size or dynamic-size. + */ +export function dynItemVec( + itemCodec: CodecLike, +): Codec, Array> { + return Codec.from({ + encode(userDefinedItems) { + try { + const encoded = userDefinedItems.reduce( + ({ offset, header, body }, item) => { + const encodedItem = itemCodec.encode(item); + const packedHeader = uint32To(offset); + return { + header: bytesConcat(header, packedHeader), + body: bytesConcat(body, encodedItem), + offset: offset + bytesFrom(encodedItem).byteLength, + }; + }, + { + header: bytesFrom([]), + body: bytesFrom([]), + offset: 4 + userDefinedItems.length * 4, + }, + ); + const packedTotalSize = uint32To( + encoded.header.byteLength + encoded.body.byteLength + 4, + ); + return bytesConcat(packedTotalSize, encoded.header, encoded.body); + } catch (e) { + throw new Error(`dynItemVec(${e?.toString()})`); + } + }, + decode(buffer) { + const value = bytesFrom(buffer); + const byteLength = uint32From(value.slice(0, 4)); + if (byteLength !== value.byteLength) { + throw new Error( + `dynItemVec: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`, + ); + } + if (value.byteLength < 4) { + throw new Error( + `fixedItemVec: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`, + ); + } + + const offset = uint32From(value.slice(4, 8)); + const itemCount = (offset - 4) / 4; + const offsets = Array.from(new Array(itemCount), (_, index) => + uint32From(value.slice(4 + index * 4, 8 + index * 4)), + ); + offsets.push(byteLength); + try { + const decodedArray: Array = []; + for (let index = 0; index < offsets.length - 1; index++) { + const start = offsets[index]; + const end = offsets[index + 1]; + const itemBuffer = value.slice(start, end); + decodedArray.push(itemCodec.decode(itemBuffer)); + } + return decodedArray; + } catch (e) { + throw new Error(`dynItemVec(${e?.toString()})`); + } + }, + }); +} + +/** + * General vector codec, if `itemCodec` is fixed size type, it will create a fixvec codec, otherwise a dynvec codec will be created. + * @param itemCodec + */ +export function vector( + itemCodec: CodecLike, +): Codec, Array> { + if (itemCodec.byteLength !== undefined) { + return fixedItemVec(itemCodec); + } + return dynItemVec(itemCodec); +} + +/** + * Option is a dynamic-size type. + * Serializing an option depends on whether it is empty or not: + * - if it's empty, there is zero bytes (the size is 0). + * - if it's not empty, just serialize the inner item (the size is same as the inner item's size). + * @param innerCodec + */ +export function option( + innerCodec: CodecLike, +): Codec { + return Codec.from({ + encode(userDefinedOrNull) { + if (!userDefinedOrNull) { + return bytesFrom([]); + } + try { + return innerCodec.encode(userDefinedOrNull); + } catch (e) { + throw new Error(`option(${e?.toString()})`); + } + }, + decode(buffer) { + const value = bytesFrom(buffer); + if (value.byteLength === 0) { + return undefined; + } + try { + return innerCodec.decode(buffer); + } catch (e) { + throw new Error(`option(${e?.toString()})`); + } + }, + }); +} + +/** + * Wrap the encoded value with a fixed-length buffer + * @param codec + */ +export function byteVec( + codec: CodecLike, +): Codec { + return Codec.from({ + encode(userDefined) { + try { + const payload = bytesFrom(codec.encode(userDefined)); + const byteLength = uint32To(payload.byteLength); + return bytesConcat(byteLength, payload); + } catch (e) { + throw new Error(`byteVec(${e?.toString()})`); + } + }, + decode(buffer) { + const value = bytesFrom(buffer); + if (value.byteLength < 4) { + throw new Error( + `byteVec: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`, + ); + } + const byteLength = uint32From(value.slice(0, 4)); + if (byteLength !== value.byteLength - 4) { + throw new Error( + `byteVec: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`, + ); + } + try { + return codec.decode(value.slice(4)); + } catch (e: unknown) { + throw new Error(`byteVec(${e?.toString()})`); + } + }, + }); +} + +export type EncodableRecordOptionalKeys< + T extends Record>, +> = { + [K in keyof T]: Extract, undefined> extends never + ? never + : K; +}[keyof T]; +export type EncodableRecord>> = { + [key in keyof Pick>]+?: EncodableType< + T[key] + >; +} & { + [key in keyof Omit>]: EncodableType; +}; + +export type DecodedRecordOptionalKeys< + T extends Record>, +> = { + [K in keyof T]: Extract, undefined> extends never + ? never + : K; +}[keyof T]; +export type DecodedRecord>> = { + [key in keyof Pick>]+?: DecodedType; +} & { + [key in keyof Omit>]: DecodedType; +}; + +/** + * Table is a dynamic-size type. It can be considered as a dynvec but the length is fixed. + * @param codecLayout + */ +export function table< + T extends Record>, + Encodable extends EncodableRecord, + Decoded extends DecodedRecord, +>(codecLayout: T): Codec { + const keys = Object.keys(codecLayout); + + return Codec.from({ + encode(object) { + const headerLength = 4 + keys.length * 4; + + const { header, body } = keys.reduce( + (result, key) => { + try { + const encodedItem = codecLayout[key].encode((object as any)[key]); + const packedOffset = uint32To(result.offset); + return { + header: bytesConcat(result.header, packedOffset), + body: bytesConcat(result.body, encodedItem), + offset: result.offset + bytesFrom(encodedItem).byteLength, + }; + } catch (e: unknown) { + throw new Error(`table.${key}(${e?.toString()})`); + } + }, + { + header: bytesFrom([]), + body: bytesFrom([]), + offset: headerLength, + }, + ); + const packedTotalSize = uint32To(header.byteLength + body.byteLength + 4); + return bytesConcat(packedTotalSize, header, body); + }, + decode(buffer) { + const value = bytesFrom(buffer); + const byteLength = uint32From(value.slice(0, 4)); + if (byteLength !== value.byteLength) { + throw new Error( + `table: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`, + ); + } + if (byteLength <= 4) { + throw new Error("table: empty buffer"); + } + const offsets = keys.map((_, index) => + uint32From(value.slice(4 + index * 4, 8 + index * 4)), + ); + offsets.push(byteLength); + const object = {}; + for (let i = 0; i < offsets.length - 1; i++) { + const start = offsets[i]; + const end = offsets[i + 1]; + const field = keys[i]; + const codec = codecLayout[field]; + const payload = value.slice(start, end); + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + Object.assign(object, { [field]: codec.decode(payload) }); + } catch (e: unknown) { + throw new Error(`table.${field}(${e?.toString()})`); + } + } + return object as Decoded; + }, + }); +} + +type UnionEncodable< + T extends Record>, + K extends keyof T = keyof T, +> = K extends unknown + ? { + type: K; + value: EncodableType; + } + : never; +type UnionDecoded< + T extends Record>, + K extends keyof T = keyof T, +> = K extends unknown + ? { + type: K; + value: DecodedType; + } + : never; + +/** + * Union is a dynamic-size type. + * Serializing a union has two steps: + * - Serialize an item type id in bytes as a 32 bit unsigned integer in little-endian. The item type id is the index of the inner items, and it's starting at 0. + * - Serialize the inner item. + * @param codecLayout the union item record + * @param fields the custom item type id record + * @example + * // without custom id + * union({ cafe: Uint8, bee: Uint8 }) + * // with custom id + * union({ cafe: Uint8, bee: Uint8 }, { cafe: 0xcafe, bee: 0xbee }) + */ +export function union>>( + codecLayout: T, + fields?: Record, +): Codec, UnionDecoded> { + const keys = Object.keys(codecLayout); + + return Codec.from({ + encode({ type, value }) { + const typeStr = type.toString(); + const codec = codecLayout[typeStr]; + if (!codec) { + throw new Error( + `union: invalid type, expected ${keys.toString()}, but got ${typeStr}`, + ); + } + const fieldId = fields ? (fields[typeStr] ?? -1) : keys.indexOf(typeStr); + if (fieldId < 0) { + throw new Error(`union: invalid field id ${fieldId} of ${typeStr}`); + } + const header = uint32To(fieldId); + try { + const body = codec.encode(value); + return bytesConcat(header, body); + } catch (e: unknown) { + throw new Error(`union.(${typeStr})(${e?.toString()})`); + } + }, + decode(buffer) { + const value = bytesFrom(buffer); + const fieldIndex = uint32From(value.slice(0, 4)); + const keys = Object.keys(codecLayout); + + const field = (() => { + if (!fields) { + return keys[fieldIndex]; + } + const entry = Object.entries(fields).find( + ([, id]) => id === fieldIndex, + ); + return entry?.[0]; + })(); + + if (!field) { + if (!fields) { + throw new Error( + `union: unknown union field index ${fieldIndex}, only ${keys.toString()} are allowed`, + ); + } + const fieldKeys = Object.keys(fields); + throw new Error( + `union: unknown union field index ${fieldIndex}, only ${fieldKeys.toString()} and ${keys.toString()} are allowed`, + ); + } + + return { + type: field, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + value: codecLayout[field].decode(value.slice(4)), + } as UnionDecoded; + }, + }); +} + +/** + * Struct is a fixed-size type: all fields in struct are fixed-size and it has a fixed quantity of fields. + * The size of a struct is the sum of all fields' size. + * @param codecLayout a object contains all fields' codec + */ +export function struct< + T extends Record>, + Encodable extends EncodableRecord, + Decoded extends DecodedRecord, +>(codecLayout: T): Codec { + const codecArray = Object.values(codecLayout); + if (codecArray.some((codec) => codec.byteLength === undefined)) { + throw new Error("struct: all fields must be fixed-size"); + } + + const keys = Object.keys(codecLayout); + + return Codec.from({ + byteLength: codecArray.reduce((sum, codec) => sum + codec.byteLength!, 0), + encode(object) { + return keys.reduce((result, key) => { + try { + const encodedItem = codecLayout[key].encode((object as any)[key]); + return bytesConcat(result, encodedItem); + } catch (e: unknown) { + throw new Error(`struct.${key}(${e?.toString()})`); + } + }, bytesFrom([])); + }, + decode(buffer) { + const value = bytesFrom(buffer); + const object = {}; + let offset = 0; + Object.entries(codecLayout).forEach(([key, codec]) => { + const payload = value.slice(offset, offset + codec.byteLength!); + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + Object.assign(object, { [key]: codec.decode(payload) }); + } catch (e: unknown) { + throw new Error(`struct.${key}(${(e as Error).toString()})`); + } + offset = offset + codec.byteLength!; + }); + return object as Decoded; + }, + }); +} + +/** + * The array is a fixed-size type: it has a fixed-size inner type and a fixed length. + * The size of an array is the size of inner type times the length. + * @param itemCodec the fixed-size array item codec + * @param itemCount + */ +export function array( + itemCodec: CodecLike, + itemCount: number, +): Codec, Array> { + if (itemCodec.byteLength === undefined) { + throw new Error("array: itemCodec requires a byte length"); + } + const byteLength = itemCodec.byteLength * itemCount; + + return Codec.from({ + byteLength, + encode(items) { + try { + return items.reduce( + (concatted, item) => bytesConcat(concatted, itemCodec.encode(item)), + bytesFrom([]), + ); + } catch (e: unknown) { + throw new Error(`array(${e?.toString()})`); + } + }, + decode(buffer) { + const value = bytesFrom(buffer); + if (value.byteLength != byteLength) { + throw new Error( + `array: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`, + ); + } + try { + const result: Array = []; + for (let i = 0; i < value.byteLength; i += itemCodec.byteLength!) { + result.push( + itemCodec.decode(value.slice(i, i + itemCodec.byteLength!)), + ); + } + return result; + } catch (e: unknown) { + throw new Error(`array(${e?.toString()})`); + } + }, + }); +} + +/** + * Create a codec to deal with fixed LE or BE bytes. + * @param byteLength + * @param littleEndian + */ +export function uint( + byteLength: number, + littleEndian = false, +): Codec { + return Codec.from({ + byteLength, + encode: (numLike) => { + if (littleEndian) { + return numToBytes(numLike, byteLength); + } else { + return numBeToBytes(numLike, byteLength); + } + }, + decode: (buffer) => { + if (littleEndian) { + return numFromBytes(buffer); + } else { + return numBeFromBytes(buffer); + } + }, + }); +} + +/** + * Create a codec to deal with fixed LE or BE bytes. + * @param byteLength + * @param littleEndian + */ +export function uintNumber( + byteLength: number, + littleEndian = false, +): Codec { + if (byteLength > 4) { + throw new Error("uintNumber: byteLength must be less than or equal to 4"); + } + return uint(byteLength, littleEndian).map({ + outMap: (num) => Number(num), + }); +} diff --git a/packages/core/src/molecule/index.ts b/packages/core/src/molecule/index.ts new file mode 100644 index 00000000..9e34e06e --- /dev/null +++ b/packages/core/src/molecule/index.ts @@ -0,0 +1,2 @@ +export * from "./codec.js"; +export * from "./predefined.js"; diff --git a/packages/core/src/molecule/predefined.ts b/packages/core/src/molecule/predefined.ts new file mode 100644 index 00000000..0940eb31 --- /dev/null +++ b/packages/core/src/molecule/predefined.ts @@ -0,0 +1,114 @@ +import { bytesFrom, bytesTo } from "../bytes/index.js"; +import * as ckb from "../ckb/index.js"; +import { Hex, hexFrom, HexLike } from "../hex/index.js"; +import { + byteVec, + Codec, + option, + struct, + table, + uint, + uintNumber, + vector, +} from "./codec.js"; + +export const Uint8 = uintNumber(1, true); +export const Uint16LE = uintNumber(2, true); +export const Uint16BE = uintNumber(2); +export const Uint16 = Uint16LE; +export const Uint32LE = uintNumber(4, true); +export const Uint32BE = uintNumber(4); +export const Uint32 = Uint32LE; +export const Uint64LE = uint(8, true); +export const Uint64BE = uint(8); +export const Uint64 = Uint64LE; +export const Uint128LE = uint(16, true); +export const Uint128BE = uint(16); +export const Uint128 = Uint128LE; +export const Uint256LE = uint(32, true); +export const Uint256BE = uint(32); +export const Uint256 = Uint256LE; +export const Uint512LE = uint(64, true); +export const Uint512BE = uint(64); +export const Uint512 = Uint512LE; + +export const Uint8Opt = option(Uint8); +export const Uint16Opt = option(Uint16); +export const Uint32Opt = option(Uint32); +export const Uint64Opt = option(Uint64); +export const Uint128Opt = option(Uint128); +export const Uint256Opt = option(Uint256); +export const Uint512Opt = option(Uint512); + +export const Bytes: Codec = byteVec({ + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); +export const BytesOpt = option(Bytes); +export const BytesVec = vector(Bytes); + +export const Byte32: Codec = Codec.from({ + byteLength: 32, + encode: (value) => bytesFrom(value), + decode: (buffer) => hexFrom(buffer), +}); +export const Byte32Opt = option(Byte32); +export const Byte32Vec = vector(Byte32); + +export const String = byteVec({ + encode: (value: string) => bytesFrom(value, "utf8"), + decode: (buffer) => bytesTo(buffer, "utf8"), +}); +export const StringVec = vector(String); +export const StringOpt = option(String); + +export const Hash = Byte32; +export const HashType: Codec = Codec.from({ + byteLength: 1, + encode: ckb.hashTypeToBytes, + decode: ckb.hashTypeFromBytes, +}); +export const Script: Codec = table({ + codeHash: Hash, + hashType: HashType, + args: Bytes, +}).map({ outMap: ckb.Script.from }); +export const ScriptOpt = option(Script); + +export const OutPoint: Codec = struct({ + txHash: Hash, + index: Uint32, +}).map({ outMap: ckb.OutPoint.from }); +export const CellInput: Codec = struct({ + previousOutput: OutPoint, + since: Uint64, +}).map({ outMap: ckb.CellInput.from }); +export const CellInputVec = vector(CellInput); + +export const CellOutput: Codec = table({ + capacity: Uint64, + lock: Script, + type: ScriptOpt, +}).map({ outMap: ckb.CellOutput.from }); +export const CellOutputVec = vector(CellOutput); + +export const DepType: Codec = Codec.from({ + byteLength: 1, + encode: ckb.depTypeToBytes, + decode: ckb.depTypeFromBytes, +}); +export const CellDep: Codec = struct({ + outPoint: OutPoint, + depType: DepType, +}).map({ outMap: ckb.CellDep.from }); +export const CellDepVec = vector(CellDep); + +export const Transaction: Codec = table({ + version: Uint32, + cellDeps: CellDepVec, + headerDeps: Byte32Vec, + inputs: CellInputVec, + outputs: CellOutputVec, + outputsData: BytesVec, + witnesses: BytesVec, +}).map({ outMap: ckb.Transaction.from }); diff --git a/packages/spore/package.json b/packages/spore/package.json index 46b05e23..81068597 100644 --- a/packages/spore/package.json +++ b/packages/spore/package.json @@ -29,7 +29,9 @@ }, "devDependencies": { "@eslint/js": "^9.1.1", + "@types/node": "^22.10.0", "copyfiles": "^2.4.1", + "dotenv": "^16.4.5", "eslint": "^9.1.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -37,16 +39,13 @@ "prettier-plugin-organize-imports": "^3.2.4", "rimraf": "^5.0.5", "typescript": "^5.4.5", - "typescript-eslint": "^7.7.0", - "dotenv": "^16.4.5" + "typescript-eslint": "^7.7.0" }, "publishConfig": { "access": "public" }, "dependencies": { "@ckb-ccc/core": "workspace:*", - "@ckb-lumos/base": "^0.24.0-next.1", - "@ckb-lumos/codec": "^0.24.0-next.1", "axios": "^1.7.7" } } diff --git a/packages/spore/src/__examples__/createDobSpore.test.ts b/packages/spore/src/__examples__/createDobSpore.test.ts index 18f2a1df..2aa89c3d 100644 --- a/packages/spore/src/__examples__/createDobSpore.test.ts +++ b/packages/spore/src/__examples__/createDobSpore.test.ts @@ -30,7 +30,7 @@ describe("createSpore [testnet]", () => { contentType: "dob/1", content: ccc.bytesFrom(content, "utf8"), clusterId: - "0xcf95169f4843b7647837c7cf7e54e5ce7fbc3c7a5ce3c56898b54525d40d72d6", + "0x52d19bd6ae411bfddaa48ede1881bcbb12c1a06f55531423aa29fc1ccb5f073c", }, clusterMode: "clusterCell", }); diff --git a/packages/spore/src/__examples__/meltSpore.test.ts b/packages/spore/src/__examples__/meltSpore.test.ts index 9ffe0f6c..6b05b096 100644 --- a/packages/spore/src/__examples__/meltSpore.test.ts +++ b/packages/spore/src/__examples__/meltSpore.test.ts @@ -16,7 +16,7 @@ describe("meltSpore [testnet]", () => { let { tx } = await meltSpore({ signer, // Change this if you have a different sporeId - id: "0x1281272e54985fa1e8c876538bad584267123eac16cbfa87534920a6d35e3a4b", + id: "0x29e4cfd388b9a01f7a853d476feb8e33af38565a1e751d55c9423bf7aa4b480b", }); // Complete transaction diff --git a/packages/spore/src/__examples__/searchClusters.test.ts b/packages/spore/src/__examples__/searchClusters.test.ts new file mode 100644 index 00000000..70100f0a --- /dev/null +++ b/packages/spore/src/__examples__/searchClusters.test.ts @@ -0,0 +1,22 @@ +import { ccc } from "@ckb-ccc/core"; +import { findSporeClustersBySigner } from "../cluster"; + +describe("searchClusters [testnet]", () => { + expect(process.env.PRIVATE_KEY).toBeDefined(); + + it("should search multiple Cluster cells under private key", async () => { + const client = new ccc.ClientPublicTestnet(); + const signer = new ccc.SignerCkbPrivateKey( + client, + process.env.PRIVATE_KEY!, + ); + + // Search Cluster cells + for await (const cluster of findSporeClustersBySigner({ + signer, + order: "desc", + })) { + console.log(cluster); + } + }, 60000); +}); diff --git a/packages/spore/src/__examples__/searchSpores.test.ts b/packages/spore/src/__examples__/searchSpores.test.ts new file mode 100644 index 00000000..75120269 --- /dev/null +++ b/packages/spore/src/__examples__/searchSpores.test.ts @@ -0,0 +1,19 @@ +import { ccc } from "@ckb-ccc/core"; +import { findSporesBySigner } from "../spore"; + +describe("searchSpores [testnet]", () => { + expect(process.env.PRIVATE_KEY).toBeDefined(); + + it("should search multiple Spore cells under private key", async () => { + const client = new ccc.ClientPublicTestnet(); + const signer = new ccc.SignerCkbPrivateKey( + client, + process.env.PRIVATE_KEY!, + ); + + // Search Spore cells + for await (const spore of findSporesBySigner({ signer, order: "desc" })) { + console.log(spore); + } + }, 60000); +}); diff --git a/packages/spore/src/cluster/index.ts b/packages/spore/src/cluster/index.ts index be5163be..bf9beccc 100644 --- a/packages/spore/src/cluster/index.ts +++ b/packages/spore/src/cluster/index.ts @@ -4,7 +4,12 @@ import { assembleTransferClusterAction, prepareSporeTransaction, } from "../advanced.js"; -import { ClusterData, packRawClusterData } from "../codec/index.js"; +import { + ClusterData, + ClusterDataV1, + ClusterDataView, + packRawClusterData, +} from "../codec/index.js"; import { findSingletonCellByArgs, injectOneCapacityCell, @@ -66,7 +71,7 @@ export async function assertCluster( */ export async function createSporeCluster(params: { signer: ccc.Signer; - data: ClusterData; + data: ClusterDataView; to?: ccc.ScriptLike; tx?: ccc.TransactionLike; scriptInfo?: SporeScriptInfoLike; @@ -178,3 +183,55 @@ export async function transferSporeCluster(params: { tx: await prepareSporeTransaction(signer, tx, actions), }; } + +/** + * Search on-chain clusters under the signer's control + * + * @param signer the owner of clusters + * @param order the order in creation time of clusters + * @param scriptInfos the deployed script infos of clusters + */ +export async function* findSporeClustersBySigner(params: { + signer: ccc.Signer; + order?: "asc" | "desc"; + scriptInfos?: SporeScriptInfoLike[]; +}): AsyncGenerator<{ + cluster: ccc.Cell; + clusterData: ClusterDataView; +}> { + const { signer, order, scriptInfos } = params; + for (const scriptInfo of scriptInfos ?? + Object.values(getClusterScriptInfos(signer.client))) { + if (!scriptInfo) { + continue; + } + for await (const cluster of signer.findCells( + { + script: { + ...scriptInfo, + args: [], + }, + }, + true, + order, + 10, + )) { + let clusterData: ClusterDataView; + try { + clusterData = ClusterData.decode(cluster.outputData); + } catch (_) { + try { + clusterData = ClusterDataV1.decode(cluster.outputData); + } catch (e: unknown) { + throw new Error( + `Cluster data decode failed: ${(e as Error).toString()}`, + ); + } + } + yield { + cluster, + clusterData, + }; + } + } +} diff --git a/packages/spore/src/cobuild/index.ts b/packages/spore/src/cobuild/index.ts index 514d1332..63784a61 100644 --- a/packages/spore/src/cobuild/index.ts +++ b/packages/spore/src/cobuild/index.ts @@ -1,5 +1,4 @@ -import { ccc } from "@ckb-ccc/core"; -import { UnpackResult } from "@ckb-lumos/codec"; +import { ccc, mol } from "@ckb-ccc/core"; import { Action, ActionVec, @@ -12,21 +11,21 @@ export function assembleCreateSporeAction( sporeOutput: ccc.CellOutputLike, sporeData: ccc.BytesLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): UnpackResult { +): mol.EncodableType { if (!sporeOutput.type) { throw new Error("Spore cell must have a type script"); } const sporeType = ccc.Script.from(sporeOutput.type); const sporeTypeHash = sporeType.hash(); - const actionData = SporeAction.pack({ + const actionData = SporeAction.encode({ type: "CreateSpore", value: { sporeId: sporeType.args, - dataHash: ccc.hashCkb(sporeData), to: { type: "Script", - value: ccc.Script.from(sporeOutput.lock), + value: sporeOutput.lock, }, + dataHash: ccc.hashCkb(sporeData), }, }); return { @@ -40,24 +39,24 @@ export function assembleTransferSporeAction( sporeInput: ccc.CellOutputLike, sporeOutput: ccc.CellOutputLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): UnpackResult { +): mol.EncodableType { if (!sporeInput.type || !sporeOutput.type) { throw new Error("Spore cell must have a type script"); } const sporeType = ccc.Script.from(sporeOutput.type); const sporeTypeHash = sporeType.hash(); - const actionData = SporeAction.pack({ + const actionData = SporeAction.encode({ type: "TransferSpore", value: { sporeId: sporeType.args, from: { type: "Script", - value: ccc.Script.from(sporeInput.lock), + value: sporeInput.lock, }, to: { type: "Script", - value: ccc.Script.from(sporeOutput.lock), + value: sporeOutput.lock, }, }, }); @@ -71,19 +70,19 @@ export function assembleTransferSporeAction( export function assembleMeltSporeAction( sporeInput: ccc.CellOutputLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): UnpackResult { +): mol.EncodableType { if (!sporeInput.type) { throw new Error("Spore cell must have a type script"); } const sporeType = ccc.Script.from(sporeInput.type); const sporeTypeHash = sporeType.hash(); - const actionData = SporeAction.pack({ + const actionData = SporeAction.encode({ type: "MeltSpore", value: { sporeId: sporeType.args, from: { type: "Script", - value: ccc.Script.from(sporeInput.lock), + value: sporeInput.lock, }, }, }); @@ -98,21 +97,21 @@ export function assembleCreateClusterAction( clusterOutput: ccc.CellOutputLike, clusterData: ccc.BytesLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): UnpackResult { +): mol.EncodableType { if (!clusterOutput.type) { throw new Error("Cluster cell must have a type script"); } const clusterType = ccc.Script.from(clusterOutput.type); const clusterTypeHash = clusterType.hash(); - const actionData = SporeAction.pack({ + const actionData = SporeAction.encode({ type: "CreateCluster", value: { clusterId: clusterType.args, - dataHash: ccc.hashCkb(clusterData), to: { type: "Script", - value: ccc.Script.from(clusterOutput.lock), + value: clusterOutput.lock, }, + dataHash: ccc.hashCkb(clusterData), }, }); return { @@ -126,23 +125,23 @@ export function assembleTransferClusterAction( clusterInput: ccc.CellOutputLike, clusterOutput: ccc.CellOutputLike, scriptInfoHash: ccc.HexLike = DEFAULT_COBUILD_INFO_HASH, -): UnpackResult { +): mol.EncodableType { if (!clusterInput.type || !clusterOutput.type) { throw new Error("Cluster cell must have a type script"); } const clusterType = ccc.Script.from(clusterOutput.type); const clusterTypeHash = clusterType.hash(); - const actionData = SporeAction.pack({ + const actionData = SporeAction.encode({ type: "TransferCluster", value: { clusterId: clusterType.args, from: { type: "Script", - value: ccc.Script.from(clusterInput.lock), + value: clusterInput.lock, }, to: { type: "Script", - value: ccc.Script.from(clusterOutput.lock), + value: clusterOutput.lock, }, }, }); @@ -156,7 +155,7 @@ export function assembleTransferClusterAction( export async function prepareSporeTransaction( signer: ccc.Signer, txLike: ccc.TransactionLike, - actions: UnpackResult, + actions: mol.EncodableType, ): Promise { let tx = ccc.Transaction.from(txLike); @@ -172,9 +171,9 @@ export async function prepareSporeTransaction( export function unpackCommonCobuildProof( data: ccc.HexLike, -): UnpackResult | undefined { +): mol.EncodableType | undefined { try { - return WitnessLayout.unpack(ccc.bytesFrom(data)); + return WitnessLayout.decode(ccc.bytesFrom(data)); } catch { return; } @@ -182,7 +181,7 @@ export function unpackCommonCobuildProof( export function extractCobuildActionsFromTx( tx: ccc.Transaction, -): UnpackResult { +): mol.EncodableType { if (tx.witnesses.length === 0) { return []; } @@ -193,7 +192,7 @@ export function extractCobuildActionsFromTx( return []; } if (witnessLayout.type !== "SighashAll") { - throw new Error("Invalid cobuild proof type: " + witnessLayout.type); + throw new Error("Invalid cobuild proof type: SighashAll"); } // Remove existed cobuild witness @@ -203,10 +202,10 @@ export function extractCobuildActionsFromTx( export function injectCobuild( tx: ccc.Transaction, - actions: UnpackResult, + actions: mol.EncodableType, ): void { const witnessLayout = ccc.hexFrom( - WitnessLayout.pack({ + WitnessLayout.encode({ type: "SighashAll", value: { seal: "0x", diff --git a/packages/spore/src/codec/base.ts b/packages/spore/src/codec/base.ts deleted file mode 100644 index b8d84ccb..00000000 --- a/packages/spore/src/codec/base.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ccc } from "@ckb-ccc/core"; -import { molecule } from "@ckb-lumos/codec"; - -/** - * The codec for packing/unpacking UTF-8 raw strings. - * Should be packed like so: String.pack('something') - */ -export const RawString = molecule.byteVecOf({ - pack: (packable: string) => ccc.bytesFrom(packable, "utf8"), - unpack: (unpackable: ccc.BytesLike) => ccc.bytesTo(unpackable, "utf8"), -}); diff --git a/packages/spore/src/codec/cluster.ts b/packages/spore/src/codec/cluster.ts index 8c4aa249..e8f7e636 100644 --- a/packages/spore/src/codec/cluster.ts +++ b/packages/spore/src/codec/cluster.ts @@ -1,58 +1,53 @@ -import { ccc } from "@ckb-ccc/core"; -import { blockchain } from "@ckb-lumos/base"; -import { molecule } from "@ckb-lumos/codec"; -import { RawString } from "./base.js"; +import { ccc, mol } from "@ckb-ccc/core"; -export const MolClusterDataV1 = molecule.table( - { - name: RawString, - description: RawString, - }, - ["name", "description"], -); -export const MolClusterDataV2 = molecule.table( - { - name: RawString, - description: RawString, - mutantId: blockchain.BytesOpt, - }, - ["name", "description", "mutantId"], -); - -export interface ClusterDataV1 { +export interface ClusterDataV1View { name: string; description: string; } -export interface ClusterDataV2 { + +export const ClusterDataV1: mol.Codec = mol.table({ + name: mol.String, + description: mol.String, +}); + +export interface ClusterDataV2View { name: string; description: string; mutantId?: ccc.HexLike; } -export type ClusterData = ClusterDataV2; + +export const ClusterDataV2: mol.Codec = mol.table({ + name: mol.String, + description: mol.String, + mutantId: mol.BytesOpt, +}); + +export type ClusterDataView = ClusterDataV2View; export type ClusterDataVersion = "v1" | "v2"; +export const ClusterData = ClusterDataV2; + /** * Pack RawClusterData to Uint8Array. * Pass an optional "version" field to select a specific packing version. */ -export function packRawClusterData(packable: ClusterData): Uint8Array; +export function packRawClusterData(packable: ClusterDataView): Uint8Array; export function packRawClusterData( - packable: ClusterDataV1, + packable: ClusterDataV1View, version: "v1", ): Uint8Array; export function packRawClusterData( - packable: ClusterDataV2, + packable: ClusterDataV2View, version: "v2", ): Uint8Array; export function packRawClusterData( - packable: ClusterDataV1 | ClusterDataV2, + packable: ClusterDataV1View | ClusterDataV2View, version?: ClusterDataVersion, ): Uint8Array { if (!version) { return packRawClusterDataV2(packable); } - switch (version) { case "v1": return packRawClusterDataV1(packable); @@ -60,33 +55,41 @@ export function packRawClusterData( return packRawClusterDataV2(packable); } } -export function packRawClusterDataV1(packable: ClusterDataV1): Uint8Array { - return MolClusterDataV1.pack({ - name: packable.name, - description: packable.description, - }); + +export function packRawClusterDataV1(packable: ClusterDataV1View): Uint8Array { + return ccc.bytesFrom( + ClusterDataV1.encode({ + name: packable.name, + description: packable.description, + }), + ); } -export function packRawClusterDataV2(packable: ClusterDataV2): Uint8Array { - return MolClusterDataV2.pack({ - name: packable.name, - description: packable.description, - mutantId: packable.mutantId, - }); + +export function packRawClusterDataV2(packable: ClusterDataV2View): Uint8Array { + return ccc.bytesFrom( + ClusterDataV2.encode({ + name: packable.name, + description: packable.description, + mutantId: packable.mutantId, + }), + ); } /** * Unpack Hex/Bytes to RawClusterData. * Pass an optional "version" field to select a specific unpacking version. */ -export function unpackToRawClusterData(unpackable: ccc.BytesLike): ClusterData; +export function unpackToRawClusterData( + unpackable: ccc.BytesLike, +): ClusterDataView; export function unpackToRawClusterData( unpackable: ccc.BytesLike, version: "v1", -): ClusterDataV1; +): ClusterDataV1View; export function unpackToRawClusterData( unpackable: ccc.BytesLike, version: "v2", -): ClusterDataV2; +): ClusterDataV2View; export function unpackToRawClusterData( unpackable: ccc.BytesLike, version?: ClusterDataVersion, @@ -99,7 +102,6 @@ export function unpackToRawClusterData( return unpackToRawClusterDataV2(unpackable); } } - try { return unpackToRawClusterDataV2(unpackable); } catch { @@ -112,22 +114,15 @@ export function unpackToRawClusterData( } } } + export function unpackToRawClusterDataV1( unpackable: ccc.BytesLike, -): ClusterDataV1 { - const decoded = MolClusterDataV1.unpack(unpackable); - return { - name: decoded.name, - description: decoded.description, - }; +): ClusterDataV1View { + return ClusterDataV1.decode(unpackable); } + export function unpackToRawClusterDataV2( unpackable: ccc.BytesLike, -): ClusterDataV2 { - const decoded = MolClusterDataV2.unpack(unpackable); - return { - name: decoded.name, - description: decoded.description, - mutantId: ccc.apply(ccc.hexFrom, decoded.mutantId), - }; +): ClusterDataV2View { + return ClusterDataV2.decode(unpackable); } diff --git a/packages/spore/src/codec/cobuild/buildingPacket.ts b/packages/spore/src/codec/cobuild/buildingPacket.ts index ec9f581f..bf9e987e 100644 --- a/packages/spore/src/codec/cobuild/buildingPacket.ts +++ b/packages/spore/src/codec/cobuild/buildingPacket.ts @@ -1,72 +1,41 @@ -import { blockchain } from "@ckb-lumos/base"; -import { molecule, number } from "@ckb-lumos/codec"; -import { RawString } from "../base.js"; - -const Uint32Opt = molecule.option(number.Uint32LE); - -const Hash = blockchain.Byte32; - -export const Action = molecule.table( - { - scriptInfoHash: Hash, - scriptHash: Hash, - data: blockchain.Bytes, - }, - ["scriptInfoHash", "scriptHash", "data"], -); - -export const ActionVec = molecule.vector(Action); - -export const Message = molecule.table( - { - actions: ActionVec, - }, - ["actions"], -); - -export const ResolvedInputs = molecule.table( - { - outputs: blockchain.CellOutputVec, - outputsData: blockchain.BytesVec, - }, - ["outputs", "outputsData"], -); - -export const ScriptInfo = molecule.table( - { - name: RawString, - url: RawString, - scriptHash: Hash, - schema: RawString, - messageType: RawString, - }, - ["name", "url", "scriptHash", "schema", "messageType"], -); - -export const ScriptInfoVec = molecule.vector(ScriptInfo); - -export const BuildingPacketV1 = molecule.table( - { - message: Message, - payload: blockchain.Transaction, - resolvedInputs: ResolvedInputs, - changeOutput: Uint32Opt, - scriptInfos: ScriptInfoVec, - lockActions: ActionVec, - }, - [ - "message", - "payload", - "resolvedInputs", - "changeOutput", - "scriptInfos", - "lockActions", - ], -); - -export const BuildingPacket = molecule.union( - { - BuildingPacketV1, - }, - ["BuildingPacketV1"], -); +import { mol } from "@ckb-ccc/core"; + +export const Action = mol.table({ + scriptInfoHash: mol.Hash, + scriptHash: mol.Hash, + data: mol.Bytes, +}); + +export const ActionVec = mol.vector(Action); + +export const Message = mol.table({ + actions: ActionVec, +}); + +export const ResolvedInputs = mol.table({ + outputs: mol.CellOutputVec, + outputsData: mol.BytesVec, +}); + +export const ScriptInfo = mol.table({ + name: mol.String, + url: mol.String, + scriptHash: mol.Hash, + schema: mol.String, + messageType: mol.String, +}); + +export const ScriptInfoVec = mol.vector(ScriptInfo); + +export const BuildingPacketV1 = mol.table({ + message: Message, + payload: mol.Transaction, + resolvedInputs: ResolvedInputs, + changeOutput: mol.Uint32Opt, + scriptInfos: ScriptInfoVec, + lockActions: ActionVec, +}); + +export const BuildingPacket = mol.union({ + BuildingPacketV1, +}); diff --git a/packages/spore/src/codec/cobuild/sporeAction.ts b/packages/spore/src/codec/cobuild/sporeAction.ts index 7b364c0e..0078fa01 100644 --- a/packages/spore/src/codec/cobuild/sporeAction.ts +++ b/packages/spore/src/codec/cobuild/sporeAction.ts @@ -1,153 +1,99 @@ -import { blockchain } from "@ckb-lumos/base"; -import { molecule } from "@ckb-lumos/codec"; +import { mol } from "@ckb-ccc/core"; -const Hash = blockchain.Byte32; - -export const Address = molecule.union( - { - Script: blockchain.Script, - }, - ["Script"], -); +export const Address = mol.union({ + Script: mol.Script, +}); /** * Spore */ -export const CreateSpore = molecule.table( - { - sporeId: Hash, - to: Address, - dataHash: Hash, - }, - ["sporeId", "to", "dataHash"], -); -export const TransferSpore = molecule.table( - { - sporeId: Hash, - from: Address, - to: Address, - }, - ["sporeId", "from", "to"], -); -export const MeltSpore = molecule.table( - { - sporeId: Hash, - from: Address, - }, - ["sporeId", "from"], -); +export const CreateSpore = mol.table({ + sporeId: mol.Hash, + to: Address, + dataHash: mol.Hash, +}); +export const TransferSpore = mol.table({ + sporeId: mol.Hash, + from: Address, + to: Address, +}); +export const MeltSpore = mol.table({ + sporeId: mol.Hash, + from: Address, +}); /** * Cluster */ -export const CreateCluster = molecule.table( - { - clusterId: Hash, - to: Address, - dataHash: Hash, - }, - ["clusterId", "to", "dataHash"], -); -export const TransferCluster = molecule.table( - { - clusterId: Hash, - from: Address, - to: Address, - }, - ["clusterId", "from", "to"], -); +export const CreateCluster = mol.table({ + clusterId: mol.Hash, + to: Address, + dataHash: mol.Hash, +}); +export const TransferCluster = mol.table({ + clusterId: mol.Hash, + from: Address, + to: Address, +}); /** * ClusterProxy */ -export const CreateClusterProxy = molecule.table( - { - clusterId: Hash, - clusterProxyId: Hash, - to: Address, - }, - ["clusterId", "clusterProxyId", "to"], -); -export const TransferClusterProxy = molecule.table( - { - clusterId: Hash, - clusterProxyId: Hash, - from: Address, - to: Address, - }, - ["clusterId", "clusterProxyId", "from", "to"], -); -export const MeltClusterProxy = molecule.table( - { - clusterId: Hash, - clusterProxyId: Hash, - from: Address, - }, - ["clusterId", "clusterProxyId", "from"], -); +export const CreateClusterProxy = mol.table({ + clusterId: mol.Hash, + clusterProxyId: mol.Hash, + to: Address, +}); +export const TransferClusterProxy = mol.table({ + clusterId: mol.Hash, + clusterProxyId: mol.Hash, + from: Address, + to: Address, +}); +export const MeltClusterProxy = mol.table({ + clusterId: mol.Hash, + clusterProxyId: mol.Hash, + from: Address, +}); /** * ClusterAgent */ -export const CreateClusterAgent = molecule.table( - { - clusterId: Hash, - clusterProxyId: Hash, - to: Address, - }, - ["clusterId", "clusterProxyId", "to"], -); -export const TransferClusterAgent = molecule.table( - { - clusterId: Hash, - from: Address, - to: Address, - }, - ["clusterId", "from", "to"], -); -export const MeltClusterAgent = molecule.table( - { - clusterId: Hash, - from: Address, - }, - ["clusterId", "from"], -); +export const CreateClusterAgent = mol.table({ + clusterId: mol.Hash, + clusterProxyId: mol.Hash, + to: Address, +}); +export const TransferClusterAgent = mol.table({ + clusterId: mol.Hash, + from: Address, + to: Address, +}); +export const MeltClusterAgent = mol.table({ + clusterId: mol.Hash, + from: Address, +}); /** * Spore ScriptInfo Actions */ -export const SporeAction = molecule.union( - { - // Spore - CreateSpore, - TransferSpore, - MeltSpore, +export const SporeAction = mol.union({ + // Spore + CreateSpore, + TransferSpore, + MeltSpore, - // Cluster - CreateCluster, - TransferCluster, + // Cluster + CreateCluster, + TransferCluster, - // ClusterProxy - CreateClusterProxy, - TransferClusterProxy, - MeltClusterProxy, + // ClusterProxy + CreateClusterProxy, + TransferClusterProxy, + MeltClusterProxy, - // ClusterAgent - CreateClusterAgent, - TransferClusterAgent, - MeltClusterAgent, - }, - [ - "CreateSpore", - "TransferSpore", - "MeltSpore", - "CreateCluster", - "TransferCluster", - "CreateClusterProxy", - "TransferClusterProxy", - "MeltClusterProxy", - "CreateClusterAgent", - "TransferClusterAgent", - "MeltClusterAgent", - ], -); + // ClusterAgent + CreateClusterAgent, + TransferClusterAgent, + MeltClusterAgent, +}); diff --git a/packages/spore/src/codec/cobuild/witnessLayout.ts b/packages/spore/src/codec/cobuild/witnessLayout.ts index 518dfdbb..82299f6d 100644 --- a/packages/spore/src/codec/cobuild/witnessLayout.ts +++ b/packages/spore/src/codec/cobuild/witnessLayout.ts @@ -1,26 +1,20 @@ -import { blockchain } from "@ckb-lumos/base"; -import { molecule } from "@ckb-lumos/codec"; +import { mol } from "@ckb-ccc/core"; import { Message } from "./buildingPacket.js"; -export const SighashAll = molecule.table( - { - seal: blockchain.Bytes, - message: Message, - }, - ["seal", "message"], -); -export const SighashAllOnly = molecule.table( - { - seal: blockchain.Bytes, - }, - ["seal"], -); +export const SighashAll = mol.table({ + seal: mol.Bytes, + message: Message, +}); + +export const SighashAllOnly = mol.table({ + seal: mol.Bytes, +}); /** * Otx related are not implemented yet, so just placeholders. */ -export const Otx = molecule.table({}, []); -export const OtxStart = molecule.table({}, []); +export const Otx = mol.table({}); +export const OtxStart = mol.table({}); export const WitnessLayoutFieldTags = { SighashAll: 4278190081, @@ -29,7 +23,7 @@ export const WitnessLayoutFieldTags = { OtxStart: 4278190084, } as const; -export const WitnessLayout = molecule.union( +export const WitnessLayout = mol.union( { SighashAll, SighashAllOnly, diff --git a/packages/spore/src/codec/spore.ts b/packages/spore/src/codec/spore.ts index afb55fe3..4f41555c 100644 --- a/packages/spore/src/codec/spore.ts +++ b/packages/spore/src/codec/spore.ts @@ -1,36 +1,27 @@ -import { ccc } from "@ckb-ccc/core"; -import { blockchain } from "@ckb-lumos/base"; -import { molecule } from "@ckb-lumos/codec"; -import { RawString } from "./base.js"; +import { ccc, mol } from "@ckb-ccc/core"; -export const MolSporeData = molecule.table( - { - contentType: RawString, - content: blockchain.Bytes, - clusterId: blockchain.BytesOpt, - }, - ["contentType", "content", "clusterId"], -); - -export interface SporeData { +export interface SporeDataView { contentType: string; content: ccc.BytesLike; clusterId?: ccc.HexLike; } -export function packRawSporeData(packable: SporeData): Uint8Array { - return MolSporeData.pack({ - contentType: packable.contentType, - content: packable.content, - clusterId: packable.clusterId, - }); +export const SporeData: mol.Codec = mol.table({ + contentType: mol.String, + content: mol.Bytes, + clusterId: mol.BytesOpt, +}); + +export function packRawSporeData(packable: SporeDataView): Uint8Array { + return ccc.bytesFrom( + SporeData.encode({ + contentType: packable.contentType, + content: packable.content, + clusterId: packable.clusterId, + }), + ); } -export function unpackToRawSporeData(unpackable: ccc.BytesLike): SporeData { - const unpacked = MolSporeData.unpack(unpackable); - return { - contentType: unpacked.contentType, - content: unpacked.content, - clusterId: ccc.apply(ccc.hexFrom, unpacked.clusterId), - }; +export function unpackToRawSporeData(unpackable: ccc.BytesLike): SporeDataView { + return SporeData.decode(unpackable); } diff --git a/packages/spore/src/spore/advanced.ts b/packages/spore/src/spore/advanced.ts index 217cda09..6f6a3ab2 100644 --- a/packages/spore/src/spore/advanced.ts +++ b/packages/spore/src/spore/advanced.ts @@ -1,17 +1,16 @@ -import { ccc } from "@ckb-ccc/core"; -import { UnpackResult } from "@ckb-lumos/codec"; +import { ccc, mol } from "@ckb-ccc/core"; import { assembleTransferClusterAction } from "../advanced.js"; import { assertCluster } from "../cluster/index.js"; -import { Action, SporeData } from "../codec/index.js"; +import { Action, SporeDataView } from "../codec/index.js"; import { searchOneCellBySigner } from "../helper/index.js"; export async function prepareCluster( signer: ccc.Signer, tx: ccc.Transaction, - data: SporeData, + data: SporeDataView, clusterMode?: "lockProxy" | "clusterCell" | "skip", scriptInfoHash?: ccc.HexLike, -): Promise | undefined> { +): Promise | undefined> { // skip if the spore is not belong to a cluster if (!data.clusterId || clusterMode === "skip") { return; diff --git a/packages/spore/src/spore/index.ts b/packages/spore/src/spore/index.ts index 62ec1346..80b3bfe5 100644 --- a/packages/spore/src/spore/index.ts +++ b/packages/spore/src/spore/index.ts @@ -5,7 +5,7 @@ import { assembleTransferSporeAction, prepareSporeTransaction, } from "../advanced.js"; -import { SporeData, packRawSporeData } from "../codec/index.js"; +import { SporeData, SporeDataView, packRawSporeData } from "../codec/index.js"; import { findSingletonCellByArgs, injectOneCapacityCell, @@ -73,7 +73,7 @@ export async function assertSpore( */ export async function createSpore(params: { signer: ccc.Signer; - data: SporeData; + data: SporeDataView; to?: ccc.ScriptLike; clusterMode?: "lockProxy" | "clusterCell" | "skip"; tx?: ccc.TransactionLike; @@ -234,3 +234,59 @@ export async function meltSpore(params: { tx: await prepareSporeTransaction(signer, tx, actions), }; } + +/** + * Search on-chain spores under the signer's control, if cluster provided, filter spores belonging to this cluster + * + * @param signer the owner of spores + * @param order the order in creation time of spores + * @param clusterId the cluster that spores belong to + * @param scriptInfos the deployed script infos of spores + * @returns speific spore cells + */ +export async function* findSporesBySigner(params: { + signer: ccc.Signer; + order?: "asc" | "desc"; + clusterId?: ccc.HexLike; + scriptInfos?: SporeScriptInfoLike[]; +}): AsyncGenerator<{ + spore: ccc.Cell; + sporeData: SporeDataView; +}> { + const { signer, clusterId, scriptInfos, order } = params; + for (const scriptInfo of scriptInfos ?? + Object.values(getSporeScriptInfos(signer.client))) { + if (!scriptInfo) { + continue; + } + for await (const spore of signer.findCells( + { + script: { + ...scriptInfo, + args: [], + }, + }, + true, + order, + 10, + )) { + try { + const sporeData = SporeData.decode(spore.outputData); + if (!clusterId) { + yield { + spore, + sporeData, + }; + } + if (sporeData.clusterId === clusterId) { + return { + spore, + sporeData, + }; + } + } catch (e: unknown) { + throw new Error(`Spore data decode failed: ${(e as Error).toString()}`); + } + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07016697..58c54904 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -781,12 +781,6 @@ importers: '@ckb-ccc/core': specifier: workspace:* version: link:../core - '@ckb-lumos/base': - specifier: ^0.24.0-next.1 - version: 0.24.0-next.2 - '@ckb-lumos/codec': - specifier: ^0.24.0-next.1 - version: 0.24.0-next.2 axios: specifier: ^1.7.7 version: 1.7.7 @@ -794,6 +788,9 @@ importers: '@eslint/js': specifier: ^9.1.1 version: 9.9.0 + '@types/node': + specifier: ^22.10.0 + version: 22.10.0 copyfiles: specifier: ^2.4.1 version: 2.4.1 @@ -1862,6 +1859,9 @@ packages: '@types/node@20.16.1': resolution: {integrity: sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==} + '@types/node@22.10.0': + resolution: {integrity: sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==} + '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -5296,6 +5296,9 @@ packages: undici-types@6.19.6: resolution: {integrity: sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -6892,6 +6895,10 @@ snapshots: dependencies: undici-types: 6.19.6 + '@types/node@22.10.0': + dependencies: + undici-types: 6.20.0 + '@types/prop-types@15.7.12': {} '@types/qs@6.9.15': {} @@ -8195,7 +8202,7 @@ snapshots: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) @@ -8222,12 +8229,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -8239,14 +8246,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -8260,7 +8267,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 @@ -11170,6 +11177,8 @@ snapshots: undici-types@6.19.6: {} + undici-types@6.20.0: {} + universalify@0.1.2: {} universalify@2.0.1: {}