Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ packages/*/dist*
.env*

*.DS_Store

.pnpm-store/
.tmp-fiber-js/
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
Loading