@ickb/utils
Version:
General utilities built on top of CCC
149 lines (140 loc) • 5.1 kB
text/typescript
import { mol, ccc } from "@ckb-ccc/core";
/**
* Boundary-checked codec for little-endian 32-bit signed integers.
*/
export const CheckedInt32LE = mol.Codec.from<ccc.NumLike, number>({
byteLength: 4,
encode: (numLike) => {
const num = Number(numLike);
if (num < -2147483648 || num > 2147483647) {
throw Error("NumLike out of int32 bounds");
}
const encoded = new Uint8Array(4);
new DataView(encoded.buffer).setInt32(0, num, true);
return encoded;
},
decode: (bytesLike) => {
const bytes = ccc.bytesFrom(bytesLike);
return new DataView(bytes.buffer).getInt32(0, true);
},
});
/**
* General union codec, if all items are of the same fixed size, it will create a fixed-size union codec, otherwise an usual dynamic-size union codec will be created.
* 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 })
*
* @credits Hanssen from CKB DevRel:
* https://github.com/ckb-devrel/ccc/blob/master/packages/core/src/molecule/codec.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function union<T extends Record<string, mol.CodecLike<any, any>>>(
codecLayout: T,
fields?: Record<keyof T, number | undefined | null>,
): mol.Codec<UnionEncodable<T>, UnionDecoded<T>> {
const keys = Object.keys(codecLayout);
const values = Object.values(codecLayout);
let byteLength = values[0]?.byteLength;
for (const { byteLength: l } of values.slice(1)) {
if (l === undefined || l !== byteLength) {
// byteLength is undefined if any of the codecs byteLength is undefined or different
byteLength = undefined;
break;
}
}
if (byteLength !== undefined) {
// Account for header size
byteLength += 4;
}
return mol.Codec.from({
byteLength,
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) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`union: invalid field id ${fieldId} of ${typeStr}`);
}
const header = uint32To(fieldId);
try {
const body = codec.encode(value);
return ccc.bytesConcat(header, body);
} catch (e: unknown) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`union.(${typeStr})(${e?.toString()})`);
}
},
decode(buffer) {
const value = ccc.bytesFrom(buffer);
const fieldIndex = uint32From(value.slice(0, 4));
const keys = Object.keys(codecLayout);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
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(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`union: unknown union field index ${fieldIndex}, only ${keys.toString()} are allowed`,
);
}
const fieldKeys = Object.keys(fields);
throw new Error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`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, @typescript-eslint/no-non-null-assertion
value: codecLayout[field]!.decode(value.slice(4)),
} as UnionDecoded<T>;
},
});
}
type UnionEncodable<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Record<string, mol.CodecLike<any, any>>,
K extends keyof T = keyof T,
> = K extends unknown
? {
type: K;
value: mol.EncodableType<T[K]>;
}
: never;
type UnionDecoded<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Record<string, mol.CodecLike<any, any>>,
K extends keyof T = keyof T,
> = K extends unknown
? {
type: K;
value: mol.DecodedType<T[K]>;
}
: never;
function uint32To(numLike: ccc.NumLike): ccc.Bytes {
return ccc.numToBytes(numLike, 4);
}
function uint32From(bytesLike: ccc.BytesLike): number {
return Number(ccc.numFromBytes(bytesLike));
}