From 0cc5099e659c965a69b130b3ea76cf85f7ba09b6 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Tue, 27 Jan 2026 10:18:49 +0800 Subject: [PATCH 01/11] chore: demo features script deloyer --- .../connected/(tools)/DeployScript/page.tsx | 585 ++++++++++++++++++ packages/demo/src/app/connected/page.tsx | 1 + packages/demo/src/components/Message.tsx | 4 +- 3 files changed, 588 insertions(+), 2 deletions(-) create mode 100644 packages/demo/src/app/connected/(tools)/DeployScript/page.tsx diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx new file mode 100644 index 000000000..08d93cd6e --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -0,0 +1,585 @@ +"use client"; + +import { Button } from "@/src/components/Button"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { TextInput } from "@/src/components/Input"; +import { Message } from "@/src/components/Message"; +import { useApp } from "@/src/context"; +import { formatString, useGetExplorerLink } from "@/src/utils"; +import { ccc } from "@ckb-ccc/connector-react"; +import { Loader2, Upload, X } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +} + +function formatDate(date: Date): string { + return date.toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +async function readFileAsBytes(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target?.result instanceof ArrayBuffer) { + resolve(new Uint8Array(e.target.result)); + } else { + reject(new Error("Failed to read file")); + } + }; + reader.onerror = () => reject(new Error("Failed to read file")); + reader.readAsArrayBuffer(file); + }); +} + +function ConfirmationModal({ + isOpen, + message, + txHash, +}: { + isOpen: boolean; + message: string; + txHash?: string; +}) { + if (!isOpen) return null; + + return ( +
+
+
+ +
+

{message}

+ {txHash && ( +

{txHash}

+ )} +

+ Please wait for transaction confirmation... +

+
+
+
+
+ ); +} + +export default function DeployScript() { + const { signer, createSender } = useApp(); + const { log, error } = createSender("Deploy Script"); + + const { explorerTransaction, explorerAddress } = useGetExplorerLink(); + + const [file, setFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [isDeploying, setIsDeploying] = useState(false); + const [typeIdArgs, setTypeIdArgs] = useState(""); + const [isWaitingConfirmation, setIsWaitingConfirmation] = useState(false); + const [confirmationMessage, setConfirmationMessage] = useState(""); + const [confirmationTxHash, setConfirmationTxHash] = useState(""); + const [foundCell, setFoundCell] = useState(null); + const [foundCellAddress, setFoundCellAddress] = useState(""); + const [userAddress, setUserAddress] = useState(""); + const [isAddressMatch, setIsAddressMatch] = useState(null); + const [isCheckingCell, setIsCheckingCell] = useState(false); + const [cellCheckError, setCellCheckError] = useState(""); + const fileInputRef = useRef(null); + const lastCheckedTypeIdRef = useRef(""); + const isCheckingRef = useRef(false); + const { client } = ccc.useCcc(); + + // Get user's wallet address + useEffect(() => { + if (!signer) { + setUserAddress(""); + setIsAddressMatch(null); + return; + } + + signer.getRecommendedAddress().then((addr) => { + setUserAddress(addr); + }); + }, [signer]); + + // Compare addresses when both are available + useEffect(() => { + if (userAddress && foundCellAddress) { + setIsAddressMatch(userAddress === foundCellAddress); + } else { + setIsAddressMatch(null); + } + }, [userAddress, foundCellAddress]); + + const handleFileSelect = (selectedFile: File) => { + setFile(selectedFile); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const droppedFile = e.dataTransfer.files[0]; + if (droppedFile) { + handleFileSelect(droppedFile); + } + }; + + const handleFileInputChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + handleFileSelect(selectedFile); + } + }; + + const handleClearFile = () => { + setFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + // Automatically check Type ID cell when typeIdArgs changes + useEffect(() => { + // Normalize Type ID args for comparison + const normalizedTypeIdArgs = typeIdArgs.trim().startsWith("0x") + ? typeIdArgs.trim().slice(2) + : typeIdArgs.trim(); + + // Skip if already checked this value or currently checking + if ( + lastCheckedTypeIdRef.current === normalizedTypeIdArgs || + isCheckingRef.current + ) { + return; + } + + // If empty, just clear state + if (!typeIdArgs.trim()) { + lastCheckedTypeIdRef.current = ""; + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + setCellCheckError(""); + return; + } + + const checkTypeIdCell = async () => { + // Mark as checking to prevent concurrent checks + if (isCheckingRef.current) { + return; + } + isCheckingRef.current = true; + lastCheckedTypeIdRef.current = normalizedTypeIdArgs; + + // Validate length + try { + const typeIdBytes = ccc.bytesFrom(normalizedTypeIdArgs); + if (typeIdBytes.length !== 32) { + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + setCellCheckError( + "Type ID args must be 32 bytes (64 hex characters)", + ); + isCheckingRef.current = false; + return; + } + } catch { + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + setCellCheckError("Invalid Type ID args format"); + isCheckingRef.current = false; + return; + } + + setIsCheckingCell(true); + setCellCheckError(""); + + try { + const typeIdScript = await ccc.Script.fromKnownScript( + client, + ccc.KnownScript.TypeId, + normalizedTypeIdArgs, + ); + + const cell = await client.findSingletonCellByType(typeIdScript, true); + + if (cell) { + setFoundCell(cell); + const address = ccc.Address.fromScript( + cell.cellOutput.lock, + client, + ).toString(); + setFoundCellAddress(address); + setCellCheckError(""); + // Address comparison will be handled by useEffect + } else { + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + setCellCheckError("Type ID cell not found on-chain"); + } + } catch (err) { + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + const errorMessage = err instanceof Error ? err.message : String(err); + setCellCheckError(`Error checking Type ID: ${errorMessage}`); + } finally { + setIsCheckingCell(false); + isCheckingRef.current = false; + } + }; + + // Debounce the check + const timeoutId = setTimeout(checkTypeIdCell, 500); + return () => { + clearTimeout(timeoutId); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [typeIdArgs, client]); + + const handleDeploy = async () => { + if (!signer) { + error("Please connect a wallet first"); + return; + } + + if (!file) { + error("Please select a file to deploy"); + return; + } + + setIsDeploying(true); + try { + log("Reading file..."); + const fileBytes = await readFileAsBytes(file); + + log("Building transaction..."); + const { script } = await signer.getRecommendedAddressObj(); + + let tx: ccc.Transaction; + let typeIdArgsValue: string; + + if (typeIdArgs.trim() !== "") { + // Update existing Type ID cell + if (!foundCell) { + error("Type ID cell not found. Please check the Type ID args."); + return; + } + + // Check if addresses match + if (isAddressMatch === false) { + error( + "Cannot update cell: The cell's lock address does not match your wallet address. You cannot unlock this cell.", + ); + return; + } + + // Normalize Type ID args - remove 0x prefix if present + const normalizedTypeIdArgs = typeIdArgs.trim().startsWith("0x") + ? typeIdArgs.trim().slice(2) + : typeIdArgs.trim(); + + log("Updating existing Type ID cell..."); + + // Create transaction to update the cell + tx = ccc.Transaction.from({ + inputs: [ + { + previousOutput: foundCell.outPoint, + }, + ], + outputs: [ + { + ...foundCell.cellOutput, + capacity: ccc.Zero, // Zero capacity means the cell will be replaced with a new one + }, + ], + outputsData: [fileBytes], + }); + + typeIdArgsValue = normalizedTypeIdArgs; + } else { + // Create new Type ID cell + tx = ccc.Transaction.from({ + outputs: [ + { + lock: script, + type: await ccc.Script.fromKnownScript( + signer.client, + ccc.KnownScript.TypeId, + "00".repeat(32), + ), + }, + ], + outputsData: [fileBytes], + }); + + // Complete inputs for capacity + await tx.completeInputsAddOne(signer); + + // Generate type_id from first input and output index + 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); + } + + // Complete fees + await tx.completeFeeBy(signer); + + // Sign and send the transaction + log("Sending transaction..."); + const txHash = await signer.sendTransaction(tx); + log("Transaction sent:", explorerTransaction(txHash)); + + // Show blocking confirmation modal + setIsWaitingConfirmation(true); + setConfirmationMessage("Waiting for transaction confirmation..."); + setConfirmationTxHash(txHash); + + await signer.client.waitTransaction(txHash); + log("Transaction committed:", explorerTransaction(txHash)); + + // Close modal after confirmation + setIsWaitingConfirmation(false); + setConfirmationMessage(""); + setConfirmationTxHash(""); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + error("Deployment failed:", errorMessage); + setIsWaitingConfirmation(false); + setConfirmationMessage(""); + setConfirmationTxHash(""); + } finally { + setIsDeploying(false); + } + }; + + return ( + <> + +
+ + Upload a file to deploy it as a CKB cell with Type ID trait. The file + will be stored on-chain and can be referenced by its Type ID. Leave + Type ID args empty to create a new cell, or provide existing Type ID + args to update an existing cell. + + + + + {isCheckingCell && ( + +
+ + Searching for Type ID cell on-chain... +
+
+ )} + + {foundCell && !isCheckingCell && ( + <> + +
+

+ Transaction:{" "} + {explorerTransaction(foundCell.outPoint.txHash)} +

+

+ Index:{" "} + {foundCell.outPoint.index} +

+

+ Capacity:{" "} + {ccc.fixedPointToString(foundCell.cellOutput.capacity)} CKB +

+

+ Lock Address:{" "} + {explorerAddress( + foundCellAddress, + formatString(foundCellAddress, 8, 6), + )} +

+ {foundCell.outputData && ( +

+ Data Size:{" "} + {formatFileSize(ccc.bytesFrom(foundCell.outputData).length)} +

+ )} +
+
+ {isAddressMatch === false && ( + +
+

+ The cell's lock address does not match your wallet address. + You will not be able to unlock this cell to update it. +

+

+ Cell Lock:{" "} + {explorerAddress( + foundCellAddress, + formatString(foundCellAddress, 8, 6), + )} +

+

+ Your Address:{" "} + {userAddress + ? explorerAddress( + userAddress, + formatString(userAddress, 8, 6), + ) + : "Not connected"} +

+

+ Deployment will fail because you cannot unlock this cell. +

+
+
+ )} + {isAddressMatch === true && ( + +
+ The cell's lock address matches your wallet address. You can + update this cell. +
+
+ )} + + )} + + {cellCheckError && !isCheckingCell && ( + + {cellCheckError} + + )} + +
+ + + {!file ? ( +
+ +
+

+ Drag and drop a file here, or click to select +

+

+ Select a file from your computer +

+
+ +
+ ) : ( +
+
+
+
+ +

+ {file.name} +

+
+
+

+ Size:{" "} + {formatFileSize(file.size)} +

+

+ Type:{" "} + {file.type || "Unknown"} +

+

+ Modified:{" "} + {formatDate(new Date(file.lastModified))} +

+
+
+ +
+ +
+ )} +
+ + + + +
+ + ); +} diff --git a/packages/demo/src/app/connected/page.tsx b/packages/demo/src/app/connected/page.tsx index fd3facab4..a3ee824e8 100644 --- a/packages/demo/src/app/connected/page.tsx +++ b/packages/demo/src/app/connected/page.tsx @@ -46,6 +46,7 @@ const TABS: [ReactNode, string, keyof typeof icons, string][] = [ "text-cyan-600", ], ["Nervos DAO", "/connected/NervosDao", "Vault", "text-pink-500"], + ["Deploy Script", "/connected/DeployScript", "Upload", "text-purple-500"], ["Dep Group", "/utils/DepGroup", "Boxes", "text-amber-500"], ["SSRI", "/connected/SSRI", "Pill", "text-blue-500"], ["Hash", "/utils/Hash", "Barcode", "text-violet-500"], diff --git a/packages/demo/src/components/Message.tsx b/packages/demo/src/components/Message.tsx index 340921727..1a5213856 100644 --- a/packages/demo/src/components/Message.tsx +++ b/packages/demo/src/components/Message.tsx @@ -66,9 +66,9 @@ export function Message({ } } > -

+

{children} -

+
); From 626e6bd8d80b83d4ce5e8756e51f79571f7ef9e7 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Wed, 28 Jan 2026 22:44:04 +0800 Subject: [PATCH 02/11] fix(lint): solve lint issue --- .../demo/src/app/connected/(tools)/DeployScript/page.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index 08d93cd6e..d4f69290a 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -449,8 +449,9 @@ export default function DeployScript() {

- The cell's lock address does not match your wallet address. - You will not be able to unlock this cell to update it. + The cell's lock address does not match your wallet + address. You will not be able to unlock this cell to update + it.

Cell Lock:{" "} @@ -477,8 +478,8 @@ export default function DeployScript() { {isAddressMatch === true && (

- The cell's lock address matches your wallet address. You can - update this cell. + The cell's lock address matches your wallet address. You + can update this cell.
)} From 8f3f504a67f6f7599f419a2240631beee7ebfe3a Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 29 Jan 2026 09:49:28 +0800 Subject: [PATCH 03/11] feat: list all type_id cells and display them inside a big button per each --- .../connected/(tools)/DeployScript/page.tsx | 176 +++++++++++++++++- 1 file changed, 170 insertions(+), 6 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index d4f69290a..eb03649ec 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { BigButton } from "@/src/components/BigButton"; import { Button } from "@/src/components/Button"; import { ButtonsPanel } from "@/src/components/ButtonsPanel"; import { TextInput } from "@/src/components/Input"; @@ -74,6 +75,38 @@ function ConfirmationModal({ ); } +function TypeIdCellButton({ + cell, + onSelect, + isSelected, +}: { + cell: ccc.Cell; + onSelect: () => void; + isSelected: boolean; +}) { + const typeIdArgs = cell.cellOutput.type?.args || ""; + const dataSize = cell.outputData ? ccc.bytesFrom(cell.outputData).length : 0; + + return ( + +
+ + {formatString(typeIdArgs, 8, 6)} + + + {formatFileSize(dataSize)} + +
+
+ ); +} + export default function DeployScript() { const { signer, createSender } = useApp(); const { log, error } = createSender("Deploy Script"); @@ -93,6 +126,8 @@ export default function DeployScript() { const [isAddressMatch, setIsAddressMatch] = useState(null); const [isCheckingCell, setIsCheckingCell] = useState(false); const [cellCheckError, setCellCheckError] = useState(""); + const [typeIdCells, setTypeIdCells] = useState([]); + const [isScanningCells, setIsScanningCells] = useState(false); const fileInputRef = useRef(null); const lastCheckedTypeIdRef = useRef(""); const isCheckingRef = useRef(false); @@ -111,6 +146,44 @@ export default function DeployScript() { }); }, [signer]); + // Scan for all Type ID cells locked under the user's address + useEffect(() => { + if (!signer) { + setTypeIdCells([]); + return; + } + + setIsScanningCells(true); + (async () => { + try { + const { script: lock } = await signer.getRecommendedAddressObj(); + const typeIdScript = await ccc.Script.fromKnownScript( + signer.client, + ccc.KnownScript.TypeId, + "", + ); + + const cells: ccc.Cell[] = []; + for await (const cell of signer.client.findCells({ + script: typeIdScript, + scriptType: "type", + scriptSearchMode: "prefix", + withData: true, + filter: { + script: lock, + }, + })) { + cells.push(cell); + setTypeIdCells([...cells]); + } + } catch (err) { + console.error("Error scanning Type ID cells:", err); + } finally { + setIsScanningCells(false); + } + })(); + }, [signer]); + // Compare addresses when both are available useEffect(() => { if (userAddress && foundCellAddress) { @@ -161,7 +234,42 @@ export default function DeployScript() { } }; - // Automatically check Type ID cell when typeIdArgs changes + // Handle selecting a Type ID cell from the scanned list + const handleSelectTypeIdCell = async (cell: ccc.Cell) => { + const cellTypeIdArgs = cell.cellOutput.type?.args || ""; + + // Set the typeIdArgs + setTypeIdArgs(cellTypeIdArgs); + + // Directly set the found cell since we already have it + setFoundCell(cell); + + // Calculate and set the cell's lock address + try { + const address = ccc.Address.fromScript( + cell.cellOutput.lock, + client, + ).toString(); + setFoundCellAddress(address); + setCellCheckError(""); + + // Since we scanned cells locked under user's address, address always matches + setIsAddressMatch(true); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + setCellCheckError(`Error getting cell address: ${errorMessage}`); + setFoundCellAddress(""); + setIsAddressMatch(null); + } + + // Update the last checked ref to prevent redundant lookup + const normalizedTypeIdArgs = cellTypeIdArgs.startsWith("0x") + ? cellTypeIdArgs.slice(2) + : cellTypeIdArgs; + lastCheckedTypeIdRef.current = normalizedTypeIdArgs; + }; + + // Automatically check Type ID cell when typeIdArgs changes (for manual input) useEffect(() => { // Normalize Type ID args for comparison const normalizedTypeIdArgs = typeIdArgs.trim().startsWith("0x") @@ -260,7 +368,6 @@ export default function DeployScript() { return () => { clearTimeout(timeoutId); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [typeIdArgs, client]); const handleDeploy = async () => { @@ -394,13 +501,70 @@ export default function DeployScript() {
Upload a file to deploy it as a CKB cell with Type ID trait. The file - will be stored on-chain and can be referenced by its Type ID. Leave - Type ID args empty to create a new cell, or provide existing Type ID - args to update an existing cell. + will be stored on-chain and can be referenced by its Type ID. Select + an existing Type ID cell below to update it, or leave empty to create + a new cell. + {isScanningCells && ( + +
+ + Scanning for Type ID cells... +
+
+ )} + + {typeIdCells.length > 0 && ( +
+ +
+ {typeIdCells.map((cell) => { + const cellTypeIdArgs = cell.cellOutput.type?.args || ""; + const normalizedCellTypeIdArgs = cellTypeIdArgs.startsWith("0x") + ? cellTypeIdArgs.slice(2) + : cellTypeIdArgs; + const normalizedTypeIdArgs = typeIdArgs.trim().startsWith("0x") + ? typeIdArgs.trim().slice(2) + : typeIdArgs.trim(); + const isSelected = + normalizedCellTypeIdArgs === normalizedTypeIdArgs && + normalizedTypeIdArgs !== ""; + + return ( + { + handleSelectTypeIdCell(cell); + }} + isSelected={isSelected} + /> + ); + })} +
+ {typeIdArgs && ( + + )} +
+ )} + From cba7172522234cb6a7f0b4884d06d2ce51871de1 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 5 Feb 2026 10:45:40 +0800 Subject: [PATCH 04/11] chore: make big button display elegancely --- .../connected/(tools)/DeployScript/page.tsx | 86 +++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index eb03649ec..0d40421a6 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -9,7 +9,7 @@ import { useApp } from "@/src/context"; import { formatString, useGetExplorerLink } from "@/src/utils"; import { ccc } from "@ckb-ccc/connector-react"; import { Loader2, Upload, X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; function formatFileSize(bytes: number): string { if (bytes === 0) return "0 Bytes"; @@ -75,17 +75,32 @@ function ConfirmationModal({ ); } +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); +} + function TypeIdCellButton({ cell, + index, onSelect, isSelected, }: { cell: ccc.Cell; + index: number; onSelect: () => void; isSelected: boolean; }) { const typeIdArgs = cell.cellOutput.type?.args || ""; const dataSize = cell.outputData ? ccc.bytesFrom(cell.outputData).length : 0; + const fourLines = typeIdArgsToFourLines(typeIdArgs.slice(2)); return ( -
- - {formatString(typeIdArgs, 8, 6)} +
+ + #{index + 1} - +
+ {fourLines.map((line, i) => ( + + {line} + + ))} +
+ {formatFileSize(dataSize)}
@@ -133,6 +155,13 @@ export default function DeployScript() { const isCheckingRef = useRef(false); const { client } = ccc.useCcc(); + const resetCellCheckState = useCallback((errorMessage = "") => { + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + setCellCheckError(errorMessage); + }, []); + // Get user's wallet address useEffect(() => { if (!signer) { @@ -141,10 +170,15 @@ export default function DeployScript() { return; } - signer.getRecommendedAddress().then((addr) => { - setUserAddress(addr); - }); - }, [signer]); + signer + .getRecommendedAddress() + .then((addr) => setUserAddress(addr)) + .catch((err) => { + console.error("Failed to get recommended address:", err); + setUserAddress(""); + error("Failed to get wallet address"); + }); + }, [signer, error]); // Scan for all Type ID cells locked under the user's address useEffect(() => { @@ -257,9 +291,7 @@ export default function DeployScript() { setIsAddressMatch(true); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); - setCellCheckError(`Error getting cell address: ${errorMessage}`); - setFoundCellAddress(""); - setIsAddressMatch(null); + resetCellCheckState(`Error getting cell address: ${errorMessage}`); } // Update the last checked ref to prevent redundant lookup @@ -287,10 +319,7 @@ export default function DeployScript() { // If empty, just clear state if (!typeIdArgs.trim()) { lastCheckedTypeIdRef.current = ""; - setFoundCell(null); - setFoundCellAddress(""); - setIsAddressMatch(null); - setCellCheckError(""); + resetCellCheckState(); return; } @@ -306,20 +335,14 @@ export default function DeployScript() { try { const typeIdBytes = ccc.bytesFrom(normalizedTypeIdArgs); if (typeIdBytes.length !== 32) { - setFoundCell(null); - setFoundCellAddress(""); - setIsAddressMatch(null); - setCellCheckError( + resetCellCheckState( "Type ID args must be 32 bytes (64 hex characters)", ); isCheckingRef.current = false; return; } } catch { - setFoundCell(null); - setFoundCellAddress(""); - setIsAddressMatch(null); - setCellCheckError("Invalid Type ID args format"); + resetCellCheckState("Invalid Type ID args format"); isCheckingRef.current = false; return; } @@ -346,17 +369,11 @@ export default function DeployScript() { setCellCheckError(""); // Address comparison will be handled by useEffect } else { - setFoundCell(null); - setFoundCellAddress(""); - setIsAddressMatch(null); - setCellCheckError("Type ID cell not found on-chain"); + resetCellCheckState("Type ID cell not found on-chain"); } } catch (err) { - setFoundCell(null); - setFoundCellAddress(""); - setIsAddressMatch(null); const errorMessage = err instanceof Error ? err.message : String(err); - setCellCheckError(`Error checking Type ID: ${errorMessage}`); + resetCellCheckState(`Error checking Type ID: ${errorMessage}`); } finally { setIsCheckingCell(false); isCheckingRef.current = false; @@ -368,7 +385,7 @@ export default function DeployScript() { return () => { clearTimeout(timeoutId); }; - }, [typeIdArgs, client]); + }); const handleDeploy = async () => { if (!signer) { @@ -521,7 +538,7 @@ export default function DeployScript() { Select Existing Type ID Cell (Optional)
- {typeIdCells.map((cell) => { + {typeIdCells.map((cell, index) => { const cellTypeIdArgs = cell.cellOutput.type?.args || ""; const normalizedCellTypeIdArgs = cellTypeIdArgs.startsWith("0x") ? cellTypeIdArgs.slice(2) @@ -537,6 +554,7 @@ export default function DeployScript() { { handleSelectTypeIdCell(cell); }} From 19da97c90aef1c2e09c124b19e09775c61e1a20b Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 5 Feb 2026 11:07:50 +0800 Subject: [PATCH 05/11] chore: tiny changes --- .../demo/src/app/connected/(tools)/DeployScript/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index 0d40421a6..d285609b0 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -9,7 +9,7 @@ import { useApp } from "@/src/context"; import { formatString, useGetExplorerLink } from "@/src/utils"; import { ccc } from "@ckb-ccc/connector-react"; import { Loader2, Upload, X } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; function formatFileSize(bytes: number): string { if (bytes === 0) return "0 Bytes"; @@ -108,7 +108,7 @@ function TypeIdCellButton({ size="sm" iconName="FileCode" onClick={onSelect} - className={isSelected ? "border-purple-500 bg-purple-50" : ""} + className={isSelected ? "border-2 border-purple-500 bg-purple-50" : ""} >
@@ -219,7 +219,7 @@ export default function DeployScript() { }, [signer]); // Compare addresses when both are available - useEffect(() => { + useMemo(() => { if (userAddress && foundCellAddress) { setIsAddressMatch(userAddress === foundCellAddress); } else { From 5a1d760b328e0bf53dfd484c222c5b2693f21851 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 5 Feb 2026 11:30:10 +0800 Subject: [PATCH 06/11] chore: seperate file uploader and tx confirmation modal from script deployer --- .../connected/(tools)/DeployScript/page.tsx | 198 +----------------- .../src/app/utils/(tools)/FileUpload/page.tsx | 168 +++++++++++++++ .../src/app/utils/(tools)/TxConfirm/page.tsx | 34 +++ 3 files changed, 212 insertions(+), 188 deletions(-) create mode 100644 packages/demo/src/app/utils/(tools)/FileUpload/page.tsx create mode 100644 packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index d285609b0..83c4286ea 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -1,5 +1,11 @@ "use client"; +import { + FileUploadArea, + formatFileSize, + readFileAsBytes, +} from "@/src/app/utils/(tools)/FileUpload/page"; +import { TxConfirm } from "@/src/app/utils/(tools)/TxConfirm/page"; import { BigButton } from "@/src/components/BigButton"; import { Button } from "@/src/components/Button"; import { ButtonsPanel } from "@/src/components/ButtonsPanel"; @@ -8,73 +14,9 @@ import { Message } from "@/src/components/Message"; import { useApp } from "@/src/context"; import { formatString, useGetExplorerLink } from "@/src/utils"; import { ccc } from "@ckb-ccc/connector-react"; -import { Loader2, Upload, X } from "lucide-react"; +import { Loader2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -function formatFileSize(bytes: number): string { - if (bytes === 0) return "0 Bytes"; - const k = 1024; - const sizes = ["Bytes", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; -} - -function formatDate(date: Date): string { - return date.toLocaleString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); -} - -async function readFileAsBytes(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target?.result instanceof ArrayBuffer) { - resolve(new Uint8Array(e.target.result)); - } else { - reject(new Error("Failed to read file")); - } - }; - reader.onerror = () => reject(new Error("Failed to read file")); - reader.readAsArrayBuffer(file); - }); -} - -function ConfirmationModal({ - isOpen, - message, - txHash, -}: { - isOpen: boolean; - message: string; - txHash?: string; -}) { - if (!isOpen) return null; - - return ( -
-
-
- -
-

{message}

- {txHash && ( -

{txHash}

- )} -

- Please wait for transaction confirmation... -

-
-
-
-
- ); -} - function typeIdArgsToFourLines(args: string): string[] { const str = args || ""; if (!str.length) return []; @@ -136,7 +78,6 @@ export default function DeployScript() { const { explorerTransaction, explorerAddress } = useGetExplorerLink(); const [file, setFile] = useState(null); - const [isDragging, setIsDragging] = useState(false); const [isDeploying, setIsDeploying] = useState(false); const [typeIdArgs, setTypeIdArgs] = useState(""); const [isWaitingConfirmation, setIsWaitingConfirmation] = useState(false); @@ -150,7 +91,6 @@ export default function DeployScript() { const [cellCheckError, setCellCheckError] = useState(""); const [typeIdCells, setTypeIdCells] = useState([]); const [isScanningCells, setIsScanningCells] = useState(false); - const fileInputRef = useRef(null); const lastCheckedTypeIdRef = useRef(""); const isCheckingRef = useRef(false); const { client } = ccc.useCcc(); @@ -227,47 +167,6 @@ export default function DeployScript() { } }, [userAddress, foundCellAddress]); - const handleFileSelect = (selectedFile: File) => { - setFile(selectedFile); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const droppedFile = e.dataTransfer.files[0]; - if (droppedFile) { - handleFileSelect(droppedFile); - } - }; - - const handleFileInputChange = (e: React.ChangeEvent) => { - const selectedFile = e.target.files?.[0]; - if (selectedFile) { - handleFileSelect(selectedFile); - } - }; - - const handleClearFile = () => { - setFile(null); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }; - // Handle selecting a Type ID cell from the scanned list const handleSelectTypeIdCell = async (cell: ccc.Cell) => { const cellTypeIdArgs = cell.cellOutput.type?.args || ""; @@ -401,7 +300,7 @@ export default function DeployScript() { setIsDeploying(true); try { log("Reading file..."); - const fileBytes = await readFileAsBytes(file); + const fileBytes = (await readFileAsBytes(file)) as ccc.Bytes; log("Building transaction..."); const { script } = await signer.getRecommendedAddressObj(); @@ -510,7 +409,7 @@ export default function DeployScript() { return ( <> - )} -
- - - {!file ? ( -
- -
-

- Drag and drop a file here, or click to select -

-

- Select a file from your computer -

-
- -
- ) : ( -
-
-
-
- -

- {file.name} -

-
-
-

- Size:{" "} - {formatFileSize(file.size)} -

-

- Type:{" "} - {file.type || "Unknown"} -

-

- Modified:{" "} - {formatDate(new Date(file.lastModified))} -

-
-
- -
- -
- )} -
+ +
+ ) : ( +
+
+
+
+ +

+ {file.name} +

+
+
+

+ Size:{" "} + {formatFileSize(file.size)} +

+

+ Type:{" "} + {file.type || "Unknown"} +

+

+ Modified:{" "} + {formatDate(new Date(file.lastModified))} +

+
+
+ +
+ +
+ )} +
+ ); +} diff --git a/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx b/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx new file mode 100644 index 000000000..564787c39 --- /dev/null +++ b/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { Loader2 } from "lucide-react"; + +export function TxConfirm({ + isOpen, + message, + txHash, +}: { + isOpen: boolean; + message: string; + txHash?: string; +}) { + if (!isOpen) return null; + + return ( +
+
+
+ +
+

{message}

+ {txHash && ( +

{txHash}

+ )} +

+ Please wait for transaction confirmation... +

+
+
+
+
+ ); +} From c559b2ed9f04ba44ef00d62ebdc3d4519b3512e0 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 5 Feb 2026 13:33:23 +0800 Subject: [PATCH 07/11] refact: refactor module --- .../DeployScript/DeployScriptComponents.tsx | 159 +++++ .../(tools)/DeployScript/deployLogic.ts | 79 +++ .../connected/(tools)/DeployScript/helpers.ts | 18 + .../connected/(tools)/DeployScript/page.tsx | 549 +++--------------- .../(tools)/DeployScript/useDeployScript.ts | 205 +++++++ 5 files changed, 541 insertions(+), 469 deletions(-) create mode 100644 packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx create mode 100644 packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts create mode 100644 packages/demo/src/app/connected/(tools)/DeployScript/helpers.ts create mode 100644 packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx new file mode 100644 index 000000000..75d57c69c --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx @@ -0,0 +1,159 @@ +"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"; + +export function TypeIdCellButton({ + cell, + index, + onSelect, + isSelected, +}: { + cell: ccc.Cell; + index: number; + onSelect: () => void; + isSelected: boolean; +}) { + const typeIdArgs = cell.cellOutput.type?.args || ""; + const dataSize = cell.outputData ? ccc.bytesFrom(cell.outputData).length : 0; + const fourLines = typeIdArgsToFourLines(typeIdArgs.slice(2)); + + return ( + +
+ + #{index + 1} + +
+ {fourLines.map((line, i) => ( + + {line} + + ))} +
+ + {formatFileSize(dataSize)} + +
+
+ ); +} + +export function LoadingMessage({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( + +
+ + {children} +
+
+ ); +} + +export function CellFoundSection({ + foundCell, + foundCellAddress, + isAddressMatch, + userAddress, +}: { + foundCell: ccc.Cell; + foundCellAddress: string; + isAddressMatch: boolean | null; + userAddress: string; +}) { + const { explorerTransaction, explorerAddress } = useGetExplorerLink(); + + return ( + <> + +
+

+ Transaction:{" "} + {explorerTransaction(foundCell.outPoint.txHash)} +

+

+ Index:{" "} + {foundCell.outPoint.index} +

+

+ Capacity:{" "} + {ccc.fixedPointToString(foundCell.cellOutput.capacity)} CKB +

+

+ Lock Address:{" "} + {explorerAddress( + foundCellAddress, + formatString(foundCellAddress, 8, 6), + )} +

+ {foundCell.outputData && ( +

+ Data Size:{" "} + {formatFileSize(ccc.bytesFrom(foundCell.outputData).length)} +

+ )} +
+
+ {isAddressMatch === false && ( + +
+

+ The cell's lock address does not match your wallet address. + You will not be able to unlock this cell to update it. +

+

+ Cell Lock:{" "} + {explorerAddress( + foundCellAddress, + formatString(foundCellAddress, 8, 6), + )} +

+

+ Your Address:{" "} + {userAddress + ? explorerAddress(userAddress, formatString(userAddress, 8, 6)) + : "Not connected"} +

+

+ Deployment will fail because you cannot unlock this cell. +

+
+
+ )} + {isAddressMatch === true && ( + +
+ The cell's lock address matches your wallet address. You can + update this cell. +
+
+ )} + + ); +} + +export function ClearSelectionButton({ onClick }: { onClick: () => void }) { + return ( + + ); +} diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts b/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts new file mode 100644 index 000000000..4c7772a20 --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts @@ -0,0 +1,79 @@ +import { readFileAsBytes } from "@/src/app/utils/(tools)/FileUpload/page"; +import { ccc } from "@ckb-ccc/connector-react"; +import { normalizeTypeIdArgs } from "./helpers"; + +export type DeployLogger = (msg: string, ...args: unknown[]) => void; + +export async function runDeploy( + signer: ccc.Signer, + file: File, + typeIdArgs: string, + foundCell: ccc.Cell | null, + isAddressMatch: boolean | null, + log: DeployLogger, + error: DeployLogger, +): Promise { + 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; +} diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/helpers.ts b/packages/demo/src/app/connected/(tools)/DeployScript/helpers.ts new file mode 100644 index 000000000..5804ef76d --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/helpers.ts @@ -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); +} diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index 83c4286ea..79ba24779 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -1,297 +1,57 @@ "use client"; -import { - FileUploadArea, - formatFileSize, - readFileAsBytes, -} from "@/src/app/utils/(tools)/FileUpload/page"; +import { FileUploadArea } from "@/src/app/utils/(tools)/FileUpload/page"; import { TxConfirm } from "@/src/app/utils/(tools)/TxConfirm/page"; -import { BigButton } from "@/src/components/BigButton"; import { Button } from "@/src/components/Button"; import { ButtonsPanel } from "@/src/components/ButtonsPanel"; import { TextInput } from "@/src/components/Input"; import { Message } from "@/src/components/Message"; import { useApp } from "@/src/context"; -import { formatString, useGetExplorerLink } from "@/src/utils"; +import { useGetExplorerLink } from "@/src/utils"; import { ccc } from "@ckb-ccc/connector-react"; -import { Loader2 } from "lucide-react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; - -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); -} - -function TypeIdCellButton({ - cell, - index, - onSelect, - isSelected, -}: { - cell: ccc.Cell; - index: number; - onSelect: () => void; - isSelected: boolean; -}) { - const typeIdArgs = cell.cellOutput.type?.args || ""; - const dataSize = cell.outputData ? ccc.bytesFrom(cell.outputData).length : 0; - const fourLines = typeIdArgsToFourLines(typeIdArgs.slice(2)); - - return ( - -
- - #{index + 1} - -
- {fourLines.map((line, i) => ( - - {line} - - ))} -
- - {formatFileSize(dataSize)} - -
-
- ); -} +import { ReactNode, useCallback, useState } from "react"; +import { + CellFoundSection, + ClearSelectionButton, + LoadingMessage, + TypeIdCellButton, +} from "./DeployScriptComponents"; +import { runDeploy } from "./deployLogic"; +import { useDeployScript } from "./useDeployScript"; export default function DeployScript() { - const { signer, createSender } = useApp(); + const { createSender } = useApp(); const { log, error } = createSender("Deploy Script"); - - const { explorerTransaction, explorerAddress } = useGetExplorerLink(); + const { explorerTransaction } = useGetExplorerLink(); const [file, setFile] = useState(null); const [isDeploying, setIsDeploying] = useState(false); - const [typeIdArgs, setTypeIdArgs] = useState(""); const [isWaitingConfirmation, setIsWaitingConfirmation] = useState(false); - const [confirmationMessage, setConfirmationMessage] = useState(""); - const [confirmationTxHash, setConfirmationTxHash] = useState(""); - const [foundCell, setFoundCell] = useState(null); - const [foundCellAddress, setFoundCellAddress] = useState(""); - const [userAddress, setUserAddress] = useState(""); - const [isAddressMatch, setIsAddressMatch] = useState(null); - const [isCheckingCell, setIsCheckingCell] = useState(false); - const [cellCheckError, setCellCheckError] = useState(""); - const [typeIdCells, setTypeIdCells] = useState([]); - const [isScanningCells, setIsScanningCells] = useState(false); - const lastCheckedTypeIdRef = useRef(""); - const isCheckingRef = useRef(false); - const { client } = ccc.useCcc(); - - const resetCellCheckState = useCallback((errorMessage = "") => { - setFoundCell(null); - setFoundCellAddress(""); - setIsAddressMatch(null); - setCellCheckError(errorMessage); - }, []); - - // Get user's wallet address - useEffect(() => { - if (!signer) { - setUserAddress(""); - setIsAddressMatch(null); - return; - } - - signer - .getRecommendedAddress() - .then((addr) => setUserAddress(addr)) - .catch((err) => { - console.error("Failed to get recommended address:", err); - setUserAddress(""); - error("Failed to get wallet address"); - }); - }, [signer, error]); - - // Scan for all Type ID cells locked under the user's address - useEffect(() => { - if (!signer) { - setTypeIdCells([]); - return; - } - - setIsScanningCells(true); - (async () => { - try { - const { script: lock } = await signer.getRecommendedAddressObj(); - const typeIdScript = await ccc.Script.fromKnownScript( - signer.client, - ccc.KnownScript.TypeId, - "", - ); - - const cells: ccc.Cell[] = []; - for await (const cell of signer.client.findCells({ - script: typeIdScript, - scriptType: "type", - scriptSearchMode: "prefix", - withData: true, - filter: { - script: lock, - }, - })) { - cells.push(cell); - setTypeIdCells([...cells]); - } - } catch (err) { - console.error("Error scanning Type ID cells:", err); - } finally { - setIsScanningCells(false); - } - })(); - }, [signer]); - - // Compare addresses when both are available - useMemo(() => { - if (userAddress && foundCellAddress) { - setIsAddressMatch(userAddress === foundCellAddress); - } else { - setIsAddressMatch(null); - } - }, [userAddress, foundCellAddress]); - - // Handle selecting a Type ID cell from the scanned list - const handleSelectTypeIdCell = async (cell: ccc.Cell) => { - const cellTypeIdArgs = cell.cellOutput.type?.args || ""; - - // Set the typeIdArgs - setTypeIdArgs(cellTypeIdArgs); - - // Directly set the found cell since we already have it - setFoundCell(cell); - - // Calculate and set the cell's lock address - try { - const address = ccc.Address.fromScript( - cell.cellOutput.lock, - client, - ).toString(); - setFoundCellAddress(address); - setCellCheckError(""); - - // Since we scanned cells locked under user's address, address always matches - setIsAddressMatch(true); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - resetCellCheckState(`Error getting cell address: ${errorMessage}`); - } - - // Update the last checked ref to prevent redundant lookup - const normalizedTypeIdArgs = cellTypeIdArgs.startsWith("0x") - ? cellTypeIdArgs.slice(2) - : cellTypeIdArgs; - lastCheckedTypeIdRef.current = normalizedTypeIdArgs; - }; - - // Automatically check Type ID cell when typeIdArgs changes (for manual input) - useEffect(() => { - // Normalize Type ID args for comparison - const normalizedTypeIdArgs = typeIdArgs.trim().startsWith("0x") - ? typeIdArgs.trim().slice(2) - : typeIdArgs.trim(); - - // Skip if already checked this value or currently checking - if ( - lastCheckedTypeIdRef.current === normalizedTypeIdArgs || - isCheckingRef.current - ) { - return; - } - - // If empty, just clear state - if (!typeIdArgs.trim()) { - lastCheckedTypeIdRef.current = ""; - resetCellCheckState(); - return; - } - - const checkTypeIdCell = async () => { - // Mark as checking to prevent concurrent checks - if (isCheckingRef.current) { - return; - } - isCheckingRef.current = true; - lastCheckedTypeIdRef.current = normalizedTypeIdArgs; - - // Validate length - try { - const typeIdBytes = ccc.bytesFrom(normalizedTypeIdArgs); - if (typeIdBytes.length !== 32) { - resetCellCheckState( - "Type ID args must be 32 bytes (64 hex characters)", - ); - isCheckingRef.current = false; - return; - } - } catch { - resetCellCheckState("Invalid Type ID args format"); - isCheckingRef.current = false; - return; - } - - setIsCheckingCell(true); - setCellCheckError(""); - - try { - const typeIdScript = await ccc.Script.fromKnownScript( - client, - ccc.KnownScript.TypeId, - normalizedTypeIdArgs, - ); - - const cell = await client.findSingletonCellByType(typeIdScript, true); - - if (cell) { - setFoundCell(cell); - const address = ccc.Address.fromScript( - cell.cellOutput.lock, - client, - ).toString(); - setFoundCellAddress(address); - setCellCheckError(""); - // Address comparison will be handled by useEffect - } else { - resetCellCheckState("Type ID cell not found on-chain"); - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - resetCellCheckState(`Error checking Type ID: ${errorMessage}`); - } finally { - setIsCheckingCell(false); - isCheckingRef.current = false; - } - }; - - // Debounce the check - const timeoutId = setTimeout(checkTypeIdCell, 500); - return () => { - clearTimeout(timeoutId); - }; - }); - - const handleDeploy = async () => { + const [confirmationMessage, setConfirmationMessage] = useState(""); + const [confirmationTxHash, setConfirmationTxHash] = useState(""); + + const { + signer, + userAddress, + typeIdArgs, + setTypeIdArgs, + typeIdCells, + isScanningCells, + foundCell, + foundCellAddress, + isAddressMatch, + isCheckingCell, + cellCheckError, + handleSelectTypeIdCell, + clearSelection, + normalizeTypeIdArgs, + } = useDeployScript(); + + const handleDeploy = useCallback(async () => { if (!signer) { error("Please connect a wallet first"); return; } - if (!file) { error("Please select a file to deploy"); return; @@ -300,112 +60,53 @@ export default function DeployScript() { setIsDeploying(true); try { log("Reading file..."); - const fileBytes = (await readFileAsBytes(file)) as ccc.Bytes; - - log("Building transaction..."); - const { script } = await signer.getRecommendedAddressObj(); - - let tx: ccc.Transaction; - let typeIdArgsValue: string; - - if (typeIdArgs.trim() !== "") { - // Update existing Type ID cell - if (!foundCell) { - error("Type ID cell not found. Please check the Type ID args."); - return; - } - - // Check if addresses match - if (isAddressMatch === false) { - error( - "Cannot update cell: The cell's lock address does not match your wallet address. You cannot unlock this cell.", - ); - return; - } - - // Normalize Type ID args - remove 0x prefix if present - const normalizedTypeIdArgs = typeIdArgs.trim().startsWith("0x") - ? typeIdArgs.trim().slice(2) - : typeIdArgs.trim(); - - log("Updating existing Type ID cell..."); - - // Create transaction to update the cell - tx = ccc.Transaction.from({ - inputs: [ - { - previousOutput: foundCell.outPoint, - }, - ], - outputs: [ - { - ...foundCell.cellOutput, - capacity: ccc.Zero, // Zero capacity means the cell will be replaced with a new one - }, - ], - outputsData: [fileBytes], - }); - - typeIdArgsValue = normalizedTypeIdArgs; - } else { - // Create new Type ID cell - tx = ccc.Transaction.from({ - outputs: [ - { - lock: script, - type: await ccc.Script.fromKnownScript( - signer.client, - ccc.KnownScript.TypeId, - "00".repeat(32), - ), - }, - ], - outputsData: [fileBytes], - }); - - // Complete inputs for capacity - await tx.completeInputsAddOne(signer); - - // Generate type_id from first input and output index - 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); + const txHash = await runDeploy( + signer, + file, + typeIdArgs, + foundCell, + isAddressMatch, + (msg, ...args) => log(msg, ...(args as ReactNode[])), + (msg, ...args) => error(msg, ...(args as ReactNode[])), + ); + + if (!txHash) { + setIsDeploying(false); + return; } - // Complete fees - await tx.completeFeeBy(signer); - - // Sign and send the transaction - log("Sending transaction..."); - const txHash = await signer.sendTransaction(tx); - log("Transaction sent:", explorerTransaction(txHash)); - - // Show blocking confirmation modal setIsWaitingConfirmation(true); setConfirmationMessage("Waiting for transaction confirmation..."); setConfirmationTxHash(txHash); + log("Transaction sent:", explorerTransaction(txHash)); await signer.client.waitTransaction(txHash); log("Transaction committed:", explorerTransaction(txHash)); - // Close modal after confirmation setIsWaitingConfirmation(false); setConfirmationMessage(""); setConfirmationTxHash(""); } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - error("Deployment failed:", errorMessage); + const msg = err instanceof Error ? err.message : String(err); + error("Deployment failed:", msg); setIsWaitingConfirmation(false); setConfirmationMessage(""); setConfirmationTxHash(""); } finally { setIsDeploying(false); } - }; + }, [ + signer, + file, + typeIdArgs, + foundCell, + isAddressMatch, + log, + error, + explorerTransaction, + ]); + + const normalizedInput = normalizeTypeIdArgs(typeIdArgs); return ( <> @@ -423,12 +124,9 @@ export default function DeployScript() { {isScanningCells && ( - -
- - Scanning for Type ID cells... -
-
+ + Scanning for Type ID cells... + )} {typeIdCells.length > 0 && ( @@ -438,45 +136,24 @@ export default function DeployScript() {
{typeIdCells.map((cell, index) => { - const cellTypeIdArgs = cell.cellOutput.type?.args || ""; - const normalizedCellTypeIdArgs = cellTypeIdArgs.startsWith("0x") - ? cellTypeIdArgs.slice(2) - : cellTypeIdArgs; - const normalizedTypeIdArgs = typeIdArgs.trim().startsWith("0x") - ? typeIdArgs.trim().slice(2) - : typeIdArgs.trim(); + const cellNorm = normalizeTypeIdArgs( + cell.cellOutput.type?.args || "", + ); const isSelected = - normalizedCellTypeIdArgs === normalizedTypeIdArgs && - normalizedTypeIdArgs !== ""; + cellNorm === normalizedInput && normalizedInput !== ""; return ( { - handleSelectTypeIdCell(cell); - }} + onSelect={() => handleSelectTypeIdCell(cell)} isSelected={isSelected} /> ); })}
- {typeIdArgs && ( - - )} + {typeIdArgs && }
)} @@ -487,84 +164,18 @@ export default function DeployScript() { /> {isCheckingCell && ( - -
- - Searching for Type ID cell on-chain... -
-
+ + Searching for Type ID cell on-chain... + )} {foundCell && !isCheckingCell && ( - <> - -
-

- Transaction:{" "} - {explorerTransaction(foundCell.outPoint.txHash)} -

-

- Index:{" "} - {foundCell.outPoint.index} -

-

- Capacity:{" "} - {ccc.fixedPointToString(foundCell.cellOutput.capacity)} CKB -

-

- Lock Address:{" "} - {explorerAddress( - foundCellAddress, - formatString(foundCellAddress, 8, 6), - )} -

- {foundCell.outputData && ( -

- Data Size:{" "} - {formatFileSize(ccc.bytesFrom(foundCell.outputData).length)} -

- )} -
-
- {isAddressMatch === false && ( - -
-

- The cell's lock address does not match your wallet - address. You will not be able to unlock this cell to update - it. -

-

- Cell Lock:{" "} - {explorerAddress( - foundCellAddress, - formatString(foundCellAddress, 8, 6), - )} -

-

- Your Address:{" "} - {userAddress - ? explorerAddress( - userAddress, - formatString(userAddress, 8, 6), - ) - : "Not connected"} -

-

- Deployment will fail because you cannot unlock this cell. -

-
-
- )} - {isAddressMatch === true && ( - -
- The cell's lock address matches your wallet address. You - can update this cell. -
-
- )} - + )} {cellCheckError && !isCheckingCell && ( diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts b/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts new file mode 100644 index 000000000..99067c2cd --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts @@ -0,0 +1,205 @@ +"use client"; + +import { useApp } from "@/src/context"; +import { ccc } from "@ckb-ccc/connector-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { normalizeTypeIdArgs } from "./helpers"; + +export function useDeployScript() { + const { signer } = useApp(); + const { client } = ccc.useCcc(); + + const [userAddress, setUserAddress] = useState(""); + const [typeIdArgs, setTypeIdArgs] = useState(""); + const [typeIdCells, setTypeIdCells] = useState([]); + const [isScanningCells, setIsScanningCells] = useState(false); + const [foundCell, setFoundCell] = useState(null); + const [foundCellAddress, setFoundCellAddress] = useState(""); + const [isAddressMatch, setIsAddressMatch] = useState(null); + const [isCheckingCell, setIsCheckingCell] = useState(false); + const [cellCheckError, setCellCheckError] = useState(""); + + const lastCheckedTypeIdRef = useRef(""); + const isCheckingRef = useRef(false); + + const resetCellCheckState = useCallback((errorMessage = "") => { + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + setCellCheckError(errorMessage); + }, []); + + // User address + useEffect(() => { + if (!signer) { + setUserAddress(""); + setIsAddressMatch(null); + return; + } + signer + .getRecommendedAddress() + .then(setUserAddress) + .catch(() => setUserAddress("")); + }, [signer]); + + // Scan Type ID cells + useEffect(() => { + if (!signer) { + setTypeIdCells([]); + return; + } + setIsScanningCells(true); + (async () => { + try { + const { script: lock } = await signer.getRecommendedAddressObj(); + const typeIdScript = await ccc.Script.fromKnownScript( + signer.client, + ccc.KnownScript.TypeId, + "", + ); + const cells: ccc.Cell[] = []; + for await (const cell of signer.client.findCells({ + script: typeIdScript, + scriptType: "type", + scriptSearchMode: "prefix", + withData: true, + filter: { script: lock }, + })) { + cells.push(cell); + setTypeIdCells([...cells]); + } + } catch { + // ignore + } finally { + setIsScanningCells(false); + } + })(); + }, [signer]); + + // Derive address match from user + found cell address + useEffect(() => { + if (userAddress && foundCellAddress) { + setIsAddressMatch(userAddress === foundCellAddress); + } else { + setIsAddressMatch(null); + } + }, [userAddress, foundCellAddress]); + + const handleSelectTypeIdCell = useCallback( + async (cell: ccc.Cell) => { + const cellTypeIdArgs = cell.cellOutput.type?.args || ""; + setTypeIdArgs(cellTypeIdArgs); + setFoundCell(cell); + try { + const address = ccc.Address.fromScript( + cell.cellOutput.lock, + client, + ).toString(); + setFoundCellAddress(address); + setCellCheckError(""); + setIsAddressMatch(true); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + resetCellCheckState(`Error getting cell address: ${msg}`); + } + lastCheckedTypeIdRef.current = normalizeTypeIdArgs(cellTypeIdArgs); + }, + [client, resetCellCheckState], + ); + + const clearSelection = useCallback(() => { + setTypeIdArgs(""); + setFoundCell(null); + setFoundCellAddress(""); + setIsAddressMatch(null); + setCellCheckError(""); + }, []); + + // Debounced Type ID cell check when typeIdArgs changes (manual input) + useEffect(() => { + const normalized = normalizeTypeIdArgs(typeIdArgs); + + if (lastCheckedTypeIdRef.current === normalized || isCheckingRef.current) { + return; + } + + if (!typeIdArgs.trim()) { + lastCheckedTypeIdRef.current = ""; + resetCellCheckState(); + return; + } + + const timeoutId = setTimeout(async () => { + if (isCheckingRef.current) return; + isCheckingRef.current = true; + lastCheckedTypeIdRef.current = normalized; + + try { + const typeIdBytes = ccc.bytesFrom(normalized); + if (typeIdBytes.length !== 32) { + resetCellCheckState( + "Type ID args must be 32 bytes (64 hex characters)", + ); + return; + } + } catch { + resetCellCheckState("Invalid Type ID args format"); + return; + } finally { + isCheckingRef.current = false; + } + + isCheckingRef.current = true; + + setIsCheckingCell(true); + setCellCheckError(""); + + try { + const typeIdScript = await ccc.Script.fromKnownScript( + client, + ccc.KnownScript.TypeId, + normalized, + ); + const cell = await client.findSingletonCellByType(typeIdScript, true); + + if (cell) { + setFoundCell(cell); + const address = ccc.Address.fromScript( + cell.cellOutput.lock, + client, + ).toString(); + setFoundCellAddress(address); + setCellCheckError(""); + } else { + resetCellCheckState("Type ID cell not found on-chain"); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + resetCellCheckState(`Error checking Type ID: ${msg}`); + } finally { + setIsCheckingCell(false); + isCheckingRef.current = false; + } + }, 500); + + return () => clearTimeout(timeoutId); + }, [typeIdArgs, client, resetCellCheckState]); + + return { + signer, + client, + userAddress, + typeIdArgs, + setTypeIdArgs, + typeIdCells, + isScanningCells, + foundCell, + foundCellAddress, + isAddressMatch, + isCheckingCell, + cellCheckError, + handleSelectTypeIdCell, + clearSelection, + normalizeTypeIdArgs, + }; +} From 91b54321997f49537089bd547eefe2be425bdca9 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 5 Feb 2026 16:50:13 +0800 Subject: [PATCH 08/11] chore: refresh big buttons after transaction confirmed --- .../DeployScript/DeployScriptComponents.tsx | 38 ++++++++++ .../(tools)/DeployScript/deployLogic.ts | 27 ++++++- .../connected/(tools)/DeployScript/page.tsx | 74 +++++++++++++++++-- .../(tools)/DeployScript/useDeployScript.ts | 41 +++++++++- 4 files changed, 170 insertions(+), 10 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx index 75d57c69c..8bf2f687a 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx @@ -9,16 +9,30 @@ 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; @@ -36,6 +50,11 @@ export function TypeIdCellButton({ #{index + 1} + {creationTimestamp != null && ( + + {formatCellCreationDate(creationTimestamp)} + + )}
{fourLines.map((line, i) => ( @@ -157,3 +176,22 @@ export function ClearSelectionButton({ onClick }: { onClick: () => void }) { ); } + +export function BurnButton({ + onClick, + disabled, +}: { + onClick: () => void; + disabled?: boolean; +}) { + return ( + + ); +} diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts b/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts index 4c7772a20..d413c538b 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts +++ b/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts @@ -1,8 +1,9 @@ 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 DeployLogger = (msg: string, ...args: unknown[]) => void; +export type Logger = (...args: ReactNode[]) => void; export async function runDeploy( signer: ccc.Signer, @@ -10,8 +11,8 @@ export async function runDeploy( typeIdArgs: string, foundCell: ccc.Cell | null, isAddressMatch: boolean | null, - log: DeployLogger, - error: DeployLogger, + log: Logger, + error: Logger, ): Promise { const fileBytes = (await readFileAsBytes(file)) as ccc.Bytes; const { script } = await signer.getRecommendedAddressObj(); @@ -77,3 +78,23 @@ export async function runDeploy( 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 { + 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; +} diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index 79ba24779..8340b73bc 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -9,14 +9,15 @@ import { Message } from "@/src/components/Message"; import { useApp } from "@/src/context"; import { useGetExplorerLink } from "@/src/utils"; import { ccc } from "@ckb-ccc/connector-react"; -import { ReactNode, useCallback, useState } from "react"; +import { useCallback, useState } from "react"; import { + BurnButton, CellFoundSection, ClearSelectionButton, LoadingMessage, TypeIdCellButton, } from "./DeployScriptComponents"; -import { runDeploy } from "./deployLogic"; +import { runBurn, runDeploy } from "./deployLogic"; import { useDeployScript } from "./useDeployScript"; export default function DeployScript() { @@ -36,6 +37,7 @@ export default function DeployScript() { typeIdArgs, setTypeIdArgs, typeIdCells, + cellCreationTimestamps, isScanningCells, foundCell, foundCellAddress, @@ -45,8 +47,50 @@ export default function DeployScript() { handleSelectTypeIdCell, clearSelection, normalizeTypeIdArgs, + refreshTypeIdCells, } = useDeployScript(); + const handleBurn = useCallback(async () => { + if (!signer || !foundCell) return; + if (isAddressMatch !== true) { + error("You can only burn a cell that you own."); + return; + } + setIsDeploying(true); + try { + const txHash = await runBurn(signer, foundCell, log); + if (!txHash) { + setIsDeploying(false); + return; + } + setIsWaitingConfirmation(true); + setConfirmationMessage("Waiting for burn transaction confirmation..."); + setConfirmationTxHash(txHash); + log("Transaction sent:", explorerTransaction(txHash)); + await signer.client.waitTransaction(txHash); + log("Transaction committed:", explorerTransaction(txHash)); + clearSelection(); + refreshTypeIdCells(); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + error("Burn failed:", msg); + } finally { + setIsDeploying(false); + setIsWaitingConfirmation(false); + setConfirmationMessage(""); + setConfirmationTxHash(""); + } + }, [ + signer, + foundCell, + isAddressMatch, + log, + error, + explorerTransaction, + clearSelection, + refreshTypeIdCells, + ]); + const handleDeploy = useCallback(async () => { if (!signer) { error("Please connect a wallet first"); @@ -66,8 +110,8 @@ export default function DeployScript() { typeIdArgs, foundCell, isAddressMatch, - (msg, ...args) => log(msg, ...(args as ReactNode[])), - (msg, ...args) => error(msg, ...(args as ReactNode[])), + log, + error, ); if (!txHash) { @@ -82,6 +126,7 @@ export default function DeployScript() { log("Transaction sent:", explorerTransaction(txHash)); await signer.client.waitTransaction(txHash); log("Transaction committed:", explorerTransaction(txHash)); + refreshTypeIdCells(); setIsWaitingConfirmation(false); setConfirmationMessage(""); @@ -104,6 +149,7 @@ export default function DeployScript() { log, error, explorerTransaction, + refreshTypeIdCells, ]); const normalizedInput = normalizeTypeIdArgs(typeIdArgs); @@ -149,11 +195,29 @@ export default function DeployScript() { index={index} onSelect={() => handleSelectTypeIdCell(cell)} isSelected={isSelected} + creationTimestamp={ + cellCreationTimestamps[ + ccc.hexFrom(cell.outPoint.toBytes()) + ] + } /> ); })}
- {typeIdArgs && } + {typeIdArgs && ( +
+ + +
+ )}
)} diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts b/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts index 99067c2cd..7c36bb6f3 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts +++ b/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts @@ -18,6 +18,10 @@ export function useDeployScript() { const [isAddressMatch, setIsAddressMatch] = useState(null); const [isCheckingCell, setIsCheckingCell] = useState(false); const [cellCheckError, setCellCheckError] = useState(""); + const [cellCreationTimestamps, setCellCreationTimestamps] = useState< + Record + >({}); + const [refreshTrigger, setRefreshTrigger] = useState(0); const lastCheckedTypeIdRef = useRef(""); const isCheckingRef = useRef(false); @@ -42,7 +46,11 @@ export function useDeployScript() { .catch(() => setUserAddress("")); }, [signer]); - // Scan Type ID cells + // Scan Type ID cells (runs on signer change or force refresh) + const refreshTypeIdCells = useCallback(() => { + setRefreshTrigger((t) => t + 1); + }, []); + useEffect(() => { if (!signer) { setTypeIdCells([]); @@ -74,7 +82,34 @@ export function useDeployScript() { setIsScanningCells(false); } })(); - }, [signer]); + }, [signer, refreshTrigger]); + + // Fetch block header timestamp for each type_id cell (creation date) + useEffect(() => { + if (!client || typeIdCells.length === 0) { + setCellCreationTimestamps({}); + return; + } + let cancelled = false; + const next: Record = {}; + (async () => { + for (const cell of typeIdCells) { + if (cancelled) return; + try { + const res = await client.getCellWithHeader(cell.outPoint); + if (cancelled || !res?.header) continue; + const key = ccc.hexFrom(cell.outPoint.toBytes()); + next[key] = Number(res.header.timestamp); + } catch { + // ignore per-cell errors + } + } + if (!cancelled) setCellCreationTimestamps(next); + })(); + return () => { + cancelled = true; + }; + }, [client, typeIdCells]); // Derive address match from user + found cell address useEffect(() => { @@ -192,6 +227,7 @@ export function useDeployScript() { typeIdArgs, setTypeIdArgs, typeIdCells, + cellCreationTimestamps, isScanningCells, foundCell, foundCellAddress, @@ -201,5 +237,6 @@ export function useDeployScript() { handleSelectTypeIdCell, clearSelection, normalizeTypeIdArgs, + refreshTypeIdCells, }; } From 9a00ad0de507269fca2d0e6e57ea76e198b894a4 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Thu, 5 Feb 2026 17:55:32 +0800 Subject: [PATCH 09/11] bug: modify FileUpload and TxConfigm with default modifier --- packages/demo/src/app/connected/(tools)/DeployScript/page.tsx | 4 ++-- packages/demo/src/app/utils/(tools)/FileUpload/page.tsx | 2 +- packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index 8340b73bc..8b20ab991 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { FileUploadArea } from "@/src/app/utils/(tools)/FileUpload/page"; -import { TxConfirm } from "@/src/app/utils/(tools)/TxConfirm/page"; +import FileUploadArea from "@/src/app/utils/(tools)/FileUpload/page"; +import TxConfirm from "@/src/app/utils/(tools)/TxConfirm/page"; import { Button } from "@/src/components/Button"; import { ButtonsPanel } from "@/src/components/ButtonsPanel"; import { TextInput } from "@/src/components/Input"; diff --git a/packages/demo/src/app/utils/(tools)/FileUpload/page.tsx b/packages/demo/src/app/utils/(tools)/FileUpload/page.tsx index b5f617bc3..295710ad0 100644 --- a/packages/demo/src/app/utils/(tools)/FileUpload/page.tsx +++ b/packages/demo/src/app/utils/(tools)/FileUpload/page.tsx @@ -37,7 +37,7 @@ export async function readFileAsBytes(file: File): Promise { }); } -export function FileUploadArea({ +export default function FileUploadArea({ file, onFileChange, }: { diff --git a/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx b/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx index 564787c39..fd4af0d05 100644 --- a/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx +++ b/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx @@ -2,7 +2,7 @@ import { Loader2 } from "lucide-react"; -export function TxConfirm({ +export default function TxConfirm({ isOpen, message, txHash, From b3b4c1ae0bb1f035450196bc55233aaf769a23c5 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Wed, 11 Feb 2026 08:36:58 +0800 Subject: [PATCH 10/11] chore: format file name --- packages/demo/src/app/connected/(tools)/DeployScript/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index 8b20ab991..60620b763 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -10,14 +10,14 @@ import { useApp } from "@/src/context"; import { useGetExplorerLink } from "@/src/utils"; import { ccc } from "@ckb-ccc/connector-react"; import { useCallback, useState } from "react"; +import { runBurn, runDeploy } from "./deployLogic"; import { BurnButton, CellFoundSection, ClearSelectionButton, LoadingMessage, TypeIdCellButton, -} from "./DeployScriptComponents"; -import { runBurn, runDeploy } from "./deployLogic"; +} from "./deployScriptComponents"; import { useDeployScript } from "./useDeployScript"; export default function DeployScript() { From 886845919ddf56ba9e185e25ff9495d5a47c3e5e Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Wed, 11 Feb 2026 10:46:14 +0800 Subject: [PATCH 11/11] chore: make message box automatically unfolded --- ...oyScriptComponents.tsx => deployComponents.tsx} | 10 +++++++--- .../app/connected/(tools)/DeployScript/page.tsx | 4 ++-- packages/demo/src/components/Message.tsx | 14 +++++++++----- 3 files changed, 18 insertions(+), 10 deletions(-) rename packages/demo/src/app/connected/(tools)/DeployScript/{DeployScriptComponents.tsx => deployComponents.tsx} (95%) diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/deployComponents.tsx similarity index 95% rename from packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx rename to packages/demo/src/app/connected/(tools)/DeployScript/deployComponents.tsx index 8bf2f687a..b05909eba 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/DeployScriptComponents.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/deployComponents.tsx @@ -102,7 +102,7 @@ export function CellFoundSection({ return ( <> - +

Transaction:{" "} @@ -132,7 +132,11 @@ export function CellFoundSection({

{isAddressMatch === false && ( - +

The cell's lock address does not match your wallet address. @@ -158,7 +162,7 @@ export function CellFoundSection({ )} {isAddressMatch === true && ( - +

The cell's lock address matches your wallet address. You can update this cell. diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx index 60620b763..0e10b3f55 100644 --- a/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -10,14 +10,14 @@ import { useApp } from "@/src/context"; import { useGetExplorerLink } from "@/src/utils"; import { ccc } from "@ckb-ccc/connector-react"; import { useCallback, useState } from "react"; -import { runBurn, runDeploy } from "./deployLogic"; import { BurnButton, CellFoundSection, ClearSelectionButton, LoadingMessage, TypeIdCellButton, -} from "./deployScriptComponents"; +} from "./deployComponents"; +import { runBurn, runDeploy } from "./deployLogic"; import { useDeployScript } from "./useDeployScript"; export default function DeployScript() { diff --git a/packages/demo/src/components/Message.tsx b/packages/demo/src/components/Message.tsx index 1a5213856..35429aa8c 100644 --- a/packages/demo/src/components/Message.tsx +++ b/packages/demo/src/components/Message.tsx @@ -7,6 +7,7 @@ export interface MessageProps { type?: "error" | "warning" | "info" | "success"; lines?: number; className?: string; + expandable?: boolean; } export function Message({ @@ -15,8 +16,9 @@ export function Message({ type = "info", lines, className = "", + expandable = true, }: MessageProps) { - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(!expandable); let colorClass = ""; let bgColorClass = ""; @@ -41,10 +43,12 @@ export function Message({ break; } + const showFull = expandable ? isExpanded : true; + return (
setIsExpanded(!isExpanded)} - className={`my-2 flex cursor-pointer flex-col items-start rounded-md p-4 ${bgColorClass} ${className}`} + onClick={expandable ? () => setIsExpanded(!isExpanded) : undefined} + className={`my-2 flex flex-col items-start rounded-md p-4 ${bgColorClass} ${className} ${expandable ? "cursor-pointer" : ""}`} > {title ? (
@@ -57,9 +61,9 @@ export function Message({
) : undefined}