From 4cb8d6dd153392b7032105f1c0e2de197acdb54b Mon Sep 17 00:00:00 2001 From: vsnation Date: Tue, 6 Oct 2026 15:29:23 +0100 Subject: [PATCH] api: invoke_contract fails fast when there's no node connection (#1695) Without a node connection the shader never starts: ShadersManager waits for a synced wallet (DoInSyncedWallet) that never comes, so invoke_contract never answered, and later calls either queued behind it (default API) or failed with "Previous shader call is still in progress" (v6.0). Check the node connection before queueing the call and answer ContractError "No connection to the node". Without a nodeNetwork (e.g. wasm) nothing changes. wallet_test: TestInvokeContractNoNodeConnection (fails without the fix). --- wallet/api/v6_0/v6_api.cpp | 10 ++++++ wallet/api/v6_0/v6_api.h | 5 +++ wallet/api/v6_0/v6_api_handle.cpp | 2 ++ wallet/api/v6_1/v6_1_api_handle.cpp | 1 + wallet/unittests/wallet_test.cpp | 47 +++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+) diff --git a/wallet/api/v6_0/v6_api.cpp b/wallet/api/v6_0/v6_api.cpp index 7f5cb6e8dc..81dbc59662 100644 --- a/wallet/api/v6_0/v6_api.cpp +++ b/wallet/api/v6_0/v6_api.cpp @@ -21,6 +21,7 @@ namespace beam::wallet , _wallet(init.wallet) , _swaps(init.swaps) , _contracts(init.contracts) + , _nodeNetwork(init.nodeNetwork) #ifdef BEAM_IPFS_SUPPORT , _ipfs(init.ipfs) #endif @@ -113,6 +114,15 @@ namespace beam::wallet return _contracts; } + void V6Api::checkNodeConnection() const + { + // nodeNetwork is not provided by every API host, in that case we cannot tell + if (_nodeNetwork && _nodeNetwork->getConnections() == 0) + { + throw jsonrpc_exception(ApiError::ContractError, "No connection to the node"); + } + } + #ifdef BEAM_IPFS_SUPPORT IPFSService::Ptr V6Api::getIPFS() const { diff --git a/wallet/api/v6_0/v6_api.h b/wallet/api/v6_0/v6_api.h index 202db2fee1..d6b2994e1b 100644 --- a/wallet/api/v6_0/v6_api.h +++ b/wallet/api/v6_0/v6_api.h @@ -52,6 +52,10 @@ namespace beam::wallet virtual void fillCoins(json& arr, const std::vector& coins); virtual void fillTransactions(json& arr, const std::vector& txs); + // Throws if the wallet has no connection to a node. Shader calls need to query + // the node and would otherwise wait forever, blocking all subsequent shader calls + void checkNodeConnection() const; + private: void FillAddressData(const AddressData& data, WalletAddress& address); void doTxAlreadyExistsError(const JsonRpcId& id); @@ -82,6 +86,7 @@ namespace beam::wallet Wallet::Ptr _wallet; ISwapsProvider::Ptr _swaps; IShadersManager::Ptr _contracts; + NodeNetwork::Ptr _nodeNetwork; #ifdef BEAM_IPFS_SUPPORT IPFSService::Ptr _ipfs; diff --git a/wallet/api/v6_0/v6_api_handle.cpp b/wallet/api/v6_0/v6_api_handle.cpp index a8a5ca7e76..5a13dae673 100644 --- a/wallet/api/v6_0/v6_api_handle.cpp +++ b/wallet/api/v6_0/v6_api_handle.cpp @@ -836,6 +836,8 @@ namespace beam::wallet throw jsonrpc_exception(ApiError::UnexpectedError, "Previous shader call is still in progress"); } + checkNodeConnection(); + if (data.createTx) { onHandleInvokeContractWithTX(id, std::move(data)); diff --git a/wallet/api/v6_1/v6_1_api_handle.cpp b/wallet/api/v6_1/v6_1_api_handle.cpp index 59ac8a0225..4c78c02d25 100644 --- a/wallet/api/v6_1/v6_1_api_handle.cpp +++ b/wallet/api/v6_1/v6_1_api_handle.cpp @@ -196,6 +196,7 @@ namespace beam::wallet { BEAM_LOG_VERBOSE() << "InvokeContract(id = " << id << ")"; auto contracts = getContracts(); + checkNodeConnection(); if (data.createTx) { diff --git a/wallet/unittests/wallet_test.cpp b/wallet/unittests/wallet_test.cpp index 16559d3beb..0941d0aaac 100644 --- a/wallet/unittests/wallet_test.cpp +++ b/wallet/unittests/wallet_test.cpp @@ -4297,6 +4297,52 @@ namespace WALLET_CHECK(txHistory[0].m_txId == txId); WALLET_CHECK(txHistory[0].m_status == wallet::TxStatus::Completed); } + + void TestInvokeContractNoNodeConnection() + { + cout << "\nTesting invoke_contract without node connection...\n"; + + io::Reactor::Ptr mainReactor{ io::Reactor::create() }; + io::Reactor::Scope scope(*mainReactor); + + TestWalletRig sender(createSenderWalletDB(), Wallet::TxCompletedAction(), TestWalletRig::Type::Offline); + + // node network that is never connected + auto nodeNetwork = std::make_shared(*sender.m_Wallet); + sender.m_Wallet->SetNodeEndpoint(nodeNetwork); + WALLET_CHECK(nodeNetwork->getConnections() == 0); + + ApiInitData data; + data.walletDB = sender.m_WalletDB; + data.wallet = sender.m_Wallet; + data.contracts = IShadersManager::CreateInstance(*sender.m_Wallet, "", "", 0); + data.nodeNetwork = nodeNetwork; + + ApiTest api(data); + + // every call must fail immediately and must not leave the shader manager busy + for (int i = 0; i < 2; ++i) + { + InvokeContract message; + message.contract = { 0, 'a', 's', 'm' }; + message.createTx = false; + + bool thrown = false; + try + { + api.onHandleInvokeContract(1, std::move(message)); + } + catch (const jsonrpc_exception& ex) + { + thrown = true; + WALLET_CHECK(ex.code() == ApiError::ContractError); + } + + WALLET_CHECK(thrown); + WALLET_CHECK(api.m_Messages.empty()); + WALLET_CHECK(data.contracts->IsDone()); + } + } } bool RunNegLoop(beam::Negotiator::IBase& a, beam::Negotiator::IBase& b, const char* szTask) @@ -5890,6 +5936,7 @@ int main() TestCalculateAssetCoinsSelection(); TestContractInvoke(); + TestInvokeContractNoNodeConnection(); assert(g_failureCount == 0); return WALLET_CHECK_RESULT;