diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index 8b5e6330f18..525e1eeaee9 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -22,70 +22,71 @@ import ( ) const ( - optionNameDataDir = "data-dir" - optionNameCacheCapacity = "cache-capacity" - optionNameDBOpenFilesLimit = "db-open-files-limit" - optionNameDBBlockCacheCapacity = "db-block-cache-capacity" - optionNameDBWriteBufferSize = "db-write-buffer-size" - optionNameDBDisableSeeksCompaction = "db-disable-seeks-compaction" - optionNamePassword = "password" - optionNamePasswordFile = "password-file" - optionNameAPIAddr = "api-addr" - optionNameP2PAddr = "p2p-addr" - optionNameNATAddr = "nat-addr" - optionNameP2PWSEnable = "p2p-ws-enable" - optionNameDebugAPIEnable = "debug-api-enable" - optionNameDebugAPIAddr = "debug-api-addr" - optionNameBootnodes = "bootnode" - optionNameNetworkID = "network-id" - optionWelcomeMessage = "welcome-message" - optionCORSAllowedOrigins = "cors-allowed-origins" - optionNameTracingEnabled = "tracing-enable" - optionNameTracingEndpoint = "tracing-endpoint" - optionNameTracingHost = "tracing-host" - optionNameTracingPort = "tracing-port" - optionNameTracingServiceName = "tracing-service-name" - optionNameVerbosity = "verbosity" - optionNamePaymentThreshold = "payment-threshold" - optionNamePaymentTolerance = "payment-tolerance-percent" - optionNamePaymentEarly = "payment-early-percent" - optionNameResolverEndpoints = "resolver-options" - optionNameBootnodeMode = "bootnode-mode" - optionNameClefSignerEnable = "clef-signer-enable" - optionNameClefSignerEndpoint = "clef-signer-endpoint" - optionNameClefSignerEthereumAddress = "clef-signer-ethereum-address" - optionNameSwapEndpoint = "swap-endpoint" // deprecated: use rpc endpoint instead - optionNameBlockchainRpcEndpoint = "blockchain-rpc-endpoint" - optionNameSwapFactoryAddress = "swap-factory-address" - optionNameSwapInitialDeposit = "swap-initial-deposit" - optionNameSwapEnable = "swap-enable" - optionNameChequebookEnable = "chequebook-enable" - optionNameSwapDeploymentGasPrice = "swap-deployment-gas-price" - optionNameFullNode = "full-node" - optionNamePostageContractAddress = "postage-stamp-address" - optionNamePostageContractStartBlock = "postage-stamp-start-block" - optionNamePriceOracleAddress = "price-oracle-address" - optionNameRedistributionAddress = "redistribution-address" - optionNameStakingAddress = "staking-address" - optionNameBlockTime = "block-time" - optionWarmUpTime = "warmup-time" - optionNameMainNet = "mainnet" - optionNameRetrievalCaching = "cache-retrieval" - optionNameDevReserveCapacity = "dev-reserve-capacity" - optionNameResync = "resync" - optionNamePProfBlock = "pprof-profile" - optionNamePProfMutex = "pprof-mutex" - optionNameStaticNodes = "static-nodes" - optionNameAllowPrivateCIDRs = "allow-private-cidrs" - optionNameSleepAfter = "sleep-after" - optionNameRestrictedAPI = "restricted" - optionNameTokenEncryptionKey = "token-encryption-key" - optionNameAdminPasswordHash = "admin-password" - optionNameUsePostageSnapshot = "use-postage-snapshot" - optionNameStorageIncentivesEnable = "storage-incentives-enable" - optionNameStateStoreCacheCapacity = "statestore-cache-capacity" - optionNameTargetNeighborhood = "target-neighborhood" - optionNameNeighborhoodSuggester = "neighborhood-suggester" + optionNameDataDir = "data-dir" + optionNameCacheCapacity = "cache-capacity" + optionNameDBOpenFilesLimit = "db-open-files-limit" + optionNameDBBlockCacheCapacity = "db-block-cache-capacity" + optionNameDBWriteBufferSize = "db-write-buffer-size" + optionNameDBDisableSeeksCompaction = "db-disable-seeks-compaction" + optionNamePassword = "password" + optionNamePasswordFile = "password-file" + optionNameAPIAddr = "api-addr" + optionNameP2PAddr = "p2p-addr" + optionNameNATAddr = "nat-addr" + optionNameP2PWSEnable = "p2p-ws-enable" + optionNameDebugAPIEnable = "debug-api-enable" + optionNameDebugAPIAddr = "debug-api-addr" + optionNameBootnodes = "bootnode" + optionNameNetworkID = "network-id" + optionWelcomeMessage = "welcome-message" + optionCORSAllowedOrigins = "cors-allowed-origins" + optionNameTracingEnabled = "tracing-enable" + optionNameTracingEndpoint = "tracing-endpoint" + optionNameTracingHost = "tracing-host" + optionNameTracingPort = "tracing-port" + optionNameTracingServiceName = "tracing-service-name" + optionNameVerbosity = "verbosity" + optionNamePaymentThreshold = "payment-threshold" + optionNamePaymentTolerance = "payment-tolerance-percent" + optionNamePaymentEarly = "payment-early-percent" + optionNameResolverEndpoints = "resolver-options" + optionNameBootnodeMode = "bootnode-mode" + optionNameClefSignerEnable = "clef-signer-enable" + optionNameClefSignerEndpoint = "clef-signer-endpoint" + optionNameClefSignerEthereumAddress = "clef-signer-ethereum-address" + optionNameSwapEndpoint = "swap-endpoint" // deprecated: use rpc endpoint instead + optionNameBlockchainRpcEndpoint = "blockchain-rpc-endpoint" + optionNameSwapFactoryAddress = "swap-factory-address" + optionNameSwapInitialDeposit = "swap-initial-deposit" + optionNameSwapEnable = "swap-enable" + optionNameChequebookEnable = "chequebook-enable" + optionNameSwapDeploymentGasPrice = "swap-deployment-gas-price" + optionNameFullNode = "full-node" + optionNamePostageContractAddress = "postage-stamp-address" + optionNamePostageContractStartBlock = "postage-stamp-start-block" + optionNamePriceOracleAddress = "price-oracle-address" + optionNameRedistributionAddress = "redistribution-address" + optionNameStakingAddress = "staking-address" + optionNameBlockTime = "block-time" + optionWarmUpTime = "warmup-time" + optionNameMainNet = "mainnet" + optionNameRetrievalCaching = "cache-retrieval" + optionNameDevReserveCapacity = "dev-reserve-capacity" + optionNameResync = "resync" + optionNamePProfBlock = "pprof-profile" + optionNamePProfMutex = "pprof-mutex" + optionNameStaticNodes = "static-nodes" + optionNameAllowPrivateCIDRs = "allow-private-cidrs" + optionNameSleepAfter = "sleep-after" + optionNameRestrictedAPI = "restricted" + optionNameTokenEncryptionKey = "token-encryption-key" + optionNameAdminPasswordHash = "admin-password" + optionNameUsePostageSnapshot = "use-postage-snapshot" + optionNameStorageIncentivesEnable = "storage-incentives-enable" + optionNameStateStoreCacheCapacity = "statestore-cache-capacity" + optionNameTargetNeighborhood = "target-neighborhood" + optionNameNeighborhoodSuggester = "neighborhood-suggester" + optionNameWhitelistedWithdrawalAddress = "withdrawal-address" ) // nolint:gochecknoinits @@ -304,6 +305,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().Uint64(optionNameStateStoreCacheCapacity, 100_000, "lru memory caching capacity in number of statestore entries") cmd.Flags().String(optionNameTargetNeighborhood, "", "neighborhood to target in binary format (ex: 111111001) for mining the initial overlay") cmd.Flags().String(optionNameNeighborhoodSuggester, "https://api.swarmscan.io/v1/network/neighborhoods/suggestion", "suggester for target neighborhood") + cmd.Flags().String(optionNameWhitelistedWithdrawalAddress, "", "Withdrawal target address") } func newLogger(cmd *cobra.Command, verbosity string) (log.Logger, error) { diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 60ce54582e7..1f343f20d39 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -340,6 +340,7 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo StatestoreCacheCapacity: c.config.GetUint64(optionNameStateStoreCacheCapacity), TargetNeighborhood: c.config.GetString(optionNameTargetNeighborhood), NeighborhoodSuggester: c.config.GetString(optionNameNeighborhoodSuggester), + WhitelistedWithdrawalAddress: c.config.GetStringSlice(optionNameWhitelistedWithdrawalAddress), }) return b, err diff --git a/pkg/api/api.go b/pkg/api/api.go index 0d9cdaab415..25ef0802692 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -197,6 +197,8 @@ type Service struct { erc20Service erc20.Service chainID int64 + whitelistedWithdrawalAddress []common.Address + preMapHooks map[string]func(v string) (string, error) validate *validator.Validate @@ -254,6 +256,7 @@ type ExtraOptions struct { func New( publicKey, pssPublicKey ecdsa.PublicKey, ethereumAddress common.Address, + whitelistedWithdrawalAddress []string, logger log.Logger, transaction transaction.Service, batchStore postage.Storer, @@ -303,6 +306,10 @@ func New( }) s.stamperStore = stamperStore + for _, v := range whitelistedWithdrawalAddress { + s.whitelistedWithdrawalAddress = append(s.whitelistedWithdrawalAddress, common.HexToAddress(v)) + } + return s } diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index cfa6794257d..5bc373da0ba 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -210,7 +210,7 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. o.BeeMode = api.FullMode } - s := api.New(o.PublicKey, o.PSSPublicKey, o.EthereumAddress, o.Logger, transaction, o.BatchStore, o.BeeMode, true, true, backend, o.CORSAllowedOrigins, inmemstore.New()) + s := api.New(o.PublicKey, o.PSSPublicKey, o.EthereumAddress, nil, o.Logger, transaction, o.BatchStore, o.BeeMode, true, true, backend, o.CORSAllowedOrigins, inmemstore.New()) testutil.CleanupCloser(t, s) s.SetP2P(o.P2P) @@ -395,7 +395,7 @@ func TestParseName(t *testing.T) { pk, _ := crypto.GenerateSecp256k1Key() signer := crypto.NewDefaultSigner(pk) - s := api.New(pk.PublicKey, pk.PublicKey, common.Address{}, log, nil, nil, 1, false, false, nil, []string{"*"}, inmemstore.New()) + s := api.New(pk.PublicKey, pk.PublicKey, common.Address{}, nil, log, nil, nil, 1, false, false, nil, []string{"*"}, inmemstore.New()) s.Configure(signer, nil, nil, api.Options{}, api.ExtraOptions{Resolver: tC.res}, 1, nil) s.MountAPI() diff --git a/pkg/api/router.go b/pkg/api/router.go index 3e4574ed896..f6f75e348ea 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -491,6 +491,12 @@ func (s *Service) mountBusinessDebug(restricted bool) { handle("/wallet", jsonhttp.MethodHandler{ "GET": http.HandlerFunc(s.walletHandler), }) + handle("/wallet/withdraw/{coin}/{addr}", jsonhttp.MethodHandler{ + "POST": web.ChainHandlers( + s.gasConfigMiddleware("wallet withdraw"), + web.FinalHandlerFunc(s.walletWithdrawHandler), + ), + }) } } diff --git a/pkg/api/wallet.go b/pkg/api/wallet.go index c98e83b5987..c5e31a68734 100644 --- a/pkg/api/wallet.go +++ b/pkg/api/wallet.go @@ -5,11 +5,17 @@ package api import ( + "context" + "math/big" "net/http" + "slices" + "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/pkg/bigint" "github.com/ethersphere/bee/pkg/jsonhttp" + "github.com/ethersphere/bee/pkg/transaction" + "github.com/gorilla/mux" ) type walletResponse struct { @@ -20,6 +26,10 @@ type walletResponse struct { WalletAddress common.Address `json:"walletAddress"` // the address of the bee wallet } +type walletTxResponse struct { + TransactionHash common.Hash `json:"transactionHash"` +} + func (s *Service) walletHandler(w http.ResponseWriter, r *http.Request) { logger := s.logger.WithName("get_wallet").Build() @@ -47,3 +57,87 @@ func (s *Service) walletHandler(w http.ResponseWriter, r *http.Request) { WalletAddress: s.ethereumAddress, }) } + +func (s *Service) walletWithdrawHandler(w http.ResponseWriter, r *http.Request) { + logger := s.logger.WithName("post_wallet_withdraw").Build() + + queries := struct { + Amount *big.Int `map:"amount" validate:"required"` + }{} + + if response := s.mapStructure(r.URL.Query(), &queries); response != nil { + response("invalid query params", logger, w) + return + } + + path := struct { + Coin *string `map:"coin" validate:"required"` + Address *common.Address `map:"address" validate:"required"` + }{} + + if response := s.mapStructure(mux.Vars(r), &path); response != nil { + response("invalid query params", logger, w) + return + } + + ctx := r.Context() + var bzz bool + // check if coin is xdai or bzz + + if !slices.Contains(s.whitelistedWithdrawalAddress, *path.Address) { + logger.Error(nil, "provided address not whitelisted") + jsonhttp.InternalServerError(w, "provided address not whitelisted") + return + } + + if bzz { + currentBalance, err := s.erc20Service.BalanceOf(ctx, s.ethereumAddress) + if err != nil { + logger.Error(err, "unable to get balance") + jsonhttp.InternalServerError(w, "unable to get balance") + return + } + + if queries.Amount.Cmp(currentBalance) > 0 { + logger.Error(err, "not enough balance") + jsonhttp.InternalServerError(w, "not enough balance") + return + } + + txHash, err := s.erc20Service.Withdraw(ctx, *path.Address, queries.Amount) + if err != nil { + logger.Error(err, "unable to transfer") + jsonhttp.InternalServerError(w, "unable to transfer amount") + return + } + jsonhttp.OK(w, walletTxResponse{TransactionHash: txHash}) + return + } + + nativeToken, err := s.chainBackend.BalanceAt(r.Context(), s.ethereumAddress, nil) + if err != nil { + logger.Debug("unable to acquire balance from the chain backend", "error", err) + logger.Error(nil, "unable to acquire balance from the chain backend") + jsonhttp.InternalServerError(w, "unable to acquire balance from the chain backend") + return + } + + if queries.Amount.Cmp(nativeToken) > 0 { + logger.Error(err, "not enough balance") + jsonhttp.InternalServerError(w, "not enough balance") + return + } + + txHash, err := withdraw(ctx, s.chainBackend, *path.Address, queries.Amount) + if err != nil { + logger.Error(err, "withdraw") + jsonhttp.InternalServerError(w, "withdraw") + return + } + + jsonhttp.OK(w, walletTxResponse{TransactionHash: txHash}) +} + +func withdraw(context.Context, transaction.Backend, common.Address, *big.Int) (common.Hash, error) { + return common.Hash{}, nil +} diff --git a/pkg/node/devnode.go b/pkg/node/devnode.go index 76b055b531f..cff06b2d711 100644 --- a/pkg/node/devnode.go +++ b/pkg/node/devnode.go @@ -202,7 +202,7 @@ func NewDevBee(logger log.Logger, o *DevOptions) (b *DevBee, err error) { return nil, fmt.Errorf("debug api listener: %w", err) } - debugApiService = api.New(mockKey.PublicKey, mockKey.PublicKey, overlayEthAddress, logger, mockTransaction, batchStore, api.DevMode, true, true, chainBackend, o.CORSAllowedOrigins, inmemstore.New()) + debugApiService = api.New(mockKey.PublicKey, mockKey.PublicKey, overlayEthAddress, nil, logger, mockTransaction, batchStore, api.DevMode, true, true, chainBackend, o.CORSAllowedOrigins, inmemstore.New()) debugAPIServer := &http.Server{ IdleTimeout: 30 * time.Second, ReadHeaderTimeout: 3 * time.Second, @@ -398,7 +398,7 @@ func NewDevBee(logger log.Logger, o *DevOptions) (b *DevBee, err error) { }), ) - apiService := api.New(mockKey.PublicKey, mockKey.PublicKey, overlayEthAddress, logger, mockTransaction, batchStore, api.DevMode, true, true, chainBackend, o.CORSAllowedOrigins, inmemstore.New()) + apiService := api.New(mockKey.PublicKey, mockKey.PublicKey, overlayEthAddress, nil, logger, mockTransaction, batchStore, api.DevMode, true, true, chainBackend, o.CORSAllowedOrigins, inmemstore.New()) apiService.Configure(signer, authenticator, tracer, api.Options{ CORSAllowedOrigins: o.CORSAllowedOrigins, diff --git a/pkg/node/node.go b/pkg/node/node.go index 8f1d6b3aa11..bc6223d477c 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -172,6 +172,7 @@ type Options struct { StatestoreCacheCapacity uint64 TargetNeighborhood string NeighborhoodSuggester string + WhitelistedWithdrawalAddress []string } const ( @@ -421,6 +422,7 @@ func NewBee( *publicKey, pssPrivateKey.PublicKey, overlayEthAddress, + o.WhitelistedWithdrawalAddress, logger, transactionService, batchStore, @@ -460,6 +462,7 @@ func NewBee( *publicKey, pssPrivateKey.PublicKey, overlayEthAddress, + o.WhitelistedWithdrawalAddress, logger, transactionService, batchStore, @@ -1094,7 +1097,7 @@ func NewBee( if o.APIAddr != "" { if apiService == nil { - apiService = api.New(*publicKey, pssPrivateKey.PublicKey, overlayEthAddress, logger, transactionService, batchStore, beeNodeMode, o.ChequebookEnable, o.SwapEnable, chainBackend, o.CORSAllowedOrigins, stamperStore) + apiService = api.New(*publicKey, pssPrivateKey.PublicKey, overlayEthAddress, o.WhitelistedWithdrawalAddress, logger, transactionService, batchStore, beeNodeMode, o.ChequebookEnable, o.SwapEnable, chainBackend, o.CORSAllowedOrigins, stamperStore) apiService.SetProbe(probe) apiService.SetRedistributionAgent(agent) } diff --git a/pkg/settlement/swap/chequebook/chequebook.go b/pkg/settlement/swap/chequebook/chequebook.go index 75d8e7876ca..85164228ddd 100644 --- a/pkg/settlement/swap/chequebook/chequebook.go +++ b/pkg/settlement/swap/chequebook/chequebook.go @@ -69,7 +69,8 @@ type service struct { lock sync.Mutex transactionService transaction.Service - address common.Address + address common.Address + contract *chequebookContract ownerAddress common.Address diff --git a/pkg/settlement/swap/chequebook/chequebook_test.go b/pkg/settlement/swap/chequebook/chequebook_test.go index ca3f705e80e..a50f401faf9 100644 --- a/pkg/settlement/swap/chequebook/chequebook_test.go +++ b/pkg/settlement/swap/chequebook/chequebook_test.go @@ -425,38 +425,40 @@ func TestChequebookIssueOutOfFunds(t *testing.T) { func TestChequebookWithdraw(t *testing.T) { t.Parallel() - address := common.HexToAddress("0xabcd") - ownerAdress := common.HexToAddress("0xfff") - balance := big.NewInt(30) - withdrawAmount := big.NewInt(20) - txHash := common.HexToHash("0xdddd") - store := storemock.NewStateStore() - chequebookService, err := chequebook.New( - transactionmock.New( - transactionmock.WithABICallSequence( - transactionmock.ABICall(&chequebookABI, address, balance.FillBytes(make([]byte, 32)), "balance"), - transactionmock.ABICall(&chequebookABI, address, big.NewInt(0).FillBytes(make([]byte, 32)), "totalPaidOut"), + t.Run("target withdrawal address is owner", func(t *testing.T) { + address := common.HexToAddress("0xabcd") + ownerAdress := common.HexToAddress("0xfff") + balance := big.NewInt(30) + withdrawAmount := big.NewInt(20) + txHash := common.HexToHash("0xdddd") + store := storemock.NewStateStore() + chequebookService, err := chequebook.New( + transactionmock.New( + transactionmock.WithABICallSequence( + transactionmock.ABICall(&chequebookABI, address, balance.FillBytes(make([]byte, 32)), "balance"), + transactionmock.ABICall(&chequebookABI, address, big.NewInt(0).FillBytes(make([]byte, 32)), "totalPaidOut"), + ), + transactionmock.WithABISend(&chequebookABI, txHash, address, big.NewInt(0), "withdraw", withdrawAmount), ), - transactionmock.WithABISend(&chequebookABI, txHash, address, big.NewInt(0), "withdraw", withdrawAmount), - ), - address, - ownerAdress, - store, - &chequeSignerMock{}, - erc20mock.New(), - ) - if err != nil { - t.Fatal(err) - } + address, + ownerAdress, + store, + &chequeSignerMock{}, + erc20mock.New(), + ) + if err != nil { + t.Fatal(err) + } - returnedTxHash, err := chequebookService.Withdraw(context.Background(), withdrawAmount) - if err != nil { - t.Fatal(err) - } + returnedTxHash, err := chequebookService.Withdraw(context.Background(), withdrawAmount) + if err != nil { + t.Fatal(err) + } - if txHash != returnedTxHash { - t.Fatalf("returned wrong transaction hash. wanted %v, got %v", txHash, returnedTxHash) - } + if txHash != returnedTxHash { + t.Fatalf("returned wrong transaction hash. wanted %v, got %v", txHash, returnedTxHash) + } + }) } func TestChequebookWithdrawInsufficientFunds(t *testing.T) { diff --git a/pkg/settlement/swap/erc20/erc20.go b/pkg/settlement/swap/erc20/erc20.go index 510cc43769b..1fe5bb515c1 100644 --- a/pkg/settlement/swap/erc20/erc20.go +++ b/pkg/settlement/swap/erc20/erc20.go @@ -25,6 +25,7 @@ var ( type Service interface { BalanceOf(ctx context.Context, address common.Address) (*big.Int, error) Transfer(ctx context.Context, address common.Address, value *big.Int) (common.Hash, error) + Withdraw(ctx context.Context, address common.Address, value *big.Int) (common.Hash, error) } type erc20Service struct { @@ -91,3 +92,26 @@ func (c *erc20Service) Transfer(ctx context.Context, address common.Address, val return txHash, nil } + +func (c *erc20Service) Withdraw(ctx context.Context, address common.Address, value *big.Int) (common.Hash, error) { + callData, err := erc20ABI.Pack("transfer", c.address, value) + if err != nil { + return common.Hash{}, err + } + + request := &transaction.TxRequest{ + To: &address, + Data: callData, + GasPrice: sctx.GetGasPrice(ctx), + GasLimit: 90000, + Value: big.NewInt(0), + Description: "token transfer", + } + + txHash, err := c.transactionService.Send(ctx, request, transaction.DefaultTipBoostPercent) + if err != nil { + return common.Hash{}, err + } + + return txHash, nil +} diff --git a/pkg/settlement/swap/erc20/mock/erc20.go b/pkg/settlement/swap/erc20/mock/erc20.go index 8d7652d2fb3..15da78c6d82 100644 --- a/pkg/settlement/swap/erc20/mock/erc20.go +++ b/pkg/settlement/swap/erc20/mock/erc20.go @@ -16,6 +16,7 @@ import ( type Service struct { balanceOfFunc func(ctx context.Context, address common.Address) (*big.Int, error) transferFunc func(ctx context.Context, address common.Address, value *big.Int) (common.Hash, error) + withdrawFunc func(ctx context.Context, address common.Address, value *big.Int) (common.Hash, error) } func WithBalanceOfFunc(f func(ctx context.Context, address common.Address) (*big.Int, error)) Option { @@ -52,6 +53,13 @@ func (s *Service) Transfer(ctx context.Context, address common.Address, value *b return common.Hash{}, errors.New("Error") } +func (s *Service) Withdraw(ctx context.Context, address common.Address, value *big.Int) (common.Hash, error) { + if s.transferFunc != nil { + return s.withdrawFunc(ctx, address, value) + } + return common.Hash{}, errors.New("Error") +} + // Option is the option passed to the mock Chequebook service type Option interface { apply(*Service)