diff --git a/toolbox/src/demo/ToolboxApp.tsx b/toolbox/src/demo/ToolboxApp.tsx index 1000be8ea8c..7c304406513 100644 --- a/toolbox/src/demo/ToolboxApp.tsx +++ b/toolbox/src/demo/ToolboxApp.tsx @@ -21,6 +21,22 @@ const componentGroups: Record = { fileNames: ["toolbox/src/demo/examples/Wallet/SwitchChain.tsx"] } ], + 'Conversion': [ + { + id: 'formatConverter', + label: "Format Converter", + component: lazy(() => import('./examples/Conversion/FormatConverter')), + fileNames: [], + skipWalletConnection: true, + }, + { + id: 'unitConverter', + label: "Unit Converter", + component: lazy(() => import('./examples/Conversion/UnitConverter')), + fileNames: [], + skipWalletConnection: true, + } + ], 'Create an L1': [ { id: 'createSubnet', diff --git a/toolbox/src/demo/examples/Conversion/FormatConverter.tsx b/toolbox/src/demo/examples/Conversion/FormatConverter.tsx new file mode 100644 index 00000000000..395296c4996 --- /dev/null +++ b/toolbox/src/demo/examples/Conversion/FormatConverter.tsx @@ -0,0 +1,381 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { utils } from "@avalabs/avalanchejs"; +import { Copy, Check } from "lucide-react"; +import { Button } from "../../../components/button"; +import { Input } from "../../../components/input"; +import { Container } from "../../../components/container"; + +// Utility functions for conversions +const hexToBytes = (hex: string): Uint8Array => { + // Remove 0x prefix if present + hex = hex.startsWith("0x") ? hex.slice(2) : hex; + // Ensure even length + if (hex.length % 2 !== 0) { + hex = "0" + hex; + } + + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; +}; + +const bytesToHex = (bytes: Uint8Array): string => { + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); +}; + +const cleanHexString = (hex: string): string => { + // Remove non-hex characters but preserve 0x prefix if present + const hasPrefix = hex.startsWith("0x"); + const cleaned = hex.replace(/^0x/, "").replace(/[^0-9a-fA-F]/g, ""); + return hasPrefix ? "0x" + cleaned : cleaned; +}; + +// Add a new formatHexString function that adds spaces between bytes +const formatHexString = (hex: string): string => { + // First clean the hex string + let cleanedHex = cleanHexString(hex); + + // Check if there's a 0x prefix and handle it + const hasPrefix = cleanedHex.startsWith("0x"); + if (hasPrefix) { + cleanedHex = cleanedHex.slice(2); + } + + // Insert a space after every 2 characters (1 byte) + let formattedHex = ""; + for (let i = 0; i < cleanedHex.length; i += 2) { + formattedHex += cleanedHex.slice(i, i + 2) + " "; + } + + // Trim the trailing space and add the prefix if needed + formattedHex = formattedHex.trim(); + return hasPrefix ? "0x " + formattedHex : formattedHex; +}; + +// Convert hex to CB58 +const hexToCB58 = (hex: string): string => { + try { + // First validate it's a valid hex string + const cleanedHex = cleanHexString(hex); + if (cleanedHex.length === 0) { + throw new Error("Empty hex string"); + } + + // Ensure it's a valid hex string (should only contain hex characters) + if (!/^[0-9a-fA-F]+$/.test(cleanedHex)) { + throw new Error("Invalid hex string: contains non-hex characters"); + } + + // Ensure it has an even length (2 hex chars = 1 byte) + if (cleanedHex.length % 2 !== 0) { + throw new Error("Invalid hex string: length must be even"); + } + + const bytes = hexToBytes(cleanedHex); + return utils.base58check.encode(bytes); + } catch (error) { + throw error instanceof Error ? error : new Error("Invalid hex string"); + } +}; + +// Convert CB58 to hex +const cb58ToHex = (cb58: string): string => { + try { + if (!cb58 || cb58.trim() === "") { + throw new Error("Empty CB58 string"); + } + + const bytes = utils.base58check.decode(cb58); + return bytesToHex(bytes); + } catch (error) { + throw error instanceof Error ? error : new Error("Invalid CB58 string"); + } +}; + +// For CB58 to hex with checksum, we add 0x prefix and preserve the checksum +//instead of using utils.base58check.decode() which removes the checksum, use base58 directly and manually work with the raw bytes +const cb58ToHexWithChecksum = (cb58: string): string => { + try { + if (!cb58 || cb58.trim() === "") { + throw new Error("Empty CB58 string"); + } + + // Step 1: Decode from Base58 (without check) to get raw bytes including checksum + const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + let value = 0n; + let base = 1n; + + // Decode from right to left + for (let i = cb58.length - 1; i >= 0; i--) { + const charIndex = base58Alphabet.indexOf(cb58[i]); + if (charIndex === -1) { + throw new Error("Invalid Base58 character"); + } + value += BigInt(charIndex) * base; + base *= 58n; + } + + // Convert to bytes + let valueHex = value.toString(16); + // Ensure even length + if (valueHex.length % 2 !== 0) { + valueHex = "0" + valueHex; + } + + // Account for leading zeros in Base58 encoding + // Each leading '1' in Base58 represents a leading zero byte + let leadingZeros = ""; + for (let i = 0; i < cb58.length; i++) { + if (cb58[i] === '1') { + leadingZeros += "00"; + } else { + break; + } + } + + // The full hex string with 0x prefix + return "0x" + leadingZeros + valueHex; + } catch (error) { + throw error instanceof Error ? error : new Error("Invalid CB58 string"); + } +}; + +// CopyableSuccess component with clipboard functionality +const CopyableSuccess = ({ label, value }: { label: string; value: string }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(value); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }, [value]); + + return ( +
+
+
+

{label}:

+ +
+ +
+
+

{value}

+
+
+ ); +}; + +export default function FormatConverter() { + // State for different conversion types + const [hexToConvert, setHexToConvert] = useState(""); + const [cb58ToConvert, setCb58ToConvert] = useState(""); + const [cb58WithChecksumToConvert, setCb58WithChecksumToConvert] = useState(""); + const [hexToClean, setHexToClean] = useState(""); + const [hexToUnformat, setHexToUnformat] = useState(""); + + // Results + const [hexToCb58Result, setHexToCb58Result] = useState(""); + const [cb58ToHexResult, setCb58ToHexResult] = useState(""); + const [cb58ToHexWithChecksumResult, setCb58ToHexWithChecksumResult] = useState(""); + const [cleanHexResult, setCleanHexResult] = useState(""); + const [unformatHexResult, setUnformatHexResult] = useState(""); + + // Error states + const [hexToCb58Error, setHexToCb58Error] = useState(""); + const [cb58ToHexError, setCb58ToHexError] = useState(""); + const [cb58ToHexWithChecksumError, setCb58ToHexWithChecksumError] = useState(""); + + // Conversion handlers + const handleHexToCb58Convert = useCallback(() => { + try { + setHexToCb58Error(""); + const result = hexToCB58(hexToConvert); + setHexToCb58Result(result); + } catch (error) { + setHexToCb58Error(error instanceof Error ? error.message : "Conversion failed"); + setHexToCb58Result(""); + } + }, [hexToConvert]); + + const handleCb58ToHexConvert = useCallback(() => { + try { + setCb58ToHexError(""); + const result = cb58ToHex(cb58ToConvert); + setCb58ToHexResult(result); + } catch (error) { + setCb58ToHexError(error instanceof Error ? error.message : "Conversion failed"); + setCb58ToHexResult(""); + } + }, [cb58ToConvert]); + + const handleCb58ToHexWithChecksumConvert = useCallback(() => { + try { + setCb58ToHexWithChecksumError(""); + const result = cb58ToHexWithChecksum(cb58WithChecksumToConvert); + setCb58ToHexWithChecksumResult(result); + } catch (error) { + setCb58ToHexWithChecksumError(error instanceof Error ? error.message : "Conversion failed"); + setCb58ToHexWithChecksumResult(""); + } + }, [cb58WithChecksumToConvert]); + + const handleCleanHex = useCallback(() => { + // Format the hex string with spaces between bytes + const result = formatHexString(hexToClean); + setCleanHexResult(result); + }, [hexToClean]); + + const handleUnformatHex = useCallback(() => { + // Remove all whitespace while preserving 0x prefix + const hasPrefix = hexToUnformat.trim().startsWith("0x"); + const cleaned = hexToUnformat.replace(/\s+/g, "").replace(/^0x/, ""); + const result = hasPrefix ? "0x" + cleaned : cleaned; + setUnformatHexResult(result); + }, [hexToUnformat]); + + return ( + +
+ {/* Hex to CB58 */} +
+

Hex to CB58 Encoded

+ setHexToConvert(value)} + placeholder="Enter hex value (must be even length)" + helperText={hexToCb58Error ? hexToCb58Error : ""} + /> + + {hexToCb58Result && !hexToCb58Error && ( + + )} +
+ + {/* CB58 to Hex */} +
+

CB58 Encoded to Hex

+ setCb58ToConvert(value)} + placeholder="Enter CB58 encoded value" + helperText={cb58ToHexError ? cb58ToHexError : ""} + /> + + {cb58ToHexResult && !cb58ToHexError && ( + + )} +
+ + {/* CB58 to Hex with Checksum */} +
+

CB58 Encoded to Hex with Checksum

+

+ This tool converts a CB58 encoded string to a hex string with checksum. It has 0x as prefix and 4 bytes + checksum at the end. It won't work if you just copy+paste the hex string into "Hex to CB58 encoded" tool. +

+ setCb58WithChecksumToConvert(value)} + placeholder="Enter CB58 encoded value" + helperText={cb58ToHexWithChecksumError ? cb58ToHexWithChecksumError : ""} + /> + + {cb58ToHexWithChecksumResult && !cb58ToHexWithChecksumError && ( + + )} +
+ + {/* Clean Hex String */} +
+

Clean Hex String

+

+ Formats hex by adding spaces between each byte. Preserves 0x prefix if present. +

+ setHexToClean(value)} + placeholder="Enter hex value to format (e.g. 0x3213213322aab101)" + /> + + {cleanHexResult && ( + + )} +
+ + {/* Unformat Hex */} +
+

Unformat Hex

+

+ Removes all spaces from hex string. Preserves 0x prefix if present. +

+ setHexToUnformat(value)} + placeholder="Enter formatted hex value (e.g. 0x 32 13 21 33 22 aa b1 01)" + /> + + {unformatHexResult && ( + + )} +
+
+
+ ); +} diff --git a/toolbox/src/demo/examples/Conversion/UnitConverter.tsx b/toolbox/src/demo/examples/Conversion/UnitConverter.tsx new file mode 100644 index 00000000000..f47a1001337 --- /dev/null +++ b/toolbox/src/demo/examples/Conversion/UnitConverter.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useErrorBoundary } from "react-error-boundary"; +import { useState, useEffect } from "react"; +import { Copy, Check } from "lucide-react"; +import { Button } from "../../../components/button"; +import { Container } from "../../../components/container"; + +export default function UnitConverter() { + const { showBoundary } = useErrorBoundary(); + const [amount, setAmount] = useState("1"); + const [selectedUnit, setSelectedUnit] = useState("AVAX"); + const [results, setResults] = useState>({}); + const [copied, setCopied] = useState(null); + + const units = [ + { id: "wei", label: "Wei 10⁻¹⁸", factor: BigInt("1"), exponent: -18 }, + { id: "kwei", label: "KWei", factor: BigInt("1000"), exponent: -15 }, + { id: "mwei", label: "MWei", factor: BigInt("1000000"), exponent: -12 }, + { id: "nAVAX", label: "nAVAX (10⁻⁹)", factor: BigInt("1000000000"), exponent: -9 }, + { id: "uAVAX", label: "µAVAX", factor: BigInt("1000000000000"), exponent: -6 }, + { id: "mAVAX", label: "mAVAX", factor: BigInt("1000000000000000"), exponent: -3 }, + { id: "AVAX", label: "AVAX", factor: BigInt("1000000000000000000"), exponent: 0 }, + { id: "kAVAX", label: "kAVAX", factor: BigInt("1000000000000000000000"), exponent: 3 }, + { id: "MAVAX", label: "MAVAX", factor: BigInt("1000000000000000000000000"), exponent: 6 }, + { id: "GAVAX", label: "GAVAX", factor: BigInt("1000000000000000000000000000"), exponent: 9 }, + { id: "TAVAX", label: "TAVAX", factor: BigInt("1000000000000000000000000000000"), exponent: 12 } + ]; + + const convertUnits = (inputAmount: string, fromUnit: string) => { + try { + if (!inputAmount || isNaN(Number(inputAmount))) { + return {}; + } + + const sourceUnit = units.find(u => u.id === fromUnit)!; + + let baseAmount: bigint; + try { + if (inputAmount.includes('.')) { + const [whole, decimal] = inputAmount.split('.'); + const wholeValue = whole === '' ? BigInt(0) : BigInt(whole); + const wholeInWei = wholeValue * sourceUnit.factor; + + const decimalPlaces = decimal.length; + const decimalValue = BigInt(decimal); + const decimalFactor = sourceUnit.factor / BigInt(10 ** decimalPlaces); + const decimalInWei = decimalValue * decimalFactor; + + baseAmount = wholeInWei + decimalInWei; + } else { + baseAmount = BigInt(inputAmount) * sourceUnit.factor; + } + } catch (error) { + throw new Error("Error converting: please verify that the number is valid"); + } + + const results: Record = {}; + units.forEach(unit => { + if (baseAmount === BigInt(0)) { + results[unit.id] = "0"; + return; + } + + const quotient = baseAmount / unit.factor; + const remainder = baseAmount % unit.factor; + + if (remainder === BigInt(0)) { + results[unit.id] = quotient.toString(); + } else { + const decimalPart = remainder.toString().padStart(unit.factor.toString().length - 1, '0'); + const trimmedDecimal = decimalPart.replace(/0+$/, ''); + results[unit.id] = `${quotient}.${trimmedDecimal}`; + } + }); + + return results; + } catch (error) { + showBoundary(error); + return {}; + } + }; + + const handleInputChange = (value: string, unit: string) => { + setAmount(value); + setSelectedUnit(unit); + }; + + const handleReset = () => { + setAmount("1"); + setSelectedUnit("AVAX"); + }; + + const handleCopy = (value: string, unitId: string) => { + navigator.clipboard.writeText(value); + setCopied(unitId); + setTimeout(() => setCopied(null), 2000); + }; + + useEffect(() => { + setResults(convertUnits(amount, selectedUnit)); + }, [amount, selectedUnit]); + + return ( + +
+
+

+ AVAX is the native token used to pay gas on Avalanche's Primary Network. Each Avalanche L1 has only 1 token used to pay for + network fees on that specific Avalanche L1, this is defined by the Avalanche L1 deployer. +

+

+ Varying denominations such as Gwei and Wei are commonly used when interacting with cryptocurrency. Use this converter + to easily navigate between them. +

+
+ +
+ {units.map((unit) => ( +
+
+ + {unit.label} + +
+
+ handleInputChange(e.target.value, unit.id)} + placeholder="0" + step={unit.exponent < 0 ? 0.000000001 : 1} + className="w-full rounded-md px-3 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary shadow-sm transition-colors duration-200 rounded-r-none border-r-0" + /> + +
+
+ ))} +
+ + +
+
+ ); +} \ No newline at end of file