diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index 6275279a9f61b..bf028bb6a9d36 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -210,6 +210,8 @@ static_library("browser") { "wallet_data_files_installer_delegate.h", "zcash/zcash_block_tracker.cc", "zcash/zcash_block_tracker.h", + "zcash/zcash_grpc_utils.cc", + "zcash/zcash_grpc_utils.h", "zcash/zcash_rpc.cc", "zcash/zcash_rpc.h", "zcash/zcash_serializer.cc", @@ -224,6 +226,8 @@ static_library("browser") { "zcash/zcash_tx_state_manager.h", "zcash/zcash_wallet_service.cc", "zcash/zcash_wallet_service.h", + "zcash/zcash_wallet_service_tasks.cc", + "zcash/zcash_wallet_service_tasks.h", ] if (enable_ipfs_local_node) { sources += [ diff --git a/components/brave_wallet/browser/brave_wallet_service.cc b/components/brave_wallet/browser/brave_wallet_service.cc index 7760c7197df92..e86abbb4fdc42 100644 --- a/components/brave_wallet/browser/brave_wallet_service.cc +++ b/components/brave_wallet/browser/brave_wallet_service.cc @@ -2077,8 +2077,8 @@ void BraveWalletService::GenerateReceiveAddress( std::move(callback).Run("", WalletInternalErrorMessage()); return; } - zcash_wallet_service_->GetReceiverAddress( - std::move(account_id), + zcash_wallet_service_->RunDiscovery( + std::move(account_id), false, base::BindOnce(&BraveWalletService::OnGenerateZecReceiveAddress, weak_ptr_factory_.GetWeakPtr(), std::move(callback))); return; diff --git a/components/brave_wallet/browser/keyring_service.cc b/components/brave_wallet/browser/keyring_service.cc index 1e338d25b6b27..9f4c8902d5bd0 100644 --- a/components/brave_wallet/browser/keyring_service.cc +++ b/components/brave_wallet/browser/keyring_service.cc @@ -2502,43 +2502,49 @@ void KeyringService::HasPendingUnlockRequest( std::move(callback).Run(HasPendingUnlockRequest()); } -absl::optional>> -KeyringService::GetZCashAddresses(const mojom::AccountId& account_id) { - CHECK(IsZCashAccount(account_id)); +absl::optional> +KeyringService::GetZCashAddresses(const mojom::AccountIdPtr& account_id) { + CHECK(account_id); + CHECK(IsZCashAccount(*account_id)); - auto* zcash_keyring = GetZCashKeyringById(account_id.keyring_id); + auto* zcash_keyring = GetZCashKeyringById(account_id->keyring_id); if (!zcash_keyring) { return absl::nullopt; } - // TODO(cypt4): store used addresses indexes in prefs. + auto zcash_account_info = GetZCashAccountInfo(account_id); + if (!zcash_account_info) { + return absl::nullopt; + } - // TODO(cypt4): temporarily just return first 5 recieve and 5 change - // addresses. - std::vector> addresses; - for (auto i = 0; i < 5; ++i) { - auto key_id = - mojom::ZCashKeyId::New(account_id.bitcoin_account_index, 0, i); + std::vector addresses; + for (auto i = 0u; + i < zcash_account_info->next_transparent_receive_address->key_id->index; + ++i) { + auto key_id = mojom::ZCashKeyId::New(account_id->bitcoin_account_index, + kBitcoinReceiveIndex, i); auto address = zcash_keyring->GetAddress(*key_id); if (!address) { return absl::nullopt; } - addresses.emplace_back(*address, std::move(key_id)); + addresses.emplace_back(std::move(address)); } - for (auto i = 0; i < 5; ++i) { - auto key_id = - mojom::ZCashKeyId::New(account_id.bitcoin_account_index, 1, i); + for (auto i = 0u; + i < zcash_account_info->next_transparent_change_address->key_id->index; + ++i) { + auto key_id = mojom::ZCashKeyId::New(account_id->bitcoin_account_index, + kBitcoinChangeIndex, i); auto address = zcash_keyring->GetAddress(*key_id); if (!address) { return absl::nullopt; } - addresses.emplace_back(*address, std::move(key_id)); + addresses.emplace_back(std::move(address)); } return addresses; } -absl::optional KeyringService::GetZCashAddress( +mojom::ZCashAddressPtr KeyringService::GetZCashAddress( const mojom::AccountId& account_id, const mojom::ZCashKeyId& key_id) { CHECK(IsZCashAccount(account_id)); @@ -2546,7 +2552,7 @@ absl::optional KeyringService::GetZCashAddress( auto* zcash_keyring = GetZCashKeyringById(account_id.keyring_id); if (!zcash_keyring) { - return absl::nullopt; + return nullptr; } return zcash_keyring->GetAddress(key_id); @@ -2599,6 +2605,38 @@ void KeyringService::UpdateNextUnusedAddressForBitcoinAccount( } } +void KeyringService::UpdateNextUnusedAddressForZCashAccount( + const mojom::AccountIdPtr& account_id, + absl::optional next_receive_index, + absl::optional next_change_index) { + CHECK(account_id); + CHECK(IsZCashAccount(*account_id)); + CHECK(next_receive_index || next_change_index); + + ScopedDictPrefUpdate keyrings_update(profile_prefs_, kBraveWalletKeyrings); + base::Value::List& account_metas = GetListPrefForKeyringUpdate( + keyrings_update, kAccountMetas, account_id->keyring_id); + for (auto& item : account_metas) { + if (auto derived_account = DerivedAccountInfo::FromValue(item)) { + if (*account_id == *MakeAccountIdForDerivedAccount( + *derived_account, account_id->keyring_id)) { + if (next_receive_index) { + derived_account->bitcoin_next_receive_address_index = + *next_receive_index; + } + if (next_change_index) { + derived_account->bitcoin_next_change_address_index = + *next_change_index; + } + + item = derived_account->ToValue(); + NotifyAccountsChanged(); + return; + } + } + } +} + mojom::BitcoinAccountInfoPtr KeyringService::GetBitcoinAccountInfo( const mojom::AccountIdPtr& account_id) { CHECK(account_id); @@ -2759,6 +2797,49 @@ KeyringService::SignMessageByBitcoinKeyring( *key_id, message); } +mojom::ZCashAccountInfoPtr KeyringService::GetZCashAccountInfo( + const mojom::AccountIdPtr& account_id) { + CHECK(account_id); + CHECK(IsZCashAccount(*account_id)); + + auto keyring_id = account_id->keyring_id; + auto* zcash_keyring = GetZCashKeyringById(keyring_id); + if (!zcash_keyring) { + return {}; + } + + for (const auto& derived_account_info : + GetDerivedAccountsForKeyring(profile_prefs_, keyring_id)) { + if (account_id->bitcoin_account_index != + derived_account_info.account_index) { + continue; + } + auto result = mojom::ZCashAccountInfo::New(); + + auto receive_key_id = mojom::ZCashKeyId::New( + account_id->bitcoin_account_index, kBitcoinReceiveIndex, + derived_account_info.bitcoin_next_receive_address_index.value_or(0)); + auto receive_address = zcash_keyring->GetAddress(*receive_key_id); + if (!receive_address) { + return {}; + } + result->next_transparent_receive_address = std::move(receive_address); + + auto change_key_id = mojom::ZCashKeyId::New( + account_id->bitcoin_account_index, kBitcoinChangeIndex, + derived_account_info.bitcoin_next_change_address_index.value_or(0)); + auto change_address = zcash_keyring->GetAddress(*change_key_id); + if (!change_address) { + return {}; + } + + result->next_transparent_change_address = std::move(change_address); + return result; + } + + return {}; +} + absl::optional> KeyringService::SignMessageByZCashKeyring( const mojom::AccountIdPtr& account_id, const mojom::ZCashKeyIdPtr& key_id, diff --git a/components/brave_wallet/browser/keyring_service.h b/components/brave_wallet/browser/keyring_service.h index 6627217fb8814..177bf2157d3b6 100644 --- a/components/brave_wallet/browser/keyring_service.h +++ b/components/brave_wallet/browser/keyring_service.h @@ -238,16 +238,22 @@ class KeyringService : public KeyedService, public mojom::KeyringService { const mojom::AccountIdPtr& account_id, const mojom::BitcoinKeyIdPtr& key_id, base::span message); + + /* ZCash */ + void UpdateNextUnusedAddressForZCashAccount( + const mojom::AccountIdPtr& account_id, + absl::optional next_receive_index, + absl::optional next_change_index); + mojom::ZCashAccountInfoPtr GetZCashAccountInfo( + const mojom::AccountIdPtr& account_id); absl::optional> SignMessageByZCashKeyring( const mojom::AccountIdPtr& account_id, const mojom::ZCashKeyIdPtr& key_id, const base::span message); - - absl::optional>> - GetZCashAddresses(const mojom::AccountId& account_id); - absl::optional GetZCashAddress( - const mojom::AccountId& account_id, - const mojom::ZCashKeyId& key_id); + mojom::ZCashAddressPtr GetZCashAddress(const mojom::AccountId& account_id, + const mojom::ZCashKeyId& key_id); + absl::optional> GetZCashAddresses( + const mojom::AccountIdPtr& account_id); absl::optional> GetZCashPubKey( const mojom::AccountIdPtr& account_id, const mojom::ZCashKeyIdPtr& key_id); diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index bfe603eec964f..2026b3a660f8a 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -94,6 +94,7 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/unstoppable_domains_dns_resolve_unittest.cc", "//brave/components/brave_wallet/browser/unstoppable_domains_multichain_calls_unittest.cc", "//brave/components/brave_wallet/browser/wallet_data_files_installer_unittest.cc", + "//brave/components/brave_wallet/browser/zcash/zcash_grpc_utils_unittest.cc", "//brave/components/brave_wallet/browser/zcash/zcash_keyring_unittest.cc", "//brave/components/brave_wallet/browser/zcash/zcash_serializer_unittest.cc", "//brave/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc", diff --git a/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto b/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto index a5f1c738580b2..b9704808809cb 100644 --- a/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto +++ b/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto @@ -45,3 +45,13 @@ message SendResponse { int32 errorCode = 1; string errorMessage = 2; } + +message BlockRange { + BlockID start = 1; + BlockID end = 2; +} + +message TransparentAddressBlockFilter { + string address = 1; + BlockRange range = 2; +} diff --git a/components/brave_wallet/browser/zcash/zcash_grpc_utils.cc b/components/brave_wallet/browser/zcash/zcash_grpc_utils.cc new file mode 100644 index 0000000000000..33e9a0347c102 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_grpc_utils.cc @@ -0,0 +1,97 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/zcash/zcash_grpc_utils.h" + +#include + +#include "base/big_endian.h" +#include "base/functional/callback.h" +#include "base/logging.h" + +namespace brave_wallet { + +namespace { +constexpr size_t kMaxMessageSizeBytes = 10000; +} // namespace + +std::string GetPrefixedProtobuf(const std::string& serialized_proto) { + char compression = 0; // 0 means no compression + char buff[4]; // big-endian 4 bytes of message size + base::WriteBigEndian(buff, serialized_proto.size()); + std::string result; + result.append(&compression, 1); + result.append(buff, 4); + result.append(serialized_proto); + return result; +} + +// Resolves serialized protobuf from length-prefixed string +absl::optional ResolveSerializedMessage( + const std::string& grpc_response_body) { + if (grpc_response_body.size() < 5) { + return absl::nullopt; + } + if (grpc_response_body[0] != 0) { + // Compression is not supported yet + return absl::nullopt; + } + uint32_t size = 0; + base::ReadBigEndian( + reinterpret_cast(&(grpc_response_body[1])), &size); + + if (grpc_response_body.size() != size + 5) { + return absl::nullopt; + } + + return grpc_response_body.substr(5); +} + +GRrpcMessageStreamHandler::GRrpcMessageStreamHandler() = default; +GRrpcMessageStreamHandler::~GRrpcMessageStreamHandler() = default; + +void GRrpcMessageStreamHandler::OnDataReceived(std::string_view string_piece, + base::OnceClosure resume) { + data_.append(string_piece); + + bool should_resume = false; + while (!should_resume) { + if (!data_to_read_ && data_.size() > 5) { + if (data_[0] != 0) { + OnComplete(false); + return; + } + uint32_t size = 0; + base::ReadBigEndian(reinterpret_cast(&(data_[1])), &size); + data_to_read_ = size; + if (*data_to_read_ > kMaxMessageSizeBytes) { + // Too large message + OnComplete(false); + return; + } + } + + if (data_to_read_ && data_.size() > *data_to_read_) { + if (!ProcessMessage(data_.substr(0, 5 + *data_to_read_))) { + OnComplete(true); + return; + } + + data_ = data_.substr(5 + *data_to_read_); + data_to_read_.reset(); + } else { + should_resume = true; + } + } + std::move(resume).Run(); +} + +void GRrpcMessageStreamHandler::OnRetry(base::OnceClosure start_retry) { + if (retry_counter_++ < 5) { + std::move(start_retry).Run(); + } +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_grpc_utils.h b/components/brave_wallet/browser/zcash/zcash_grpc_utils.h new file mode 100644 index 0000000000000..912e2bcbd891c --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_grpc_utils.h @@ -0,0 +1,47 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_GRPC_UTILS_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_GRPC_UTILS_H_ + +#include +#include + +#include "services/network/public/cpp/simple_url_loader_stream_consumer.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace brave_wallet { + +// Resolves serialized protobuf from length-prefixed string +absl::optional ResolveSerializedMessage( + const std::string& grpc_response_body); + +// Prefixes provided serialized protobuf with compression byte and 4 bytes of +// message size. See +// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md +std::string GetPrefixedProtobuf(const std::string& serialized_proto); + +// Handles a stream of GRPC objects +class GRrpcMessageStreamHandler + : public network::SimpleURLLoaderStreamConsumer { + public: + GRrpcMessageStreamHandler(); + ~GRrpcMessageStreamHandler() override; + + void OnDataReceived(std::string_view string_piece, + base::OnceClosure resume) override; + void OnRetry(base::OnceClosure start_retry) override; + + private: + virtual bool ProcessMessage(const std::string& message) = 0; + + absl::optional data_to_read_; + std::string data_; + size_t retry_counter_ = 0; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_GRPC_UTILS_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_grpc_utils_unittest.cc b/components/brave_wallet/browser/zcash/zcash_grpc_utils_unittest.cc new file mode 100644 index 0000000000000..21c5db69f5fb0 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_grpc_utils_unittest.cc @@ -0,0 +1,92 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/zcash/zcash_grpc_utils.h" + +#include +#include +#include +#include + +#include "base/functional/callback.h" +#include "base/strings/strcat.h" +#include "base/test/bind.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace brave_wallet { + +namespace { + +class TestTxStreamHanlder : public GRrpcMessageStreamHandler { + public: + TestTxStreamHanlder() {} + ~TestTxStreamHanlder() override {} + + std::vector messages() { return messages_; } + absl::optional complete_result() { return complete_result_; } + void set_should_continue(bool r) { should_continue = r; } + + private: + bool ProcessMessage(const std::string& message) override { + messages_.push_back(message); + return should_continue; + } + + void OnComplete(bool success) override { complete_result_ = success; } + + std::vector messages_; + absl::optional complete_result_; + bool should_continue = true; +}; + +} // namespace + +TEST(ZCashGrpcUtilsTest, TestTxStreamHanlder) { + TestTxStreamHanlder handler; + { + absl::optional resume_called; + auto resume_closure = + base::BindLambdaForTesting([&]() { resume_called = true; }); + handler.OnDataReceived("", std::move(resume_closure)); + EXPECT_EQ(0u, handler.messages().size()); + EXPECT_TRUE(resume_called.value()); + } + + auto message = GetPrefixedProtobuf("message1"); + // Not enough data to complete message + { + absl::optional resume_called; + auto resume_closure = + base::BindLambdaForTesting([&]() { resume_called = true; }); + handler.OnDataReceived(message.substr(0, 3), std::move(resume_closure)); + EXPECT_EQ(0u, handler.messages().size()); + EXPECT_TRUE(resume_called.value()); + } + + // Complete message + { + absl::optional resume_called; + auto resume_closure = + base::BindLambdaForTesting([&]() { resume_called = true; }); + handler.OnDataReceived(message.substr(3, message.size()), + std::move(resume_closure)); + EXPECT_EQ(1u, handler.messages().size()); + EXPECT_TRUE(resume_called.value()); + } + + auto bundled_message = base::StrCat( + {GetPrefixedProtobuf("message1"), GetPrefixedProtobuf("message2")}); + // Several messages received at once + { + absl::optional resume_called; + auto resume_closure = + base::BindLambdaForTesting([&]() { resume_called = true; }); + handler.OnDataReceived(bundled_message, std::move(resume_closure)); + EXPECT_EQ(3u, handler.messages().size()); + EXPECT_TRUE(resume_called.value()); + } +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_keyring.cc b/components/brave_wallet/browser/zcash/zcash_keyring.cc index becead04bf3ff..f964e01c1742c 100644 --- a/components/brave_wallet/browser/zcash/zcash_keyring.cc +++ b/components/brave_wallet/browser/zcash/zcash_keyring.cc @@ -14,16 +14,17 @@ namespace brave_wallet { ZCashKeyring::ZCashKeyring(bool testnet) : testnet_(testnet) {} -absl::optional ZCashKeyring::GetAddress( +mojom::ZCashAddressPtr ZCashKeyring::GetAddress( const mojom::ZCashKeyId& key_id) { auto key = DeriveKey(key_id); if (!key) { - return absl::nullopt; + return nullptr; } HDKey* hd_key = static_cast(key.get()); - return hd_key->GetZCashTransparentAddress(testnet_); + return mojom::ZCashAddress::New(hd_key->GetZCashTransparentAddress(testnet_), + key_id.Clone()); } absl::optional> ZCashKeyring::GetPubkey( diff --git a/components/brave_wallet/browser/zcash/zcash_keyring.h b/components/brave_wallet/browser/zcash/zcash_keyring.h index e2420b1400022..43a219b04d840 100644 --- a/components/brave_wallet/browser/zcash/zcash_keyring.h +++ b/components/brave_wallet/browser/zcash/zcash_keyring.h @@ -22,7 +22,7 @@ class ZCashKeyring : public HDKeyring { ZCashKeyring(const ZCashKeyring&) = delete; ZCashKeyring& operator=(const ZCashKeyring&) = delete; - absl::optional GetAddress(const mojom::ZCashKeyId& key_id); + mojom::ZCashAddressPtr GetAddress(const mojom::ZCashKeyId& key_id); absl::optional> GetPubkey( const mojom::ZCashKeyId& key_id); diff --git a/components/brave_wallet/browser/zcash/zcash_keyring_unittest.cc b/components/brave_wallet/browser/zcash/zcash_keyring_unittest.cc index 326612bedf831..34afe1ce35491 100644 --- a/components/brave_wallet/browser/zcash/zcash_keyring_unittest.cc +++ b/components/brave_wallet/browser/zcash/zcash_keyring_unittest.cc @@ -44,48 +44,48 @@ TEST(ZCashKeyringUnitTest, GetAddress) { keyring.ConstructRootHDKey(*MnemonicToSeed(kBip84TestMnemonic, ""), "m/44'/133'"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 0)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 0))->address_string, "t1fhcesXQLT3U1t7caBYTpd59LiRV8tVfqj"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 1)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 1))->address_string, "t1WTZNzKCvU2GeM1ZWRyF7EvhMHhr7magiT"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 2)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 2))->address_string, "t1NPwPhNPHc4S8Xktzq4ygnLLLeuVKgCBZz"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 3)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 3))->address_string, "t1fe6a9otS98EdnucHLx6cT4am6Ae6THUgj"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 4)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 4))->address_string, "t1dA52xDndfEo9jJU7DBFSKuxKMqZMMLEGL"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 0)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 0))->address_string, "t1RnTVUMzs1Hi2smgk6EJpVMA2upwTpESYP"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 1)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 1))->address_string, "t1YRbQiMhwS7mui9r7E2aWGmkLC1xHQnFoc"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 2)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 2))->address_string, "t1ZnyiWoaS9sceAQKjiK4ughBRQu9z2MXcB"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 3)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 3))->address_string, "t1WwSQBqGUYf4AXKE6nNR3fp61awb6qsM8z"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 4)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 1, 4))->address_string, "t1WLKBRDxENW35SLrsW125Yt5tYJDYpBs1i"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 0)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 0))->address_string, "t1LquzEnJVAqdRGeZJZbomrsKwPHuGhNJtm"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 1)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 1))->address_string, "t1JgGisPzWWN1KMbN82bfKvk9Faa5eKqHUg"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 2)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 2))->address_string, "t1fmqM2ud5FEgRTLo17QS4c2i575GD8QQCq"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 3)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 3))->address_string, "t1SoBVfwQvRi7X8RuSxNvkm8NBrbbaUJo5c"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 4)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 0, 4))->address_string, "t1YuvWycJoxFVvuGHSJEkP5jtfe5VjXyGyj"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 0)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 0))->address_string, "t1NFJ9Jn6su9bGH7mxjowBT6UsPTQdSpVF7"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 1)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 1))->address_string, "t1SfWeR1cGcxv9G7sBMFP1JubyKBujSFu1C"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 2)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 2))->address_string, "t1efWNVCp5sDvkRtrJxq6jrdJDbxwsYBxfb"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 3)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 3))->address_string, "t1g4WDhMPb8e8GUWuV1JEAPa32bgVkeoSuJ"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 4)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(1, 1, 4))->address_string, "t1ZTmbmys2n8UjVRb2tfE17A9V6ehZNqZ4W"); } @@ -99,7 +99,7 @@ TEST(ZCashKeyringUnitTest, GetAddress) { "deal person", ""), "m/44'/133'"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 0)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 0))->address_string, "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4"); } @@ -112,7 +112,7 @@ TEST(ZCashKeyringUnitTest, GetAddress) { "deal person", ""), "m/44'/1'"); - EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 0)), + EXPECT_EQ(keyring.GetAddress(ZCashKeyId(0, 0, 0))->address_string, "tm9v3KTsjXK8XWSqiwFjic6Vda6eHY9Mjjq"); } } diff --git a/components/brave_wallet/browser/zcash/zcash_rpc.cc b/components/brave_wallet/browser/zcash/zcash_rpc.cc index f0b198ae914b3..ba2235840aaf0 100644 --- a/components/brave_wallet/browser/zcash/zcash_rpc.cc +++ b/components/brave_wallet/browser/zcash/zcash_rpc.cc @@ -5,7 +5,6 @@ #include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" -#include "base/big_endian.h" #include "base/functional/bind.h" #include "brave/components/brave_wallet/browser/brave_wallet_utils.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" @@ -14,11 +13,46 @@ #include "net/http/http_status_code.h" #include "services/network/public/cpp/resource_request.h" #include "services/network/public/mojom/url_loader.mojom.h" +#include "services/network/public/mojom/url_response_head.mojom.h" namespace brave_wallet { namespace { +// Checks if provided stream contains any messages. +class IsKnownAddressTxStreamHandler : public GRrpcMessageStreamHandler { + public: + using ResultCallback = + base::OnceCallback)>; + + IsKnownAddressTxStreamHandler() {} + ~IsKnownAddressTxStreamHandler() override {} + + void set_callback(ResultCallback callback) { + callback_ = std::move(callback); + } + + bool is_message_found() { return tx_found_; } + + private: + bool ProcessMessage(const std::string& message) override { + tx_found_ = true; + return false; + } + + void OnComplete(bool success) override { + if (!success) { + std::move(callback_).Run(base::unexpected("stream error")); + return; + } + std::move(callback_).Run(tx_found_); + } + + private: + bool tx_found_ = false; + ResultCallback callback_; +}; + net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() { return net::DefineNetworkTrafficAnnotation("zcash_rpc", R"( semantics { @@ -81,6 +115,23 @@ const GURL MakeSendTransactionURL(const GURL& base_url) { return base_url.ReplaceComponents(replacements); } +const GURL MakeGetTaddressTxURL(const GURL& base_url) { + if (!base_url.is_valid()) { + return GURL(); + } + if (!UrlPathEndsWithSlash(base_url)) { + return GURL(); + } + + GURL::Replacements replacements; + std::string path = base::StrCat( + {base_url.path(), + "cash.z.wallet.sdk.rpc.CompactTxStreamer/GetTaddressTxids"}); + replacements.SetPathStr(path); + + return base_url.ReplaceComponents(replacements); +} + const GURL MakeGetLatestBlockHeightURL(const GURL& base_url) { if (!base_url.is_valid()) { return GURL(); @@ -115,20 +166,6 @@ const GURL MakeGetTransactionURL(const GURL& base_url) { return base_url.ReplaceComponents(replacements); } -// Prefixes provided serialized protobuf with compression byte and 4 bytes of -// message size. See -// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md -std::string GetPrefixedProtobuf(const std::string& serialized_proto) { - char compression = 0; // 0 means no compression - char buff[4]; // big-endian 4 bytes of message size - base::WriteBigEndian(buff, serialized_proto.size()); - std::string result; - result.append(&compression, 1); - result.append(buff, 4); - result.append(serialized_proto); - return result; -} - std::string MakeGetAddressUtxosURLParams(const std::string& address) { zcash::GetAddressUtxosRequest request; request.add_addresses(address); @@ -156,6 +193,26 @@ std::string MakeSendTransactionParams(const std::string& data) { return GetPrefixedProtobuf(request.SerializeAsString()); } +std::string MakeGetAddressTxParams(const std::string& address, + uint64_t block_start, + uint64_t block_end) { + zcash::TransparentAddressBlockFilter request; + request.New(); + + zcash::BlockRange range; + zcash::BlockID bottom; + zcash::BlockID top; + + bottom.set_height(block_start); + top.set_height(block_end); + + request.set_address(address); + range.mutable_start()->CopyFrom(bottom); + range.mutable_end()->CopyFrom(top); + request.mutable_range()->CopyFrom(range); + return GetPrefixedProtobuf(request.SerializeAsString()); +} + std::unique_ptr MakeGRPCLoader( const GURL& url, const std::string& body) { @@ -176,27 +233,6 @@ std::unique_ptr MakeGRPCLoader( return url_loader; } -// Resolves serialized protobuf from length-prefixed string -absl::optional ResolveSerializedMessage( - const std::string& grpc_response_body) { - if (grpc_response_body.size() < 5) { - return absl::nullopt; - } - if (grpc_response_body[0] != 0) { - // Compression is not supported yet - return absl::nullopt; - } - uint32_t size = 0; - base::ReadBigEndian( - reinterpret_cast(&(grpc_response_body[1])), &size); - - if (grpc_response_body.size() != size + 5) { - return absl::nullopt; - } - - return grpc_response_body.substr(5); -} - } // namespace ZCashRpc::ZCashRpc( @@ -399,6 +435,36 @@ void ZCashRpc::SendTransaction(const std::string& chain_id, 5000); } +void ZCashRpc::IsKnownAddress(const std::string& chain_id, + const std::string& addr, + uint64_t block_start, + uint64_t block_end, + IsKnownAddressCallback callback) { + GURL request_url = MakeGetTaddressTxURL( + GetNetworkURL(prefs_, chain_id, mojom::CoinType::ZEC)); + + if (!request_url.is_valid()) { + std::move(callback).Run(base::unexpected("Request URL is invalid.")); + return; + } + + auto url_loader = MakeGRPCLoader( + request_url, MakeGetAddressTxParams(addr, block_start, block_end)); + + UrlLoadersList::iterator it = url_loaders_list_.insert( + url_loaders_list_.begin(), std::move(url_loader)); + + StreamHandlersList::iterator handler_it = stream_handlers_list_.insert( + stream_handlers_list_.begin(), + std::make_unique()); + static_cast(handler_it->get()) + ->set_callback(base::BindOnce(&ZCashRpc::OnGetAddressTxResponse, + weak_ptr_factory_.GetWeakPtr(), + std::move(callback), it, handler_it)); + + (*it)->DownloadAsStream(url_loader_factory_.get(), handler_it->get()); +} + void ZCashRpc::OnSendTransactionResponse( ZCashRpc::SendTransactionCallback callback, UrlLoadersList::iterator it, @@ -429,4 +495,25 @@ void ZCashRpc::OnSendTransactionResponse( std::move(callback).Run(response); } + +void ZCashRpc::OnGetAddressTxResponse( + ZCashRpc::IsKnownAddressCallback callback, + UrlLoadersList::iterator it, + StreamHandlersList::iterator handler_it, + base::expected result) { + auto current_loader = std::move(*it); + auto current_handler = std::move(*handler_it); + + url_loaders_list_.erase(it); + stream_handlers_list_.erase(handler_it); + + if (!result.has_value() || + (current_loader->CompletionStatus() && current_loader->NetError())) { + std::move(callback).Run(base::unexpected("Network error")); + return; + } + + std::move(callback).Run(result.value()); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_rpc.h b/components/brave_wallet/browser/zcash/zcash_rpc.h index 037523b5e3e4c..d9a29684bf5a8 100644 --- a/components/brave_wallet/browser/zcash/zcash_rpc.h +++ b/components/brave_wallet/browser/zcash/zcash_rpc.h @@ -16,10 +16,11 @@ #include "base/memory/weak_ptr.h" #include "base/types/expected.h" #include "brave/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.pb.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_grpc_utils.h" #include "components/prefs/pref_service.h" #include "services/network/public/cpp/shared_url_loader_factory.h" #include "services/network/public/cpp/simple_url_loader.h" -#include "third_party/abseil-cpp/absl/types/optional.h" +#include "services/network/public/cpp/simple_url_loader_stream_consumer.h" namespace brave_wallet { @@ -34,6 +35,10 @@ class ZCashRpc { base::expected)>; using SendTransactionCallback = base::OnceCallback)>; + using GetTransactionsCallback = base::OnceCallback)>; + using IsKnownAddressCallback = + base::OnceCallback)>; ZCashRpc(PrefService* prefs, scoped_refptr url_loader_factory); @@ -54,10 +59,18 @@ class ZCashRpc { const std::string& data, SendTransactionCallback callback); + virtual void IsKnownAddress(const std::string& chain_id, + const std::string& addr, + uint64_t block_start, + uint64_t block_end, + IsKnownAddressCallback callback); + private: friend class base::RefCountedThreadSafe; using UrlLoadersList = std::list>; + using StreamHandlersList = + std::list>; void OnGetUtxosResponse(ZCashRpc::GetUtxoListCallback callback, UrlLoadersList::iterator it, @@ -78,7 +91,13 @@ class ZCashRpc { UrlLoadersList::iterator it, const std::unique_ptr response_body); + void OnGetAddressTxResponse(ZCashRpc::IsKnownAddressCallback callback, + UrlLoadersList::iterator it, + StreamHandlersList::iterator handler_it, + base::expected result); + UrlLoadersList url_loaders_list_; + StreamHandlersList stream_handlers_list_; raw_ptr prefs_ = nullptr; scoped_refptr url_loader_factory_ = nullptr; base::WeakPtrFactory weak_ptr_factory_{this}; diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service.cc b/components/brave_wallet/browser/zcash/zcash_wallet_service.cc index a49b841b301fb..e6b360b8dfb91 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service.cc +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service.cc @@ -10,260 +10,16 @@ #include "base/containers/span.h" #include "brave/components/brave_wallet/browser/zcash/zcash_serializer.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.h" #include "brave/components/brave_wallet/common/btc_like_serializer_stream.h" #include "brave/components/brave_wallet/common/common_utils.h" #include "brave/components/brave_wallet/common/hex_utils.h" -#include "brave/components/brave_wallet/common/zcash_utils.h" namespace brave_wallet { - namespace { const uint32_t kDefaultBlockHeightDelta = 20; - -bool OutputAddressSupported(const std::string& address, bool is_testnet) { - auto decoded_address = DecodeZCashAddress(address); - if (!decoded_address) { - return false; - } - if (decoded_address->testnet != is_testnet) { - return false; - } - - return true; -} - } // namespace -class GetTransparentUtxosContext - : public base::RefCountedThreadSafe { - public: - using GetUtxosCallback = ZCashWalletService::GetUtxosCallback; - - std::set addresses; - ZCashWalletService::UtxoMap utxos; - absl::optional error; - GetUtxosCallback callback; - - bool ShouldRespond() { return callback && (error || addresses.empty()); } - - void SetError(const std::string& error_string) { error = error_string; } - - protected: - friend class base::RefCountedThreadSafe; - virtual ~GetTransparentUtxosContext() = default; -}; - -class CreateTransparentTransactionTask { - public: - using UtxoMap = ZCashWalletService::UtxoMap; - using CreateTransactionCallback = - ZCashWalletService::CreateTransactionCallback; - - CreateTransparentTransactionTask(ZCashWalletService* zcash_wallet_service, - const std::string& chain_id, - const mojom::AccountIdPtr& account_id, - const std::string& address_to, - uint64_t amount, - CreateTransactionCallback callback); - - void ScheduleWorkOnTask(); - - private: - bool IsTestnet() { return chain_id_ == mojom::kZCashTestnet; } - void WorkOnTask(); - - void SetError(const std::string& error_string) { error_ = error_string; } - - bool PickInputs(); - bool PrepareOutputs(); - - void OnGetChainHeight(base::expected result); - void OnGetUtxos( - base::expected utxo_map); - - raw_ptr zcash_wallet_service_; // Owns `this`. - std::string chain_id_; - mojom::AccountIdPtr account_id_; - CreateTransactionCallback callback_; - - absl::optional chain_height_; - ZCashWalletService::UtxoMap utxo_map_; - - absl::optional error_; - ZCashTransaction transaction_; - - base::WeakPtrFactory weak_ptr_factory_{ - this}; -}; - -CreateTransparentTransactionTask::CreateTransparentTransactionTask( - ZCashWalletService* zcash_wallet_service, - const std::string& chain_id, - const mojom::AccountIdPtr& account_id, - const std::string& address_to, - uint64_t amount, - CreateTransactionCallback callback) - : zcash_wallet_service_(zcash_wallet_service), - chain_id_(chain_id), - account_id_(account_id.Clone()), - callback_(std::move(callback)) { - transaction_.set_to(address_to); - transaction_.set_amount(amount); -} - -void CreateTransparentTransactionTask::ScheduleWorkOnTask() { - base::SequencedTaskRunner::GetCurrentDefault()->PostTask( - FROM_HERE, base::BindOnce(&CreateTransparentTransactionTask::WorkOnTask, - weak_ptr_factory_.GetWeakPtr())); -} - -void CreateTransparentTransactionTask::WorkOnTask() { - if (!callback_) { - return; - } - - if (error_) { - std::move(callback_).Run(base::unexpected(*error_)); - zcash_wallet_service_->CreateTransactionTaskDone(this); - return; - } - - if (!chain_height_) { - zcash_wallet_service_->zcash_rpc()->GetLatestBlock( - chain_id_, - base::BindOnce(&CreateTransparentTransactionTask::OnGetChainHeight, - weak_ptr_factory_.GetWeakPtr())); - return; - } - - if (utxo_map_.empty()) { - zcash_wallet_service_->GetUtxos( - chain_id_, account_id_.Clone(), - base::BindOnce(&CreateTransparentTransactionTask::OnGetUtxos, - weak_ptr_factory_.GetWeakPtr())); - return; - } - - // TODO(cypt4): fetch fee estimations, calculate transaction size and set - // transaction fee - transaction_.set_fee(2000); - - // TODO(cypt4): random shift locktime - // https://github.com/bitcoin/bitcoin/blob/v24.0/src/wallet/spend.cpp#L739-L747 - transaction_.set_locktime(chain_height_.value()); - - if (!PickInputs()) { - SetError("Couldn't pick transaction inputs"); - ScheduleWorkOnTask(); - return; - } - - if (!PrepareOutputs()) { - SetError("Couldn't prepare outputs"); - ScheduleWorkOnTask(); - return; - } - - std::move(callback_).Run(base::ok(std::move(transaction_))); - zcash_wallet_service_->CreateTransactionTaskDone(this); -} - -void CreateTransparentTransactionTask::OnGetChainHeight( - base::expected result) { - if (!result.has_value()) { - SetError(std::move(result).error()); - WorkOnTask(); - return; - } - - chain_height_ = result.value().height(); - WorkOnTask(); -} - -void CreateTransparentTransactionTask::OnGetUtxos( - base::expected utxo_map) { - if (!utxo_map.has_value()) { - SetError(std::move(utxo_map).error()); - WorkOnTask(); - return; - } - - utxo_map_ = std::move(utxo_map.value()); - WorkOnTask(); -} - -bool CreateTransparentTransactionTask::PickInputs() { - bool done = false; - - // TODO(apaymyshev): This just picks ouputs one by one and stops when picked - // amount is GE to send amount plus fee. Needs something better than such - // greedy strategy. - std::vector all_inputs; - for (const auto& item : utxo_map_) { - for (const auto& utxo : item.second) { - if (auto input = - ZCashTransaction::TxInput::FromRpcUtxo(item.first, utxo)) { - all_inputs.emplace_back(std::move(*input)); - } - } - } - - base::ranges::sort(all_inputs, [](auto& input1, auto& input2) { - return input1.utxo_value < input2.utxo_value; - }); - - for (auto& input : all_inputs) { - transaction_.inputs().push_back(std::move(input)); - - if (transaction_.TotalInputsAmount() >= - transaction_.amount() + transaction_.fee()) { - done = true; - } - - if (done) { - break; - } - } - - DCHECK(!transaction_.inputs().empty()); - return done; -} - -bool CreateTransparentTransactionTask::PrepareOutputs() { - auto& target_output = transaction_.outputs().emplace_back(); - target_output.address = transaction_.to(); - if (!OutputAddressSupported(target_output.address, IsTestnet())) { - return false; - } - - target_output.amount = transaction_.amount(); - target_output.script_pubkey = - ZCashAddressToScriptPubkey(target_output.address, IsTestnet()); - - CHECK_GE(transaction_.TotalInputsAmount(), - transaction_.amount() + transaction_.fee()); - uint64_t change_amount = transaction_.TotalInputsAmount() - - transaction_.amount() - transaction_.fee(); - if (change_amount == 0) { - return true; - } - - // TODO(cypt4): should always pick new change address. - const auto& change_address = - zcash_wallet_service_->GetUnusedChangeAddress(*account_id_); - if (!change_address) { - return false; - } - - CHECK(OutputAddressSupported(*change_address, IsTestnet())); - auto& change_output = transaction_.outputs().emplace_back(); - change_output.address = *change_address; - change_output.amount = change_amount; - change_output.script_pubkey = - ZCashAddressToScriptPubkey(change_output.address, IsTestnet()); - return true; -} - mojo::PendingRemote ZCashWalletService::MakeRemote() { mojo::PendingRemote remote; @@ -288,19 +44,12 @@ ZCashWalletService::ZCashWalletService(KeyringService* keyring_service, std::unique_ptr zcash_rpc) : keyring_service_(keyring_service) { zcash_rpc_ = std::move(zcash_rpc); + keyring_service_->AddObserver( + keyring_observer_receiver_.BindNewPipeAndPassRemote()); } ZCashWalletService::~ZCashWalletService() = default; -absl::optional ZCashWalletService::GetUnusedChangeAddress( - const mojom::AccountId& account_id) { - CHECK(IsZCashAccount(account_id)); - // TODO(cypt4): this always returns first change address. Should return - // first unused change address. - return keyring_service_->GetZCashAddress( - account_id, mojom::ZCashKeyId(account_id.bitcoin_account_index, 0, 0)); -} - void ZCashWalletService::GetBalance(const std::string& chain_id, mojom::AccountIdPtr account_id, GetBalanceCallback callback) { @@ -313,15 +62,77 @@ void ZCashWalletService::GetReceiverAddress( mojom::AccountIdPtr account_id, GetReceiverAddressCallback callback) { auto id = mojom::ZCashKeyId::New(account_id->bitcoin_account_index, 0, 0); - auto str_addr = keyring_service_->GetZCashAddress(*account_id, *id); - if (!str_addr) { + auto addr = keyring_service_->GetZCashAddress(*account_id, *id); + if (!addr) { std::move(callback).Run(nullptr, "Failed to retreive new receiver address"); } + auto str_addr = addr->address_string; // TODO(cypt4): Return unused receiver address - std::move(callback).Run(mojom::ZCashAddress::New(*str_addr, std::move(id)), + std::move(callback).Run(mojom::ZCashAddress::New(str_addr, std::move(id)), absl::nullopt); } +void ZCashWalletService::GetZCashAccountInfo( + mojom::AccountIdPtr account_id, + GetZCashAccountInfoCallback callback) { + std::move(callback).Run(keyring_service_->GetZCashAccountInfo(account_id)); +} + +void ZCashWalletService::RunDiscovery(mojom::AccountIdPtr account_id, + bool change, + RunDiscoveryCallback callback) { + DiscoverNextUnusedAddress( + account_id, change, + base::BindOnce(&ZCashWalletService::OnRunDiscoveryDone, + weak_ptr_factory_.GetWeakPtr(), account_id.Clone(), + std::move(callback))); +} + +void ZCashWalletService::OnRunDiscoveryDone( + mojom::AccountIdPtr account_id, + RunDiscoveryCallback callback, + base::expected discovered_address) { + if (!discovered_address.has_value()) { + std::move(callback).Run(nullptr, discovered_address.error()); + return; + } + + UpdateNextUnusedAddressForAccount(account_id, discovered_address.value()); + std::move(callback).Run(discovered_address.value().Clone(), absl::nullopt); +} + +void ZCashWalletService::UpdateNextUnusedAddressForAccount( + const mojom::AccountIdPtr& account_id, + const mojom::ZCashAddressPtr& address) { + absl::optional next_receive_index = address->key_id->change + ? absl::optional() + : address->key_id->index; + absl::optional next_change_index = !address->key_id->change + ? absl::optional() + : address->key_id->index; + keyring_service_->UpdateNextUnusedAddressForZCashAccount( + account_id, next_receive_index, next_change_index); +} + +void ZCashWalletService::DiscoverNextUnusedAddress( + const mojom::AccountIdPtr& account_id, + bool change, + DiscoverNextUnusedAddressCallback callback) { + CHECK(IsZCashAccount(*account_id)); + + auto account_info = keyring_service_->GetZCashAccountInfo(account_id); + if (!account_info) { + return std::move(callback).Run(base::unexpected("Invalid account id")); + } + auto start_address = + change ? account_info->next_transparent_change_address.Clone() + : account_info->next_transparent_receive_address.Clone(); + auto task = base::MakeRefCounted( + weak_ptr_factory_.GetWeakPtr(), account_id.Clone(), + std::move(start_address), std::move(callback)); + task->ScheduleWorkOnTask(); +} + void ZCashWalletService::GetUtxos(const std::string& chain_id, mojom::AccountIdPtr account_id, GetUtxosCallback callback) { @@ -332,7 +143,7 @@ void ZCashWalletService::GetUtxos(const std::string& chain_id, return; } - const auto& addresses = keyring_service_->GetZCashAddresses(*account_id); + const auto& addresses = keyring_service_->GetZCashAddresses(account_id); if (!addresses) { std::move(callback).Run(base::unexpected("Couldn't get balance")); return; @@ -341,7 +152,7 @@ void ZCashWalletService::GetUtxos(const std::string& chain_id, auto context = base::MakeRefCounted(); context->callback = std::move(callback); for (const auto& address : addresses.value()) { - context->addresses.insert(address.first); + context->addresses.insert(address->address_string); } for (const auto& address : context->addresses) { @@ -355,14 +166,15 @@ void ZCashWalletService::GetUtxos(const std::string& chain_id, bool ZCashWalletService::SignTransactionInternal( ZCashTransaction& tx, const mojom::AccountIdPtr& account_id) { - auto addresses = keyring_service_->GetZCashAddresses(*account_id); + auto addresses = keyring_service_->GetZCashAddresses(account_id); if (!addresses || addresses->empty()) { return false; } std::map address_map; for (auto& addr : *addresses) { - address_map.emplace(std::move(addr.first), std::move(addr.second)); + address_map.emplace(std::move(addr->address_string), + std::move(addr->key_id)); } for (size_t input_index = 0; input_index < tx.inputs().size(); @@ -553,4 +365,14 @@ ZCashRpc* ZCashWalletService::zcash_rpc() { return zcash_rpc_.get(); } +void ZCashWalletService::Unlocked() { + auto accounts = keyring_service_->GetAllAccountsSync(); + for (const auto& account : accounts->accounts) { + if (IsZCashKeyring(account->account_id->keyring_id)) { + RunDiscovery(account->account_id.Clone(), false, base::NullCallback()); + RunDiscovery(account->account_id.Clone(), true, base::NullCallback()); + } + } +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service.h b/components/brave_wallet/browser/zcash/zcash_wallet_service.h index c36bc68ba2461..657bd5424a316 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service.h +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service.h @@ -58,12 +58,19 @@ class ZCashWalletService : public KeyedService, mojom::AccountIdPtr account_id, GetBalanceCallback) override; + void GetZCashAccountInfo(mojom::AccountIdPtr account_id, + GetZCashAccountInfoCallback callback) override; + /** * Used for internal transfers between own accounts */ void GetReceiverAddress(mojom::AccountIdPtr account_id, GetReceiverAddressCallback callback) override; + void RunDiscovery(mojom::AccountIdPtr account_id, + bool change, + RunDiscoveryCallback callback) override; + void GetUtxos(const std::string& chain_id, mojom::AccountIdPtr account_id, GetUtxosCallback); @@ -86,14 +93,23 @@ class ZCashWalletService : public KeyedService, private: friend class ZCashWalletServiceUnitTest; friend class CreateTransparentTransactionTask; + friend class DiscoverNextUnusedZCashAddressTask; friend class ZCashTxManager; - absl::optional GetUnusedChangeAddress( - const mojom::AccountId& account_id); + /*KeyringServiceObserverBase*/ + void Unlocked() override; bool SignTransactionInternal(ZCashTransaction& tx, const mojom::AccountIdPtr& account_id); + using DiscoverNextUnusedAddressCallback = base::OnceCallback)>; + void DiscoverNextUnusedAddress(const mojom::AccountIdPtr& account_id, + bool change, + DiscoverNextUnusedAddressCallback callback); + void OnRunDiscoveryDone(mojom::AccountIdPtr account_id, + RunDiscoveryCallback callback, + base::expected); void OnGetUtxos( scoped_refptr context, const std::string& current_address, @@ -120,6 +136,9 @@ class ZCashWalletService : public KeyedService, void CreateTransactionTaskDone(CreateTransparentTransactionTask* task); + void UpdateNextUnusedAddressForAccount(const mojom::AccountIdPtr& account_id, + const mojom::ZCashAddressPtr& address); + ZCashRpc* zcash_rpc(); raw_ptr keyring_service_; @@ -128,6 +147,8 @@ class ZCashWalletService : public KeyedService, std::list> create_transaction_tasks_; mojo::ReceiverSet receivers_; + mojo::Receiver + keyring_observer_receiver_{this}; base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.cc b/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.cc new file mode 100644 index 0000000000000..a0925497b4a95 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.cc @@ -0,0 +1,342 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.h" + +#include + +#include "brave/components/brave_wallet/common/common_utils.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet { + +namespace { + +const uint32_t kDefaultTransparentOutputsCount = 2; +const uint32_t kGraceActionsCount = 2; +const uint64_t kMarginalFee = 5000; + +bool OutputAddressSupported(const std::string& address, bool is_testnet) { + auto decoded_address = DecodeZCashAddress(address); + if (!decoded_address) { + return false; + } + if (decoded_address->testnet != is_testnet) { + return false; + } + + return true; +} + +// https://zips.z.cash/zip-0317 +uint64_t CalculateTxFee(const uint32_t tx_input_count) { + // Use simplified calcultion fee form since we don't support p2psh + // and shielded addresses + auto actions_count = + std::max(tx_input_count, kDefaultTransparentOutputsCount); + return kMarginalFee * std::max(kGraceActionsCount, actions_count); +} + +} // namespace + +// GetTransparentUtxosContext +GetTransparentUtxosContext::GetTransparentUtxosContext() = default; +GetTransparentUtxosContext::~GetTransparentUtxosContext() = default; + +// DiscoverNextUnusedZCashAddressTask +DiscoverNextUnusedZCashAddressTask::DiscoverNextUnusedZCashAddressTask( + base::WeakPtr zcash_wallet_service, + mojom::AccountIdPtr account_id, + mojom::ZCashAddressPtr start_address, + ZCashWalletService::DiscoverNextUnusedAddressCallback callback) + : zcash_wallet_service_(std::move(zcash_wallet_service)), + account_id_(std::move(account_id)), + start_address_(std::move(start_address)), + callback_(std::move(callback)) {} + +DiscoverNextUnusedZCashAddressTask::~DiscoverNextUnusedZCashAddressTask() = + default; + +void DiscoverNextUnusedZCashAddressTask::ScheduleWorkOnTask() { + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&DiscoverNextUnusedZCashAddressTask::WorkOnTask, + weak_ptr_factory_.GetWeakPtr())); +} + +mojom::ZCashAddressPtr DiscoverNextUnusedZCashAddressTask::GetNextAddress( + const mojom::ZCashAddressPtr& address) { + auto* keyring_service = zcash_wallet_service_->keyring_service_.get(); + CHECK(keyring_service); + auto next_key_id = current_address_->key_id.Clone(); + + next_key_id->index++; + + return keyring_service->GetZCashAddress(*account_id_, *next_key_id); +} + +void DiscoverNextUnusedZCashAddressTask::WorkOnTask() { + if (!callback_) { + return; + } + + if (!zcash_wallet_service_) { + std::move(callback_).Run(base::unexpected("Internal error")); + return; + } + + if (error_) { + std::move(callback_).Run(base::unexpected(std::move(*error_))); + return; + } + + if (result_) { + std::move(callback_).Run(base::ok(std::move(result_))); + return; + } + + if (start_address_) { + current_address_ = std::move(start_address_); + } else { + current_address_ = GetNextAddress(current_address_); + } + + if (!current_address_) { + error_ = "Internal error"; + ScheduleWorkOnTask(); + return; + } + + if (!block_end_) { + zcash_wallet_service_->zcash_rpc()->GetLatestBlock( + GetNetworkForZCashKeyring(account_id_->keyring_id), + base::BindOnce(&DiscoverNextUnusedZCashAddressTask::OnGetLastBlock, + weak_ptr_factory_.GetWeakPtr())); + return; + } + + zcash_wallet_service_->zcash_rpc()->IsKnownAddress( + GetNetworkForZCashKeyring(account_id_->keyring_id), + current_address_->address_string, 1, block_end_.value(), + base::BindOnce(&DiscoverNextUnusedZCashAddressTask::OnGetIsKnownAddress, + weak_ptr_factory_.GetWeakPtr())); +} + +void DiscoverNextUnusedZCashAddressTask::OnGetLastBlock( + base::expected result) { + if (!result.has_value()) { + error_ = result.error(); + WorkOnTask(); + return; + } + + block_end_ = result.value().height(); + WorkOnTask(); +} + +void DiscoverNextUnusedZCashAddressTask::OnGetIsKnownAddress( + base::expected result) { + if (!result.has_value()) { + error_ = result.error(); + WorkOnTask(); + return; + } + + if (!result.value()) { + result_ = current_address_->Clone(); + } + + WorkOnTask(); +} + +// CreateTransparentTransactionTask +CreateTransparentTransactionTask::CreateTransparentTransactionTask( + ZCashWalletService* zcash_wallet_service, + const std::string& chain_id, + const mojom::AccountIdPtr& account_id, + const std::string& address_to, + uint64_t amount, + CreateTransactionCallback callback) + : zcash_wallet_service_(zcash_wallet_service), + chain_id_(chain_id), + account_id_(account_id.Clone()), + callback_(std::move(callback)) { + transaction_.set_to(address_to); + transaction_.set_amount(amount); +} + +CreateTransparentTransactionTask::~CreateTransparentTransactionTask() = default; + +void CreateTransparentTransactionTask::ScheduleWorkOnTask() { + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&CreateTransparentTransactionTask::WorkOnTask, + weak_ptr_factory_.GetWeakPtr())); +} + +void CreateTransparentTransactionTask::WorkOnTask() { + if (!callback_) { + return; + } + + if (error_) { + std::move(callback_).Run(base::unexpected(*error_)); + zcash_wallet_service_->CreateTransactionTaskDone(this); + return; + } + + if (!chain_height_) { + zcash_wallet_service_->zcash_rpc()->GetLatestBlock( + chain_id_, + base::BindOnce(&CreateTransparentTransactionTask::OnGetChainHeight, + weak_ptr_factory_.GetWeakPtr())); + return; + } + + if (!change_address_) { + zcash_wallet_service_->DiscoverNextUnusedAddress( + account_id_, true, + base::BindOnce(&CreateTransparentTransactionTask::OnGetChangeAddress, + weak_ptr_factory_.GetWeakPtr())); + return; + } + + if (utxo_map_.empty()) { + zcash_wallet_service_->GetUtxos( + chain_id_, account_id_.Clone(), + base::BindOnce(&CreateTransparentTransactionTask::OnGetUtxos, + weak_ptr_factory_.GetWeakPtr())); + return; + } + + // TODO(cypt4): random shift locktime + // https://github.com/bitcoin/bitcoin/blob/v24.0/src/wallet/spend.cpp#L739-L747 + transaction_.set_locktime(chain_height_.value()); + + if (!PickInputs()) { + SetError("Couldn't pick transaction inputs"); + ScheduleWorkOnTask(); + return; + } + + if (!PrepareOutputs()) { + SetError("Couldn't prepare outputs"); + ScheduleWorkOnTask(); + return; + } + + DCHECK_EQ(kDefaultTransparentOutputsCount, transaction_.outputs().size()); + + std::move(callback_).Run(base::ok(std::move(transaction_))); + zcash_wallet_service_->CreateTransactionTaskDone(this); +} + +void CreateTransparentTransactionTask::OnGetChainHeight( + base::expected result) { + if (!result.has_value()) { + SetError(std::move(result).error()); + WorkOnTask(); + return; + } + + chain_height_ = result.value().height(); + WorkOnTask(); +} + +void CreateTransparentTransactionTask::OnGetChangeAddress( + base::expected result) { + if (!result.has_value()) { + SetError(std::move(result).error()); + WorkOnTask(); + return; + } + + change_address_ = std::move(result.value()); + WorkOnTask(); +} + +void CreateTransparentTransactionTask::OnGetUtxos( + base::expected utxo_map) { + if (!utxo_map.has_value()) { + SetError(std::move(utxo_map).error()); + WorkOnTask(); + return; + } + + utxo_map_ = std::move(utxo_map.value()); + WorkOnTask(); +} + +bool CreateTransparentTransactionTask::PickInputs() { + bool done = false; + + // TODO(apaymyshev): This just picks ouputs one by one and stops when picked + // amount is GE to send amount plus fee. Needs something better than such + // greedy strategy. + std::vector all_inputs; + for (const auto& item : utxo_map_) { + for (const auto& utxo : item.second) { + if (auto input = + ZCashTransaction::TxInput::FromRpcUtxo(item.first, utxo)) { + all_inputs.emplace_back(std::move(*input)); + } + } + } + + base::ranges::sort(all_inputs, [](auto& input1, auto& input2) { + return input1.utxo_value < input2.utxo_value; + }); + + for (auto& input : all_inputs) { + transaction_.inputs().push_back(std::move(input)); + transaction_.set_fee(CalculateTxFee(transaction_.inputs().size())); + + if (transaction_.TotalInputsAmount() >= + transaction_.amount() + transaction_.fee()) { + done = true; + } + + if (done) { + break; + } + } + + DCHECK(!transaction_.inputs().empty()); + return done; +} + +bool CreateTransparentTransactionTask::PrepareOutputs() { + auto& target_output = transaction_.outputs().emplace_back(); + target_output.address = transaction_.to(); + if (!OutputAddressSupported(target_output.address, IsTestnet())) { + return false; + } + + target_output.amount = transaction_.amount(); + target_output.script_pubkey = + ZCashAddressToScriptPubkey(target_output.address, IsTestnet()); + + CHECK_GE(transaction_.TotalInputsAmount(), + transaction_.amount() + transaction_.fee()); + uint64_t change_amount = transaction_.TotalInputsAmount() - + transaction_.amount() - transaction_.fee(); + if (change_amount == 0) { + return true; + } + + // TODO(cypt4): should always pick new change address. + const auto& change_address = change_address_; + if (!change_address) { + return false; + } + + CHECK(OutputAddressSupported(change_address->address_string, IsTestnet())); + auto& change_output = transaction_.outputs().emplace_back(); + change_output.address = change_address_->address_string; + change_output.amount = change_amount; + change_output.script_pubkey = + ZCashAddressToScriptPubkey(change_output.address, IsTestnet()); + return true; +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.h b/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.h new file mode 100644 index 0000000000000..04120fb9fde80 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service_tasks.h @@ -0,0 +1,122 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_WALLET_SERVICE_TASKS_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_WALLET_SERVICE_TASKS_H_ + +#include +#include +#include +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" + +namespace brave_wallet { + +class GetTransparentUtxosContext + : public base::RefCountedThreadSafe { + public: + GetTransparentUtxosContext(); + using GetUtxosCallback = ZCashWalletService::GetUtxosCallback; + + std::set addresses; + ZCashWalletService::UtxoMap utxos; + absl::optional error; + GetUtxosCallback callback; + + bool ShouldRespond() { return callback && (error || addresses.empty()); } + + void SetError(const std::string& error_string) { error = error_string; } + + protected: + friend class base::RefCountedThreadSafe; + + virtual ~GetTransparentUtxosContext(); +}; + +class DiscoverNextUnusedZCashAddressTask + : public base::RefCounted { + public: + DiscoverNextUnusedZCashAddressTask( + base::WeakPtr zcash_wallet_service, + mojom::AccountIdPtr account_id, + mojom::ZCashAddressPtr start_address, + ZCashWalletService::DiscoverNextUnusedAddressCallback callback); + void ScheduleWorkOnTask(); + + private: + friend class base::RefCounted; + virtual ~DiscoverNextUnusedZCashAddressTask(); + + mojom::ZCashAddressPtr GetNextAddress(const mojom::ZCashAddressPtr& address); + + void WorkOnTask(); + void OnGetIsKnownAddress(base::expected stats); + void OnGetLastBlock(base::expected result); + + base::WeakPtr zcash_wallet_service_; + mojom::AccountIdPtr account_id_; + mojom::ZCashAddressPtr start_address_; + mojom::ZCashAddressPtr current_address_; + mojom::ZCashAddressPtr result_; + absl::optional block_end_; + absl::optional error_; + ZCashWalletService::DiscoverNextUnusedAddressCallback callback_; + base::WeakPtrFactory weak_ptr_factory_{ + this}; +}; + +class CreateTransparentTransactionTask { + public: + using UtxoMap = ZCashWalletService::UtxoMap; + using CreateTransactionCallback = + ZCashWalletService::CreateTransactionCallback; + + CreateTransparentTransactionTask(ZCashWalletService* zcash_wallet_service, + const std::string& chain_id, + const mojom::AccountIdPtr& account_id, + const std::string& address_to, + uint64_t amount, + CreateTransactionCallback callback); + virtual ~CreateTransparentTransactionTask(); + + void ScheduleWorkOnTask(); + + private: + bool IsTestnet() { return chain_id_ == mojom::kZCashTestnet; } + void WorkOnTask(); + + void SetError(const std::string& error_string) { error_ = error_string; } + + bool PickInputs(); + bool PrepareOutputs(); + + void OnGetChainHeight(base::expected result); + void OnGetUtxos( + base::expected utxo_map); + void OnGetChangeAddress( + base::expected result); + + raw_ptr zcash_wallet_service_; // Owns `this`. + std::string chain_id_; + mojom::AccountIdPtr account_id_; + CreateTransactionCallback callback_; + + absl::optional chain_height_; + ZCashWalletService::UtxoMap utxo_map_; + + absl::optional error_; + ZCashTransaction transaction_; + + mojom::ZCashAddressPtr change_address_; + + base::WeakPtrFactory weak_ptr_factory_{ + this}; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_WALLET_SERVICE_TASKS_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc b/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc index 948731ee63e33..09b03951dda32 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc @@ -12,6 +12,7 @@ #include "base/memory/scoped_refptr.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" +#include "base/test/bind.h" #include "base/test/mock_callback.h" #include "base/test/scoped_feature_list.h" #include "base/test/task_environment.h" @@ -19,6 +20,7 @@ #include "brave/components/brave_wallet/browser/test_utils.h" #include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "brave/components/brave_wallet/common/common_utils.h" #include "brave/components/brave_wallet/common/features.h" #include "brave/components/brave_wallet/common/hex_utils.h" #include "brave/components/brave_wallet/common/zcash_utils.h" @@ -68,6 +70,13 @@ class MockZCashRPC : public ZCashRpc { void(const std::string& chain_id, const std::string& data, SendTransactionCallback callback)); + + MOCK_METHOD5(IsKnownAddress, + void(const std::string& chain_id, + const std::string& addr, + uint64_t block_start, + uint64_t block_end, + IsKnownAddressCallback callback)); }; } // namespace @@ -124,6 +133,13 @@ class ZCashWalletServiceUnitTest : public testing::Test { // https://zcashblockexplorer.com/transactions/3bc513afc84befb9774f667eb4e63266a7229ab1fdb43476dd7c3a33d16b3101/raw TEST_F(ZCashWalletServiceUnitTest, SignAndPostTransaction) { + { + auto account_id = MakeZCashAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + keyring_service_->UpdateNextUnusedAddressForZCashAccount(account_id, 2, 2); + } + ZCashTransaction zcash_transaction; zcash_transaction.set_locktime(2286687); { @@ -210,4 +226,233 @@ TEST_F(ZCashWalletServiceUnitTest, SignAndPostTransaction) { "cb443e547988b992adc1b47427ce6c40f3ca9e88ac000000"); } +TEST_F(ZCashWalletServiceUnitTest, AddressDiscovery) { + ON_CALL(*zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + zcash::BlockID response; + response.set_height(2286687); + std::move(callback).Run(std::move(response)); + })); + + ON_CALL(*zcash_rpc(), IsKnownAddress(_, _, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, const std::string& addr, + uint64_t block_start, uint64_t block_end, + ZCashRpc::IsKnownAddressCallback callback) { + EXPECT_EQ(2286687u, block_end); + if (addr == "t1c61yifRMgyhMsBYsFDBa5aEQkgU65CGau") { + std::move(callback).Run(true); + return; + } + if (addr == "t1V7JBWXRYPA19nBLBFTm8669DhQgErMAnK") { + std::move(callback).Run(true); + return; + } + if (addr == "t1UCYMSUdkGXEyeKPqgwiDn8NwGv5JKmJoL") { + std::move(callback).Run(false); + return; + } + })); + + { + bool callback_called = false; + auto discovery_callback = base::BindLambdaForTesting( + [&](mojom::ZCashAddressPtr addr, const absl::optional&) { + EXPECT_EQ(addr->address_string, + "t1UCYMSUdkGXEyeKPqgwiDn8NwGv5JKmJoL"); + callback_called = true; + }); + + auto account_id = MakeZCashAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + + zcash_wallet_service_->RunDiscovery(std::move(account_id), false, + std::move(discovery_callback)); + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback_called); + } + + ON_CALL(*zcash_rpc(), IsKnownAddress(_, _, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, const std::string& addr, + uint64_t block_start, uint64_t block_end, + ZCashRpc::IsKnownAddressCallback callback) { + EXPECT_EQ(2286687u, block_end); + if (addr == "t1UCYMSUdkGXEyeKPqgwiDn8NwGv5JKmJoL") { + std::move(callback).Run(true); + return; + } + if (addr == "t1JEfEPQDGruzd7Q42pdwHmR4sRHGLRF48m") { + std::move(callback).Run(false); + return; + } + })); + + { + bool callback_called = false; + auto discovery_callback = base::BindLambdaForTesting( + [&](mojom::ZCashAddressPtr addr, const absl::optional&) { + EXPECT_EQ(addr->address_string, + "t1JEfEPQDGruzd7Q42pdwHmR4sRHGLRF48m"); + callback_called = true; + }); + + auto account_id = MakeZCashAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + + zcash_wallet_service_->RunDiscovery(std::move(account_id), false, + std::move(discovery_callback)); + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback_called); + } +} + +TEST_F(ZCashWalletServiceUnitTest, AddressDiscovery_Change) { + ON_CALL(*zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + zcash::BlockID response; + response.set_height(2286687); + std::move(callback).Run(std::move(response)); + })); + + ON_CALL(*zcash_rpc(), IsKnownAddress(_, _, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, const std::string& addr, + uint64_t block_start, uint64_t block_end, + ZCashRpc::IsKnownAddressCallback callback) { + EXPECT_EQ(2286687u, block_end); + if (addr == "t1RDtGXzcfchmtrE8pGLorefgtspgcNZbrE") { + std::move(callback).Run(true); + return; + } + if (addr == "t1VUdyCuqWgeBPJvfhWvHLD5iDUfkdLrwWz") { + std::move(callback).Run(true); + return; + } + if (addr == "t1QJuws2nGqDNJEKsKniUPDNLbMw5R9ixGj") { + std::move(callback).Run(false); + return; + } + })); + + { + bool callback_called = false; + auto discovery_callback = base::BindLambdaForTesting( + [&](mojom::ZCashAddressPtr addr, const absl::optional&) { + EXPECT_EQ(addr->address_string, + "t1QJuws2nGqDNJEKsKniUPDNLbMw5R9ixGj"); + callback_called = true; + }); + + auto account_id = MakeZCashAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + + zcash_wallet_service_->RunDiscovery(std::move(account_id), true, + std::move(discovery_callback)); + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback_called); + } + + ON_CALL(*zcash_rpc(), IsKnownAddress(_, _, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, const std::string& addr, + uint64_t block_start, uint64_t block_end, + ZCashRpc::IsKnownAddressCallback callback) { + EXPECT_EQ(2286687u, block_end); + if (addr == "t1QJuws2nGqDNJEKsKniUPDNLbMw5R9ixGj") { + std::move(callback).Run(true); + return; + } + if (addr == "t1gKxueg76TtvVmMQ6swDmvHxtmLTSQv6KP") { + std::move(callback).Run(false); + return; + } + })); + + { + bool callback_called = false; + auto discovery_callback = base::BindLambdaForTesting( + [&](mojom::ZCashAddressPtr addr, const absl::optional&) { + EXPECT_EQ(addr->address_string, + "t1gKxueg76TtvVmMQ6swDmvHxtmLTSQv6KP"); + callback_called = true; + }); + + auto account_id = MakeZCashAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + + zcash_wallet_service_->RunDiscovery(std::move(account_id), true, + std::move(discovery_callback)); + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback_called); + } +} + +TEST_F(ZCashWalletServiceUnitTest, AddressDiscovery_FromPrefs) { + ON_CALL(*zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + zcash::BlockID response; + response.set_height(2286687); + std::move(callback).Run(std::move(response)); + })); + + ON_CALL(*zcash_rpc(), IsKnownAddress(_, _, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, const std::string& addr, + uint64_t block_start, uint64_t block_end, + ZCashRpc::IsKnownAddressCallback callback) { + EXPECT_EQ(2286687u, block_end); + if (addr == "t1UCYMSUdkGXEyeKPqgwiDn8NwGv5JKmJoL") { + std::move(callback).Run(true); + return; + } + if (addr == "t1JEfEPQDGruzd7Q42pdwHmR4sRHGLRF48m") { + std::move(callback).Run(false); + return; + } + })); + + { + auto account_id = MakeZCashAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + keyring_service_->UpdateNextUnusedAddressForZCashAccount(account_id, 2, + absl::nullopt); + } + + { + bool callback_called = false; + auto discovery_callback = base::BindLambdaForTesting( + [&](mojom::ZCashAddressPtr addr, const absl::optional&) { + EXPECT_EQ(addr->address_string, + "t1JEfEPQDGruzd7Q42pdwHmR4sRHGLRF48m"); + callback_called = true; + }); + + auto account_id = MakeZCashAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + + zcash_wallet_service_->RunDiscovery(std::move(account_id), false, + std::move(discovery_callback)); + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback_called); + } +} + } // namespace brave_wallet diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index ace2381c2ec87..256ce4b51e989 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -1420,9 +1420,16 @@ struct ZCashAddress { ZCashKeyId key_id; }; +struct ZCashAccountInfo { + ZCashAddress next_transparent_receive_address; + ZCashAddress next_transparent_change_address; +}; + interface ZCashWalletService { GetBalance(string network_id, AccountId account_id) => (ZCashBalance? balance, string? error_message); GetReceiverAddress(AccountId account_id) => (ZCashAddress? address, string? error_message); + RunDiscovery(AccountId account_id, bool change) => (ZCashAddress? address, string? error_message); + GetZCashAccountInfo(AccountId account_id) => (ZCashAccountInfo? account_info); }; enum TransactionStatus { diff --git a/components/brave_wallet/common/common_utils.cc b/components/brave_wallet/common/common_utils.cc index c2a50f0d8b6bf..9d4aac034c4d7 100644 --- a/components/brave_wallet/common/common_utils.cc +++ b/components/brave_wallet/common/common_utils.cc @@ -70,6 +70,14 @@ bool IsZCashKeyring(mojom::KeyringId keyring_id) { keyring_id == mojom::KeyringId::kZCashTestnet; } +bool IsZCashMainnetKeyring(mojom::KeyringId keyring_id) { + return keyring_id == mojom::KeyringId::kZCashMainnet; +} + +bool IsZCashTestnetKeyring(mojom::KeyringId keyring_id) { + return keyring_id == mojom::KeyringId::kZCashTestnet; +} + bool IsBitcoinMainnetKeyring(mojom::KeyringId keyring_id) { return keyring_id == mojom::KeyringId::kBitcoin84; } @@ -248,6 +256,16 @@ std::string GetNetworkForBitcoinAccount(const mojom::AccountIdPtr& account_id) { return GetNetworkForBitcoinKeyring(account_id->keyring_id); } +std::string GetNetworkForZCashKeyring(const mojom::KeyringId& keyring_id) { + if (IsZCashMainnetKeyring(keyring_id)) { + return mojom::kZCashMainnet; + } + if (IsZCashTestnetKeyring(keyring_id)) { + return mojom::kZCashTestnet; + } + NOTREACHED_NORETURN(); +} + mojom::AccountIdPtr MakeZCashAccountId(mojom::CoinType coin, mojom::KeyringId keyring_id, mojom::AccountKind kind, diff --git a/components/brave_wallet/common/common_utils.h b/components/brave_wallet/common/common_utils.h index 9761cf8a33677..67bfdedf170c5 100644 --- a/components/brave_wallet/common/common_utils.h +++ b/components/brave_wallet/common/common_utils.h @@ -65,6 +65,8 @@ mojom::AccountIdPtr MakeBitcoinAccountId(mojom::CoinType coin, std::string GetNetworkForBitcoinKeyring(const mojom::KeyringId& keyring_id); std::string GetNetworkForBitcoinAccount(const mojom::AccountIdPtr& account_id); +std::string GetNetworkForZCashKeyring(const mojom::KeyringId& keyring_id); + mojom::AccountIdPtr MakeZCashAccountId(mojom::CoinType coin, mojom::KeyringId keyring_id, mojom::AccountKind kind,