Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amounts in decimal format #177

Merged
merged 15 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1300,7 +1354,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
Loading