Skip to content

feat: @ckb-ccc/molecule #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
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
133 changes: 133 additions & 0 deletions packages/demo/src/app/utils/(tools)/Molecule/DataInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { useEffect, useState } from "react";
import { ccc } from "@ckb-ccc/connector-react";
import JsonView from "@uiw/react-json-view";
import { useApp } from "@/src/context";
import { Button } from "@/src/components/Button";
import { Textarea } from "@/src/components/Textarea";
import { darkTheme } from "@uiw/react-json-view/dark";
import { Dropdown } from "@/src/components/Dropdown";
export type UnpackType =
| string
| number
| undefined
| { [property: string]: UnpackType }
| UnpackType[];

type Props = {
codec: ccc.mol.Codec<any, any> | undefined;
mode: "decode" | "encode";
};

const formatInput = (input: string): string => {
if (!input.startsWith("0x")) {
return `0x${input}`;
}
return input;
};

const isBlank = (data: UnpackType): boolean => {
if (!data) {
return true;
}
return false;
};

export const DataInput: React.FC<Props> = ({ codec, mode }) => {
const [inputData, setInputData] = useState<string>("");
const [decodeResult, setDecodeResult] = useState<UnpackType>(undefined);
const [encodeResult, setEncodeResult] = useState<ccc.Hex | undefined>(
undefined,
);
const { createSender } = useApp();
const { log, error } = createSender("Molecule");

const handleDecode = () => {
if (!codec) {
error("please select codec");
return;
}
try {
const result = codec.decode(formatInput(inputData));
log("Successfully decoded data");
setDecodeResult(result);
} catch (e: unknown) {
setDecodeResult(undefined);
error((e as Error).message);
}
};

const handleEncode = () => {
if (!codec) {
error("please select codec");
return;
}
try {
const inputObject = JSON.parse(inputData);
const result = codec.encode(inputObject);
log("Successfully encoded data");
setEncodeResult(ccc.hexFrom(result));
} catch (e: unknown) {
setEncodeResult(undefined);
error((e as Error).message);
}
};

// If mode changes, clear the input data
useEffect(() => {
setInputData("");
setDecodeResult(undefined);
setEncodeResult(undefined);
}, [mode]);

return (
<div>
<div style={{ marginBottom: 16 }}>
<label htmlFor="input-data">Input data</label>
<div>
<Textarea
id="input-data"
state={[inputData, setInputData]}
placeholder={
mode === "decode" ? "0x..." : "Please input data in JSON Object"
}
/>
</div>
</div>

{mode === "decode" && (
<div style={{ marginBottom: 16 }}>
<Button type="button" onClick={handleDecode}>
Decode
</Button>
</div>
)}

{mode === "encode" && (
<div style={{ marginBottom: 16 }}>
<Button type="button" onClick={handleEncode}>
Encode
</Button>
</div>
)}

{!isBlank(decodeResult) && (
<div>
<JsonView
value={
typeof decodeResult === "object"
? decodeResult
: { value: decodeResult }
}
style={darkTheme}
/>
</div>
)}

{encodeResult && (
<div>
<JsonView value={{ value: encodeResult }} style={darkTheme} />
</div>
)}
</div>
);
};
73 changes: 73 additions & 0 deletions packages/demo/src/app/utils/(tools)/Molecule/Molecule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useCallback, useState } from "react";
import {
blockchainSchema,
builtinCodecs,
mergeBuiltinCodecs,
} from "./constants";
import { ccc } from "@ckb-ccc/connector-react";
import { useApp } from "@/src/context";
import { Textarea } from "@/src/components/Textarea";
import { Button } from "@/src/components/Button";

type Props = {
updateCodecRecord: (codecRecord: ccc.molecule.CodecRecord) => void;
};

export const MoleculeParser: React.FC<Props> = ({ updateCodecRecord }) => {
const [inputMol, setInputMol] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("cachedMol") || "";
}
return "";
});
const [parseSuccess, setParseSuccess] = useState(false);
const { createSender } = useApp();
const { log, error } = createSender("Molecule");

const handleConfirm = useCallback(() => {
try {
// get user input schema, and append with primitive schema
const userCodecRecord = ccc.molecule.parseMoleculeSchema(
inputMol + blockchainSchema,
{
extraReferences: builtinCodecs,
},
);
const codecRecord = mergeBuiltinCodecs(userCodecRecord);
setParseSuccess(true);
updateCodecRecord(codecRecord);
log("Successfully parsed schema");
if (typeof window !== "undefined") {
localStorage.setItem("cachedMol", inputMol);
}
} catch (e: any) {
setParseSuccess(false);
updateCodecRecord({});
error(e.message);
}
}, [error, inputMol, log, updateCodecRecord]);

return (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 16 }}>
<label htmlFor="input-schema">
Input schema(mol)
<label title="Uint8/16/.../512, Byte32, BytesVec, Bytes, BytesVec, BytesOpt are used as primitive schemas, please do not override." />
</label>
<div>
<Textarea
id="input-schema"
state={[inputMol, setInputMol]}
placeholder="e.g. vector OutPointVec <OutPoint>;"
/>
</div>
</div>

<div>
<Button type="button" onClick={handleConfirm}>
Parse
</Button>
</div>
</div>
);
};
66 changes: 66 additions & 0 deletions packages/demo/src/app/utils/(tools)/Molecule/SchemaSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import { ccc } from "@ckb-ccc/connector-react";
import { Dropdown } from "@/src/components/Dropdown";

type Props = {
codecRecord: ccc.molecule.CodecRecord;
selectedCodecName: string;
onSelectCodec: (name: string) => void;
mode: "decode" | "encode";
onSelectMode: (mode: "decode" | "encode") => void;
};

const createCodecOptionsFromMap = (
codecRecord: ccc.molecule.CodecRecord,
): string[] => {
return Object.keys(codecRecord);
};

export const SchemaSelect: React.FC<Props> = ({
onSelectCodec,
selectedCodecName,
codecRecord,
mode,
onSelectMode,
}) => {
const handleChange = (newValue: string | null) => {
onSelectCodec(newValue as string);
};
const schemaOptions = createCodecOptionsFromMap(codecRecord).map(
(schema) => ({
name: schema,
displayName: schema,
iconName: "Hash" as const,
}),
);

return (
<div className="flex flex-row items-center gap-4">
<label className="min-w-32 shrink-0">Select schema(mol)</label>
<Dropdown
options={schemaOptions}
selected={selectedCodecName}
onSelect={(value: string | null) => handleChange(value)}
className="flex-1"
/>
<label htmlFor="mode">Mode</label>
<Dropdown
options={[
{
name: "decode",
displayName: "Decode",
iconName: "ArrowRight",
},
{
name: "encode",
displayName: "Encode",
iconName: "ArrowLeft",
},
]}
selected={mode}
onSelect={(value) => onSelectMode(value as "decode" | "encode")}
className="flex-2"
/>
</div>
);
};
Loading