diff --git a/.github/workflows/check_with_mesh_cli.yml b/.github/workflows/check_with_mesh_cli.yml index 33e4631..1eb221c 100644 --- a/.github/workflows/check_with_mesh_cli.yml +++ b/.github/workflows/check_with_mesh_cli.yml @@ -15,7 +15,7 @@ jobs: with: python-version: 3.11 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install dependencies run: | diff --git a/.github/workflows/check_with_mesh_cli_on_localnet.yml b/.github/workflows/check_with_mesh_cli_on_localnet.yml index 2760490..4489a53 100644 --- a/.github/workflows/check_with_mesh_cli_on_localnet.yml +++ b/.github/workflows/check_with_mesh_cli_on_localnet.yml @@ -15,7 +15,7 @@ jobs: with: python-version: 3.11 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install dependencies run: | diff --git a/.github/workflows/check_with_mesh_cli_on_mainnet.yml b/.github/workflows/check_with_mesh_cli_on_mainnet.yml index 380338c..c841f09 100644 --- a/.github/workflows/check_with_mesh_cli_on_mainnet.yml +++ b/.github/workflows/check_with_mesh_cli_on_mainnet.yml @@ -15,7 +15,7 @@ jobs: with: python-version: 3.11 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install dependencies run: | diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index bfa5353..8dfa483 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -15,10 +15,10 @@ jobs: runs-on: ${{ matrix.runs-on }} name: Build steps: - - name: Set up Go 1.23 - uses: actions/setup-go@v5 + - name: Set up Go 1.26 + uses: actions/setup-go@v6 with: - go-version: 1.23.6 + go-version: 1.26.6 id: go - name: Check out code into the Go module directory diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e8ca4ad..daef430 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -11,17 +11,17 @@ jobs: name: golangci linter runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: - go-version: 1.23.6 - - uses: actions/checkout@v4 + go-version: 1.26.6 + - uses: actions/checkout@v6 - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v7 with: - version: v1.64.5 + version: v2.12.2 # Optional: golangci-lint command line arguments. - args: --timeout 10m0s --max-issues-per-linter 0 --max-same-issues 0 --print-issued-lines + args: --timeout 10m0s --max-issues-per-linter 0 --max-same-issues 0 # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index af39fe9..c91ff7a 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Go 1.x - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: 1.23.6 + go-version: 1.26.6 id: go - name: Check out code into the Go module directory diff --git a/.github/workflows/regularly_check_devnet.yml b/.github/workflows/regularly_check_devnet.yml index ed24edd..feaca88 100644 --- a/.github/workflows/regularly_check_devnet.yml +++ b/.github/workflows/regularly_check_devnet.yml @@ -17,7 +17,7 @@ jobs: with: python-version: 3.11 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install dependencies run: | diff --git a/.github/workflows/regularly_check_mainnet.yml b/.github/workflows/regularly_check_mainnet.yml index 1dc5b54..77e36b5 100644 --- a/.github/workflows/regularly_check_mainnet.yml +++ b/.github/workflows/regularly_check_mainnet.yml @@ -17,7 +17,7 @@ jobs: with: python-version: 3.11 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install dependencies run: | diff --git a/.github/workflows/regularly_check_testnet.yml b/.github/workflows/regularly_check_testnet.yml index 5f9f126..368ad1e 100644 --- a/.github/workflows/regularly_check_testnet.yml +++ b/.github/workflows/regularly_check_testnet.yml @@ -15,7 +15,7 @@ jobs: with: python-version: 3.11 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install dependencies run: | diff --git a/go.mod b/go.mod index a40460d..5f2382e 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,17 @@ module github.com/multiversx/mx-chain-rosetta -go 1.23.6 +go 1.26 require ( - github.com/coinbase/rosetta-sdk-go v0.8.3 - github.com/coinbase/rosetta-sdk-go/types v1.0.0 + github.com/coinbase/rosetta-sdk-go v0.12.0 + github.com/coinbase/rosetta-sdk-go/types v1.7.2 github.com/gin-gonic/gin v1.10.0 github.com/multiversx/mx-chain-core-go v1.4.2-0.20260219122727-014ae9f9311f github.com/multiversx/mx-chain-go v1.11.1 github.com/multiversx/mx-chain-logger-go v1.1.0 github.com/multiversx/mx-chain-proxy-go v1.4.0 github.com/multiversx/mx-chain-storage-go v1.1.0 + github.com/multiversx/mx-chain-vm-common-go v1.6.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.16 ) @@ -45,7 +46,6 @@ require ( github.com/multiversx/mx-chain-communication-go v1.3.0 // indirect github.com/multiversx/mx-chain-crypto-go v1.3.0 // indirect github.com/multiversx/mx-chain-es-indexer-go v1.9.3-0.20260112102658-97d6a0ceb5f6 // indirect - github.com/multiversx/mx-chain-vm-common-go v1.6.0 // indirect github.com/pelletier/go-toml v1.9.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 118c005..4ae0921 100644 --- a/go.sum +++ b/go.sum @@ -28,10 +28,10 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/coinbase/rosetta-sdk-go v0.8.3 h1:IYqd+Ser5NVh0s7p8p2Ir82iCvi75E1l0NH2H4NEr0Y= -github.com/coinbase/rosetta-sdk-go v0.8.3/go.mod h1:ChOHc+BNq7zqJDDkui0DA124GOvlAiRbdgAc1U9GMDQ= -github.com/coinbase/rosetta-sdk-go/types v1.0.0 h1:jpVIwLcPoOeCR6o1tU+Xv7r5bMONNbHU7MuEHboiFuA= -github.com/coinbase/rosetta-sdk-go/types v1.0.0/go.mod h1:eq7W2TMRH22GTW0N0beDnN931DW0/WOI1R2sdHNHG4c= +github.com/coinbase/rosetta-sdk-go v0.12.0 h1:x3T5ScBw0J5fmu9Nkx4QNI+s8NqvSYupb39RIOm5HjQ= +github.com/coinbase/rosetta-sdk-go v0.12.0/go.mod h1:rdzdWYdC3AKsR7IDXKFu7HW7sa9q+czdYGxw44vNRSQ= +github.com/coinbase/rosetta-sdk-go/types v1.7.2 h1:iRt/R+Esn4GNyd2cijjKPiIine/nAlUaCleaAdj8d+A= +github.com/coinbase/rosetta-sdk-go/types v1.7.2/go.mod h1:bN5JAIEG/dGZNnYzsfizeHhzFOf9s5rkJoaNM/7L5QI= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/server/provider/currenciesProvider.go b/server/provider/currenciesProvider.go index 96f483a..a62f8b2 100644 --- a/server/provider/currenciesProvider.go +++ b/server/provider/currenciesProvider.go @@ -49,7 +49,7 @@ func (provider *currenciesProvider) GetCustomCurrencies() []resources.Currency { return provider.customCurrencies } -// GetCustomCurrencyBySymbol gets a custom currency (ESDT) by symbol (identifier) +// GetCustomCurrenciesSymbols returns the enabled custom currency (ESDT) symbols. func (provider *currenciesProvider) GetCustomCurrenciesSymbols() []string { return provider.customCurrenciesSymbols } diff --git a/server/services/accountService.go b/server/services/accountService.go index 0c79ed8..d7be27f 100644 --- a/server/services/accountService.go +++ b/server/services/accountService.go @@ -109,3 +109,8 @@ func (service *accountService) getNativeSymbol() string { func (service *accountService) AccountCoins(_ context.Context, _ *types.AccountCoinsRequest) (*types.AccountCoinsResponse, *types.Error) { return nil, service.errFactory.newErr(ErrNotImplemented) } + +// AllAccountBalances implements the /account/all-balances. +func (service *accountService) AllAccountBalances(_ context.Context, _ *types.AllAccountBalancesRequest) (*types.AllAccountBalancesResponse, *types.Error) { + return nil, service.errFactory.newErr(ErrNotImplemented) +} diff --git a/server/services/constructionOptions.go b/server/services/constructionOptions.go index 3cdc714..1601d8c 100644 --- a/server/services/constructionOptions.go +++ b/server/services/constructionOptions.go @@ -2,6 +2,7 @@ package services import ( "errors" + "math/big" ) type constructionOptions struct { @@ -40,6 +41,19 @@ func (options *constructionOptions) coalesceGasPrice(minGasPrice uint64) uint64 return options.GasPrice } +func validateTransferAmount(amount string) error { + bigAmount, ok := big.NewInt(0).SetString(amount, 10) + if !ok { + return errors.New("option 'amount' is not a valid integer") + } + + if bigAmount.Sign() <= 0 { + return errors.New("option 'amount' must be a positive integer") + } + + return nil +} + func (options *constructionOptions) validate(nativeCurrencySymbol string) error { if len(options.Sender) == 0 { return errors.New("missing option: 'sender'") diff --git a/server/services/constructionService.go b/server/services/constructionService.go index 81efa33..15cc943 100644 --- a/server/services/constructionService.go +++ b/server/services/constructionService.go @@ -414,3 +414,83 @@ func (service *constructionService) ConstructionSubmit( }, }, nil } + +// ConstructionPreprocessOperations will validate the requested operations and options, +// prepare the corresponding transaction (handling native or custom currency transfers) +// and return the resulting operations along with the maximum fee and metadata required +// for the subsequent payload construction +func (service *constructionService) ConstructionPreprocessOperations( + _ context.Context, + request *types.ConstructionPreprocessOperationsRequest, +) (*types.ConstructionPreprocessOperationsResponse, *types.Error) { + log.Debug("constructionService.ConstructionPreprocessOperations()", "construct_op", request.ConstructOp, "from_address", request.FromAddress) + + requestOptions, err := newConstructionOptions(request.Options) + if err != nil { + return nil, service.errFactory.newErrWithOriginal(ErrConstruction, err) + } + if request.FromAddress != nil && len(*request.FromAddress) > 0 && len(requestOptions.Sender) == 0 { + requestOptions.Sender = *request.FromAddress + } + + isNative := service.extension.isNativeCurrencySymbol(requestOptions.CurrencySymbol) + if !isNative && !service.provider.HasCustomCurrency(requestOptions.CurrencySymbol) { + return nil, service.errFactory.newErrWithOriginal(ErrConstruction, fmt.Errorf("unsupported currency: %s", requestOptions.CurrencySymbol)) + } + + nativeCurrencySymbol := service.extension.getNativeCurrencySymbol() + if request.ConstructOp == "transfer" { + err = requestOptions.validate(nativeCurrencySymbol) + if err != nil { + return nil, service.errFactory.newErrWithOriginal(ErrConstruction, err) + } + err = validateTransferAmount(requestOptions.Amount) + if err != nil { + return nil, service.errFactory.newErrWithOriginal(ErrConstruction, err) + } + } else { + return nil, service.errFactory.newErrWithOriginal(ErrNotImplemented, fmt.Errorf("unsupported construct_op: %s", request.ConstructOp)) + } + + tx := &data.Transaction{ + Sender: requestOptions.Sender, + Receiver: requestOptions.Receiver, + Value: requestOptions.Amount, + Data: requestOptions.Data, + } + + if !isNative { + tx.Value = amountZero + tx.Data = service.computeDataForCustomCurrencyTransfer(requestOptions.CurrencySymbol, requestOptions.Amount) + } else if isCustomCurrencyTransfer(string(tx.Data)) { + return nil, service.errFactory.newErrWithOriginal( + ErrConstruction, + errors.New("for native currency transfers, option 'data' must not encode a custom currency (ESDT) transfer"), + ) + } + + operations, err := service.createOperationsFromPreparedTx(tx) + if err != nil { + return nil, service.errFactory.newErrWithOriginal(ErrConstruction, err) + } + + _, gasLimit, gasPrice, errTyped := service.computeFeeComponents(requestOptions, tx.Data) + if errTyped != nil { + return nil, errTyped + } + + requestOptions.GasLimit = gasLimit + requestOptions.GasPrice = gasPrice + + maxFee := service.computeMaxFee(tx.Data, gasLimit, gasPrice) + + metadataBytes, err := json.Marshal(requestOptions) + if err != nil { + return nil, service.errFactory.newErrWithOriginal(ErrConstruction, err) + } + return &types.ConstructionPreprocessOperationsResponse{ + Operations: operations, + MaxFee: service.extension.valueToNativeAmount(maxFee.String()), + Metadata: new(string(metadataBytes)), + }, nil +} diff --git a/server/services/constructionServiceFee.go b/server/services/constructionServiceFee.go index edcc76e..90f6079 100644 --- a/server/services/constructionServiceFee.go +++ b/server/services/constructionServiceFee.go @@ -36,6 +36,20 @@ func (service *constructionService) computeFeeComponents(options *constructionOp return fee, gasLimit, gasPrice, nil } +// computeMaxFee returns the maximum fee chargeable for a transaction built with the given options, +// taking into account the user-provided gas limit, so it stays consistent with the metadata returned +// to the client. Following the protocol's gas categorization (see SplitTxGasInCategoriesInEpoch), +// movement gas is MinGasLimit + GasPerDataByte * len(data), charged at the base gas price, while all +// remaining gas is processing gas, charged with the gas price modifier. +func (service *constructionService) computeMaxFee(computedData []byte, gasLimit uint64, gasPrice uint64) *big.Int { + networkConfig := service.provider.GetNetworkConfig() + + movementGasLimit := networkConfig.MinGasLimit + networkConfig.GasPerDataByte*uint64(len(computedData)) + processingGasLimit := gasLimit - movementGasLimit + + return computeFee(movementGasLimit, processingGasLimit, gasPrice, networkConfig.GasPriceModifier) +} + func computeFee(movementGasLimit uint64, executionGasLimit uint64, gasPrice uint64, gasPriceModifier float64) *big.Int { movementFee := multiplyUint64(movementGasLimit, gasPrice) executionGasPrice := uint64(float64(gasPrice) * gasPriceModifier) diff --git a/server/services/constructionService_test.go b/server/services/constructionService_test.go index 167dac1..053bf7d 100644 --- a/server/services/constructionService_test.go +++ b/server/services/constructionService_test.go @@ -2,6 +2,8 @@ package services import ( "context" + "encoding/json" + "math/big" "testing" "github.com/coinbase/rosetta-sdk-go/types" @@ -643,6 +645,322 @@ func TestConstructionService_ConstructionSubmit(t *testing.T) { require.Equal(t, uint64(42), calledWithTransaction.Nonce) } +func TestConstructionService_ConstructionPreprocessOperations(t *testing.T) { + t.Parallel() + + networkProvider := testscommon.NewNetworkProviderMock() + networkProvider.MockCustomCurrencies = []resources.Currency{ + {Symbol: "TEST-abcdef", Decimals: 6}, + } + extension := newNetworkProviderExtension(networkProvider) + service := NewConstructionService(networkProvider) + + t.Run("native transfer", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "XeGLD", + }, + }, + ) + + require.Nil(t, errTyped) + + expectedOperations := []*types.Operation{ + { + OperationIdentifier: indexToOperationIdentifier(0), + Type: opTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressAlice), + Amount: extension.valueToNativeAmount("-1234"), + }, + { + OperationIdentifier: indexToOperationIdentifier(1), + Type: opTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressBob), + Amount: extension.valueToNativeAmount("1234"), + }, + } + + require.Equal(t, expectedOperations, response.Operations) + require.Equal(t, "50000000000000", response.MaxFee.Value) + require.NotNil(t, response.Metadata) + }) + + t.Run("custom transfer", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "TEST-abcdef", + }, + }, + ) + + require.Nil(t, errTyped) + + expectedOperations := []*types.Operation{ + { + OperationIdentifier: indexToOperationIdentifier(0), + Type: opCustomTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressAlice), + Amount: extension.valueToCustomAmount("-1234", "TEST-abcdef"), + }, + { + OperationIdentifier: indexToOperationIdentifier(1), + Type: opCustomTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressBob), + Amount: extension.valueToCustomAmount("1234", "TEST-abcdef"), + }, + } + + require.Equal(t, expectedOperations, response.Operations) + require.Equal(t, "112000000000000", response.MaxFee.Value) + require.NotNil(t, response.Metadata) + }) + + t.Run("native transfer with data", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "XeGLD", + "data": []byte("hello"), + }, + }, + ) + + require.Nil(t, errTyped) + + expectedOperations := []*types.Operation{ + { + OperationIdentifier: indexToOperationIdentifier(0), + Type: opTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressAlice), + Amount: extension.valueToNativeAmount("-1234"), + }, + { + OperationIdentifier: indexToOperationIdentifier(1), + Type: opTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressBob), + Amount: extension.valueToNativeAmount("1234"), + }, + } + + require.Equal(t, expectedOperations, response.Operations) + require.Equal(t, "57500000000000", response.MaxFee.Value) + require.NotNil(t, response.Metadata) + }) + + t.Run("native transfer with custom currency shaped data", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "XeGLD", + "data": []byte("ESDTTransfer@544553542d616263646566@2a"), + }, + }, + ) + + require.Equal(t, int32(ErrConstruction), errTyped.Code) + require.Nil(t, response) + }) + + t.Run("native transfer with malformed amount", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "not-a-number", + "currencySymbol": "XeGLD", + }, + }, + ) + + require.Equal(t, int32(ErrConstruction), errTyped.Code) + require.Nil(t, response) + }) + + t.Run("native transfer with explicit gas limit", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "XeGLD", + "gasLimit": 1000000, + }, + }, + ) + + require.Nil(t, errTyped) + + var metadata struct { + GasLimit uint64 `json:"gasLimit"` + GasPrice uint64 `json:"gasPrice"` + } + err := json.Unmarshal([]byte(*response.Metadata), &metadata) + require.Nil(t, err) + require.Equal(t, uint64(1000000), metadata.GasLimit) + + expectedMaxFee := big.NewInt(50000*1000000000 + 950000*10000000) + require.Equal(t, expectedMaxFee.String(), response.MaxFee.Value) + }) + + t.Run("custom transfer with explicit gas limit", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "TEST-abcdef", + "gasLimit": 1000000, + }, + }, + ) + + require.Nil(t, errTyped) + + var metadata struct { + GasLimit uint64 `json:"gasLimit"` + GasPrice uint64 `json:"gasPrice"` + } + err := json.Unmarshal([]byte(*response.Metadata), &metadata) + require.Nil(t, err) + require.Equal(t, uint64(1000000), metadata.GasLimit) + + expectedMaxFee := big.NewInt(110000*1000000000 + 890000*10000000) + require.Equal(t, expectedMaxFee.String(), response.MaxFee.Value) + }) + + t.Run("custom transfer with negative amount", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "-1234", + "currencySymbol": "TEST-abcdef", + }, + }, + ) + + require.Equal(t, int32(ErrConstruction), errTyped.Code) + require.Nil(t, response) + }) + + t.Run("from_address fallback", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + FromAddress: new(testscommon.TestAddressAlice), + Options: map[string]interface{}{ + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "XeGLD", + }, + }, + ) + + require.Nil(t, errTyped) + require.Equal(t, testscommon.TestAddressAlice, response.Operations[0].Account.Address) + }) + + t.Run("unsupported construct_op", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "stake", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "XeGLD", + }, + }, + ) + + require.Equal(t, int32(ErrNotImplemented), errTyped.Code) + require.Nil(t, response) + }) + + t.Run("unsupported currency", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "amount": "1234", + "currencySymbol": "UNKNOWN-abc", + }, + }, + ) + + require.Equal(t, int32(ErrConstruction), errTyped.Code) + require.Nil(t, response) + }) + + t.Run("missing required fields", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionPreprocessOperations(context.Background(), + &types.ConstructionPreprocessOperationsRequest{ + ConstructOp: "transfer", + Options: map[string]interface{}{ + "sender": testscommon.TestAddressAlice, + "receiver": testscommon.TestAddressBob, + "currencySymbol": "XeGLD", + }, + }, + ) + + require.Equal(t, int32(ErrConstruction), errTyped.Code) + require.Nil(t, response) + }) +} + func TestConstructionService_CreateOperationsFromPreparedTx(t *testing.T) { t.Parallel() diff --git a/server/services/offline_services.go b/server/services/offline_services.go index 70924d1..178be71 100644 --- a/server/services/offline_services.go +++ b/server/services/offline_services.go @@ -81,3 +81,8 @@ func (service *offlineService) NetworkList( ) (*types.NetworkListResponse, *types.Error) { return nil, service.errFactory.newErr(ErrOfflineMode) } + +// AllAccountBalances is not implemented yet +func (service *offlineService) AllAccountBalances(_ context.Context, _ *types.AllAccountBalancesRequest) (*types.AllAccountBalancesResponse, *types.Error) { + return nil, service.errFactory.newErr(ErrOfflineMode) +}