Skip to content

Commit

Permalink
Merge pull request #60 from keep-network/ethers-units
Browse files Browse the repository at this point in the history
Parse ether value from a text

In a configuration file, we are providing value for MaxGasPrice and
BalanceAlertThreshold. If we provide it in wei we are limited to around 9.22 
ether. To be more flexible and allow providing greater value especially for balance 
monitoring we added a possibility to provide the value as a string with three types 
of units: wei, Gwei and ether. We are also supporting decimals.

Few exaples of how the value can be provided:

    BalanceAlertThreshold = 100000000000
    BalanceAlertThreshold = "100000000000 wei"
    BalanceAlertThreshold = "100 Gwei"
    BalanceAlertThreshold = "2 ether"
    BalanceAlertThreshold = "3.4 ether"
  • Loading branch information
pdyraga committed Nov 25, 2020
2 parents e3f1963 + 13b8bae commit e39a681
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 9 deletions.
8 changes: 4 additions & 4 deletions pkg/chain/ethereum/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ type Config struct {
// time, the gas price is increased and transaction is resubmitted.
MiningCheckInterval int

// MaxGasPrice specifies the maximum gas price the client is
// MaxGasPrice specifies the maximum gas price (in wei) the client is
// willing to pay for the transaction to be mined. The offered transaction
// gas price can not be higher than the max gas price value. If the maximum
// allowed gas price is reached, no further resubmission attempts are
// performed.
MaxGasPrice uint64
MaxGasPrice *Wei

// RequestsPerSecondLimit sets the maximum average number of requests
// per second which can be executed against the Ethereum node.
Expand All @@ -58,9 +58,9 @@ type Config struct {
// including view function calls.
ConcurrencyLimit int

// BalanceAlertThreshold defines a minimum value of the operator's account
// BalanceAlertThreshold defines a minimum value (wei) of the operator's account
// balance below which an alert will be triggered.
BalanceAlertThreshold uint64
BalanceAlertThreshold *Wei
}

// ContractAddress finds a given contract's address configuration and returns it
Expand Down
75 changes: 75 additions & 0 deletions pkg/chain/ethereum/wei.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package ethereum

import (
"fmt"
"math/big"
"regexp"
"strings"

"github.com/ethereum/go-ethereum/params"
)

// Wei is a custom type to handle Ether value parsing in configuration files
// using BurntSushi/toml package. It supports wei, Gwei and ether units. The
// Ether value is kept as `wei` and `wei` is the default unit.
// The value can be provided in the text file as e.g.: `1 wei`, `200 Gwei` or
// `0.5 ether`.
type Wei struct {
*big.Int
}

// The most common units for ether values.
const (
wei unit = iota
gwei
ether
)

// unit represents Ether value unit.
type unit int

func (u unit) String() string {
return [...]string{"wei", "Gwei", "ether"}[u]
}

// UnmarshalText is a function used to parse a value of Ethers.
func (e *Wei) UnmarshalText(text []byte) error {
re := regexp.MustCompile(`^(\d+[\.]?[\d]*)[ ]?([\w]*)$`)
matched := re.FindSubmatch(text)

if len(matched) != 3 {
return fmt.Errorf("failed to parse value: [%s]", text)
}

number, ok := new(big.Float).SetString(string(matched[1]))
if !ok {
return fmt.Errorf(
"failed to set float value from string [%s]",
string(matched[1]),
)
}

unit := matched[2]
if len(unit) == 0 {
unit = []byte("wei")
}

switch strings.ToLower(string(unit)) {
case strings.ToLower(ether.String()):
number.Mul(number, big.NewFloat(params.Ether))
e.Int, _ = number.Int(nil)
case strings.ToLower(gwei.String()):
number.Mul(number, big.NewFloat(params.GWei))
e.Int, _ = number.Int(nil)
case strings.ToLower(wei.String()):
number.Mul(number, big.NewFloat(params.Wei))
e.Int, _ = number.Int(nil)
default:
return fmt.Errorf(
"invalid unit: %s; please use one of: wei, Gwei, ether",
unit,
)
}

return nil
}
166 changes: 166 additions & 0 deletions pkg/chain/ethereum/wei_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package ethereum

import (
"fmt"
"math/big"
"reflect"
"testing"
)

func TestUnmarshalText(t *testing.T) {
int5000ether, _ := new(big.Int).SetString("5000000000000000000000", 10)

var tests = map[string]struct {
value string
expectedResult *big.Int
expectedError error
}{
"decimal value": {
value: "0.9",
expectedResult: big.NewInt(0),
},
"lowest value": {
value: "1",
expectedResult: big.NewInt(1),
},
"missing unit": {
value: "702",
expectedResult: big.NewInt(702),
},
"unit: wei": {
value: "4 wei",
expectedResult: big.NewInt(4),
},
"unit: gwei": {
value: "30 gwei",
expectedResult: big.NewInt(30000000000),
},
"unit: ether": {
value: "2 ether",
expectedResult: big.NewInt(2000000000000000000),
},
"unit: mixed case": {
value: "5 GWei",
expectedResult: big.NewInt(5000000000),
},
"decimal wei": {
value: "2.9 wei",
expectedResult: big.NewInt(2),
},
"decimal ether": {
value: "0.8 ether",
expectedResult: big.NewInt(800000000000000000),
},
"multiple decimal digits": {
value: "5.6789 Gwei",
expectedResult: big.NewInt(5678900000),
},
"missing decimal digit": {
value: "6. Gwei",
expectedResult: big.NewInt(6000000000),
},
"no space": {
value: "9ether",
expectedResult: big.NewInt(9000000000000000000),
},
"int overflow amount": {
value: "5000000000000000000000",
expectedResult: int5000ether,
},
"int overflow amount after conversion": {
value: "5000 ether",
expectedResult: int5000ether,
},
"double space": {
value: "100 Gwei",
expectedError: fmt.Errorf("failed to parse value: [100 Gwei]"),
},
"leading space": {
value: " 3 wei",
expectedError: fmt.Errorf("failed to parse value: [ 3 wei]"),
},
"trailing space": {
value: "3 wei ",
expectedError: fmt.Errorf("failed to parse value: [3 wei ]"),
},

"invalid comma delimeter": {
value: "3,5 ether",
expectedError: fmt.Errorf("failed to parse value: [3,5 ether]"),
},
"only decimal number": {
value: ".7 Gwei",
expectedError: fmt.Errorf("failed to parse value: [.7 Gwei]"),
},
"duplicated delimeters": {
value: "3..4 wei",
expectedError: fmt.Errorf("failed to parse value: [3..4 wei]"),
},
"multiple decimals": {
value: "3.4.5 wei",
expectedError: fmt.Errorf("failed to parse value: [3.4.5 wei]"),
},
"invalid thousand separator": {
value: "4 500 gwei",
expectedError: fmt.Errorf("failed to parse value: [4 500 gwei]"),
},
"two values": {
value: "3 wei2wei",
expectedError: fmt.Errorf("invalid unit: wei2wei; please use one of: wei, Gwei, ether"),
},
"two values separated with space": {
value: "3 wei 2wei",
expectedError: fmt.Errorf("failed to parse value: [3 wei 2wei]"),
},
"two values separated with break line": {
value: "3 wei\n2wei",
expectedError: fmt.Errorf("failed to parse value: [3 wei\n2wei]"),
},
"invalid unit: ETH": {
value: "6 ETH",
expectedError: fmt.Errorf("invalid unit: ETH; please use one of: wei, Gwei, ether"),
},
"invalid unit: weinot": {
value: "100 weinot",
expectedError: fmt.Errorf("invalid unit: weinot; please use one of: wei, Gwei, ether"),
},
"invalid unit: notawei": {
value: "100 notawei",
expectedError: fmt.Errorf("invalid unit: notawei; please use one of: wei, Gwei, ether"),
},
"only unit": {
value: "wei",
expectedError: fmt.Errorf("failed to parse value: [wei]"),
},
"invalid number": {
value: "one wei",
expectedError: fmt.Errorf("failed to parse value: [one wei]"),
},
}
for testName, test := range tests {
t.Run(testName, func(t *testing.T) {

e := &Wei{}
err := e.UnmarshalText([]byte(test.value))
if test.expectedError != nil {
if !reflect.DeepEqual(test.expectedError, err) {
t.Errorf(
"invalid error\nexpected: %v\nactual: %v",
test.expectedError,
err,
)
}
} else if err != nil {
t.Errorf("unexpected error: %v", err)
}

if test.expectedResult != nil && test.expectedResult.Cmp(e.Int) != 0 {
t.Errorf(
"invalid value\nexpected: %v\nactual: %v",
test.expectedResult.String(),
e.Int.String(),
)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ var (
// gas price can not be higher than the max gas price value. If the maximum
// allowed gas price is reached, no further resubmission attempts are
// performed. This value can be overwritten in the configuration file.
DefaultMaxGasPrice = big.NewInt(50000000000) // 50 Gwei
DefaultMaxGasPrice = big.NewInt(500000000000) // 500 Gwei
)

// AvailableCommands is the exported list of generated commands that can be
Expand Down
4 changes: 2 additions & 2 deletions tools/generators/ethereum/command.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
if config.MiningCheckInterval != 0 {
checkInterval = time.Duration(config.MiningCheckInterval) * time.Second
}
if config.MaxGasPrice != 0 {
maxGasPrice = new(big.Int).SetUint64(config.MaxGasPrice)
if config.MaxGasPrice != nil {
maxGasPrice = config.MaxGasPrice.Int
}

miningWaiter := ethutil.NewMiningWaiter(client, checkInterval, maxGasPrice)
Expand Down
4 changes: 2 additions & 2 deletions tools/generators/ethereum/command_template_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
if config.MiningCheckInterval != 0 {
checkInterval = time.Duration(config.MiningCheckInterval) * time.Second
}
if config.MaxGasPrice != 0 {
maxGasPrice = new(big.Int).SetUint64(config.MaxGasPrice)
if config.MaxGasPrice != nil {
maxGasPrice = config.MaxGasPrice.Int
}
miningWaiter := ethutil.NewMiningWaiter(client, checkInterval, maxGasPrice)
Expand Down

0 comments on commit e39a681

Please sign in to comment.