Skip to content

Commit

Permalink
Merge pull request #177 from aergoio/feature/decimal-amounts
Browse files Browse the repository at this point in the history
Amounts in decimal format
  • Loading branch information
kroggen authored May 17, 2024
2 parents efb9c50 + bd14ec7 commit 97bae14
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 29 deletions.
49 changes: 49 additions & 0 deletions cmd/brick/context/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,55 @@ func ParseFirstWord(input string) (string, string) {
return strings.ToLower(splitedStr[0]), strings.Join(splitedStr[1:], " ")
}

func ParseDecimalAmount(str string, digits int) string {

// if there is no 'aergo' unit, just return the amount str
if !strings.HasSuffix(strings.ToLower(str), "aergo") {
return str
}

// remove the 'aergo' unit
str = str[:len(str)-5]
// trim trailing spaces
str = strings.TrimRight(str, " ")

// get the position of the decimal point
idx := strings.Index(str, ".")

// if not found, just add the leading zeros
if idx == -1 {
return str + strings.Repeat("0", digits)
}

// Get the integer and decimal parts
p1 := str[0:idx]
p2 := str[idx+1:]

// Check for another decimal point
if strings.Index(p2, ".") != -1 {
return "error"
}

// Compute the amount of zero digits to add
to_add := digits - len(p2)
if to_add > 0 {
p2 = p2 + strings.Repeat("0", to_add)
} else if to_add < 0 {
// Do not truncate decimal amounts
return "error"
}

// Join the integer and decimal parts
str = p1 + p2

// Remove leading zeros
str = strings.TrimLeft(str, "0")
if str == "" {
str = "0"
}
return str
}

type Chunk struct {
Accent bool
Text string
Expand Down
3 changes: 2 additions & 1 deletion cmd/brick/exec/callContract.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ func (c *callContract) parse(args string) (string, *big.Int, string, string, str
return "", nil, "", "", "", "", "", fmt.Errorf("need at least 4 arguments. usage: %s", c.Usage())
}

amount, success := new(big.Int).SetString(splitArgs[1].Text, 10)
amountStr := context.ParseDecimalAmount(splitArgs[1].Text, 18)
amount, success := new(big.Int).SetString(amountStr, 10)
if success == false {
return "", nil, "", "", "", "", "", fmt.Errorf("fail to parse number %s", splitArgs[1].Text)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/brick/exec/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (c *setb) parse(args string) (uint64, string, error) {

line, err := strconv.ParseUint(splitArgs[0].Text, 10, 64)
if err != nil {
return 0, "", fmt.Errorf("fail to parse number %s: %s", splitArgs[1].Text, err.Error())
return 0, "", fmt.Errorf("fail to parse number %s: %s", splitArgs[0].Text, err.Error())
}

contractIDHex := contract.PlainStrToHexAddr(splitArgs[1].Text)
Expand Down Expand Up @@ -119,7 +119,7 @@ func (c *delb) parse(args string) (uint64, string, error) {

line, err := strconv.ParseUint(splitArgs[0].Text, 10, 64)
if err != nil {
return 0, "", fmt.Errorf("fail to parse number %s: %s", splitArgs[1].Text, err.Error())
return 0, "", fmt.Errorf("fail to parse number %s: %s", splitArgs[0].Text, err.Error())
}

contractIDHex := contract.PlainStrToHexAddr(splitArgs[1].Text)
Expand Down
3 changes: 2 additions & 1 deletion cmd/brick/exec/deployContract.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ func (c *deployContract) parse(args string) (string, *big.Int, string, string, s
return "", nil, "", "", "", fmt.Errorf("need 4 arguments. usage: %s", c.Usage())
}

amount, success := new(big.Int).SetString(splitArgs[1].Text, 10)
amountStr := context.ParseDecimalAmount(splitArgs[1].Text, 18)
amount, success := new(big.Int).SetString(amountStr, 10)
if success == false {
return "", nil, "", "", "", fmt.Errorf("fail to parse number %s", splitArgs[1].Text)
}
Expand Down
7 changes: 6 additions & 1 deletion cmd/brick/exec/getstateAccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exec

import (
"fmt"
"math/big"

"github.com/aergoio/aergo/v2/cmd/brick/context"
"github.com/aergoio/aergo/v2/contract/vm_dummy"
Expand Down Expand Up @@ -48,7 +49,11 @@ func (c *getStateAccount) parse(args string) (string, string, error) {

expectedResult := ""
if len(splitArgs) == 2 {
expectedResult = splitArgs[1].Text
expectedResult = context.ParseDecimalAmount(splitArgs[1].Text, 18)
_, success := new(big.Int).SetString(expectedResult, 10)
if expectedResult == "error" || success == false {
return "", "", fmt.Errorf("fail to parse number: %s", splitArgs[1].Text)
}
} else if len(splitArgs) > 2 {
return "", "", fmt.Errorf("too many arguments. usage: %s", c.Usage())
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/brick/exec/injectAccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ func (c *injectAccount) parse(args string) (string, *big.Int, error) {
return "", nil, fmt.Errorf("need 2 arguments. usage: %s", c.Usage())
}

amount, success := new(big.Int).SetString(splitArgs[1].Text, 10)
amountStr := context.ParseDecimalAmount(splitArgs[1].Text, 18)
amount, success := new(big.Int).SetString(amountStr, 10)
if success == false {
return "", nil, fmt.Errorf("fail to parse number %s", splitArgs[1].Text)
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/brick/exec/sendCoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ func (c *sendCoin) parse(args string) (string, string, *big.Int, error) {
return "", "", nil, fmt.Errorf("need 3 arguments. usage: %s", c.Usage())
}

amount, success := new(big.Int).SetString(splitArgs[2].Text, 10)
amountStr := context.ParseDecimalAmount(splitArgs[2].Text, 18)
amount, success := new(big.Int).SetString(amountStr, 10)
if success == false {
return "", "", nil, fmt.Errorf("fail to parse number %s", splitArgs[1].Text)
return "", "", nil, fmt.Errorf("fail to parse number %s", splitArgs[2].Text)
}

return splitArgs[0].Text,
Expand Down
74 changes: 64 additions & 10 deletions contract/vm_callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ func luaCallContract(L *LState, service C.int, contractId *C.char, fname *C.char
aid := types.ToAccountID(cid)

// read the amount for the contract call
amountBig, err := transformAmount(C.GoString(amount))
amountBig, err := transformAmount(C.GoString(amount), ctx.blockInfo.ForkVersion)
if err != nil {
return -1, C.CString("[Contract.LuaCallContract] invalid amount: " + err.Error())
}
Expand Down Expand Up @@ -443,7 +443,7 @@ func luaSendAmount(L *LState, service C.int, contractId *C.char, amount *C.char)
}

// read the amount to be sent
amountBig, err := transformAmount(C.GoString(amount))
amountBig, err := transformAmount(C.GoString(amount), ctx.blockInfo.ForkVersion)
if err != nil {
return C.CString("[Contract.LuaSendAmount] invalid amount: " + err.Error())
}
Expand Down Expand Up @@ -940,11 +940,30 @@ func luaCryptoKeccak256(data unsafe.Pointer, dataLen C.int) (unsafe.Pointer, int

// transformAmount processes the input string to calculate the total amount,
// taking into account the different units ("aergo", "gaer", "aer")
func transformAmount(amountStr string) (*big.Int, error) {
func transformAmount(amountStr string, forkVersion int32) (*big.Int, error) {
if len(amountStr) == 0 {
return zeroBig, nil
}

if forkVersion >= 4 {
// Check for amount in decimal format
if strings.Contains(amountStr,".") && strings.HasSuffix(amountStr,"aergo") {
// Extract the part before the unit
decimalAmount := strings.TrimSuffix(amountStr, "aergo")
decimalAmount = strings.TrimRight(decimalAmount, " ")
// Parse the decimal amount
decimalAmount = parseDecimalAmount(decimalAmount, 18)
if decimalAmount == "error" {
return nil, errors.New("converting error for BigNum: " + amountStr)
}
amount, valid := new(big.Int).SetString(decimalAmount, 10)
if !valid {
return nil, errors.New("converting error for BigNum: " + amountStr)
}
return amount, nil
}
}

totalAmount := new(big.Int)
remainingStr := amountStr

Expand Down Expand Up @@ -990,18 +1009,53 @@ func transformAmount(amountStr string) (*big.Int, error) {
return totalAmount, nil
}

// convert decimal amount into big integer string
func parseDecimalAmount(str string, num_decimals int) string {
// Get the integer and decimal parts
idx := strings.Index(str, ".")
if idx == -1 {
return str
}
p1 := str[0:idx]
p2 := str[idx+1:]

// Check for another decimal point
if strings.Index(p2, ".") != -1 {
return "error"
}

// Compute the amount of zero digits to add
to_add := num_decimals - len(p2)
if to_add > 0 {
p2 = p2 + strings.Repeat("0", to_add)
} else if to_add < 0 {
// Do not truncate decimal amounts
return "error"
}

// Join the integer and decimal parts
str = p1 + p2

// Remove leading zeros
str = strings.TrimLeft(str, "0")
if str == "" {
str = "0"
}
return str
}

// parseAndConvert is a helper function to parse the substring as a big integer
// and apply the necessary multiplier based on the unit.
func parseAndConvert(subStr, unit string, mulUnit *big.Int, amountStr string) (*big.Int, error) {
trimmedStr := strings.TrimSpace(subStr)
func parseAndConvert(subStr, unit string, mulUnit *big.Int, fullStr string) (*big.Int, error) {
subStr = strings.TrimSpace(subStr)

// Convert the trimmed string to a big integer
amountBig, valid := new(big.Int).SetString(trimmedStr, 10)
// Convert the string to a big integer
amountBig, valid := new(big.Int).SetString(subStr, 10)
if !valid {
// Emits a backwards compatible error message
// the same as: dataType := len(unit) > 0 ? "BigNum" : "Integer"
dataType := map[bool]string{true: "BigNum", false: "Integer"}[len(unit) > 0]
return nil, errors.New("converting error for " + dataType + ": " + strings.TrimSpace(amountStr))
return nil, errors.New("converting error for " + dataType + ": " + strings.TrimSpace(fullStr))
}

// Check for negative amounts
Expand Down Expand Up @@ -1098,7 +1152,7 @@ func luaDeployContract(
ctx.callState[newContract.AccountID()] = cs

// read the amount transferred to the contract
amountBig, err := transformAmount(C.GoString(amount))
amountBig, err := transformAmount(C.GoString(amount), ctx.blockInfo.ForkVersion)
if err != nil {
return -1, C.CString("[Contract.LuaDeployContract]value not proper format:" + err.Error())
}
Expand Down Expand Up @@ -1376,7 +1430,7 @@ func luaGovernance(L *LState, service C.int, gType C.char, arg *C.char) *C.char
switch gType {
case 'S', 'U':
var err error
amountBig, err = transformAmount(C.GoString(arg))
amountBig, err = transformAmount(C.GoString(arg), ctx.blockInfo.ForkVersion)
if err != nil {
return C.CString("[Contract.LuaGovernance] invalid amount: " + err.Error())
}
Expand Down
104 changes: 93 additions & 11 deletions contract/vm_callback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"github.com/stretchr/testify/assert"
)

const min_version int32 = 2
const max_version int32 = 4

func bigIntFromString(str string) *big.Int {
bigInt, success := new(big.Int).SetString(str, 10)
if !success {
Expand Down Expand Up @@ -98,13 +101,6 @@ func TestTransformAmount(t *testing.T) {
{"100 invalid 200", nil, errors.New("converting error for Integer: 100 invalid 200")},
{"invalid 200", nil, errors.New("converting error for Integer: invalid 200")},
{"100 invalid", nil, errors.New("converting error for Integer: 100 invalid")},
// Non-Integer Values
{"123.456", nil, errors.New("converting error for Integer: 123.456")},
{"123.456 aergo", nil, errors.New("converting error for BigNum: 123.456 aergo")},
{".1", nil, errors.New("converting error for Integer: .1")},
{".1aergo", nil, errors.New("converting error for BigNum: .1aergo")},
{".1 aergo", nil, errors.New("converting error for BigNum: .1 aergo")},
{".10", nil, errors.New("converting error for Integer: .10")},
// Exponents
{"1e+18", nil, errors.New("converting error for Integer: 1e+18")},
{"2e18", nil, errors.New("converting error for Integer: 2e18")},
Expand All @@ -121,16 +117,102 @@ func TestTransformAmount(t *testing.T) {
{"5e+3aergo", nil, errors.New("converting error for BigNum: 5e+3aergo")},
}

for _, tt := range tests {
result, err := transformAmount(tt.amountStr)
for version := min_version; version <= max_version; version++ {
for _, tt := range tests {
result, err := transformAmount(tt.amountStr, version)

if tt.expectedError != nil {
if assert.Error(t, err, "Expected error: %s", tt.expectedError.Error()) {
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
} else {
if assert.NoError(t, err) && tt.expectedAmount != nil {
assert.Equal(t, tt.expectedAmount, result)
}
}
}
}

// Define the test cases for amounts in decimal format
decimal_tests := []struct {
forkVersion int32
amountStr string
expectedAmount *big.Int
expectedError error
}{
// V3 - decimal amounts not supported
{3, "123.456", nil, errors.New("converting error for Integer: 123.456")},
{3, "123.456 aergo", nil, errors.New("converting error for BigNum: 123.456 aergo")},
{3, ".1", nil, errors.New("converting error for Integer: .1")},
{3, ".1aergo", nil, errors.New("converting error for BigNum: .1aergo")},
{3, ".1 aergo", nil, errors.New("converting error for BigNum: .1 aergo")},
{3, ".10", nil, errors.New("converting error for Integer: .10")},
// V4 - decimal amounts supported
{4, "123.456aergo", bigIntFromString("123456000000000000000"), nil},
{4, "123.4aergo", bigIntFromString("123400000000000000000"), nil},
{4, "123.aergo", bigIntFromString("123000000000000000000"), nil},
{4, "100.aergo", bigIntFromString("100000000000000000000"), nil},
{4, "10.aergo", bigIntFromString("10000000000000000000"), nil},
{4, "1.aergo", bigIntFromString("1000000000000000000"), nil},
{4, "100.0aergo", bigIntFromString("100000000000000000000"), nil},
{4, "10.0aergo", bigIntFromString("10000000000000000000"), nil},
{4, "1.0aergo", bigIntFromString("1000000000000000000"), nil},
{4, ".1aergo", bigIntFromString("100000000000000000"), nil},
{4, "0.1aergo", bigIntFromString("100000000000000000"), nil},
{4, ".01aergo", bigIntFromString("10000000000000000"), nil},
{4, "0.01aergo", bigIntFromString("10000000000000000"), nil},
{4, "0.0000000001aergo", bigIntFromString("100000000"), nil},
{4, "0.000000000000000001aergo", bigIntFromString("1"), nil},
{4, "0.000000000000000123aergo", bigIntFromString("123"), nil},
{4, "0.000000000000000000aergo", bigIntFromString("0"), nil},
{4, "0.000000000000123000aergo", bigIntFromString("123000"), nil},
{4, "0.100000000000000123aergo", bigIntFromString("100000000000000123"), nil},
{4, "1.000000000000000123aergo", bigIntFromString("1000000000000000123"), nil},
{4, "123.456000000000000789aergo", bigIntFromString("123456000000000000789"), nil},

{4, "123.456 aergo", bigIntFromString("123456000000000000000"), nil},
{4, "123.4 aergo", bigIntFromString("123400000000000000000"), nil},
{4, "123. aergo", bigIntFromString("123000000000000000000"), nil},
{4, "100. aergo", bigIntFromString("100000000000000000000"), nil},
{4, "10. aergo", bigIntFromString("10000000000000000000"), nil},
{4, "1. aergo", bigIntFromString("1000000000000000000"), nil},
{4, "100.0 aergo", bigIntFromString("100000000000000000000"), nil},
{4, "10.0 aergo", bigIntFromString("10000000000000000000"), nil},
{4, "1.0 aergo", bigIntFromString("1000000000000000000"), nil},
{4, ".1 aergo", bigIntFromString("100000000000000000"), nil},
{4, "0.1 aergo", bigIntFromString("100000000000000000"), nil},
{4, ".01 aergo", bigIntFromString("10000000000000000"), nil},
{4, "0.01 aergo", bigIntFromString("10000000000000000"), nil},
{4, "0.0000000001 aergo", bigIntFromString("100000000"), nil},
{4, "0.000000000000000001 aergo", bigIntFromString("1"), nil},
{4, "0.000000000000000123 aergo", bigIntFromString("123"), nil},
{4, "0.000000000000000000 aergo", bigIntFromString("0"), nil},
{4, "0.000000000000123000 aergo", bigIntFromString("123000"), nil},
{4, "0.100000000000000123 aergo", bigIntFromString("100000000000000123"), nil},
{4, "1.000000000000000123 aergo", bigIntFromString("1000000000000000123"), nil},
{4, "123.456000000000000789 aergo", bigIntFromString("123456000000000000789"), nil},

{4, "0.0000000000000000001aergo", nil, errors.New("converting error for BigNum: 0.0000000000000000001aergo")},
{4, "0.000000000000000000000000001aergo", nil, errors.New("converting error for BigNum: 0.000000000000000000000000001aergo")},
{4, "0.000000000000000123000aergo", nil, errors.New("converting error for BigNum: 0.000000000000000123000aergo")},
{4, "0.0000000000000000000000aergo", nil, errors.New("converting error for BigNum: 0.0000000000000000000000aergo")},

{4, "0.0000000000000000001 aergo", nil, errors.New("converting error for BigNum: 0.0000000000000000001 aergo")},
{4, "0.000000000000000000000000001 aergo", nil, errors.New("converting error for BigNum: 0.000000000000000000000000001 aergo")},
{4, "0.000000000000000123000 aergo", nil, errors.New("converting error for BigNum: 0.000000000000000123000 aergo")},
{4, "0.0000000000000000000000 aergo", nil, errors.New("converting error for BigNum: 0.0000000000000000000000 aergo")},
}

for _, tt := range decimal_tests {
result, err := transformAmount(tt.amountStr, tt.forkVersion)

if tt.expectedError != nil {
if assert.Error(t, err, "Expected error: %s", tt.expectedError.Error()) {
assert.Equal(t, tt.expectedError.Error(), err.Error())
assert.Equal(t, tt.expectedError.Error(), err.Error(), tt.amountStr)
}
} else {
if assert.NoError(t, err) && tt.expectedAmount != nil {
assert.Equal(t, tt.expectedAmount, result)
assert.Equal(t, tt.expectedAmount, result, tt.amountStr)
}
}
}
Expand Down

0 comments on commit 97bae14

Please sign in to comment.