Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-dryers-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-ccc/core": minor
---

feat: compatible mode for molecule decode
80 changes: 56 additions & 24 deletions packages/core/src/molecule/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ import {

export type CodecLike<Encodable, Decoded = Encodable> = {
readonly encode: (encodable: Encodable) => Bytes;
readonly decode: (decodable: BytesLike) => Decoded;
readonly decode: (
decodable: BytesLike,
config?: { isExtraFieldIgnored?: boolean },
) => Decoded;
readonly byteLength?: number;
};
export class Codec<Encodable, Decoded = Encodable> {
constructor(
public readonly encode: (encodable: Encodable) => Bytes,
public readonly decode: (decodable: BytesLike) => Decoded,
public readonly decode: (
decodable: BytesLike,
config?: { isExtraFieldIgnored?: boolean }, // This is equivalent to "compatible" in the Rust implementation of Molecule.
) => Decoded,
public readonly byteLength?: number, // if provided, treat codec as fixed length
) {}

Expand All @@ -43,7 +49,7 @@ export class Codec<Encodable, Decoded = Encodable> {
}
return encoded;
},
(decodable) => {
(decodable, config) => {
const decodableBytes = bytesFrom(decodable);
if (
byteLength !== undefined &&
Expand All @@ -53,7 +59,7 @@ export class Codec<Encodable, Decoded = Encodable> {
`Codec.decode: expected byte length ${byteLength}, got ${decodableBytes.byteLength}`,
);
}
return decode(decodable);
return decode(decodable, config);
},
byteLength,
);
Expand All @@ -69,10 +75,10 @@ export class Codec<Encodable, Decoded = Encodable> {
return new Codec(
(encodable) =>
this.encode((inMap ? inMap(encodable) : encodable) as Encodable),
(buffer) =>
(buffer, config) =>
(outMap
? outMap(this.decode(buffer))
: this.decode(buffer)) as NewDecoded,
? outMap(this.decode(buffer, config))
: this.decode(buffer, config)) as NewDecoded,
this.byteLength,
);
}
Expand Down Expand Up @@ -128,7 +134,7 @@ export function fixedItemVec<Encodable, Decoded>(
throw new Error(`fixedItemVec(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
Expand All @@ -147,7 +153,10 @@ export function fixedItemVec<Encodable, Decoded>(
const decodedArray: Array<Decoded> = [];
for (let offset = 4; offset < byteLength; offset += itemByteLength) {
decodedArray.push(
itemCodec.decode(value.slice(offset, offset + itemByteLength)),
itemCodec.decode(
value.slice(offset, offset + itemByteLength),
config,
),
);
}
return decodedArray;
Expand Down Expand Up @@ -185,7 +194,7 @@ export function dynItemVec<Encodable, Decoded>(
throw new Error(`dynItemVec(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
Expand Down Expand Up @@ -215,7 +224,7 @@ export function dynItemVec<Encodable, Decoded>(
const start = offsets[index];
const end = offsets[index + 1];
const itemBuffer = value.slice(start, end);
decodedArray.push(itemCodec.decode(itemBuffer));
decodedArray.push(itemCodec.decode(itemBuffer, config));
}
return decodedArray;
} catch (e) {
Expand Down Expand Up @@ -259,13 +268,13 @@ export function option<Encodable, Decoded>(
throw new Error(`option(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength === 0) {
return undefined;
}
try {
return innerCodec.decode(buffer);
return innerCodec.decode(buffer, config);
} catch (e) {
throw new Error(`option(${e?.toString()})`);
}
Expand All @@ -290,7 +299,7 @@ export function byteVec<Encodable, Decoded>(
throw new Error(`byteVec(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
Expand All @@ -304,7 +313,7 @@ export function byteVec<Encodable, Decoded>(
);
}
try {
return codec.decode(value.slice(4));
return codec.decode(value.slice(4), config);
} catch (e: unknown) {
throw new Error(`byteVec(${e?.toString()})`);
}
Expand Down Expand Up @@ -371,23 +380,46 @@ export function table<
const packedTotalSize = uint32To(header.length + body.length + 4);
return bytesConcat(packedTotalSize, header, body);
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
`table: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`,
);
}
const byteLength = uint32From(value.slice(0, 4));
const headerLength = uint32From(value.slice(4, 8));
const actualFieldCount = (headerLength - 4) / 4;

if (byteLength !== value.byteLength) {
throw new Error(
`table: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`,
);
}

if (actualFieldCount < keys.length) {
throw new Error(
`table: invalid field count, expected ${keys.length}, but got ${actualFieldCount}`,
);
}

if (actualFieldCount > keys.length && !config?.isExtraFieldIgnored) {
throw new Error(
`table: invalid field count, expected ${keys.length}, but got ${actualFieldCount}, and extra fields are not allowed in the current configuration. If you want to ignore extra fields, set isExtraFieldIgnored to true.`,
);
}
const offsets = keys.map((_, index) =>
uint32From(value.slice(4 + index * 4, 8 + index * 4)),
);
offsets.push(byteLength);
// If there are extra fields, add the last offset to the offsets array
if (actualFieldCount > keys.length) {
offsets.push(
uint32From(value.slice(4 + keys.length * 4, 8 + keys.length * 4)),
);
} else {
// If there are no extra fields, add the byte length to the offsets array
offsets.push(byteLength);
}
const object = {};
for (let i = 0; i < offsets.length - 1; i++) {
const start = offsets[i];
Expand All @@ -397,7 +429,7 @@ export function table<
const payload = value.slice(start, end);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Object.assign(object, { [field]: codec.decode(payload) });
Object.assign(object, { [field]: codec.decode(payload, config) });
} catch (e: unknown) {
throw new Error(`table.${field}(${e?.toString()})`);
}
Expand Down Expand Up @@ -466,7 +498,7 @@ export function union<T extends Record<string, CodecLike<any, any>>>(
throw new Error(`union.(${typeStr})(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
const fieldIndex = uint32From(value.slice(0, 4));
const keys = Object.keys(codecLayout);
Expand Down Expand Up @@ -496,7 +528,7 @@ export function union<T extends Record<string, CodecLike<any, any>>>(
return {
type: field,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
value: codecLayout[field].decode(value.slice(4)),
value: codecLayout[field].decode(value.slice(4), config),
} as UnionDecoded<T>;
},
});
Expand Down Expand Up @@ -535,15 +567,15 @@ export function struct<

return bytesFrom(bytes);
},
decode(buffer) {
decode(buffer, config) {
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) });
Object.assign(object, { [key]: codec.decode(payload, config) });
} catch (e: unknown) {
throw new Error(`struct.${key}(${(e as Error).toString()})`);
}
Expand Down Expand Up @@ -583,7 +615,7 @@ export function array<Encodable, Decoded>(
throw new Error(`array(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength != byteLength) {
throw new Error(
Expand All @@ -594,7 +626,7 @@ export function array<Encodable, Decoded>(
const result: Array<Decoded> = [];
for (let i = 0; i < value.byteLength; i += itemCodec.byteLength!) {
result.push(
itemCodec.decode(value.slice(i, i + itemCodec.byteLength!)),
itemCodec.decode(value.slice(i, i + itemCodec.byteLength!), config),
);
}
return result;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/molecule/predefined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export const Bool: Codec<boolean> = Codec.from({
export const BoolOpt = option(Bool);
export const BoolVec = vector(Bool);

export const Byte: Codec<HexLike, Hex> = Codec.from({
byteLength: 1,
encode: (value) => bytesFrom(value),
decode: (buffer) => hexFrom(buffer),
});
export const ByteOpt = option(Byte);

export const Byte4: Codec<HexLike, Hex> = Codec.from({
byteLength: 4,
encode: (value) => bytesFrom(value),
Expand Down
1 change: 1 addition & 0 deletions packages/demo/src/app/connected/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const TABS: [ReactNode, string, keyof typeof icons, string][] = [
["Hash", "/utils/Hash", "Barcode", "text-violet-500"],
["Mnemonic", "/utils/Mnemonic", "SquareAsterisk", "text-fuchsia-500"],
["Keystore", "/utils/Keystore", "Notebook", "text-rose-500"],
["Molecule", "/utils/Molecule", "Hash", "text-emerald-500"],
];
/* eslint-enable react/jsx-key */

Expand Down
Loading