Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"use client";

import { formatFileSize } from "@/src/app/utils/(tools)/FileUpload/page";
import { BigButton } from "@/src/components/BigButton";
import { Button } from "@/src/components/Button";
import { Message } from "@/src/components/Message";
import { formatString, useGetExplorerLink } from "@/src/utils";
import { ccc } from "@ckb-ccc/connector-react";
import { Loader2 } from "lucide-react";
import { typeIdArgsToFourLines } from "./helpers";

function formatCellCreationDate(timestampMs: number): string {
try {
return new Date(timestampMs).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
} catch {
return "";
}
}

export function TypeIdCellButton({
cell,
index,
onSelect,
isSelected,
creationTimestamp,
}: {
cell: ccc.Cell;
index: number;
onSelect: () => void;
isSelected: boolean;
creationTimestamp?: number;
}) {
const typeIdArgs = cell.cellOutput.type?.args || "";
const dataSize = cell.outputData ? ccc.bytesFrom(cell.outputData).length : 0;
const fourLines = typeIdArgsToFourLines(typeIdArgs.slice(2));

return (
<BigButton
key={ccc.hexFrom(cell.outPoint.toBytes())}
size="sm"
iconName="FileCode"
onClick={onSelect}
className={isSelected ? "border-2 border-purple-500 bg-purple-50" : ""}
>
<div className="text-md flex w-full min-w-0 flex-col">
<span className="shrink-0 text-xs font-medium text-gray-500">
#{index + 1}
</span>
{creationTimestamp != null && (
<span className="mt-0.5 shrink-0 text-[10px] font-normal text-gray-400">
{formatCellCreationDate(creationTimestamp)}
</span>
)}
<div className="mt-1 flex w-full min-w-0 flex-col font-mono text-[10px]">
{fourLines.map((line, i) => (
<span key={i} className="truncate">
{line}
</span>
))}
</div>
<span className="mt-2 shrink-0 truncate text-xs text-gray-500">
{formatFileSize(dataSize)}
</span>
</div>
</BigButton>
);
}

export function LoadingMessage({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<Message title={title} type="info">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-gray-600" />
<span>{children}</span>
</div>
</Message>
);
}

export function CellFoundSection({
foundCell,
foundCellAddress,
isAddressMatch,
userAddress,
}: {
foundCell: ccc.Cell;
foundCellAddress: string;
isAddressMatch: boolean | null;
userAddress: string;
}) {
const { explorerTransaction, explorerAddress } = useGetExplorerLink();

return (
<>
<Message title="Cell Found" type="success" expandable={false}>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Transaction:</span>{" "}
{explorerTransaction(foundCell.outPoint.txHash)}
</p>
<p>
<span className="font-medium">Index:</span>{" "}
{foundCell.outPoint.index}
</p>
<p>
<span className="font-medium">Capacity:</span>{" "}
{ccc.fixedPointToString(foundCell.cellOutput.capacity)} CKB
</p>
<p>
<span className="font-medium">Lock Address:</span>{" "}
{explorerAddress(
foundCellAddress,
formatString(foundCellAddress, 8, 6),
)}
</p>
{foundCell.outputData && (
<p>
<span className="font-medium">Data Size:</span>{" "}
{formatFileSize(ccc.bytesFrom(foundCell.outputData).length)}
</p>
)}
</div>
</Message>
{isAddressMatch === false && (
<Message
title="Address Mismatch Warning"
type="error"
expandable={false}
>
<div className="space-y-1 text-sm">
<p>
The cell&apos;s lock address does not match your wallet address.
You will not be able to unlock this cell to update it.
</p>
<p className="mt-2">
<span className="font-medium">Cell Lock:</span>{" "}
{explorerAddress(
foundCellAddress,
formatString(foundCellAddress, 8, 6),
)}
</p>
<p>
<span className="font-medium">Your Address:</span>{" "}
{userAddress
? explorerAddress(userAddress, formatString(userAddress, 8, 6))
: "Not connected"}
</p>
<p className="mt-2 font-semibold">
Deployment will fail because you cannot unlock this cell.
</p>
</div>
</Message>
)}
{isAddressMatch === true && (
<Message title="Address Match" type="success" expandable={false}>
<div className="text-sm">
The cell&apos;s lock address matches your wallet address. You can
update this cell.
</div>
</Message>
)}
</>
);
}

export function ClearSelectionButton({ onClick }: { onClick: () => void }) {
return (
<Button variant="info" className="mt-2" onClick={onClick}>
Clear Selection
</Button>
);
}

export function BurnButton({
onClick,
disabled,
}: {
onClick: () => void;
disabled?: boolean;
}) {
return (
<Button
variant="danger"
className="mt-2"
onClick={onClick}
disabled={disabled}
>
Burn
</Button>
);
}
100 changes: 100 additions & 0 deletions packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { readFileAsBytes } from "@/src/app/utils/(tools)/FileUpload/page";
import { ccc } from "@ckb-ccc/connector-react";
import { ReactNode } from "react";
import { normalizeTypeIdArgs } from "./helpers";

export type Logger = (...args: ReactNode[]) => void;

export async function runDeploy(
signer: ccc.Signer,
file: File,
typeIdArgs: string,
foundCell: ccc.Cell | null,
isAddressMatch: boolean | null,
log: Logger,
error: Logger,
): Promise<string | null> {
const fileBytes = (await readFileAsBytes(file)) as ccc.Bytes;
const { script } = await signer.getRecommendedAddressObj();

let tx: ccc.Transaction;
let typeIdArgsValue: string;

if (typeIdArgs.trim() !== "") {
if (!foundCell) {
error("Type ID cell not found. Please check the Type ID args.");
return null;
}
if (isAddressMatch === false) {
error(
"Cannot update cell: The cell's lock address does not match your wallet address. You cannot unlock this cell.",
);
return null;
}

const normalized = normalizeTypeIdArgs(typeIdArgs);
log("Updating existing Type ID cell...");

tx = ccc.Transaction.from({
inputs: [{ previousOutput: foundCell.outPoint }],
outputs: [
{
...foundCell.cellOutput,
capacity: ccc.Zero,
},
],
outputsData: [fileBytes],
});
typeIdArgsValue = normalized;
} else {
log("Building transaction...");
tx = ccc.Transaction.from({
outputs: [
{
lock: script,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.TypeId,
"00".repeat(32),
),
},
],
outputsData: [fileBytes],
});

await tx.completeInputsAddOne(signer);

if (!tx.outputs[0].type) {
throw new Error("Unexpected disappeared output");
}
tx.outputs[0].type.args = ccc.hashTypeId(tx.inputs[0], 0);
typeIdArgsValue = tx.outputs[0].type.args;
log("Type ID created:", typeIdArgsValue);
}

await tx.completeFeeBy(signer);
log("Sending transaction...");
const txHash = await signer.sendTransaction(tx);
log("Transaction sent:", txHash);
return txHash;
}

/** Burn the selected type_id cell: consume it and send capacity back to the lock (no type script). */
export async function runBurn(
signer: ccc.Signer,
foundCell: ccc.Cell,
log: Logger,
): Promise<string | null> {
const { lock } = foundCell.cellOutput;
const tx = ccc.Transaction.from({
inputs: [{ previousOutput: foundCell.outPoint }],
outputs: [{ lock, capacity: ccc.Zero }],
outputsData: ["0x"],
});
await tx.addCellDepsOfKnownScripts(signer.client, ccc.KnownScript.TypeId);
await tx.completeFeeChangeToOutput(signer, 0);
log("Sending burn transaction...");
const txHash = await signer.sendTransaction(tx);
log("Transaction sent:", txHash);
return txHash;
}
18 changes: 18 additions & 0 deletions packages/demo/src/app/connected/(tools)/DeployScript/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** Normalize Type ID args (strip 0x, trim). */
export function normalizeTypeIdArgs(args: string): string {
const s = (args || "").trim();
return s.startsWith("0x") ? s.slice(2) : s;
}

/** Split Type ID args into up to 4 display lines. */
export function typeIdArgsToFourLines(args: string): string[] {
const str = args || "";
if (!str.length) return [];
const chunkSize = Math.ceil(str.length / 4);
return [
str.slice(0, chunkSize),
str.slice(chunkSize, chunkSize * 2),
str.slice(chunkSize * 2, chunkSize * 3),
str.slice(chunkSize * 3),
].filter(Boolean);
}
Loading