Skip to content

Commit 4932220

Browse files
committed
feat(demo): dep group management
1 parent 4516095 commit 4932220

File tree

4 files changed

+287
-2
lines changed

4 files changed

+287
-2
lines changed

packages/demo/src/app/connected/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const TABS: [ReactNode, string, keyof typeof icons, string][] = [
4747
"text-cyan-600",
4848
],
4949
["Nervos DAO", "/connected/NervosDao", "Vault", "text-pink-500"],
50+
["Dep Group", "/utils/DepGroup", "Boxes", "text-amber-500"],
5051
["SSRI", "/connected/SSRI", "Pill", "text-blue-500"],
5152
["Hash", "/utils/Hash", "Barcode", "text-violet-500"],
5253
["Mnemonic", "/utils/Mnemonic", "SquareAsterisk", "text-fuchsia-500"],
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
"use client";
2+
3+
import { Button } from "@/src/components/Button";
4+
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
5+
import { TextInput } from "@/src/components/Input";
6+
import { Message } from "@/src/components/Message";
7+
import { Textarea } from "@/src/components/Textarea";
8+
import { useApp } from "@/src/context";
9+
import { useGetExplorerLink } from "@/src/utils";
10+
import { ccc } from "@ckb-ccc/connector-react";
11+
import { FileSearch, Plus, Search, X } from "lucide-react";
12+
import Link from "next/link";
13+
import { useCallback, useEffect, useState } from "react";
14+
15+
const OutPointVec = ccc.mol.vector(ccc.OutPoint);
16+
17+
export default function DepGroup() {
18+
const { signer, createSender } = useApp();
19+
const { client, open } = ccc.useCcc();
20+
21+
const { log, error } = createSender("Manage dep group");
22+
23+
const { explorerTransaction, explorerAddress } = useGetExplorerLink();
24+
25+
const [address, setAddress] = useState<string | undefined>();
26+
27+
const [typeId, setTypeId] = useState<string>("");
28+
const [isValid, setIsValid] = useState<boolean>(false);
29+
const [rawData, setRawData] = useState<string>("");
30+
const [outPoint, setOutPoint] = useState<string>("");
31+
const [outPoints, setOutPoints] = useState<ccc.OutPoint[]>([]);
32+
33+
const [advancedEnabled, setAdvancedEnabled] = useState<boolean>(false);
34+
35+
const validate = useCallback(async () => {
36+
if (ccc.bytesFrom(typeId).length !== 32) {
37+
error(`Type ID length should be 32`);
38+
return {};
39+
}
40+
41+
const cell = await client.findSingletonCellByType(
42+
{
43+
...(await ccc.Script.fromKnownScript(
44+
client,
45+
ccc.KnownScript.TypeId,
46+
typeId,
47+
)),
48+
},
49+
true,
50+
);
51+
52+
if (!cell) {
53+
error(`Cell with Type Id ${typeId} not found`);
54+
return {};
55+
}
56+
57+
const outPoints = OutPointVec.decode(cell.outputData);
58+
setIsValid(true);
59+
60+
return { cell, outPoints };
61+
}, [typeId, client, error]);
62+
63+
const search = useCallback(async () => {
64+
const { cell, outPoints } = await validate();
65+
if (cell && outPoints) {
66+
setOutPoints(outPoints);
67+
log(`Found cell`, explorerTransaction(cell.outPoint.txHash));
68+
}
69+
}, [validate, explorerTransaction]);
70+
71+
useEffect(() => {
72+
(async () => {
73+
setAddress(await signer?.getRecommendedAddress());
74+
})();
75+
}, [signer]);
76+
77+
return (
78+
<>
79+
<div className="flex w-full flex-col items-stretch">
80+
<Message title="Hint" type="info">
81+
According to{" "}
82+
<Link
83+
className="underline"
84+
href="https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md#dep-group"
85+
target="_blank"
86+
>
87+
RFC 22: CKB Transaction Structure
88+
</Link>
89+
: Dep Group is a cell which bundles several cells as its members.
90+
{address ? (
91+
<>
92+
<br />
93+
Find existing dep groups by explorer: {explorerAddress(address)}
94+
</>
95+
) : undefined}
96+
</Message>
97+
<div className="flex items-center">
98+
<TextInput
99+
className="flex-1"
100+
label="Type ID (Empty to create a new one)"
101+
placeholder="0x0000000000000000000000000000000000000000000000000000000000000000"
102+
state={[
103+
typeId,
104+
(val) => {
105+
setTypeId(val);
106+
setIsValid(false);
107+
},
108+
]}
109+
/>
110+
<div
111+
className="flex cursor-pointer items-center self-stretch bg-white/75"
112+
onClick={search}
113+
>
114+
<Search
115+
className="mx-4 rounded-full bg-sky-200 p-1 text-sky-500"
116+
width="36"
117+
height="36"
118+
/>
119+
</div>
120+
</div>
121+
<div className="bg-white/75 px-3 py-2">
122+
Outpoints
123+
<span
124+
className="ml-2 cursor-pointer text-xs underline"
125+
onClick={() => setAdvancedEnabled(!advancedEnabled)}
126+
>
127+
Parse raw data (advanced)
128+
</span>
129+
</div>
130+
{advancedEnabled ? (
131+
<div className="flex items-center">
132+
<Textarea
133+
className="flex-1"
134+
label="Outpoints raw data"
135+
placeholder="0x00..."
136+
state={[rawData, setRawData]}
137+
/>
138+
<div
139+
className="flex cursor-pointer items-center self-stretch bg-white/75"
140+
onClick={async () => setOutPoints(OutPointVec.decode(rawData))}
141+
>
142+
<FileSearch
143+
className="mx-4 rounded-full bg-neutral-200 p-1 text-neutral-500"
144+
width="36"
145+
height="36"
146+
/>
147+
</div>
148+
</div>
149+
) : undefined}
150+
<div className="flex items-center">
151+
<TextInput
152+
className="flex-1"
153+
label="Outpoint to be added to the list"
154+
placeholder="0x0000000000000000000000000000000000000000000000000000000000000000:0"
155+
state={[outPoint, setOutPoint]}
156+
/>
157+
<div
158+
className="flex cursor-pointer items-center self-stretch bg-white/75"
159+
onClick={async () => {
160+
const [txHash, index] = outPoint.split(":");
161+
const newOutPoint = ccc.OutPoint.from({
162+
txHash: ccc.hexFrom(txHash),
163+
index: ccc.numFrom(index),
164+
});
165+
166+
setOutPoints([...outPoints, newOutPoint]);
167+
}}
168+
>
169+
<Plus
170+
className="mx-4 rounded-full bg-green-200 p-1 text-green-500"
171+
width="36"
172+
height="36"
173+
/>
174+
</div>
175+
</div>
176+
{outPoints.length === 0 ? (
177+
<div className="flex justify-center bg-white/75 py-4 text-neutral-400">
178+
Empty outpoints list
179+
</div>
180+
) : undefined}
181+
{outPoints.map((outPoint, i) => {
182+
return (
183+
<div className="bg-white/75 py-1" key={i}>
184+
<div
185+
className="w-100 flex cursor-pointer flex-col items-center justify-center break-all rounded px-16 py-2 hover:bg-red-200 xl:flex-row xl:justify-between"
186+
onClick={() =>
187+
setOutPoints(outPoints.filter((_, j) => j !== i))
188+
}
189+
>
190+
{i + 1}. {outPoint.txHash}:{outPoint.index}
191+
<X className="my-2 rounded-full bg-red-200 p-1 text-red-500 xl:my-0" />
192+
</div>
193+
</div>
194+
);
195+
})}
196+
<ButtonsPanel>
197+
<Button
198+
variant="success"
199+
onClick={async () => {
200+
if (typeId !== "" && !isValid) {
201+
return search();
202+
}
203+
204+
if (!signer) {
205+
return open();
206+
}
207+
208+
if (typeId === "") {
209+
const { script: lock } =
210+
await signer.getRecommendedAddressObj();
211+
const tx = ccc.Transaction.from({
212+
outputs: [
213+
{
214+
lock,
215+
type: await ccc.Script.fromKnownScript(
216+
client,
217+
ccc.KnownScript.TypeId,
218+
"00".repeat(32),
219+
),
220+
},
221+
],
222+
outputsData: [OutPointVec.encode(outPoints)],
223+
});
224+
await tx.completeInputsAtLeastOne(signer);
225+
const typeId = ccc.hashTypeId(tx.inputs[0], 0);
226+
227+
if (!tx.outputs[0].type) {
228+
error("Unexpected disappeared output");
229+
return;
230+
}
231+
tx.outputs[0].type!.args = typeId;
232+
233+
await tx.completeFeeBy(signer);
234+
235+
const txHash = await signer.sendTransaction(tx);
236+
log(
237+
"Type ID created: ",
238+
typeId,
239+
"tx hash: ",
240+
explorerTransaction(txHash),
241+
);
242+
setTypeId(typeId);
243+
setIsValid(true);
244+
await signer.client.waitTransaction(txHash);
245+
log("Transaction committed:", explorerTransaction(txHash));
246+
} else {
247+
const { cell } = await validate();
248+
if (!cell) {
249+
return;
250+
}
251+
252+
const tx = ccc.Transaction.from({
253+
inputs: [cell],
254+
outputs: [{ ...cell.cellOutput, capacity: ccc.Zero }],
255+
outputsData: [OutPointVec.encode(outPoints)],
256+
});
257+
258+
await tx.completeFeeBy(signer);
259+
const txHash = await signer.sendTransaction(tx);
260+
log(
261+
"Type ID updated: ",
262+
typeId,
263+
"tx hash: ",
264+
explorerTransaction(txHash),
265+
);
266+
await signer.client.waitTransaction(txHash);
267+
log("Transaction committed:", explorerTransaction(txHash));
268+
}
269+
}}
270+
>
271+
{!signer
272+
? "Connect"
273+
: typeId !== ""
274+
? isValid
275+
? "Update"
276+
: "Search"
277+
: "Create"}
278+
</Button>
279+
</ButtonsPanel>
280+
</div>
281+
</>
282+
);
283+
}

packages/demo/src/app/utils/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const TABS: [string, string, keyof typeof icons, string][] = [
1010
["Hash", "/utils/Hash", "Barcode", "text-violet-500"],
1111
["Mnemonic", "/utils/Mnemonic", "SquareAsterisk", "text-fuchsia-500"],
1212
["Keystore", "/utils/Keystore", "Notebook", "text-rose-500"],
13+
["Dep Group", "/utils/DepGroup", "Boxes", "text-amber-500"],
1314
];
1415

1516
export default function Page() {

packages/demo/src/components/Message.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export function Message({
4343

4444
return (
4545
<div
46-
className={`my-2 flex flex-col items-start rounded-md p-4 ${bgColorClass} ${className}`}
46+
onClick={() => setIsExpanded(!isExpanded)}
47+
className={`my-2 flex cursor-pointer flex-col items-start rounded-md p-4 ${bgColorClass} ${className}`}
4748
>
4849
{title ? (
4950
<div className="flex w-full items-center">
@@ -56,7 +57,6 @@ export function Message({
5657
</div>
5758
) : undefined}
5859
<div
59-
onClick={() => setIsExpanded(!isExpanded)}
6060
className={`relative mt-2 ${isExpanded ? "" : "line-clamp-1"}`}
6161
style={
6262
isExpanded

0 commit comments

Comments
 (0)