From a0c59532494d5813a1be0b148abb02febd430393 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 20 May 2026 10:20:17 +0300 Subject: [PATCH 1/3] fix(api): allow GET /wallet when swap or chequebook is disabled --- pkg/api/router.go | 6 ++-- pkg/api/router_test.go | 8 ++--- pkg/api/wallet.go | 19 +++++++---- pkg/api/wallet_test.go | 52 ++++++++++++++++++++++++++++++ pkg/node/node.go | 72 +++++++++++++++++++++++++----------------- 5 files changed, 114 insertions(+), 43 deletions(-) diff --git a/pkg/api/router.go b/pkg/api/router.go index 3d61fc98c00..987f4b995dd 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -563,16 +563,14 @@ func (s *Service) mountBusinessDebug() { )) handle("/wallet", web.ChainHandlers( - s.checkChequebookAvailability, - s.checkSwapAvailability, + s.checkChainAvailability, web.FinalHandler(jsonhttp.MethodHandler{ "GET": http.HandlerFunc(s.walletHandler), }), )) handle("/wallet/withdraw/{coin}", web.ChainHandlers( - s.checkChequebookAvailability, - s.checkSwapAvailability, + s.checkChainAvailability, web.FinalHandler(jsonhttp.MethodHandler{ "POST": web.ChainHandlers( s.gasConfigMiddleware("wallet withdraw"), diff --git a/pkg/api/router_test.go b/pkg/api/router_test.go index 8c13f459375..4e972f9f1a6 100644 --- a/pkg/api/router_test.go +++ b/pkg/api/router_test.go @@ -290,8 +290,8 @@ func TestEndpointOptions(t *testing.T) { {"/chequebook/address", []string{"GET"}, http.StatusNoContent}, {"/chequebook/deposit", []string{"POST"}, http.StatusNoContent}, {"/chequebook/withdraw", []string{"POST"}, http.StatusNoContent}, - {"/wallet", nil, http.StatusForbidden}, - {"/wallet/withdraw/{coin}", nil, http.StatusForbidden}, + {"/wallet", []string{"GET"}, http.StatusNoContent}, + {"/wallet/withdraw/{coin}", []string{"POST"}, http.StatusNoContent}, {"/stamps", []string{"GET"}, http.StatusNoContent}, {"/stamps/{batch_id}", []string{"GET"}, http.StatusNoContent}, {"/stamps/{batch_id}/buckets", []string{"GET"}, http.StatusNoContent}, @@ -385,8 +385,8 @@ func TestEndpointOptions(t *testing.T) { {"/chequebook/address", nil, http.StatusForbidden}, {"/chequebook/deposit", nil, http.StatusForbidden}, {"/chequebook/withdraw", nil, http.StatusForbidden}, - {"/wallet", nil, http.StatusForbidden}, - {"/wallet/withdraw/{coin}", nil, http.StatusForbidden}, + {"/wallet", []string{"GET"}, http.StatusNoContent}, + {"/wallet/withdraw/{coin}", []string{"POST"}, http.StatusNoContent}, {"/stamps", []string{"GET"}, http.StatusNoContent}, {"/stamps/{batch_id}", []string{"GET"}, http.StatusNoContent}, {"/stamps/{batch_id}/buckets", []string{"GET"}, http.StatusNoContent}, diff --git a/pkg/api/wallet.go b/pkg/api/wallet.go index 75674e1f48b..57f3c2de8e3 100644 --- a/pkg/api/wallet.go +++ b/pkg/api/wallet.go @@ -37,12 +37,15 @@ func (s *Service) walletHandler(w http.ResponseWriter, r *http.Request) { return } - bzz, err := s.erc20Service.BalanceOf(r.Context(), s.ethereumAddress) - if err != nil { - logger.Debug("unable to acquire erc20 balance", "error", err) - logger.Error(nil, "unable to acquire erc20 balance") - jsonhttp.InternalServerError(w, "unable to acquire erc20 balance") - return + bzz := new(big.Int) + if s.erc20Service != nil { + bzz, err = s.erc20Service.BalanceOf(r.Context(), s.ethereumAddress) + if err != nil { + logger.Debug("unable to acquire erc20 balance", "error", err) + logger.Error(nil, "unable to acquire erc20 balance") + jsonhttp.InternalServerError(w, "unable to acquire erc20 balance") + return + } } jsonhttp.OK(w, walletResponse{ @@ -95,6 +98,10 @@ func (s *Service) walletWithdrawHandler(w http.ResponseWriter, r *http.Request) } if bzz { + if s.erc20Service == nil { + jsonhttp.ServiceUnavailable(w, "BZZ token address unavailable") + return + } currentBalance, err := s.erc20Service.BalanceOf(r.Context(), s.ethereumAddress) if err != nil { logger.Error(err, "unable to get balance") diff --git a/pkg/api/wallet_test.go b/pkg/api/wallet_test.go index d5c09efaeeb..1633b8f4249 100644 --- a/pkg/api/wallet_test.go +++ b/pkg/api/wallet_test.go @@ -84,6 +84,58 @@ func TestWallet(t *testing.T) { Code: 500, })) }) + + t.Run("swap disabled", func(t *testing.T) { + t.Parallel() + + srv, _, _, _ := newTestServer(t, testServerOptions{ + SwapDisabled: true, + Erc20Opts: []erc20mock.Option{ + erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { + return big.NewInt(10000000000000000), nil + }), + }, + BackendOpts: []backendmock.Option{ + backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { + return big.NewInt(2000000000000000000), nil + }), + }, + }) + + jsonhttptest.Request(t, srv, http.MethodGet, "/wallet", http.StatusOK, + jsonhttptest.WithExpectedJSONResponse(api.WalletResponse{ + BZZ: bigint.Wrap(big.NewInt(10000000000000000)), + NativeToken: bigint.Wrap(big.NewInt(2000000000000000000)), + ChainID: 1, + }), + ) + }) + + t.Run("chequebook disabled", func(t *testing.T) { + t.Parallel() + + srv, _, _, _ := newTestServer(t, testServerOptions{ + ChequebookDisabled: true, + Erc20Opts: []erc20mock.Option{ + erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { + return big.NewInt(10000000000000000), nil + }), + }, + BackendOpts: []backendmock.Option{ + backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { + return big.NewInt(2000000000000000000), nil + }), + }, + }) + + jsonhttptest.Request(t, srv, http.MethodGet, "/wallet", http.StatusOK, + jsonhttptest.WithExpectedJSONResponse(api.WalletResponse{ + BZZ: bigint.Wrap(big.NewInt(10000000000000000)), + NativeToken: bigint.Wrap(big.NewInt(2000000000000000000)), + ChainID: 1, + }), + ) + }) } func TestWalletWithdraw(t *testing.T) { diff --git a/pkg/node/node.go b/pkg/node/node.go index 53d96ec5419..a01938891f7 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -393,6 +393,10 @@ func NewBee( chainEnabled := isChainEnabled(o, o.BlockchainRpcEndpoint, logger) + if o.SwapEnable && !chainEnabled { + return nil, errors.New("swap is enabled but the chain backend is not; provide --blockchain-rpc-endpoint or disable swap") + } + var batchStore postage.Storer = new(postage.NoOpBatchStore) var evictFn func([]byte) error @@ -535,46 +539,56 @@ func NewBee( } } - if o.SwapEnable { - chequebookFactory, err := InitChequebookFactory(logger, chainBackend, chainID, transactionService, o.SwapFactoryAddress) - if err != nil { - return nil, fmt.Errorf("init chequebook factory: %w", err) + if chainEnabled { + chequebookFactory, ferr := InitChequebookFactory(logger, chainBackend, chainID, transactionService, o.SwapFactoryAddress) + if o.SwapEnable && ferr != nil { + return nil, fmt.Errorf("init chequebook factory: %w", ferr) } - erc20Address, err := chequebookFactory.ERC20Address(ctx) - if err != nil { - return nil, fmt.Errorf("factory fail: %w", err) + if ferr == nil { + erc20Address, err := chequebookFactory.ERC20Address(ctx) + if err != nil { + if o.SwapEnable { + return nil, fmt.Errorf("factory fail: %w", err) + } + logger.Warning("unable to resolve ERC20 token address; BZZ balance will be unavailable via /wallet", "error", err) + } else { + erc20Service = erc20.New(transactionService, erc20Address) + } + } else { + logger.Warning("unable to init chequebook factory; BZZ balance will be unavailable via /wallet", "error", ferr) } - erc20Service = erc20.New(transactionService, erc20Address) + if o.SwapEnable { + if o.ChequebookEnable { + var err error + chequebookService, err = InitChequebookService( + ctx, + logger, + stateStore, + signer, + chainID, + chainBackend, + overlayEthAddress, + transactionService, + chequebookFactory, + o.SwapInitialDeposit, + erc20Service, + ) + if err != nil { + return nil, fmt.Errorf("init chequebook service: %w", err) + } + } - if o.ChequebookEnable && chainEnabled { - chequebookService, err = InitChequebookService( - ctx, - logger, + chequeStore, cashoutService = initChequeStoreCashout( stateStore, - signer, - chainID, chainBackend, + chequebookFactory, + chainID, overlayEthAddress, transactionService, - chequebookFactory, - o.SwapInitialDeposit, - erc20Service, ) - if err != nil { - return nil, fmt.Errorf("init chequebook service: %w", err) - } } - - chequeStore, cashoutService = initChequeStoreCashout( - stateStore, - chainBackend, - chequebookFactory, - chainID, - overlayEthAddress, - transactionService, - ) } lightNodes := lightnode.NewContainer(swarmAddress) From 36c578dac1a241827bf095faf85d95d519f981fe Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Tue, 26 May 2026 22:08:02 +0300 Subject: [PATCH 2/3] fix(api): return null for /wallet bzzBalance when erc20 service unavailable --- pkg/api/api_test.go | 11 ++++++++--- pkg/api/wallet.go | 2 +- pkg/api/wallet_test.go | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 4dd7aa1d3f7..bd64261e8c1 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -47,7 +47,7 @@ import ( resolverMock "github.com/ethersphere/bee/v2/pkg/resolver/mock" "github.com/ethersphere/bee/v2/pkg/settlement/pseudosettle" chequebookmock "github.com/ethersphere/bee/v2/pkg/settlement/swap/chequebook/mock" - "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20" + erc20pkg "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20" erc20mock "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20/mock" swapmock "github.com/ethersphere/bee/v2/pkg/settlement/swap/mock" "github.com/ethersphere/bee/v2/pkg/spinlock" @@ -135,6 +135,7 @@ type testServerOptions struct { FullAPIDisabled bool ChequebookDisabled bool SwapDisabled bool + Erc20ServiceNil bool } func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.Conn, string, *chanStorer) { @@ -181,6 +182,10 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. o.StateStorer = storeRecipient } erc20 := erc20mock.New(o.Erc20Opts...) + var erc20APIService erc20pkg.Service = erc20 + if o.Erc20ServiceNil { + erc20APIService = nil + } backend := backendmock.New(o.BackendOpts...) extraOpts := api.ExtraOptions{ @@ -234,7 +239,7 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. s.Configure(signer, noOpTracer, api.Options{ CORSAllowedOrigins: o.CORSAllowedOrigins, WsPingPeriod: o.WsPingPeriod, - }, extraOpts, 1, erc20) + }, extraOpts, 1, erc20APIService) s.Mount() if !o.FullAPIDisabled { @@ -666,7 +671,7 @@ func createRedistributionAgentService( t *testing.T, addr swarm.Address, storer storage.StateStorer, - erc20Service erc20.Service, + erc20Service erc20pkg.Service, tranService transaction.Service, backend storageincentives.ChainBackend, chainStateGetter postage.ChainStateGetter, diff --git a/pkg/api/wallet.go b/pkg/api/wallet.go index 57f3c2de8e3..0dea489515d 100644 --- a/pkg/api/wallet.go +++ b/pkg/api/wallet.go @@ -37,7 +37,7 @@ func (s *Service) walletHandler(w http.ResponseWriter, r *http.Request) { return } - bzz := new(big.Int) + var bzz *big.Int if s.erc20Service != nil { bzz, err = s.erc20Service.BalanceOf(r.Context(), s.ethereumAddress) if err != nil { diff --git a/pkg/api/wallet_test.go b/pkg/api/wallet_test.go index 1633b8f4249..c8bf92b1c01 100644 --- a/pkg/api/wallet_test.go +++ b/pkg/api/wallet_test.go @@ -111,6 +111,28 @@ func TestWallet(t *testing.T) { ) }) + t.Run("erc20 service unavailable", func(t *testing.T) { + t.Parallel() + + srv, _, _, _ := newTestServer(t, testServerOptions{ + SwapDisabled: true, + Erc20ServiceNil: true, + BackendOpts: []backendmock.Option{ + backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { + return big.NewInt(2000000000000000000), nil + }), + }, + }) + + jsonhttptest.Request(t, srv, http.MethodGet, "/wallet", http.StatusOK, + jsonhttptest.WithExpectedJSONResponse(api.WalletResponse{ + BZZ: bigint.Wrap(nil), + NativeToken: bigint.Wrap(big.NewInt(2000000000000000000)), + ChainID: 1, + }), + ) + }) + t.Run("chequebook disabled", func(t *testing.T) { t.Parallel() From 777e1068b15a357bfccb0386db618bd06b1b130e Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Tue, 26 May 2026 22:10:35 +0300 Subject: [PATCH 3/3] fix(api): return null for /wallet bzzBalance when erc20 service unavailable --- pkg/api/api_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index bd64261e8c1..babd816dd06 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -182,9 +182,9 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. o.StateStorer = storeRecipient } erc20 := erc20mock.New(o.Erc20Opts...) - var erc20APIService erc20pkg.Service = erc20 - if o.Erc20ServiceNil { - erc20APIService = nil + var erc20APIService erc20pkg.Service + if !o.Erc20ServiceNil { + erc20APIService = erc20 } backend := backendmock.New(o.BackendOpts...)