From d799e07f57a82436fb2f9aac732f0e37855d6193 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Feb 2026 20:29:40 +0000 Subject: [PATCH] cmd/ethunit: add Ethereum denomination conversion tool A small CLI utility that converts values between wei, gwei, and ether. Supports fractional values and is useful for quick denomination lookups during development. https://claude.ai/code/session_01A9zYMuSNDYCY3H5DdbD5vG --- cmd/ethunit/main.go | 149 +++++++++++++++++++++++++++++++++++++++ cmd/ethunit/main_test.go | 84 ++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 cmd/ethunit/main.go create mode 100644 cmd/ethunit/main_test.go diff --git a/cmd/ethunit/main.go b/cmd/ethunit/main.go new file mode 100644 index 0000000000..060ce42c97 --- /dev/null +++ b/cmd/ethunit/main.go @@ -0,0 +1,149 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +// ethunit converts values between Ethereum denominations (wei, gwei, ether). +package main + +import ( + "flag" + "fmt" + "math/big" + "os" + "strings" +) + +var ( + fromUnit = flag.String("from", "wei", "source denomination (wei, gwei, ether)") + toUnit = flag.String("to", "ether", "target denomination (wei, gwei, ether)") +) + +// unitMultipliers maps denomination names to their wei multipliers. +var unitMultipliers = map[string]*big.Int{ + "wei": new(big.Int).SetUint64(1), + "gwei": new(big.Int).SetUint64(1e9), + "ether": new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil), +} + +func init() { + flag.Usage = func() { + fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "[-from unit] [-to unit] ") + flag.PrintDefaults() + fmt.Fprintln(os.Stderr, ` +Converts a value between Ethereum denominations. + +Supported units: wei, gwei, ether + +Examples: + ethunit -from wei -to ether 1000000000000000000 + ethunit -from ether -to gwei 1.5 + ethunit -from gwei -to wei 100`) + } +} + +func main() { + flag.Parse() + if flag.NArg() != 1 { + flag.Usage() + os.Exit(1) + } + result, err := convert(flag.Arg(0), *fromUnit, *toUnit) + if err != nil { + die(err) + } + fmt.Println(result) +} + +// convert converts a decimal string value from one denomination to another. +func convert(value, from, to string) (string, error) { + from = strings.ToLower(from) + to = strings.ToLower(to) + + fromMul, ok := unitMultipliers[from] + if !ok { + return "", fmt.Errorf("unknown source unit %q", from) + } + toMul, ok := unitMultipliers[to] + if !ok { + return "", fmt.Errorf("unknown target unit %q", to) + } + + // Parse input as a decimal number. We support both integer and fractional + // values by splitting on the decimal point and scaling to wei. + weiValue, err := parseToWei(value, fromMul) + if err != nil { + return "", err + } + + return formatFromWei(weiValue, toMul), nil +} + +// parseToWei parses a decimal string and returns the value in wei. +func parseToWei(s string, multiplier *big.Int) (*big.Int, error) { + parts := strings.SplitN(s, ".", 2) + if len(parts) == 1 { + // Integer value. + intPart, ok := new(big.Int).SetString(parts[0], 10) + if !ok { + return nil, fmt.Errorf("invalid number %q", s) + } + return intPart.Mul(intPart, multiplier), nil + } + // Fractional value: combine integer and fractional parts into a single + // integer, then divide out the fractional scaling. + intPart, ok := new(big.Int).SetString(parts[0], 10) + if !ok { + return nil, fmt.Errorf("invalid number %q", s) + } + fracStr := parts[1] + // Remove trailing zeros for cleanliness, but track original length for scale. + fracLen := len(fracStr) + fracPart, ok := new(big.Int).SetString(fracStr, 10) + if !ok { + return nil, fmt.Errorf("invalid number %q", s) + } + // scale = 10^fracLen + scale := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(fracLen)), nil) + + // weiValue = intPart * multiplier + fracPart * multiplier / scale + weiFromInt := new(big.Int).Mul(intPart, multiplier) + weiFromFrac := new(big.Int).Mul(fracPart, multiplier) + weiFromFrac.Div(weiFromFrac, scale) + + return weiFromInt.Add(weiFromInt, weiFromFrac), nil +} + +// formatFromWei formats a wei value into the target denomination. +func formatFromWei(wei *big.Int, divisor *big.Int) string { + if divisor.Cmp(big.NewInt(1)) == 0 { + return wei.String() + } + whole := new(big.Int).Div(wei, divisor) + remainder := new(big.Int).Mod(wei, divisor) + + if remainder.Sign() == 0 { + return whole.String() + } + // Format remainder with leading zeros to match divisor magnitude. + divisorStr := divisor.String() + fracStr := fmt.Sprintf("%0*s", len(divisorStr)-1, remainder.String()) + fracStr = strings.TrimRight(fracStr, "0") + return whole.String() + "." + fracStr +} + +func die(args ...interface{}) { + fmt.Fprintln(os.Stderr, args...) + os.Exit(1) +} diff --git a/cmd/ethunit/main_test.go b/cmd/ethunit/main_test.go new file mode 100644 index 0000000000..c3bbd7b6b0 --- /dev/null +++ b/cmd/ethunit/main_test.go @@ -0,0 +1,84 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import "testing" + +func TestConvert(t *testing.T) { + tests := []struct { + value string + from string + to string + want string + }{ + // Wei to ether + {"1000000000000000000", "wei", "ether", "1"}, + {"500000000000000000", "wei", "ether", "0.5"}, + {"1", "wei", "ether", "0.000000000000000001"}, + + // Ether to wei + {"1", "ether", "wei", "1000000000000000000"}, + {"0.5", "ether", "wei", "500000000000000000"}, + {"1.5", "ether", "wei", "1500000000000000000"}, + + // Gwei conversions + {"1000000000", "gwei", "ether", "1"}, + {"1", "ether", "gwei", "1000000000"}, + {"1000000000", "wei", "gwei", "1"}, + {"1", "gwei", "wei", "1000000000"}, + + // Identity conversions + {"42", "wei", "wei", "42"}, + {"1", "ether", "ether", "1"}, + {"100", "gwei", "gwei", "100"}, + + // Case insensitivity + {"1", "ETHER", "WEI", "1000000000000000000"}, + {"1", "Gwei", "Wei", "1000000000"}, + } + for _, tt := range tests { + t.Run(tt.value+"_"+tt.from+"_to_"+tt.to, func(t *testing.T) { + got, err := convert(tt.value, tt.from, tt.to) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("convert(%q, %q, %q) = %q, want %q", tt.value, tt.from, tt.to, got, tt.want) + } + }) + } +} + +func TestConvertErrors(t *testing.T) { + tests := []struct { + value string + from string + to string + }{ + {"abc", "wei", "ether"}, + {"1", "foo", "ether"}, + {"1", "wei", "bar"}, + } + for _, tt := range tests { + t.Run(tt.value+"_"+tt.from+"_to_"+tt.to, func(t *testing.T) { + _, err := convert(tt.value, tt.from, tt.to) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + } +}