diff --git a/packages/demo/src/app/connected/(tools)/DeployScript/deployComponents.tsx b/packages/demo/src/app/connected/(tools)/DeployScript/deployComponents.tsx new file mode 100644 index 000000000..b05909eba --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/deployComponents.tsx @@ -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 ( + +
+ + #{index + 1} + + {creationTimestamp != null && ( + + {formatCellCreationDate(creationTimestamp)} + + )} +
+ {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 ( + + ); +} + +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 new file mode 100644 index 000000000..d413c538b --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/deployLogic.ts @@ -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 { + 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 { + 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/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 new file mode 100644 index 000000000..0e10b3f55 --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/page.tsx @@ -0,0 +1,265 @@ +"use client"; + +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"; +import { Message } from "@/src/components/Message"; +import { useApp } from "@/src/context"; +import { useGetExplorerLink } from "@/src/utils"; +import { ccc } from "@ckb-ccc/connector-react"; +import { useCallback, useState } from "react"; +import { + BurnButton, + CellFoundSection, + ClearSelectionButton, + LoadingMessage, + TypeIdCellButton, +} from "./deployComponents"; +import { runBurn, runDeploy } from "./deployLogic"; +import { useDeployScript } from "./useDeployScript"; + +export default function DeployScript() { + const { createSender } = useApp(); + const { log, error } = createSender("Deploy Script"); + const { explorerTransaction } = useGetExplorerLink(); + + const [file, setFile] = useState(null); + const [isDeploying, setIsDeploying] = useState(false); + const [isWaitingConfirmation, setIsWaitingConfirmation] = useState(false); + const [confirmationMessage, setConfirmationMessage] = useState(""); + const [confirmationTxHash, setConfirmationTxHash] = useState(""); + + const { + signer, + userAddress, + typeIdArgs, + setTypeIdArgs, + typeIdCells, + cellCreationTimestamps, + isScanningCells, + foundCell, + foundCellAddress, + isAddressMatch, + isCheckingCell, + cellCheckError, + 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"); + return; + } + if (!file) { + error("Please select a file to deploy"); + return; + } + + setIsDeploying(true); + try { + log("Reading file..."); + const txHash = await runDeploy( + signer, + file, + typeIdArgs, + foundCell, + isAddressMatch, + log, + error, + ); + + if (!txHash) { + setIsDeploying(false); + return; + } + + setIsWaitingConfirmation(true); + setConfirmationMessage("Waiting for transaction confirmation..."); + setConfirmationTxHash(txHash); + + log("Transaction sent:", explorerTransaction(txHash)); + await signer.client.waitTransaction(txHash); + log("Transaction committed:", explorerTransaction(txHash)); + refreshTypeIdCells(); + + setIsWaitingConfirmation(false); + setConfirmationMessage(""); + setConfirmationTxHash(""); + } catch (err) { + 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, + refreshTypeIdCells, + ]); + + const normalizedInput = normalizeTypeIdArgs(typeIdArgs); + + 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. 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, index) => { + const cellNorm = normalizeTypeIdArgs( + cell.cellOutput.type?.args || "", + ); + const isSelected = + cellNorm === normalizedInput && normalizedInput !== ""; + + return ( + handleSelectTypeIdCell(cell)} + isSelected={isSelected} + creationTimestamp={ + cellCreationTimestamps[ + ccc.hexFrom(cell.outPoint.toBytes()) + ] + } + /> + ); + })} +
+ {typeIdArgs && ( +
+ + +
+ )} +
+ )} + + + + {isCheckingCell && ( + + Searching for Type ID cell on-chain... + + )} + + {foundCell && !isCheckingCell && ( + + )} + + {cellCheckError && !isCheckingCell && ( + + {cellCheckError} + + )} + + + + + + +
+ + ); +} 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..7c36bb6f3 --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/DeployScript/useDeployScript.ts @@ -0,0 +1,242 @@ +"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 [cellCreationTimestamps, setCellCreationTimestamps] = useState< + Record + >({}); + const [refreshTrigger, setRefreshTrigger] = useState(0); + + 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 (runs on signer change or force refresh) + const refreshTypeIdCells = useCallback(() => { + setRefreshTrigger((t) => t + 1); + }, []); + + 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, 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(() => { + 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, + cellCreationTimestamps, + isScanningCells, + foundCell, + foundCellAddress, + isAddressMatch, + isCheckingCell, + cellCheckError, + handleSelectTypeIdCell, + clearSelection, + normalizeTypeIdArgs, + refreshTypeIdCells, + }; +} 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/app/utils/(tools)/FileUpload/page.tsx b/packages/demo/src/app/utils/(tools)/FileUpload/page.tsx new file mode 100644 index 000000000..295710ad0 --- /dev/null +++ b/packages/demo/src/app/utils/(tools)/FileUpload/page.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { Button } from "@/src/components/Button"; +import { Upload, X } from "lucide-react"; +import { useRef, useState } from "react"; + +export 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", + }); +} + +export 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); + }); +} + +export default function FileUploadArea({ + file, + onFileChange, +}: { + file: File | null; + onFileChange: (file: File | null) => void; +}) { + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + + const handleFileSelect = (selectedFile: File) => { + onFileChange(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 = () => { + onFileChange(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + 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))} +

+
+
+ +
+ +
+ )} +
+ ); +} 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..fd4af0d05 --- /dev/null +++ b/packages/demo/src/app/utils/(tools)/TxConfirm/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { Loader2 } from "lucide-react"; + +export default function TxConfirm({ + isOpen, + message, + txHash, +}: { + isOpen: boolean; + message: string; + txHash?: string; +}) { + if (!isOpen) return null; + + return ( +
+
+
+ +
+

{message}

+ {txHash && ( +

{txHash}

+ )} +

+ Please wait for transaction confirmation... +

+
+
+
+
+ ); +} diff --git a/packages/demo/src/components/Message.tsx b/packages/demo/src/components/Message.tsx index 340921727..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,18 +61,18 @@ export function Message({
) : undefined}
-

+

{children} -

+
);