diff --git a/components/tools/common/utils/cb58.ts b/components/tools/common/utils/cb58.ts index b18aeb84381..c2496d54371 100644 --- a/components/tools/common/utils/cb58.ts +++ b/components/tools/common/utils/cb58.ts @@ -19,8 +19,15 @@ export function cb58ToBytes(cb58: string): Uint8Array { } -export function cb58ToHex(cb58: string, include0x: boolean = true): string { - const rawBytes = cb58ToBytes(cb58); +export function cb58ToHex(cb58: string, include0x: boolean = true, checkSum: boolean = false): string { + let rawBytes = cb58ToBytes(cb58); + if (checkSum) { + const checksum = calculateChecksum(rawBytes); + const combined = new Uint8Array(rawBytes.length + CHECKSUM_LENGTH); + combined.set(rawBytes); + combined.set(checksum, rawBytes.length); + rawBytes = combined; + } return (include0x ? '0x' : '') + bytesToHex(rawBytes); } @@ -32,7 +39,7 @@ export function hexToCB58(hex: string): string { const combined = new Uint8Array(bytes.length + CHECKSUM_LENGTH); combined.set(bytes); combined.set(checksum, bytes.length); - + return base58.encode(combined); } diff --git a/toolbox/src/demo/ToolboxApp.tsx b/toolbox/src/demo/ToolboxApp.tsx index 6c6914ffeeb..9cdfbbbcda3 100644 --- a/toolbox/src/demo/ToolboxApp.tsx +++ b/toolbox/src/demo/ToolboxApp.tsx @@ -27,6 +27,20 @@ const componentGroups: Record = { fileNames: ["toolbox/src/demo/examples/Wallet/SwitchChain.tsx"] } ], + "Converter": [ + { + id: 'unitConverter', + label: "Unit Converter", + component: lazy(() => import('./examples/Converter/UnitConverter')), + fileNames: ["toolbox/src/demo/examples/Converter/UnitConverter.tsx"] + }, + { + id: 'cipherConverter', + label: "Cipher Converter", + component: lazy(() => import('./examples/Converter/CipherConverter')), + fileNames: ["toolbox/src/demo/examples/Converter/CipherConverter.tsx"] + }, + ], 'Create an L1': [ { id: 'createSubnet', diff --git a/toolbox/src/demo/examples/Converter/CipherConverter.tsx b/toolbox/src/demo/examples/Converter/CipherConverter.tsx new file mode 100644 index 00000000000..f4de7b5772c --- /dev/null +++ b/toolbox/src/demo/examples/Converter/CipherConverter.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useErrorBoundary } from "react-error-boundary"; +import { useState } from "react"; +import { Button, Input } from "../../ui"; +import { cb58ToHex, hexToCB58 } from "../../../../../components/tools/common/utils/cb58"; + +export default function CipherConverter() { + const { showBoundary } = useErrorBoundary(); + const [hexInput, setHexInput] = useState(""); + const [cb58Output, setCb58Output] = useState(""); + const [cb58Input, setCb58Input] = useState(""); + const [hexOutput, setHexOutput] = useState(""); + const [cb58ChecksumInput, setCb58ChecksumInput] = useState(""); + const [hexChecksumOutput, setHexChecksumOutput] = useState(""); + const [dirtyHexInput, setDirtyHexInput] = useState(""); + const [cleanHexOutput, setCleanHexOutput] = useState(""); + const [formattedHexInput, setFormattedHexInput] = useState(""); + const [unformattedHexOutput, setUnformattedHexOutput] = useState(""); + + const convertHexToCb58 = () => { + try { + if (!hexInput) return; + + const cleanHex = hexInput.startsWith('0x') ? hexInput.slice(2) : hexInput; + + if (!/^[0-9a-fA-F]+$/.test(cleanHex)) { + throw new Error("Invalid hexadecimal input"); + } + + const cb58 = hexToCB58(cleanHex); + + setCb58Output(cb58); + } catch (error) { + showBoundary(error); + } + }; + + const convertCb58ToHex = () => { + try { + if (!cb58Input) return; + + const hex = cb58ToHex(cb58Input); + + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + + setHexOutput(cleanHex); + } catch (error) { + showBoundary(error); + } + }; + + const convertCb58ToHexWithChecksum = () => { + try { + if (!cb58ChecksumInput) return; + + const hex = cb58ToHex(cb58ChecksumInput, false, true); + + setHexChecksumOutput(`${hex}`); + } catch (error) { + showBoundary(error); + } + }; + + const cleanHex = () => { + try { + if (!dirtyHexInput) return; + + const cleaned = dirtyHexInput.replace(/[^0-9a-fA-F]/g, ''); + + setCleanHexOutput(`${cleaned}`); + } catch (error) { + showBoundary(error); + } + }; + + const unformatHex = () => { + try { + if (!formattedHexInput) return; + const cleanHex = formattedHexInput.startsWith('0x') ? formattedHexInput.slice(2) : formattedHexInput; + const unformatted = cleanHex.replace(/\s+/g, ''); + setUnformattedHexOutput(unformatted); + } catch (error) { + showBoundary(error); + } + }; + + return ( +
+

Cipher Converter

+ +
+

+ Tools to convert between different encryption formats used in Avalanche. + Includes conversions between hexadecimal and CB58, cleaning of hexadecimal strings and more. +

+
+ +
+

Hex to cb58 encoded

+
+ + + {cb58Output && ( +
+
Result:
+
{cb58Output}
+
+ )} +
+
+ +
+

cb58 encoded to Hex

+
+ + + {hexOutput && ( +
+
Result:
+
{hexOutput}
+
+ )} +
+
+ +
+

cb58 encoded to Hex with checksum

+
+

+ This tool converts a cb58 encoded string to a hexadecimal string with checksum, + has 0x as a prefix and 4 bytes of checksum at the end. It will not work if you only copy and paste the + hexadecimal string into the "Hex to cb58 encoded" tool. +

+ + + {hexChecksumOutput && ( +
+
Result:
+
{hexChecksumOutput}
+
+ )} +
+
+ +
+

Clean Hex String

+
+ + + {cleanHexOutput && ( +
+
Result:
+
{cleanHexOutput}
+
+ )} +
+
+ +
+

Unformat Hex

+
+

+ Removes white spaces. +

+ + + {unformattedHexOutput && ( +
+
Result:
+
{unformattedHexOutput}
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/toolbox/src/demo/examples/Converter/UnitConverter.tsx b/toolbox/src/demo/examples/Converter/UnitConverter.tsx new file mode 100644 index 00000000000..43f2e4b3258 --- /dev/null +++ b/toolbox/src/demo/examples/Converter/UnitConverter.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useErrorBoundary } from "react-error-boundary"; +import { useState, useEffect } from "react"; +import { Button, Input } from "../../ui"; + +export default function UnitConverter() { + const { showBoundary } = useErrorBoundary(); + const [amount, setAmount] = useState("1"); + const [selectedUnit, setSelectedUnit] = useState("AVAX"); + const [results, setResults] = useState>({}); + + 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"); + }; + + 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. Network Tokens are used to pay for gas + on Avalanche L1s, and can be native to that Avalanche L1 or bridged from another chain. +

+

+ Varying denominations such as Gwei and Wei are commonly used when interacting with cryptocurrency. Use Avalanche's Unit 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