From 3bc65002db9b173555078d09ee566a26a2253bc7 Mon Sep 17 00:00:00 2001 From: federiconardelli7 Date: Mon, 17 Mar 2025 10:28:32 +0100 Subject: [PATCH 1/4] added the format converter in the toolbox --- toolbox/src/demo/ToolboxApp.tsx | 9 + .../examples/Conversion/FormatConverter.tsx | 358 ++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 toolbox/src/demo/examples/Conversion/FormatConverter.tsx diff --git a/toolbox/src/demo/ToolboxApp.tsx b/toolbox/src/demo/ToolboxApp.tsx index 6c6914ffeeb..456b44b8eba 100644 --- a/toolbox/src/demo/ToolboxApp.tsx +++ b/toolbox/src/demo/ToolboxApp.tsx @@ -27,6 +27,15 @@ 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, + } + ], '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..b39749c6e13 --- /dev/null +++ b/toolbox/src/demo/examples/Conversion/FormatConverter.tsx @@ -0,0 +1,358 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { Button, Input } from "../../ui"; +import { utils } from "@avalabs/avalanchejs"; +import { Copy, Check } from "lucide-react"; + +// 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

+ + + {hexToCb58Result && !hexToCb58Error && ( + + )} +
+ + {/* CB58 to Hex */} +
+

CB58 Encoded to Hex

+ + + {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. +
+ + + {cb58ToHexWithChecksumResult && !cb58ToHexWithChecksumError && ( + + )} +
+ + {/* Clean Hex String */} +
+

Clean Hex String

+
+ Formats hex by adding spaces between each byte. Preserves 0x prefix if present. +
+ + + {cleanHexResult && ( + + )} +
+ + {/* Unformat Hex */} +
+

Unformat Hex

+
+ Removes all spaces from hex string. Preserves 0x prefix if present. +
+ + + {unformatHexResult && ( + + )} +
+
+ ); +} From 8fa061b627fe94aaaa10649d67ce4c2b4481eaba Mon Sep 17 00:00:00 2001 From: federiconardelli7 Date: Tue, 25 Mar 2025 15:12:21 +0100 Subject: [PATCH 2/4] added UnitConverter to Conversion section --- toolbox/src/demo/ToolboxApp.tsx | 7 + .../examples/Conversion/UnitConverter.tsx | 164 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 toolbox/src/demo/examples/Conversion/UnitConverter.tsx diff --git a/toolbox/src/demo/ToolboxApp.tsx b/toolbox/src/demo/ToolboxApp.tsx index 456b44b8eba..0ca5be9d890 100644 --- a/toolbox/src/demo/ToolboxApp.tsx +++ b/toolbox/src/demo/ToolboxApp.tsx @@ -34,6 +34,13 @@ const componentGroups: Record = { 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': [ diff --git a/toolbox/src/demo/examples/Conversion/UnitConverter.tsx b/toolbox/src/demo/examples/Conversion/UnitConverter.tsx new file mode 100644 index 00000000000..48808137212 --- /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 { Button, Input } from "../../ui"; +import { Copy, Check } from "lucide-react"; + +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 ( +
+
+

Unit Converter

+ +
+

+ 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(value, unit.id); + }} + className="flex-grow" + placeholder="0" + type="number" + step={unit.exponent < 0 ? 0.000000001 : 1} + /> + +
+ ))} +
+ + +
+
+
+ ); +} \ No newline at end of file From 32a98d90611c58febb020c2110bb15095d74aa90 Mon Sep 17 00:00:00 2001 From: federiconardelli7 Date: Tue, 25 Mar 2025 16:19:47 +0100 Subject: [PATCH 3/4] apply new UI components to the conversionUnit and conversionFormat tools --- .../examples/Conversion/FormatConverter.tsx | 217 ++++++++++-------- .../examples/Conversion/UnitConverter.tsx | 79 +++---- 2 files changed, 160 insertions(+), 136 deletions(-) diff --git a/toolbox/src/demo/examples/Conversion/FormatConverter.tsx b/toolbox/src/demo/examples/Conversion/FormatConverter.tsx index b39749c6e13..395296c4996 100644 --- a/toolbox/src/demo/examples/Conversion/FormatConverter.tsx +++ b/toolbox/src/demo/examples/Conversion/FormatConverter.tsx @@ -1,9 +1,11 @@ "use client"; import { useState, useCallback } from "react"; -import { Button, Input } from "../../ui"; 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 => { @@ -155,15 +157,15 @@ const CopyableSuccess = ({ label, value }: { label: string; value: string }) => }, [value]); return ( -
+
-

{label}:

+

{label}:

-

{value}

+

{value}

); @@ -255,104 +257,125 @@ export default function FormatConverter() { }, [hexToUnformat]); return ( -
- {/* Hex to CB58 */} -
-

Hex to CB58 Encoded

- - - {hexToCb58Result && !hexToCb58Error && ( - - )} -
+ +
+ {/* 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

- - - {cb58ToHexResult && !cb58ToHexError && ( - - )} -
+ {/* 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. + {/* 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 && ( + + )}
- - - {cb58ToHexWithChecksumResult && !cb58ToHexWithChecksumError && ( - - )} -
- {/* Clean Hex String */} -
-

Clean Hex String

-
- Formats hex by adding spaces between each byte. Preserves 0x prefix if present. + {/* 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 && ( + + )}
- - - {cleanHexResult && ( - - )} -
- {/* Unformat Hex */} -
-

Unformat Hex

-
- Removes all spaces from hex string. Preserves 0x prefix if present. + {/* 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 && ( + + )}
- - - {unformatHexResult && ( - - )}
-
+ ); } diff --git a/toolbox/src/demo/examples/Conversion/UnitConverter.tsx b/toolbox/src/demo/examples/Conversion/UnitConverter.tsx index 48808137212..3146b466e5f 100644 --- a/toolbox/src/demo/examples/Conversion/UnitConverter.tsx +++ b/toolbox/src/demo/examples/Conversion/UnitConverter.tsx @@ -2,8 +2,10 @@ import { useErrorBoundary } from "react-error-boundary"; import { useState, useEffect } from "react"; -import { Button, Input } from "../../ui"; import { Copy, Check } from "lucide-react"; +import { Button } from "../../../components/button"; +import { Input } from "../../../components/input"; +import { Container } from "../../../components/container"; export default function UnitConverter() { const { showBoundary } = useErrorBoundary(); @@ -101,64 +103,63 @@ export default function UnitConverter() { }, [amount, selectedUnit]); return ( -
+
-

Unit Converter

- -
-

+

+

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} - -
- + {units.map((unit) => ( +
+
+ + {unit.label} + +
+
+ { - handleInputChange(value, unit.id); - }} - className="flex-grow" + onChange={(e) => handleInputChange(e.target.value, unit.id)} placeholder="0" - type="number" - step={unit.exponent < 0 ? 0.000000001 : 1} + 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 From f4490bbad110df85dfaf920e68cec4c1e0812366 Mon Sep 17 00:00:00 2001 From: federiconardelli7 Date: Tue, 25 Mar 2025 16:39:25 +0100 Subject: [PATCH 4/4] removed unused dependency --- toolbox/src/demo/examples/Conversion/UnitConverter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/toolbox/src/demo/examples/Conversion/UnitConverter.tsx b/toolbox/src/demo/examples/Conversion/UnitConverter.tsx index 3146b466e5f..f47a1001337 100644 --- a/toolbox/src/demo/examples/Conversion/UnitConverter.tsx +++ b/toolbox/src/demo/examples/Conversion/UnitConverter.tsx @@ -4,7 +4,6 @@ import { useErrorBoundary } from "react-error-boundary"; import { useState, useEffect } from "react"; import { Copy, Check } from "lucide-react"; import { Button } from "../../../components/button"; -import { Input } from "../../../components/input"; import { Container } from "../../../components/container"; export default function UnitConverter() {