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

Refactor construction API #51

Merged
merged 14 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
51 changes: 51 additions & 0 deletions server/services/common.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,54 @@
package services

import (
"encoding/json"
"math/big"
"strings"
)

type objectsMap map[string]interface{}

func toObjectsMap(value any) (objectsMap, error) {
data, err := json.Marshal(value)
if err != nil {
return nil, err
}

var result objectsMap
err = json.Unmarshal(data, &result)
if err != nil {
return nil, err
}

return result, nil
}

func fromObjectsMap(obj objectsMap, value any) error {
data, err := json.Marshal(obj)
if err != nil {
return err
}

err = json.Unmarshal(data, value)
if err != nil {
return err
}

return nil
}

func isZeroAmount(amount string) bool {
return amount == "" || amount == "0" || amount == "-0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about "0.0"? or "00". Do we have a more robust way to find out that we cover all these cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, added a more robust solution - for integers, though (decimal numbers do not need to be handled - at least, not yet).

}

func getMagnitudeOfAmount(amount string) string {
return strings.Trim(amount, "-")
}

func multiplyUint64(a uint64, b uint64) *big.Int {
return big.NewInt(0).Mul(big.NewInt(0).SetUint64(a), big.NewInt(0).SetUint64(b))
}

func addBigInt(a *big.Int, b *big.Int) *big.Int {
return big.NewInt(0).Add(a, b)
}
56 changes: 56 additions & 0 deletions server/services/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package services

import (
"math"
"math/big"
"testing"

"github.com/stretchr/testify/require"
)

type dummy struct {
A string `json:"a"`
B string `json:"b"`
C uint64 `json:"c"`
}

func Test_ToObjectsMapAndFromObjectsMap(t *testing.T) {
t.Parallel()

dummyOriginal := &dummy{
A: "a",
B: "b",
C: 42,
}

dummyMap, err := toObjectsMap(dummyOriginal)
require.Nil(t, err)

dummyConverted := &dummy{}
err = fromObjectsMap(dummyMap, dummyConverted)
require.Nil(t, err)

require.Equal(t, dummyOriginal, dummyConverted)
}

func Test_IsZeroAmount(t *testing.T) {
require.True(t, isZeroAmount(""))
require.True(t, isZeroAmount("0"))
require.True(t, isZeroAmount("-0"))
require.False(t, isZeroAmount("1"))
require.False(t, isZeroAmount("-1"))
}

func Test_GetMagnitudeOfAmount(t *testing.T) {
require.Equal(t, "100", getMagnitudeOfAmount("100"))
require.Equal(t, "100", getMagnitudeOfAmount("-100"))
}

func Test_MultiplyUint64(t *testing.T) {
require.Equal(t, "340282366920938463426481119284349108225", multiplyUint64(math.MaxUint64, math.MaxUint64).String())
require.Equal(t, "1", multiplyUint64(1, 1).String())
}

func Test_AddBigInt(t *testing.T) {
require.Equal(t, "12", addBigInt(big.NewInt(7), big.NewInt(5)).String())
}
88 changes: 88 additions & 0 deletions server/services/constructionMetadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package services

import (
"encoding/json"
"errors"

"github.com/ElrondNetwork/elrond-proxy-go/data"
)

type constructionMetadata struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
Nonce uint64 `json:"nonce"`
Amount string `json:"amount"`
CurrencySymbol string `json:"currencySymbol"`
GasLimit uint64 `json:"gasLimit"`
GasPrice uint64 `json:"gasPrice"`
Data []byte `json:"data"`
ChainID string `json:"chainID"`
Version int `json:"version"`
}

func newConstructionMetadata(obj objectsMap) (*constructionMetadata, error) {
result := &constructionMetadata{}
err := fromObjectsMap(obj, result)
if err != nil {
return nil, err
}

return result, nil
}

func (metadata *constructionMetadata) toTransactionJson() ([]byte, error) {
tx, err := metadata.toTransaction()
if err != nil {
return nil, err
}

txJson, err := json.Marshal(tx)
if err != nil {
return nil, err
}

return txJson, nil
}

func (metadata *constructionMetadata) toTransaction() (*data.Transaction, error) {
err := metadata.validate()
if err != nil {
return nil, err
}

tx := &data.Transaction{}
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
tx.Sender = metadata.Sender
tx.Receiver = metadata.Receiver
tx.Nonce = metadata.Nonce
tx.Value = metadata.Amount
tx.GasLimit = metadata.GasLimit
tx.GasPrice = metadata.GasPrice
tx.Data = metadata.Data
tx.ChainID = metadata.ChainID
tx.Version = uint32(metadata.Version)

return tx, nil
}

func (metadata *constructionMetadata) validate() error {
if len(metadata.Sender) == 0 {
return errors.New("missing metadata: 'sender'")
}
if len(metadata.Receiver) == 0 {
return errors.New("missing metadata: 'receiver'")
}
if metadata.GasLimit == 0 {
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
return errors.New("missing metadata: 'gasLimit'")
}
if metadata.GasPrice == 0 {
return errors.New("missing metadata: 'gasPrice'")
}
if metadata.Version != 1 {
return errors.New("bad metadata: unexpected 'version'")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe

return fmt.Errorf("bad metadata: unexpected 'version' %v", metadata.Version)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, fixed.

}
if len(metadata.ChainID) == 0 {
return errors.New("missing metadata: 'chainID'")
}

return nil
}
82 changes: 82 additions & 0 deletions server/services/constructionMetadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package services

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestConstructionMetadata_ToTransactionJson(t *testing.T) {
t.Parallel()

options := &constructionMetadata{
Sender: "alice",
Receiver: "bob",
Nonce: 42,
Amount: "1234",
CurrencySymbol: "XeGLD",
GasLimit: 80000,
GasPrice: 1000000000,
Data: []byte("hello"),
ChainID: "T",
Version: 1,
}

expectedJson := `{"nonce":42,"value":"1234","receiver":"bob","sender":"alice","gasPrice":1000000000,"gasLimit":80000,"data":"aGVsbG8=","chainID":"T","version":1}`
actualJson, err := options.toTransactionJson()
require.Nil(t, err)
require.Equal(t, expectedJson, string(actualJson))
}

func TestConstructionMetadata_Validate(t *testing.T) {
t.Parallel()

require.ErrorContains(t, (&constructionMetadata{}).validate(), "missing metadata: 'sender'")

require.ErrorContains(t, (&constructionMetadata{
Sender: "alice",
}).validate(), "missing metadata: 'receiver'")

require.ErrorContains(t, (&constructionMetadata{
Sender: "alice",
Receiver: "bob",
}).validate(), "missing metadata: 'gasLimit'")

require.ErrorContains(t, (&constructionMetadata{
Sender: "alice",
Receiver: "bob",
GasLimit: 50000,
}).validate(), "missing metadata: 'gasPrice'")

require.ErrorContains(t, (&constructionMetadata{
Sender: "alice",
Receiver: "bob",
GasLimit: 50000,
GasPrice: 1000000000,
}).validate(), "bad metadata: unexpected 'version'")

require.ErrorContains(t, (&constructionMetadata{
Sender: "alice",
Receiver: "bob",
GasLimit: 50000,
GasPrice: 1000000000,
Version: 42,
}).validate(), "bad metadata: unexpected 'version'")

require.ErrorContains(t, (&constructionMetadata{
Sender: "alice",
Receiver: "bob",
GasLimit: 50000,
GasPrice: 1000000000,
Version: 1,
}).validate(), "missing metadata: 'chainID'")

require.Nil(t, (&constructionMetadata{
Sender: "alice",
Receiver: "bob",
GasLimit: 50000,
GasPrice: 1000000000,
Version: 1,
ChainID: "T",
}).validate())
}
61 changes: 61 additions & 0 deletions server/services/constructionOptions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package services

import (
"errors"
)

type constructionOptions struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
Amount string `json:"amount"`
CurrencySymbol string `json:"currencySymbol"`
GasLimit uint64 `json:"gasLimit"`
GasPrice uint64 `json:"gasPrice"`
Data []byte `json:"data"`
}

func newConstructionOptions(obj objectsMap) (*constructionOptions, error) {
result := &constructionOptions{}
err := fromObjectsMap(obj, result)
if err != nil {
return nil, err
}

return result, nil
}

func (options *constructionOptions) coalesceGasLimit(estimatedGasLimit uint64) uint64 {
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
if options.GasLimit == 0 {
return estimatedGasLimit
}

return options.GasLimit
}

func (options *constructionOptions) coalesceGasPrice(minGasPrice uint64) uint64 {
if options.GasPrice == 0 {
return minGasPrice
}

return options.GasPrice
}

func (options *constructionOptions) validate(nativeCurrencySymbol string) error {
if len(options.Sender) == 0 {
return errors.New("missing option: 'sender'")
}
if len(options.Receiver) == 0 {
return errors.New("missing option: 'receiver'")
}
if isZeroAmount(options.Amount) {
return errors.New("missing option: 'amount'")
}
if len(options.CurrencySymbol) == 0 {
return errors.New("missing option: 'currencySymbol'")
}
if len(options.Data) > 0 && options.CurrencySymbol != nativeCurrencySymbol {
return errors.New("for custom currencies, option 'data' must be empty")
}

return nil
}
Loading