diff --git a/browser/brave_wallet/ethereum_provider_impl_unittest.cc b/browser/brave_wallet/ethereum_provider_impl_unittest.cc index a70013e36d41..cb46921466d9 100644 --- a/browser/brave_wallet/ethereum_provider_impl_unittest.cc +++ b/browser/brave_wallet/ethereum_provider_impl_unittest.cc @@ -144,6 +144,12 @@ class TestEventsListener : public brave_wallet::mojom::EventsListener { accounts_changed_fired_ = true; } + void MessageEvent(const std::string& subscription_id, + base::Value result) override { + message_event_fired_ = true; + last_message_ = std::move(result); + } + bool ChainChangedFired() const { base::RunLoop().RunUntilIdle(); return chain_changed_fired_; @@ -154,11 +160,21 @@ class TestEventsListener : public brave_wallet::mojom::EventsListener { return accounts_changed_fired_; } + bool MessageEventFired() const { + base::RunLoop().RunUntilIdle(); + return message_event_fired_; + } + std::string GetChainId() const { base::RunLoop().RunUntilIdle(); return chain_id_; } + base::Value GetLastMessage() const { + base::RunLoop().RunUntilIdle(); + return last_message_.Clone(); + } + std::vector GetLowercaseAccounts() const { base::RunLoop().RunUntilIdle(); return lowercase_accounts_; @@ -173,14 +189,18 @@ class TestEventsListener : public brave_wallet::mojom::EventsListener { lowercase_accounts_.clear(); chain_changed_fired_ = false; accounts_changed_fired_ = false; + message_event_fired_ = false; EXPECT_FALSE(ChainChangedFired()); EXPECT_FALSE(AccountsChangedFired()); + EXPECT_FALSE(MessageEventFired()); } bool chain_changed_fired_ = false; bool accounts_changed_fired_ = false; + bool message_event_fired_ = false; std::vector lowercase_accounts_; std::string chain_id_; + base::Value last_message_; private: mojo::Receiver observer_receiver_{this}; @@ -189,7 +209,9 @@ class TestEventsListener : public brave_wallet::mojom::EventsListener { class EthereumProviderImplUnitTest : public testing::Test { public: EthereumProviderImplUnitTest() - : shared_url_loader_factory_( + : browser_task_environment_( + base::test::TaskEnvironment::TimeSource::MOCK_TIME), + shared_url_loader_factory_( base::MakeRefCounted( &url_loader_factory_)) {} @@ -875,6 +897,8 @@ class EthereumProviderImplUnitTest : public testing::Test { raw_ptr json_rpc_service_ = nullptr; raw_ptr brave_wallet_service_ = nullptr; std::unique_ptr observer_; + network::TestURLLoaderFactory url_loader_factory_; + std::unique_ptr provider_; private: std::unique_ptr local_state_; @@ -883,8 +907,6 @@ class EthereumProviderImplUnitTest : public testing::Test { raw_ptr tx_service_; raw_ptr asset_ratio_service_; std::unique_ptr web_contents_; - std::unique_ptr provider_; - network::TestURLLoaderFactory url_loader_factory_; data_decoder::test::InProcessDataDecoder in_process_data_decoder_; scoped_refptr shared_url_loader_factory_; base::ScopedTempDir temp_dir_; @@ -1964,6 +1986,97 @@ TEST_F(EthereumProviderImplUnitTest, AccountsChangedEvent) { EXPECT_FALSE(observer_->AccountsChangedFired()); } +TEST_F(EthereumProviderImplUnitTest, EthSubscribe) { + CreateWallet(); + + // Unsupported subscription type + std::string request_payload_json = + R"({"id":1,"jsonrpc:": "2.0","method":"eth_subscribe", + "params": ["foo"]})"; + absl::optional request_payload = base::JSONReader::Read( + request_payload_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + auto response = CommonRequestOrSendAsync(request_payload.value()); + + mojom::ProviderError error_code; + std::string error_message; + GetErrorCodeMessage(std::move(response.second), &error_code, &error_message); + EXPECT_EQ(response.first, true); + EXPECT_EQ(error_code, mojom::ProviderError::kInternalError); + EXPECT_EQ(error_message, + l10n_util::GetStringUTF8(IDS_WALLET_UNSUPPORTED_SUBSCRIPTION_TYPE)); + + url_loader_factory_.SetInterceptor( + base::BindLambdaForTesting([&](const network::ResourceRequest& request) { + url_loader_factory_.ClearResponses(); + + std::string header_value; + EXPECT_TRUE(request.headers.GetHeader("X-Eth-Method", &header_value)); + std::string content; + if (header_value == "eth_blockNumber" || + header_value == "getBlockHeight") + content = R"({"id":1,"jsonrpc":"2.0","result":"0x131131"})"; + else if (header_value == "eth_getBlockByNumber") + content = R"({"id":1,"jsonrpc":"2.0","result":{"difficulty":"0x1"}})"; + url_loader_factory_.AddResponse(request.url.spec(), content); + })); + + // Subscribing to newHeads + request_payload_json = + R"({"id":1,"jsonrpc:": "2.0","method":"eth_subscribe", + "params": ["newHeads"]})"; + request_payload = base::JSONReader::Read( + request_payload_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + response = CommonRequestOrSendAsync(request_payload.value()); + EXPECT_EQ(response.first, false); + EXPECT_TRUE(response.second.is_string()); + std::string first_subscription = *response.second.GetIfString(); + browser_task_environment_.FastForwardBy( + base::Seconds(kBlockTrackerDefaultTimeInSeconds)); + EXPECT_TRUE(observer_->MessageEventFired()); + base::Value rv = observer_->GetLastMessage(); + ASSERT_TRUE(rv.is_dict()); + auto& dict = rv.GetDict(); + std::string* difficulty = dict.FindString("difficulty"); + ASSERT_TRUE(difficulty); + EXPECT_EQ(*difficulty, "0x1"); + + // Make a second subscription + request_payload_json = + R"({"id":1,"jsonrpc:": "2.0","method":"eth_subscribe", + "params": ["newHeads"]})"; + request_payload = base::JSONReader::Read( + request_payload_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + response = CommonRequestOrSendAsync(request_payload.value()); + EXPECT_EQ(response.first, false); + EXPECT_TRUE(response.second.is_string()); + std::string second_subscription = *response.second.GetIfString(); + + // The first unsubscribe should not stop the block tracker + request_payload_json = base::StringPrintf(R"({"id":1,"jsonrpc:": "2.0", + "method":"eth_unsubscribe", + "params": ["%s"]})", + first_subscription.c_str()); + request_payload = base::JSONReader::Read( + request_payload_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + response = CommonRequestOrSendAsync(request_payload.value()); + EXPECT_TRUE(provider_->eth_block_tracker_.IsRunning()); + + // The second unsubscribe should stop the block tracker + request_payload_json = base::StringPrintf(R"({"id":1,"jsonrpc:": "2.0", + "method":"eth_unsubscribe", + "params": ["%s"]})", + second_subscription.c_str()); + request_payload = base::JSONReader::Read( + request_payload_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + response = CommonRequestOrSendAsync(request_payload.value()); + EXPECT_FALSE(provider_->eth_block_tracker_.IsRunning()); +} + TEST_F(EthereumProviderImplUnitTest, Web3ClientVersion) { std::string expected_version = base::StringPrintf( "BraveWallet/v%s", version_info::GetBraveChromiumVersionNumber().c_str()); diff --git a/components/brave_wallet/browser/ethereum_provider_impl.cc b/components/brave_wallet/browser/ethereum_provider_impl.cc index 8ce706fd73de..72a04752c081 100644 --- a/components/brave_wallet/browser/ethereum_provider_impl.cc +++ b/components/brave_wallet/browser/ethereum_provider_impl.cc @@ -32,6 +32,7 @@ #include "brave/components/brave_wallet/common/web3_provider_constants.h" #include "components/content_settings/core/browser/host_content_settings_map.h" #include "components/grit/brave_components_strings.h" +#include "crypto/random.h" #include "ui/base/l10n/l10n_util.h" #include "url/origin.h" @@ -88,6 +89,7 @@ EthereumProviderImpl::EthereumProviderImpl( tx_service_(tx_service), keyring_service_(keyring_service), brave_wallet_service_(brave_wallet_service), + eth_block_tracker_(json_rpc_service), prefs_(prefs), weak_factory_(this) { DCHECK(json_rpc_service); @@ -105,10 +107,13 @@ EthereumProviderImpl::EthereumProviderImpl( // Get the current so we can compare for changed events if (delegate_) UpdateKnownAccounts(); + + eth_block_tracker_.AddObserver(this); } EthereumProviderImpl::~EthereumProviderImpl() { host_content_settings_map_->RemoveObserver(this); + eth_block_tracker_.RemoveObserver(this); } void EthereumProviderImpl::AddEthereumChain(const std::string& json_payload, @@ -569,6 +574,43 @@ void EthereumProviderImpl::RecoverAddress(const std::string& message, false); } +void EthereumProviderImpl::EthSubscribe(const std::string& event_type, + RequestCallback callback, + base::Value id) { + if (event_type != kEthSubscribeNewHeads) { + base::Value formed_response = GetProviderErrorDictionary( + mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_UNSUPPORTED_SUBSCRIPTION_TYPE)); + bool reject = true; + std::move(callback).Run(std::move(id), std::move(formed_response), reject, + "", false); + return; + } + std::vector bytes(16); + crypto::RandBytes(&bytes.front(), bytes.size()); + std::string hex_bytes = ToHex(bytes); + eth_subscriptions_.push_back(hex_bytes); + if (eth_subscriptions_.size() == 1) + eth_block_tracker_.Start(base::Seconds(kBlockTrackerDefaultTimeInSeconds)); + std::move(callback).Run(std::move(id), base::Value(hex_bytes), false, "", + false); +} + +void EthereumProviderImpl::EthUnsubscribe(const std::string& subscription_id, + RequestCallback callback, + base::Value id) { + auto it = std::find(eth_subscriptions_.begin(), eth_subscriptions_.end(), + subscription_id); + bool found = it != eth_subscriptions_.end(); + if (found) { + if (eth_subscriptions_.size() == 1) { + eth_block_tracker_.Stop(); + } + eth_subscriptions_.erase(it); + } + std::move(callback).Run(std::move(id), base::Value(found), false, "", false); +} + void EthereumProviderImpl::GetEncryptionPublicKey(const std::string& address, RequestCallback callback, base::Value id) { @@ -1152,6 +1194,22 @@ void EthereumProviderImpl::CommonRequestOrSendAsync(base::ValueView input_value, std::move(id), method, delegate_->GetOrigin())); } else if (method == kWeb3ClientVersion) { Web3ClientVersion(std::move(callback), std::move(id)); + } else if (method == kEthSubscribe) { + std::string event_type; + if (!ParseEthSubscribeParams(normalized_json_request, &event_type)) { + SendErrorOnRequest(error, error_message, std::move(callback), + std::move(id)); + return; + } + EthSubscribe(event_type, std::move(callback), std::move(id)); + } else if (method == kEthUnsubscribe) { + std::string subscription_id; + if (!ParseEthUnsubscribeParams(normalized_json_request, &subscription_id)) { + SendErrorOnRequest(error, error_message, std::move(callback), + std::move(id)); + return; + } + EthUnsubscribe(subscription_id, std::move(callback), std::move(id)); } else { json_rpc_service_->Request(normalized_json_request, true, std::move(id), mojom::CoinType::ETH, std::move(callback)); @@ -1583,4 +1641,27 @@ void EthereumProviderImpl::OnSendRawTransaction( error != mojom::ProviderError::kSuccess, "", false); } +// EthBlockTracker::Observer: +void EthereumProviderImpl::OnLatestBlock(uint256_t block_num) { + json_rpc_service_->GetBlockByNumber( + kEthereumBlockTagLatest, + base::BindOnce(&EthereumProviderImpl::OnGetBlockByNumber, + weak_factory_.GetWeakPtr())); +} + +void EthereumProviderImpl::OnGetBlockByNumber( + base::Value result, + mojom::ProviderError error, + const std::string& error_message) { + if (events_listener_.is_bound() && error == mojom::ProviderError::kSuccess) { + base::ranges::for_each(eth_subscriptions_, + [this, &result](const std::string& subscription_id) { + events_listener_->MessageEvent(subscription_id, + result.Clone()); + }); + } +} + +void EthereumProviderImpl::OnNewBlock(uint256_t block_num) {} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/ethereum_provider_impl.h b/components/brave_wallet/browser/ethereum_provider_impl.h index 8e9926896b10..99fb9db2e2fd 100644 --- a/components/brave_wallet/browser/ethereum_provider_impl.h +++ b/components/brave_wallet/browser/ethereum_provider_impl.h @@ -14,6 +14,7 @@ #include "base/containers/flat_map.h" #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" +#include "brave/components/brave_wallet/browser/eth_block_tracker.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/web3_provider_constants.h" #include "components/content_settings/core/browser/content_settings_observer.h" @@ -39,7 +40,8 @@ class EthereumProviderImpl final public mojom::JsonRpcServiceObserver, public mojom::TxServiceObserver, public brave_wallet::mojom::KeyringServiceObserver, - public content_settings::Observer { + public content_settings::Observer, + public EthBlockTracker::Observer { public: using GetAllowedAccountsCallback = base::OnceCallback& accounts, @@ -84,6 +86,13 @@ class EthereumProviderImpl final RequestCallback callback, base::Value id); + void EthSubscribe(const std::string& event_type, + RequestCallback callback, + base::Value id); + void EthUnsubscribe(const std::string& subscription_id, + RequestCallback callback, + base::Value id); + void GetEncryptionPublicKey(const std::string& address, RequestCallback callback, base::Value id); @@ -160,6 +169,7 @@ class EthereumProviderImpl final FRIEND_TEST_ALL_PREFIXES(EthereumProviderImplUnitTest, RequestEthCoinbase); FRIEND_TEST_ALL_PREFIXES(EthereumProviderImplUnitTest, RequestEthereumPermissionsWithAccounts); + FRIEND_TEST_ALL_PREFIXES(EthereumProviderImplUnitTest, EthSubscribe); friend class EthereumProviderImplUnitTest; // mojom::BraveWalletProvider: @@ -353,6 +363,13 @@ class EthereumProviderImpl final const std::string& tx_hash, mojom::ProviderError error, const std::string& error_message); + void OnGetBlockByNumber(base::Value result, + mojom::ProviderError, + const std::string&); + + // EthBlockTracker::Observer: + void OnLatestBlock(uint256_t block_num) override; + void OnNewBlock(uint256_t block_num) override; raw_ptr host_content_settings_map_ = nullptr; std::unique_ptr delegate_; @@ -374,6 +391,8 @@ class EthereumProviderImpl final mojo::Receiver keyring_observer_receiver_{this}; std::vector known_allowed_accounts; + std::vector eth_subscriptions_; + EthBlockTracker eth_block_tracker_; bool first_known_accounts_check = true; PrefService* prefs_ = nullptr; bool wallet_onboarding_shown_ = false; diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index ec4dfaf80e3b..72ac7e5cbe9a 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -1846,6 +1846,39 @@ void JsonRpcService::OnGetIsEip1559(GetIsEip1559Callback callback, mojom::ProviderError::kSuccess, ""); } +void JsonRpcService::GetBlockByNumber(const std::string& block_number, + GetBlockByNumberCallback callback) { + auto internal_callback = + base::BindOnce(&JsonRpcService::OnGetBlockByNumber, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + RequestInternal(eth::eth_getBlockByNumber(block_number, false), true, + network_urls_[mojom::CoinType::ETH], + std::move(internal_callback)); +} + +void JsonRpcService::OnGetBlockByNumber(GetBlockByNumberCallback callback, + APIRequestResult api_request_result) { + if (!api_request_result.Is2XXResponseCode()) { + std::move(callback).Run( + base::Value(), mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + return; + } + + auto result = ParseResultValue(api_request_result.body()); + if (!result) { + mojom::ProviderError error; + std::string error_message; + ParseErrorResult(api_request_result.body(), &error, + &error_message); + std::move(callback).Run(base::Value(), error, error_message); + return; + } + + std::move(callback).Run(std::move(*result), mojom::ProviderError::kSuccess, + ""); +} + /*static*/ bool JsonRpcService::IsValidDomain(const std::string& domain) { static const base::NoDestructor kDomainRegex(kDomainPattern); diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index 1d615575cad2..514cec62424c 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -282,6 +282,14 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& error_message)>; void GetIsEip1559(GetIsEip1559Callback callback); + using GetBlockByNumberCallback = + base::OnceCallback; + // block_number can be kEthereumBlockTagLatest + void GetBlockByNumber(const std::string& block_number, + GetBlockByNumberCallback callback); + void GetERC721OwnerOf(const std::string& contract, const std::string& token_id, const std::string& chain_id, @@ -490,6 +498,8 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { APIRequestResult api_request_result); void OnGetIsEip1559(GetIsEip1559Callback callback, APIRequestResult api_request_result); + void OnGetBlockByNumber(GetBlockByNumberCallback callback, + APIRequestResult api_request_result); void MaybeUpdateIsEip1559(const std::string& chain_id); void UpdateIsEip1559(const std::string& chain_id, diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index ae9b5fc753ad..b388e05465b7 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -19,6 +19,9 @@ interface EventsListener { // Fired when the accounts have changed such as when the wallet locked, the // selected account changes, a new account is given permission, etc. AccountsChangedEvent(array accounts); + + // Fired when there is a message from eth_subscribe + MessageEvent(string subscription_id, mojo_base.mojom.Value result); }; interface SolanaEventsListener { diff --git a/components/brave_wallet/common/eth_request_helper.cc b/components/brave_wallet/common/eth_request_helper.cc index 719a07333f55..4619bc9fffb8 100644 --- a/components/brave_wallet/common/eth_request_helper.cc +++ b/components/brave_wallet/common/eth_request_helper.cc @@ -674,7 +674,39 @@ bool ParseEthSendRawTransactionParams(const std::string& json, return false; *signed_transaction = *signed_tx_str; + return true; +} + +bool ParseEthSubscribeParams(const std::string& json, std::string* event_type) { + if (!event_type) + return false; + + auto list = GetParamsList(json); + if (!list || list->size() != 1) + return false; + + const std::string* event_type_str = (*list)[0].GetIfString(); + if (!event_type_str) + return false; + + *event_type = *event_type_str; + return true; +} + +bool ParseEthUnsubscribeParams(const std::string& json, + std::string* subscription_id) { + if (!subscription_id) + return false; + + auto list = GetParamsList(json); + if (!list || list->size() != 1) + return false; + + const std::string* subscription_id_str = (*list)[0].GetIfString(); + if (!subscription_id_str) + return false; + *subscription_id = *subscription_id_str; return true; } diff --git a/components/brave_wallet/common/eth_request_helper.h b/components/brave_wallet/common/eth_request_helper.h index e6cb93881277..76519990f21d 100644 --- a/components/brave_wallet/common/eth_request_helper.h +++ b/components/brave_wallet/common/eth_request_helper.h @@ -74,6 +74,9 @@ bool ParseRequestPermissionsParams( bool ParseEthSendRawTransactionParams(const std::string& json, std::string* signed_transaction); +bool ParseEthSubscribeParams(const std::string& json, std::string* event_type); +bool ParseEthUnsubscribeParams(const std::string& json, + std::string* subscription_id); } // namespace brave_wallet diff --git a/components/brave_wallet/common/web3_provider_constants.cc b/components/brave_wallet/common/web3_provider_constants.cc index a0c1b50ac798..284e1476ef4a 100644 --- a/components/brave_wallet/common/web3_provider_constants.cc +++ b/components/brave_wallet/common/web3_provider_constants.cc @@ -13,6 +13,7 @@ const char kDisconnectEvent[] = "disconnect"; namespace ethereum { const char kChainChangedEvent[] = "chainChanged"; const char kAccountsChangedEvent[] = "accountsChanged"; +const char kMessageEvent[] = "message"; } // namespace ethereum namespace solana { diff --git a/components/brave_wallet/common/web3_provider_constants.h b/components/brave_wallet/common/web3_provider_constants.h index 652857509696..e0f76c184993 100644 --- a/components/brave_wallet/common/web3_provider_constants.h +++ b/components/brave_wallet/common/web3_provider_constants.h @@ -16,6 +16,7 @@ extern const char kDisconnectEvent[]; namespace ethereum { extern const char kChainChangedEvent[]; extern const char kAccountsChangedEvent[]; +extern const char kMessageEvent[]; } // namespace ethereum namespace solana { @@ -44,6 +45,9 @@ constexpr char kEthDecrypt[] = "eth_decrypt"; constexpr char kWalletWatchAsset[] = "wallet_watchAsset"; constexpr char kMetamaskWatchAsset[] = "metamask_watchAsset"; constexpr char kWeb3ClientVersion[] = "web3_clientVersion"; +constexpr char kEthSubscribe[] = "eth_subscribe"; +constexpr char kEthUnsubscribe[] = "eth_unsubscribe"; +constexpr char kEthSubscribeNewHeads[] = "newHeads"; // We currently don't handle it until MetaMask point it to v3 or v4 other than // v1 or v2 diff --git a/components/brave_wallet/renderer/js_ethereum_provider.cc b/components/brave_wallet/renderer/js_ethereum_provider.cc index 8cec9334d601..be1e991ea0b6 100644 --- a/components/brave_wallet/renderer/js_ethereum_provider.cc +++ b/components/brave_wallet/renderer/js_ethereum_provider.cc @@ -577,4 +577,15 @@ void JSEthereumProvider::AccountsChangedEvent( FireEvent(ethereum::kAccountsChangedEvent, std::move(event_args)); } +void JSEthereumProvider::MessageEvent(const std::string& subscription_id, + base::Value result) { + base::Value::Dict event_args; + base::Value::Dict data; + data.Set("subscription", subscription_id); + data.Set("result", std::move(result)); + event_args.Set("type", "eth_subscription"); + event_args.Set("data", std::move(data)); + FireEvent(ethereum::kMessageEvent, base::Value(std::move(event_args))); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/renderer/js_ethereum_provider.h b/components/brave_wallet/renderer/js_ethereum_provider.h index c14af13b905e..87c5bac6604b 100644 --- a/components/brave_wallet/renderer/js_ethereum_provider.h +++ b/components/brave_wallet/renderer/js_ethereum_provider.h @@ -44,6 +44,8 @@ class JSEthereumProvider final : public gin::Wrappable, // mojom::EventsListener void AccountsChangedEvent(const std::vector& accounts) override; void ChainChangedEvent(const std::string& chain_id) override; + void MessageEvent(const std::string& subscription_id, + base::Value result) override; private: explicit JSEthereumProvider(content::RenderFrame* render_frame); diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index 1341dd3ba449..8fcf45f81b35 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -9,6 +9,7 @@ Expected single, object parameter. This Chain ID is currently used An internal error has occurred + Unsupported subscription type This RPC method is not supported. A response parsing error has occurred A generic request processing error has occurred