diff --git a/crypto/address.go b/crypto/address.go index 5c71574..ff25dba 100644 --- a/crypto/address.go +++ b/crypto/address.go @@ -18,6 +18,22 @@ func AddressFromPassphrase(passphrase string) (string, error) { return privateKey.ToAddress(), nil } +func AddressFromPublicKey(publicKeyHex string) (string, error) { + publicKey, err := PublicKeyFromHex(publicKeyHex) + if err != nil { + return "", err + } + return publicKey.ToAddress(), nil +} + +func AddressFromPrivateKey(privateKeyHex string) (string, error) { + privateKey, err := PrivateKeyFromHex(privateKeyHex) + if err != nil { + return "", err + } + return privateKey.ToAddress(), nil +} + func AddressToBytes(address string) ([]byte, error) { if !strings.HasPrefix(address, "0x") || len(address) != 2+AddressByteLength*2 { return nil, ErrInvalidAddress diff --git a/crypto/address_test.go b/crypto/address_test.go index 6cd3e99..4e7285f 100644 --- a/crypto/address_test.go +++ b/crypto/address_test.go @@ -15,6 +15,26 @@ func TestAddressFromPassphrase(t *testing.T) { assert.Equal(fixture.Data.Address, address) } +func TestAddressFromPublicKey(t *testing.T) { + fixture := GetIdentityFixture() + + address, err := AddressFromPublicKey(fixture.Data.PublicKey) + + assert := assert.New(t) + assert.NoError(err) + assert.Equal(fixture.Data.Address, address) +} + +func TestAddressFromPrivateKey(t *testing.T) { + fixture := GetIdentityFixture() + + address, err := AddressFromPrivateKey(fixture.Data.PrivateKey) + + assert := assert.New(t) + assert.NoError(err) + assert.Equal(fixture.Data.Address, address) +} + func TestValidateAddress(t *testing.T) { fixture := GetIdentityFixture() diff --git a/crypto/builder.go b/crypto/builder.go index 080edd8..dd07914 100644 --- a/crypto/builder.go +++ b/crypto/builder.go @@ -13,8 +13,6 @@ var ( DefaultGasLimit = big.NewInt(1_000_000) ) -// Concrete transaction-type builders (BuildTransfer, BuildVote, etc.) are -// layered on top of this. func NewTransaction() *Transaction { return &Transaction{ Nonce: big.NewInt(1), @@ -37,14 +35,14 @@ func BuildTransfer(to string, value *big.Int) (*Transaction, error) { } func BuildVote(validatorAddress string) (*Transaction, error) { - voteArg, err := AbiAddress(validatorAddress) + data, err := EncodeVoteData(validatorAddress) if err != nil { return nil, err } transaction := NewTransaction() transaction.To = ContractConsensus - transaction.Data = AbiEncodeFunctionCall(AbiSignatureVote, voteArg) + transaction.Data = data transaction.Vote = validatorAddress return transaction, nil @@ -53,7 +51,7 @@ func BuildVote(validatorAddress string) (*Transaction, error) { func BuildUnvote() *Transaction { transaction := NewTransaction() transaction.To = ContractConsensus - transaction.Data = AbiEncodeFunctionCall(AbiSignatureUnvote) + transaction.Data = EncodeUnvoteData() return transaction } @@ -92,7 +90,7 @@ func BuildValidatorUpdate(validatorPassphrase string) (*Transaction, error) { func BuildValidatorResignation() *Transaction { transaction := NewTransaction() transaction.To = ContractConsensus - transaction.Data = AbiEncodeFunctionCall(AbiSignatureResignValidator) + transaction.Data = EncodeValidatorResignationData() return transaction } @@ -122,13 +120,14 @@ func validateUsername(username string) error { } func BuildUsernameRegistration(username string) (*Transaction, error) { - if err := validateUsername(username); err != nil { + data, err := EncodeUsernameRegistrationData(username) + if err != nil { return nil, err } transaction := NewTransaction() transaction.To = ContractUsernames - transaction.Data = AbiEncodeFunctionCall(AbiSignatureRegisterUsername, AbiString(username)) + transaction.Data = data transaction.Username = username return transaction, nil @@ -137,7 +136,7 @@ func BuildUsernameRegistration(username string) (*Transaction, error) { func BuildUsernameResignation() *Transaction { transaction := NewTransaction() transaction.To = ContractUsernames - transaction.Data = AbiEncodeFunctionCall(AbiSignatureResignUsername) + transaction.Data = EncodeUsernameResignationData() return transaction } @@ -150,11 +149,7 @@ func BuildMultiPayment(addresses []string, amounts []*big.Int) (*Transaction, er return nil, errors.New("crypto: multi-payment requires at least one recipient") } - addressesArg, err := AbiAddressArray(addresses) - if err != nil { - return nil, err - } - amountsArg, err := AbiUint256Array(amounts) + data, err := EncodeMultiPaymentData(addresses, amounts) if err != nil { return nil, err } @@ -167,7 +162,7 @@ func BuildMultiPayment(addresses []string, amounts []*big.Int) (*Transaction, er transaction := NewTransaction() transaction.To = ContractMultipayment transaction.Value = total - transaction.Data = AbiEncodeFunctionCall(AbiSignatureMultipayment, addressesArg, amountsArg) + transaction.Data = data transaction.PaymentAddresses = addresses transaction.PaymentAmounts = amounts @@ -194,22 +189,14 @@ func BuildBatchTransfer(tokenAddress string, recipients []string, amounts []*big return nil, errors.New("crypto: batch transfer requires at least one recipient") } - tokenArg, err := AbiAddress(tokenAddress) - if err != nil { - return nil, err - } - recipientsArg, err := AbiAddressArray(recipients) - if err != nil { - return nil, err - } - amountsArg, err := AbiUint256Array(amounts) + data, err := EncodeBatchTransferData(tokenAddress, recipients, amounts) if err != nil { return nil, err } transaction := NewTransaction() transaction.To = ContractBatchTransfer - transaction.Data = AbiEncodeFunctionCall(AbiSignatureERC20BatchTransferFrom, tokenArg, recipientsArg, amountsArg) + transaction.Data = data return transaction, nil } @@ -240,18 +227,14 @@ func BuildTokenTransfer(tokenAddress string, recipient string, amount *big.Int) return nil, err } - recipientArg, err := AbiAddress(recipient) - if err != nil { - return nil, err - } - amountArg, err := AbiUint256(amount) + data, err := EncodeTokenTransferData(recipient, amount) if err != nil { return nil, err } transaction := NewTransaction() transaction.To = tokenAddress - transaction.Data = AbiEncodeFunctionCall(AbiSignatureERC20Transfer, recipientArg, amountArg) + transaction.Data = data return transaction, nil } diff --git a/crypto/fixtures.go b/crypto/fixtures.go index 6857ff4..5a0ceb4 100644 --- a/crypto/fixtures.go +++ b/crypto/fixtures.go @@ -77,12 +77,15 @@ func GetTransactionFixture(name string) TestingTransactionFixture { type TestingIdentityFixture struct { Data struct { - PrivateKey string `json:"privateKey,omitempty"` - PublicKey string `json:"publicKey,omitempty"` - Address string `json:"address,omitempty"` - WIF string `json:"wif,omitempty"` + PrivateKey string `json:"privateKey,omitempty"` + PublicKey string `json:"publicKey,omitempty"` + Address string `json:"address,omitempty"` + WIF string `json:"wif,omitempty"` + ValidatorPublicKey string `json:"validatorPublicKey,omitempty"` + ValidatorPrivateKey string `json:"validatorPrivateKey,omitempty"` } `json:"data,omitempty"` - Passphrase string `json:"passphrase,omitempty"` + Passphrase string `json:"passphrase,omitempty"` + SecondPassphrase string `json:"secondPassphrase,omitempty"` } type TestingMessageFixture struct { diff --git a/crypto/fixtures/identity.json b/crypto/fixtures/identity.json index 228d89b..bd9f200 100644 --- a/crypto/fixtures/identity.json +++ b/crypto/fixtures/identity.json @@ -1,9 +1,12 @@ { "data": { - "privateKey": "d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712", - "publicKey": "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192", - "address": "0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01", - "wif": "Ue7A6vSx7ewATPp2dA6UbJ8F39DbZwaHTqhD1MrhzmJqRJmvfZ6C" + "publicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "privateKey": "50829dd3b7ffbe2df401d730b5e60cea7520ba3f3a18e5b1490707667fb43fae", + "address": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "wif": "UZYnRZ8qpeQWTLeCNzw93guWSdKLmr2vHEWGG4sNv7TJofL7TZvy", + "validatorPublicKey": "b209f4a7454ae17c5808991dffbf204c747b851f351d2ce72a6e18903d0e2f609e0328ebbc3fb97cd4d3660b4bc156f1", + "validatorPrivateKey": "6ec4993df152b10e672567c1fdf854a4cee50708fa30986a7d9b259673099175" }, - "passphrase": "this is a top secret passphrase" + "passphrase": "found lobster oblige describe ready addict body brave live vacuum display salute lizard combine gift resemble race senior quality reunion proud tell adjust angle", + "secondPassphrase": "gold favorite math anchor detect march purpose such sausage crucial reform novel connect misery update episode invite salute barely garbage exclude winner visa cruise" } diff --git a/crypto/private_key.go b/crypto/private_key.go index 32172d9..ac229aa 100644 --- a/crypto/private_key.go +++ b/crypto/private_key.go @@ -2,6 +2,7 @@ package crypto import ( "crypto/sha256" + "errors" "fmt" "github.com/btcsuite/btcutil/base58" @@ -9,6 +10,8 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" ) +var ErrInvalidWif = errors.New("crypto: invalid WIF") + const ecdsaCurveByteLength = 32 // EcdsaSignature is an ECDSA secp256k1 recoverable signature: R and S are @@ -31,6 +34,33 @@ func PrivateKeyFromHex(privateKeyHex string) (*PrivateKey, error) { return PrivateKeyFromBytes(HexDecode(privateKeyHex)), nil } +func PrivateKeyFromWif(wif string) (*PrivateKey, error) { + decoded, version, err := base58.CheckDecode(wif) + if err != nil { + return nil, ErrInvalidWif + } + if version != GetNetwork().Wif { + return nil, ErrInvalidWif + } + + switch len(decoded) { + case ecdsaCurveByteLength: + return PrivateKeyFromBytes(decoded), nil + case ecdsaCurveByteLength + 1: + return PrivateKeyFromBytes(decoded[:ecdsaCurveByteLength]), nil + default: + return nil, ErrInvalidWif + } +} + +func WIFFromPassphrase(passphrase string) (string, error) { + privateKey, err := PrivateKeyFromPassphrase(passphrase) + if err != nil { + return "", err + } + return privateKey.ToWif(), nil +} + func PrivateKeyFromBytes(bytes []byte) *PrivateKey { privateKey := secp256k1.PrivKeyFromBytes(bytes) return &PrivateKey{ diff --git a/crypto/private_key_test.go b/crypto/private_key_test.go index d7b20c7..1333e79 100644 --- a/crypto/private_key_test.go +++ b/crypto/private_key_test.go @@ -32,3 +32,30 @@ func TestPrivateKeyToWif(t *testing.T) { assert := assert.New(t) assert.Equal(fixture.Data.WIF, privateKey.ToWif()) } + +func TestPrivateKeyFromWif(t *testing.T) { + fixture := GetIdentityFixture() + + privateKey, err := PrivateKeyFromWif(fixture.Data.WIF) + + assert := assert.New(t) + assert.NoError(err) + assert.Equal(fixture.Data.PrivateKey, privateKey.ToHex()) +} + +func TestPrivateKeyFromWifInvalidWifErrors(t *testing.T) { + assert := assert.New(t) + + _, err := PrivateKeyFromWif("not-a-wif") + assert.ErrorIs(err, ErrInvalidWif) +} + +func TestWIFFromPassphrase(t *testing.T) { + fixture := GetIdentityFixture() + + wif, err := WIFFromPassphrase(fixture.Passphrase) + + assert := assert.New(t) + assert.NoError(err) + assert.Equal(fixture.Data.WIF, wif) +} diff --git a/crypto/slot.go b/crypto/slot.go index f3c5eae..a2bfa41 100644 --- a/crypto/slot.go +++ b/crypto/slot.go @@ -10,5 +10,5 @@ func GetTime() int32 { } func GetEpoch() uint32 { - return uint32(GetNetwork().Epoch.Second()) + return uint32(GetNetwork().Epoch.Unix()) } diff --git a/crypto/slot_test.go b/crypto/slot_test.go new file mode 100644 index 0000000..4c85fd7 --- /dev/null +++ b/crypto/slot_test.go @@ -0,0 +1,16 @@ +package crypto + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetEpoch(t *testing.T) { + assert.Equal(t, uint32(GetNetwork().Epoch.Unix()), GetEpoch()) + assert.EqualValues(t, 1490101200, GetEpoch()) // 2017-03-21T13:00:00Z +} + +func TestGetTime(t *testing.T) { + assert.Greater(t, GetTime(), int32(0)) +} diff --git a/crypto/transaction_data_encoder.go b/crypto/transaction_data_encoder.go new file mode 100644 index 0000000..96b5f21 --- /dev/null +++ b/crypto/transaction_data_encoder.go @@ -0,0 +1,98 @@ +package crypto + +import "math/big" + +func EncodeVoteData(validatorAddress string) ([]byte, error) { + voteArg, err := AbiAddress(validatorAddress) + if err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureVote, voteArg), nil +} + +func EncodeUnvoteData() []byte { + return AbiEncodeFunctionCall(AbiSignatureUnvote) +} + +func EncodeValidatorRegistrationData(validatorPassphrase string) ([]byte, error) { + pop, err := FromMnemonic(validatorPassphrase) + if err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureRegisterValidator, AbiBytes(pop.PK), AbiBytes(pop.POP)), nil +} + +func EncodeValidatorUpdateData(validatorPassphrase string) ([]byte, error) { + pop, err := FromMnemonic(validatorPassphrase) + if err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureUpdateValidator, AbiBytes(pop.PK), AbiBytes(pop.POP)), nil +} + +func EncodeValidatorResignationData() []byte { + return AbiEncodeFunctionCall(AbiSignatureResignValidator) +} + +func EncodeUsernameRegistrationData(username string) ([]byte, error) { + if err := validateUsername(username); err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureRegisterUsername, AbiString(username)), nil +} + +func EncodeUsernameResignationData() []byte { + return AbiEncodeFunctionCall(AbiSignatureResignUsername) +} + +func EncodeMultiPaymentData(addresses []string, amounts []*big.Int) ([]byte, error) { + addressesArg, err := AbiAddressArray(addresses) + if err != nil { + return nil, err + } + amountsArg, err := AbiUint256Array(amounts) + if err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureMultipayment, addressesArg, amountsArg), nil +} + +func EncodeTokenTransferData(recipientAddress string, amount *big.Int) ([]byte, error) { + recipientArg, err := AbiAddress(recipientAddress) + if err != nil { + return nil, err + } + amountArg, err := AbiUint256(amount) + if err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureERC20Transfer, recipientArg, amountArg), nil +} + +func EncodeApproveContractData(amount *big.Int) ([]byte, error) { + spenderArg, err := AbiAddress(ContractBatchTransfer) + if err != nil { + return nil, err + } + amountArg, err := AbiUint256(amount) + if err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureERC20Approve, spenderArg, amountArg), nil +} + +func EncodeBatchTransferData(tokenAddress string, recipients []string, amounts []*big.Int) ([]byte, error) { + tokenArg, err := AbiAddress(tokenAddress) + if err != nil { + return nil, err + } + recipientsArg, err := AbiAddressArray(recipients) + if err != nil { + return nil, err + } + amountsArg, err := AbiUint256Array(amounts) + if err != nil { + return nil, err + } + return AbiEncodeFunctionCall(AbiSignatureERC20BatchTransferFrom, tokenArg, recipientsArg, amountsArg), nil +} diff --git a/crypto/transaction_data_encoder_test.go b/crypto/transaction_data_encoder_test.go new file mode 100644 index 0000000..27368db --- /dev/null +++ b/crypto/transaction_data_encoder_test.go @@ -0,0 +1,120 @@ +package crypto + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeDataMatchesFixtures(t *testing.T) { + t.Run("vote", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("vote") + validatorAddress := AddressFromBytes(HexDecode("c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763")) + + data, err := EncodeVoteData(validatorAddress) + require.NoError(err) + assert.Equal(HexDecode(fixture.Data.Data), data) + }) + + t.Run("unvote", func(t *testing.T) { + assert := assert.New(t) + fixture := GetTransactionFixture("unvote") + assert.Equal(HexDecode(fixture.Data.Data), EncodeUnvoteData()) + }) + + t.Run("validator_registration", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("validator_registration") + data, err := EncodeValidatorRegistrationData(validatorPassphraseFixture) + require.NoError(err) + assert.Equal(HexDecode(fixture.Data.Data), data) + }) + + t.Run("validator_update", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("validator_update") + data, err := EncodeValidatorUpdateData(validatorPassphraseFixture) + require.NoError(err) + assert.Equal(HexDecode(fixture.Data.Data), data) + }) + + t.Run("validator_resignation", func(t *testing.T) { + assert := assert.New(t) + fixture := GetTransactionFixture("validator_resignation") + assert.Equal(HexDecode(fixture.Data.Data), EncodeValidatorResignationData()) + }) + + t.Run("username_registration", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("username_registration") + data, err := EncodeUsernameRegistrationData("fixture") + require.NoError(err) + assert.Equal(HexDecode(fixture.Data.Data), data) + }) + + t.Run("username_resignation", func(t *testing.T) { + assert := assert.New(t) + fixture := GetTransactionFixture("username_resignation") + assert.Equal(HexDecode(fixture.Data.Data), EncodeUsernameResignationData()) + }) + + t.Run("multi_payment", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fixture := GetTransactionFixture("multi_payment") + addresses := []string{ + AddressFromBytes(HexDecode("6f0182a0cc707b055322ccf6d4cb6a5aff1aeb22")), + AddressFromBytes(HexDecode("c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763")), + } + amounts := []*big.Int{big.NewInt(100000), big.NewInt(200000)} + + data, err := EncodeMultiPaymentData(addresses, amounts) + require.NoError(err) + assert.Equal(HexDecode(fixture.Data.Data), data) + }) +} + +func TestEncodeTokenTransferData(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + data, err := EncodeTokenTransferData(testAddress(0x01), big.NewInt(750)) + require.NoError(err) + assert.True(IsTokenTransfer(data)) +} + +func TestEncodeApproveContractData(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + data, err := EncodeApproveContractData(big.NewInt(500)) + require.NoError(err) + assert.True(IsApprove(data)) + + decoder, err := NewAbiDecoder(data, AbiSignatureERC20Approve, 2) + require.NoError(err) + spender, err := decoder.Address(0) + require.NoError(err) + assert.Equal(ContractBatchTransfer, spender) +} + +func TestEncodeBatchTransferData(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + data, err := EncodeBatchTransferData(testAddress(0x04), []string{testAddress(0x01), testAddress(0x02)}, []*big.Int{big.NewInt(100), big.NewInt(200)}) + require.NoError(err) + assert.True(IsBatchTransfer(data)) +} diff --git a/crypto/unit_converter.go b/crypto/unit_converter.go new file mode 100644 index 0000000..05c6b22 --- /dev/null +++ b/crypto/unit_converter.go @@ -0,0 +1,120 @@ +package crypto + +import ( + "errors" + "fmt" + "math/big" + "strings" +) + +const ( + UnitWei = "wei" + UnitGwei = "gwei" + UnitArk = "ark" +) + +var ErrUnsupportedUnit = errors.New("crypto: unsupported unit") + +func unitDecimals(unit string) (int, error) { + switch strings.ToLower(unit) { + case UnitWei: + return 0, nil + case UnitGwei: + return 9, nil + case UnitArk: + return 18, nil + default: + return 0, fmt.Errorf("%w: %q (supported units are %q, %q, and %q)", ErrUnsupportedUnit, unit, UnitWei, UnitGwei, UnitArk) + } +} + +func ParseUnits(value string, unit string) (*big.Int, error) { + decimals, err := unitDecimals(unit) + if err != nil { + return nil, err + } + + negative := strings.HasPrefix(value, "-") + v := strings.TrimPrefix(value, "-") + + if v == "" || strings.Contains(v, "-") { + return nil, fmt.Errorf("crypto: %q is not a valid decimal number", value) + } + + intPart, fracPart := v, "" + if i := strings.IndexByte(v, '.'); i >= 0 { + intPart, fracPart = v[:i], v[i+1:] + } + if intPart == "" { + intPart = "0" + } + + if len(fracPart) > decimals { + if strings.Trim(fracPart[decimals:], "0") != "" { + return nil, fmt.Errorf("crypto: %s has more precision than %s (%d decimals) supports", value, unit, decimals) + } + fracPart = fracPart[:decimals] + } else { + fracPart += strings.Repeat("0", decimals-len(fracPart)) + } + + digits := strings.TrimLeft(intPart+fracPart, "0") + if digits == "" { + digits = "0" + } + + result, ok := new(big.Int).SetString(digits, 10) + if !ok { + return nil, fmt.Errorf("crypto: %q is not a valid decimal number", value) + } + if negative { + result.Neg(result) + } + + return result, nil +} + +func FormatUnits(valueWei *big.Int, unit string) (string, error) { + decimals, err := unitDecimals(unit) + if err != nil { + return "", err + } + + negative := valueWei.Sign() < 0 + digits := new(big.Int).Abs(valueWei).String() + + if len(digits) <= decimals { + digits = strings.Repeat("0", decimals-len(digits)+1) + digits + } + + intPart := digits[:len(digits)-decimals] + fracPart := strings.TrimRight(digits[len(digits)-decimals:], "0") + + result := intPart + if fracPart != "" { + result += "." + fracPart + } + if negative && result != "0" { + result = "-" + result + } + + return result, nil +} + +func withUnitSuffix(value string, suffix []string) string { + if len(suffix) > 0 && suffix[0] != "" { + return value + " " + suffix[0] + } + return value +} + +func WeiToArk(valueWei *big.Int, suffix ...string) string { + formatted, _ := FormatUnits(valueWei, UnitArk) + return withUnitSuffix(formatted, suffix) +} + +func GweiToArk(valueGwei *big.Int, suffix ...string) string { + wei := new(big.Int).Mul(valueGwei, big.NewInt(1_000_000_000)) + formatted, _ := FormatUnits(wei, UnitArk) + return withUnitSuffix(formatted, suffix) +} diff --git a/crypto/unit_converter_test.go b/crypto/unit_converter_test.go new file mode 100644 index 0000000..d5b8579 --- /dev/null +++ b/crypto/unit_converter_test.go @@ -0,0 +1,222 @@ +package crypto + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseUnitsWei(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := []struct{ value, expected string }{ + {"1", "1"}, {"10", "10"}, {"100", "100"}, {"1000", "1000"}, {"10000", "10000"}, + } + for _, c := range cases { + got, err := ParseUnits(c.value, UnitWei) + require.NoError(err) + assert.Equal(c.expected, got.String()) + } +} + +func TestParseUnitsGwei(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := []struct{ value, expected string }{ + {"0.01", "10000000"}, {"0.1", "100000000"}, {"1", "1000000000"}, + {"10", "10000000000"}, {"100", "100000000000"}, {"1000", "1000000000000"}, + {"10000", "10000000000000"}, + } + for _, c := range cases { + got, err := ParseUnits(c.value, UnitGwei) + require.NoError(err) + assert.Equal(c.expected, got.String()) + } +} + +func TestParseUnitsArk(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := []struct{ value, expected string }{ + {"0.01", "10000000000000000"}, {"0.1", "100000000000000000"}, {"1", "1000000000000000000"}, + {"10", "10000000000000000000"}, {"100", "100000000000000000000"}, + {"1000", "1000000000000000000000"}, {"10000", "10000000000000000000000"}, + } + for _, c := range cases { + got, err := ParseUnits(c.value, UnitArk) + require.NoError(err) + assert.Equal(c.expected, got.String()) + } +} + +func TestFormatUnitsWei(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := []struct{ value, expected string }{ + {"1", "1"}, {"10", "10"}, {"100", "100"}, {"1000", "1000"}, {"10000", "10000"}, + } + for _, c := range cases { + got, err := FormatUnits(mustBigInt(t, c.value), UnitWei) + require.NoError(err) + assert.Equal(c.expected, got) + } +} + +func TestFormatUnitsGwei(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := []struct{ value, expected string }{ + {"1000000000", "1"}, {"10000000000", "10"}, {"100000000000", "100"}, + {"1000000000000", "1000"}, {"10000000000000", "10000"}, + } + for _, c := range cases { + got, err := FormatUnits(mustBigInt(t, c.value), UnitGwei) + require.NoError(err) + assert.Equal(c.expected, got) + } +} + +func TestFormatUnitsArk(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cases := []struct{ value, expected string }{ + {"1000000000000000000", "1"}, {"10000000000000000000", "10"}, + {"100000000000000000000", "100"}, {"1000000000000000000000", "1000"}, + {"10000000000000000000000", "10000"}, + } + for _, c := range cases { + got, err := FormatUnits(mustBigInt(t, c.value), UnitArk) + require.NoError(err) + assert.Equal(c.expected, got) + } +} + +func TestParseUnitsUnsupportedUnitErrors(t *testing.T) { + assert := assert.New(t) + + _, err := ParseUnits("1", "unsupported") + assert.ErrorIs(err, ErrUnsupportedUnit) +} + +func TestParseUnitsSubUnitPrecisionErrors(t *testing.T) { + assert := assert.New(t) + + _, err := ParseUnits("0.0000000001", UnitWei) + assert.Error(err) +} + +func TestParseUnitsSubUnitPrecisionAllowsTrailingZeros(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + got, err := ParseUnits("1.00", UnitWei) + require.NoError(err) + assert.Equal("1", got.String()) +} + +func TestParseUnitsNegative(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + got, err := ParseUnits("-1.5", UnitArk) + require.NoError(err) + assert.Equal("-1500000000000000000", got.String()) +} + +func TestParseUnitsZero(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + for _, value := range []string{"0", "0.0", "-0"} { + got, err := ParseUnits(value, UnitArk) + require.NoError(err, "value=%q", value) + assert.Equal("0", got.String(), "value=%q", value) + } +} + +func TestParseUnitsMalformedInputErrors(t *testing.T) { + assert := assert.New(t) + + for _, value := range []string{"", "abc", "1.2.3", "1..5", "--1"} { + _, err := ParseUnits(value, UnitArk) + assert.Error(err, "value=%q should be rejected", value) + } +} + +func TestFormatUnitsNegative(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + got, err := FormatUnits(mustBigInt(t, "-1500000000000000000"), UnitArk) + require.NoError(err) + assert.Equal("-1.5", got) +} + +func TestFormatUnitsZero(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + got, err := FormatUnits(big.NewInt(0), UnitArk) + require.NoError(err) + assert.Equal("0", got) +} + +func TestParseFormatUnitsRoundTrip(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + values := []string{"0", "1", "-1", "1.5", "-1.5", "123456789.123456789", "0.000000000000000001"} + + for _, unit := range []string{UnitWei, UnitGwei, UnitArk} { + for _, value := range values { + wei, err := ParseUnits(value, unit) + if err != nil { + continue // value has more precision than this unit supports — not a round-trip case + } + + formatted, err := FormatUnits(wei, unit) + require.NoError(err) + assert.Equal(value, formatted, "unit=%s value=%s", unit, value) + } + } +} + +func TestWeiToArk(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + value *big.Int + expected string + }{ + {big.NewInt(1), "0.000000000000000001"}, + {mustBigInt(t, "1000000000000000000"), "1"}, + } + for _, c := range cases { + assert.Equal(c.expected+" DARK", WeiToArk(c.value, "DARK")) + assert.Equal(c.expected, WeiToArk(c.value)) + } +} + +func TestGweiToArk(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + value *big.Int + expected string + }{ + {big.NewInt(1), "0.000000001"}, + {mustBigInt(t, "1000000000"), "1"}, + } + for _, c := range cases { + assert.Equal(c.expected+" DARK", GweiToArk(c.value, "DARK")) + assert.Equal(c.expected, GweiToArk(c.value)) + } +}