diff --git a/browser/brave_wallet/BUILD.gn b/browser/brave_wallet/BUILD.gn index d471f53014bf..b4efee0a4179 100644 --- a/browser/brave_wallet/BUILD.gn +++ b/browser/brave_wallet/BUILD.gn @@ -8,8 +8,12 @@ source_set("brave_wallet") { "asset_ratio_service_factory.h", "blockchain_images_source.cc", "blockchain_images_source.h", + "brave_wallet_auto_pin_service_factory.cc", + "brave_wallet_auto_pin_service_factory.h", "brave_wallet_context_utils.cc", "brave_wallet_context_utils.h", + "brave_wallet_pin_service_factory.cc", + "brave_wallet_pin_service_factory.h", "brave_wallet_service_factory.cc", "brave_wallet_service_factory.h", "json_rpc_service_factory.cc", diff --git a/browser/brave_wallet/brave_wallet_auto_pin_service_factory.cc b/browser/brave_wallet/brave_wallet_auto_pin_service_factory.cc new file mode 100644 index 000000000000..f7d3a9dc6ac0 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_auto_pin_service_factory.cc @@ -0,0 +1,88 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h" + +#include +#include + +#include "brave/browser/brave_wallet/brave_wallet_context_utils.h" + +#include "brave/browser/brave_wallet/brave_wallet_pin_service_factory.h" +#include "brave/browser/brave_wallet/brave_wallet_service_factory.h" + +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" +#include "brave/components/brave_wallet/browser/brave_wallet_service.h" + +#include "chrome/browser/profiles/incognito_helpers.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/user_prefs/user_prefs.h" + +namespace brave_wallet { + +// static +BraveWalletAutoPinServiceFactory* +BraveWalletAutoPinServiceFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +mojo::PendingRemote +BraveWalletAutoPinServiceFactory::GetForContext( + content::BrowserContext* context) { + if (!IsAllowedForContext(context)) { + return mojo::PendingRemote(); + } + + return GetServiceForContext(context)->MakeRemote(); +} + +// static +BraveWalletAutoPinService* +BraveWalletAutoPinServiceFactory::GetServiceForContext( + content::BrowserContext* context) { + if (!IsAllowedForContext(context)) { + return nullptr; + } + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +// static +void BraveWalletAutoPinServiceFactory::BindForContext( + content::BrowserContext* context, + mojo::PendingReceiver receiver) { + auto* service = + BraveWalletAutoPinServiceFactory::GetServiceForContext(context); + if (service) { + service->Bind(std::move(receiver)); + } +} + +BraveWalletAutoPinServiceFactory::BraveWalletAutoPinServiceFactory() + : BrowserContextKeyedServiceFactory( + "BraveWalletAutoPinService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(BraveWalletServiceFactory::GetInstance()); + DependsOn(BraveWalletPinServiceFactory::GetInstance()); +} + +BraveWalletAutoPinServiceFactory::~BraveWalletAutoPinServiceFactory() = default; + +KeyedService* BraveWalletAutoPinServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new BraveWalletAutoPinService( + user_prefs::UserPrefs::Get(context), + BraveWalletServiceFactory::GetServiceForContext(context), + BraveWalletPinServiceFactory::GetServiceForContext(context)); +} + +content::BrowserContext* +BraveWalletAutoPinServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextRedirectedInIncognito(context); +} + +} // namespace brave_wallet diff --git a/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h b/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h new file mode 100644 index 000000000000..0d1ea5ae7832 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h @@ -0,0 +1,54 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_BRAVE_WALLET_BRAVE_WALLET_AUTO_PIN_SERVICE_FACTORY_H_ +#define BRAVE_BROWSER_BRAVE_WALLET_BRAVE_WALLET_AUTO_PIN_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" + +#include "brave/components/brave_wallet/browser/brave_wallet_auto_pin_service.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/browser_context.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" + +namespace brave_wallet { + +class BraveWalletAutoPinService; + +class BraveWalletAutoPinServiceFactory + : public BrowserContextKeyedServiceFactory { + public: + static mojo::PendingRemote GetForContext( + content::BrowserContext* context); + static BraveWalletAutoPinService* GetServiceForContext( + content::BrowserContext* context); + static BraveWalletAutoPinServiceFactory* GetInstance(); + static void BindForContext( + content::BrowserContext* context, + mojo::PendingReceiver receiver); + + private: + friend struct base::DefaultSingletonTraits; + + BraveWalletAutoPinServiceFactory(); + ~BraveWalletAutoPinServiceFactory() override; + + BraveWalletAutoPinServiceFactory(const BraveWalletAutoPinServiceFactory&) = + delete; + BraveWalletAutoPinServiceFactory& operator=( + const BraveWalletAutoPinServiceFactory&) = delete; + + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +} // namespace brave_wallet + +#endif // BRAVE_BROWSER_BRAVE_WALLET_BRAVE_WALLET_AUTO_PIN_SERVICE_FACTORY_H_ diff --git a/browser/brave_wallet/brave_wallet_pin_service_factory.cc b/browser/brave_wallet/brave_wallet_pin_service_factory.cc new file mode 100644 index 000000000000..51d92f7e1d01 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_pin_service_factory.cc @@ -0,0 +1,81 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/brave_wallet/brave_wallet_pin_service_factory.h" + +#include +#include + +#include "brave/browser/brave_wallet/brave_wallet_context_utils.h" +#include "brave/browser/brave_wallet/json_rpc_service_factory.h" +#include "brave/browser/ipfs/ipfs_local_pin_service_factory.h" +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" +#include "brave/components/brave_wallet/browser/brave_wallet_service.h" +#include "brave/components/brave_wallet/browser/brave_wallet_service_delegate.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/user_prefs/user_prefs.h" + +namespace brave_wallet { + +// static +BraveWalletPinServiceFactory* BraveWalletPinServiceFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +mojo::PendingRemote +BraveWalletPinServiceFactory::GetForContext(content::BrowserContext* context) { + if (!IsAllowedForContext(context)) { + return mojo::PendingRemote(); + } + + return GetServiceForContext(context)->MakeRemote(); +} + +// static +BraveWalletPinService* BraveWalletPinServiceFactory::GetServiceForContext( + content::BrowserContext* context) { + if (!IsAllowedForContext(context)) { + return nullptr; + } + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +// static +void BraveWalletPinServiceFactory::BindForContext( + content::BrowserContext* context, + mojo::PendingReceiver receiver) { + auto* service = BraveWalletPinServiceFactory::GetServiceForContext(context); + if (service) { + service->Bind(std::move(receiver)); + } +} + +BraveWalletPinServiceFactory::BraveWalletPinServiceFactory() + : BrowserContextKeyedServiceFactory( + "BraveWalletPinService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(brave_wallet::JsonRpcServiceFactory::GetInstance()); + DependsOn(ipfs::IpfsLocalPinServiceFactory::GetInstance()); +} + +BraveWalletPinServiceFactory::~BraveWalletPinServiceFactory() = default; + +KeyedService* BraveWalletPinServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new BraveWalletPinService( + user_prefs::UserPrefs::Get(context), + JsonRpcServiceFactory::GetServiceForContext(context), + ipfs::IpfsLocalPinServiceFactory::GetServiceForContext(context)); +} + +content::BrowserContext* BraveWalletPinServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextRedirectedInIncognito(context); +} + +} // namespace brave_wallet diff --git a/browser/brave_wallet/brave_wallet_pin_service_factory.h b/browser/brave_wallet/brave_wallet_pin_service_factory.h new file mode 100644 index 000000000000..05f589429638 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_pin_service_factory.h @@ -0,0 +1,51 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_BRAVE_WALLET_BRAVE_WALLET_PIN_SERVICE_FACTORY_H_ +#define BRAVE_BROWSER_BRAVE_WALLET_BRAVE_WALLET_PIN_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "brave/browser/ipfs/ipfs_service_factory.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/browser_context.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" + +namespace brave_wallet { + +class BraveWalletPinService; + +class BraveWalletPinServiceFactory : public BrowserContextKeyedServiceFactory { + public: + static mojo::PendingRemote GetForContext( + content::BrowserContext* context); + static BraveWalletPinService* GetServiceForContext( + content::BrowserContext* context); + static BraveWalletPinServiceFactory* GetInstance(); + static void BindForContext( + content::BrowserContext* context, + mojo::PendingReceiver receiver); + + private: + friend struct base::DefaultSingletonTraits; + + BraveWalletPinServiceFactory(); + ~BraveWalletPinServiceFactory() override; + + BraveWalletPinServiceFactory(const BraveWalletPinServiceFactory&) = delete; + BraveWalletPinServiceFactory& operator=(const BraveWalletPinServiceFactory&) = + delete; + + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +} // namespace brave_wallet + +#endif // BRAVE_BROWSER_BRAVE_WALLET_BRAVE_WALLET_PIN_SERVICE_FACTORY_H_ diff --git a/browser/ipfs/ipfs_local_pin_service_factory.cc b/browser/ipfs/ipfs_local_pin_service_factory.cc new file mode 100644 index 000000000000..e52e6bdb9347 --- /dev/null +++ b/browser/ipfs/ipfs_local_pin_service_factory.cc @@ -0,0 +1,52 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ipfs/ipfs_local_pin_service_factory.h" + +#include +#include + +#include "brave/browser/ipfs/ipfs_service_factory.h" +#include "brave/components/ipfs/pin/ipfs_local_pin_service.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/prefs/pref_service.h" +#include "components/user_prefs/user_prefs.h" + +namespace ipfs { + +// static +IpfsLocalPinServiceFactory* IpfsLocalPinServiceFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +IpfsLocalPinService* IpfsLocalPinServiceFactory::GetServiceForContext( + content::BrowserContext* context) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +IpfsLocalPinServiceFactory::IpfsLocalPinServiceFactory() + : BrowserContextKeyedServiceFactory( + "IpfsLocalPinService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(ipfs::IpfsServiceFactory::GetInstance()); +} + +IpfsLocalPinServiceFactory::~IpfsLocalPinServiceFactory() = default; + +KeyedService* IpfsLocalPinServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new IpfsLocalPinService(user_prefs::UserPrefs::Get(context), + IpfsServiceFactory::GetForContext(context)); +} + +content::BrowserContext* IpfsLocalPinServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextRedirectedInIncognito(context); +} + +} // namespace ipfs diff --git a/browser/ipfs/ipfs_local_pin_service_factory.h b/browser/ipfs/ipfs_local_pin_service_factory.h new file mode 100644 index 000000000000..f8e7fc6f8f56 --- /dev/null +++ b/browser/ipfs/ipfs_local_pin_service_factory.h @@ -0,0 +1,42 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_IPFS_IPFS_LOCAL_PIN_SERVICE_FACTORY_H_ +#define BRAVE_BROWSER_IPFS_IPFS_LOCAL_PIN_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/browser_context.h" + +namespace ipfs { + +class IpfsLocalPinService; + +class IpfsLocalPinServiceFactory : public BrowserContextKeyedServiceFactory { + public: + static IpfsLocalPinService* GetServiceForContext( + content::BrowserContext* context); + static IpfsLocalPinServiceFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits; + + IpfsLocalPinServiceFactory(); + ~IpfsLocalPinServiceFactory() override; + + IpfsLocalPinServiceFactory(const IpfsLocalPinServiceFactory&) = delete; + IpfsLocalPinServiceFactory& operator=(const IpfsLocalPinServiceFactory&) = + delete; + + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +} // namespace ipfs + +#endif // BRAVE_BROWSER_IPFS_IPFS_LOCAL_PIN_SERVICE_FACTORY_H_ diff --git a/browser/ipfs/sources.gni b/browser/ipfs/sources.gni index e5d8a5498b4e..4ab3c9446647 100644 --- a/browser/ipfs/sources.gni +++ b/browser/ipfs/sources.gni @@ -23,6 +23,8 @@ if (enable_ipfs) { "//brave/browser/ipfs/ipfs_dns_resolver_impl.h", "//brave/browser/ipfs/ipfs_host_resolver.cc", "//brave/browser/ipfs/ipfs_host_resolver.h", + "//brave/browser/ipfs/ipfs_local_pin_service_factory.cc", + "//brave/browser/ipfs/ipfs_local_pin_service_factory.h", "//brave/browser/ipfs/ipfs_service_factory.cc", "//brave/browser/ipfs/ipfs_service_factory.h", "//brave/browser/ipfs/ipfs_subframe_navigation_throttle.cc", diff --git a/browser/profiles/brave_profile_manager.cc b/browser/profiles/brave_profile_manager.cc index a78ad2ef656e..a8e52df50a5c 100644 --- a/browser/profiles/brave_profile_manager.cc +++ b/browser/profiles/brave_profile_manager.cc @@ -15,6 +15,7 @@ #include "brave/browser/brave_federated/brave_federated_service_factory.h" #include "brave/browser/brave_news/brave_news_controller_factory.h" #include "brave/browser/brave_rewards/rewards_service_factory.h" +#include "brave/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h" #include "brave/browser/brave_wallet/brave_wallet_service_factory.h" #include "brave/browser/profiles/profile_util.h" #include "brave/browser/url_sanitizer/url_sanitizer_service_factory.h" @@ -94,6 +95,7 @@ void BraveProfileManager::DoFinalInitForServices(Profile* profile, brave_ads::AdsServiceFactory::GetForProfile(profile); brave_rewards::RewardsServiceFactory::GetForProfile(profile); brave_wallet::BraveWalletServiceFactory::GetServiceForContext(profile); + brave_wallet::BraveWalletAutoPinServiceFactory::GetServiceForContext(profile); #if BUILDFLAG(ENABLE_IPFS) ipfs::IpfsServiceFactory::GetForContext(profile); #endif diff --git a/browser/ui/webui/brave_wallet/wallet_page_ui.cc b/browser/ui/webui/brave_wallet/wallet_page_ui.cc index 6b9648688689..414d7e22c789 100644 --- a/browser/ui/webui/brave_wallet/wallet_page_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_page_ui.cc @@ -11,6 +11,8 @@ #include "base/command_line.h" #include "base/files/file_path.h" #include "brave/browser/brave_wallet/asset_ratio_service_factory.h" +#include "brave/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h" +#include "brave/browser/brave_wallet/brave_wallet_pin_service_factory.h" #include "brave/browser/brave_wallet/brave_wallet_service_factory.h" #include "brave/browser/brave_wallet/json_rpc_service_factory.h" #include "brave/browser/brave_wallet/keyring_service_factory.h" @@ -21,6 +23,7 @@ #include "brave/components/brave_wallet/browser/asset_ratio_service.h" #include "brave/components/brave_wallet/browser/blockchain_registry.h" #include "brave/components/brave_wallet/browser/brave_wallet_constants.h" +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" #include "brave/components/brave_wallet/browser/brave_wallet_service.h" #include "brave/components/brave_wallet/browser/json_rpc_service.h" #include "brave/components/brave_wallet/browser/keyring_service.h" @@ -111,7 +114,11 @@ void WalletPageUI::CreatePageHandler( mojo::PendingReceiver brave_wallet_service_receiver, mojo::PendingReceiver - brave_wallet_p3a_receiver) { + brave_wallet_p3a_receiver, + mojo::PendingReceiver + brave_wallet_pin_service_receiver, + mojo::PendingReceiver + brave_wallet_auto_pin_service_receiver) { DCHECK(page); auto* profile = Profile::FromWebUI(web_ui()); DCHECK(profile); @@ -142,6 +149,10 @@ void WalletPageUI::CreatePageHandler( wallet_service->Bind(std::move(brave_wallet_service_receiver)); wallet_service->GetBraveWalletP3A()->Bind( std::move(brave_wallet_p3a_receiver)); + brave_wallet::BraveWalletPinServiceFactory::BindForContext( + profile, std::move(brave_wallet_pin_service_receiver)); + brave_wallet::BraveWalletAutoPinServiceFactory::BindForContext( + profile, std::move(brave_wallet_auto_pin_service_receiver)); auto* blockchain_registry = brave_wallet::BlockchainRegistry::GetInstance(); if (blockchain_registry) { diff --git a/browser/ui/webui/brave_wallet/wallet_page_ui.h b/browser/ui/webui/brave_wallet/wallet_page_ui.h index f197e15aef5f..2d6895d31f87 100644 --- a/browser/ui/webui/brave_wallet/wallet_page_ui.h +++ b/browser/ui/webui/brave_wallet/wallet_page_ui.h @@ -58,7 +58,11 @@ class WalletPageUI : public ui::MojoWebUIController, mojo::PendingReceiver brave_wallet_service, mojo::PendingReceiver - brave_wallet_p3a) override; + brave_wallet_p3a, + mojo::PendingReceiver + brave_wallet_pin_service_receiver, + mojo::PendingReceiver + brave_wallet_auto_pin_service_receiver) override; std::unique_ptr page_handler_; std::unique_ptr wallet_handler_; diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index 43442db2c3b0..632e845d7e79 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -27,10 +27,14 @@ static_library("browser") { "blockchain_list_parser.h", "blockchain_registry.cc", "blockchain_registry.h", + "brave_wallet_auto_pin_service.cc", + "brave_wallet_auto_pin_service.h", "brave_wallet_p3a.cc", "brave_wallet_p3a.h", "brave_wallet_p3a_private.cc", "brave_wallet_p3a_private.h", + "brave_wallet_pin_service.cc", + "brave_wallet_pin_service.h", "brave_wallet_prefs.cc", "brave_wallet_prefs.h", "brave_wallet_provider_delegate.cc", diff --git a/components/brave_wallet/browser/brave_wallet_auto_pin_service.cc b/components/brave_wallet/browser/brave_wallet_auto_pin_service.cc new file mode 100644 index 000000000000..a50fffde34e8 --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_auto_pin_service.cc @@ -0,0 +1,243 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/brave_wallet_auto_pin_service.h" + +#include "brave/components/brave_wallet/browser/pref_names.h" + +namespace brave_wallet { + +IntentData::IntentData(const BlockchainTokenPtr& token, + Operation operation, + absl::optional service) + : token(token.Clone()), operation(operation), service(service) {} + +IntentData::~IntentData() {} + +BraveWalletAutoPinService::BraveWalletAutoPinService( + PrefService* prefs, + BraveWalletService* brave_wallet_service, + BraveWalletPinService* brave_wallet_pin_service) + : pref_service_(prefs), + brave_wallet_service_(brave_wallet_service), + brave_wallet_pin_service_(brave_wallet_pin_service) { + Restore(); + brave_wallet_service->AddTokenObserver( + token_observer_.BindNewPipeAndPassRemote()); +} + +BraveWalletAutoPinService::~BraveWalletAutoPinService() {} + +void BraveWalletAutoPinService::Bind( + mojo::PendingReceiver receiver) { + receivers_.Add(this, std::move(receiver)); +} + +void BraveWalletAutoPinService::OnTokenAdded(BlockchainTokenPtr token) { + if (!IsAutoPinEnabled()) { + return; + } + PostPinToken(std::move(token), base::OnceCallback()); +} + +void BraveWalletAutoPinService::OnTokenRemoved(BlockchainTokenPtr token) { + const auto iter = + std::remove_if(queue_.begin(), queue_.end(), + [&token](const std::unique_ptr& intent) { + return intent->token == token; + }); + queue_.erase(iter, queue_.end()); + PostUnpinToken(std::move(token), base::OnceCallback()); +} + +void BraveWalletAutoPinService::Restore() { + brave_wallet_service_->GetUserAssets( + mojom::kMainnetChainId, mojom::CoinType::ETH, + base::BindOnce(&BraveWalletAutoPinService::OnTokenListResolved, + base::Unretained(this))); +} + +void BraveWalletAutoPinService::OnTokenListResolved( + std::vector token_list) { + bool autopin_enabled = IsAutoPinEnabled(); + std::set known_tokens = + brave_wallet_pin_service_->GetTokens(absl::nullopt); + for (const auto& token : token_list) { + if (!token->is_erc721) { + continue; + } + + std::string current_token_path = + BraveWalletPinService::GetPath(absl::nullopt, token); + known_tokens.erase(known_tokens.find(current_token_path)); + + mojom::TokenPinStatusPtr status = + brave_wallet_pin_service_->GetTokenStatus(absl::nullopt, token); + + if (!status || + status->code == mojom::TokenPinStatusCode::STATUS_NOT_PINNED) { + if (autopin_enabled) { + AddOrExecute( + std::make_unique(token, Operation::ADD, absl::nullopt)); + } + } else if (status->code == + mojom::TokenPinStatusCode::STATUS_PINNING_FAILED || + status->code == + mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS || + status->code == + mojom::TokenPinStatusCode::STATUS_PINNING_PENDING) { + AddOrExecute( + std::make_unique(token, Operation::ADD, absl::nullopt)); + } else if (status->code == + mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED || + status->code == + mojom::TokenPinStatusCode::STATUS_UNPINNING_IN_PROGRESS || + status->code == + mojom::TokenPinStatusCode::STATUS_UNPINNING_PENDING) { + AddOrExecute(std::make_unique(token, Operation::DELETE, + absl::nullopt)); + } else if (status->code == mojom::TokenPinStatusCode::STATUS_PINNED) { + auto t1 = status->validate_time; + if (!t1 || (base::Time::Now() - t1.value()) > base::Days(1) || + t1.value() > base::Time::Now()) { + AddOrExecute(std::make_unique(token, Operation::VALIDATE, + absl::nullopt)); + } + } + } + + for (const auto& t : known_tokens) { + mojom::BlockchainTokenPtr token = BraveWalletPinService::TokenFromPath(t); + if (token) { + AddOrExecute(std::make_unique(token, Operation::DELETE, + absl::nullopt)); + } + } + + CheckQueue(); +} + +void BraveWalletAutoPinService::PostPinToken(BlockchainTokenPtr token, + PostPinTokenCallback callback) { + queue_.push_back( + std::make_unique(token, Operation::ADD, absl::nullopt)); + CheckQueue(); +} + +void BraveWalletAutoPinService::PostUnpinToken(BlockchainTokenPtr token, + PostPinTokenCallback callback) { + queue_.push_back( + std::make_unique(token, Operation::DELETE, absl::nullopt)); + CheckQueue(); +} + +void BraveWalletAutoPinService::ValidateToken( + const std::unique_ptr& data) { + brave_wallet_pin_service_->Validate( + data->token->Clone(), data->service, + base::BindOnce(&BraveWalletAutoPinService::OnValidateTaskFinished, + base::Unretained(this))); +} + +void BraveWalletAutoPinService::PinToken( + const std::unique_ptr& data) { + brave_wallet_pin_service_->AddPin( + data->token->Clone(), data->service, + base::BindOnce(&BraveWalletAutoPinService::OnTaskFinished, + base::Unretained(this))); +} + +void BraveWalletAutoPinService::UnpinToken( + const std::unique_ptr& data) { + brave_wallet_pin_service_->RemovePin( + data->token->Clone(), data->service, + base::BindOnce(&BraveWalletAutoPinService::OnTaskFinished, + base::Unretained(this))); +} + +void BraveWalletAutoPinService::AddOrExecute(std::unique_ptr data) { + DCHECK(data); + for (const auto& v : queue_) { + if (v->token == data->token && v->service == data->service) { + return; + } + } + if (current_ && current_->token == data->token && + current_->service == data->service) { + return; + } + if (data->operation == Operation::ADD) { + brave_wallet_pin_service_->MarkAsPendingForPinning(data->token, + data->service); + } else if (data->operation == Operation::DELETE) { + brave_wallet_pin_service_->MarkAsPendingForUnpinning(data->token, + data->service); + } + queue_.push_back(std::move(data)); + CheckQueue(); +} + +void BraveWalletAutoPinService::PostRetry(std::unique_ptr data) { + int multiply = ++data->attempt; + base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&BraveWalletAutoPinService::AddOrExecute, + base::Unretained(this), std::move(data)), + base::Minutes(1 * multiply)); +} + +void BraveWalletAutoPinService::CheckQueue() { + if (queue_.empty() || current_) { + return; + } + + current_ = std::move(queue_.front()); + queue_.pop_front(); + + if (current_->operation == Operation::ADD) { + PinToken(current_); + } else if (current_->operation == Operation::DELETE) { + UnpinToken(current_); + } else if (current_->operation == Operation::VALIDATE) { + ValidateToken(current_); + } +} + +void BraveWalletAutoPinService::OnTaskFinished(bool result, + mojom::PinErrorPtr error) { + CHECK(current_); + if (!result) { + PostRetry(std::move(current_)); + } + current_.reset(); + CheckQueue(); +} + +void BraveWalletAutoPinService::OnValidateTaskFinished( + bool result, + mojom::PinErrorPtr error) { + if (!result) { + AddOrExecute(std::make_unique(current_->token, Operation::ADD, + current_->service)); + } + current_.reset(); + CheckQueue(); +} + +void BraveWalletAutoPinService::SetAutoPinEnabled(bool enabled) { + pref_service_->SetBoolean(kAutoPinEnabled, enabled); + Restore(); +} + +bool BraveWalletAutoPinService::IsAutoPinEnabled() { + return pref_service_->GetBoolean(kAutoPinEnabled); +} + +void BraveWalletAutoPinService::IsAutoPinEnabled( + IsAutoPinEnabledCallback callback) { + std::move(callback).Run(IsAutoPinEnabled()); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/brave_wallet_auto_pin_service.h b/components/brave_wallet/browser/brave_wallet_auto_pin_service.h new file mode 100644 index 000000000000..6fe28f592848 --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_auto_pin_service.h @@ -0,0 +1,105 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_AUTO_PIN_SERVICE_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_AUTO_PIN_SERVICE_H_ + +#include +#include +#include +#include +#include +#include + +#include "base/memory/scoped_refptr.h" +#include "base/task/sequenced_task_runner.h" +#include "brave/components/brave_wallet/browser/blockchain_registry.h" +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" +#include "brave/components/brave_wallet/browser/brave_wallet_service.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "components/prefs/pref_service.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver_set.h" +#include "mojo/public/cpp/bindings/remote_set.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +using brave_wallet::mojom::BlockchainTokenPtr; + +namespace brave_wallet { + +enum Operation { ADD = 0, DELETE = 1, VALIDATE = 2 }; + +struct IntentData { + BlockchainTokenPtr token; + Operation operation; + absl::optional service; + size_t attempt = 0; + IntentData(const BlockchainTokenPtr& token, + Operation operation, + absl::optional service); + ~IntentData(); +}; + +class BraveWalletAutoPinService + : public KeyedService, + public brave_wallet::mojom::WalletAutoPinService, + public brave_wallet::mojom::BraveWalletServiceTokenObserver { + public: + BraveWalletAutoPinService(PrefService* prefs, + BraveWalletService* brave_wallet_service, + BraveWalletPinService* brave_wallet_pin_service); + ~BraveWalletAutoPinService() override; + + mojo::PendingRemote MakeRemote(); + void Bind(mojo::PendingReceiver receiver); + + void SetAutoPinEnabled(bool enabled) override; + void IsAutoPinEnabled(IsAutoPinEnabledCallback callback) override; + + void PostPinToken(BlockchainTokenPtr token, + PostPinTokenCallback callback) override; + void PostUnpinToken(BlockchainTokenPtr token, + PostUnpinTokenCallback callback) override; + + // BraveWalletServiceTokenObserver + void OnTokenAdded(mojom::BlockchainTokenPtr token) override; + void OnTokenRemoved(mojom::BlockchainTokenPtr token) override; + + private: + void Restore(); + void OnTokenListResolved(std::vector); + + void CheckQueue(); + void AddOrExecute(std::unique_ptr data); + void PostRetry(std::unique_ptr data); + + bool IsAutoPinEnabled(); + + std::vector> GetServicesToPin(); + std::vector> GetKnownServices(); + + void ValidateToken(const std::unique_ptr& data); + void PinToken(const std::unique_ptr& data); + void UnpinToken(const std::unique_ptr& data); + + void OnTaskFinished(bool result, mojom::PinErrorPtr error); + void OnValidateTaskFinished(bool result, mojom::PinErrorPtr error); + + mojo::Receiver + token_observer_{this}; + mojo::ReceiverSet receivers_; + + PrefService* pref_service_; + BraveWalletService* brave_wallet_service_; + BraveWalletPinService* brave_wallet_pin_service_; + + std::unique_ptr current_; + std::deque> queue_; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_AUTO_PIN_SERVICE_H_ diff --git a/components/brave_wallet/browser/brave_wallet_auto_pin_service_unittest.cc b/components/brave_wallet/browser/brave_wallet_auto_pin_service_unittest.cc new file mode 100644 index 000000000000..964e9a87f1dc --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_auto_pin_service_unittest.cc @@ -0,0 +1,514 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/brave_wallet_auto_pin_service.h" + +#include +#include +#include +#include + +#include "base/test/bind.h" +#include "base/time/time_override.h" +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" +#include "brave/components/brave_wallet/browser/brave_wallet_prefs.h" +#include "brave/components/brave_wallet/browser/json_rpc_service.h" +#include "brave/components/brave_wallet/browser/pref_names.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "brave/components/ipfs/pin/ipfs_local_pin_service.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/prefs/testing_pref_service.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; + +namespace brave_wallet { + +class MockBraveWalletPinService : public BraveWalletPinService { + public: + MockBraveWalletPinService() + : BraveWalletPinService(nullptr, nullptr, nullptr) {} + MOCK_METHOD3(AddPin, + void(mojom::BlockchainTokenPtr, + const absl::optional&, + BraveWalletPinService::AddPinCallback callback)); + MOCK_METHOD3(RemovePin, + void(mojom::BlockchainTokenPtr, + const absl::optional&, + BraveWalletPinService::RemovePinCallback callback)); + MOCK_METHOD3(Validate, + void(mojom::BlockchainTokenPtr, + const absl::optional&, + BraveWalletPinService::ValidateCallback)); + MOCK_METHOD1(GetTokens, + std::set(const absl::optional&)); + MOCK_METHOD2(GetTokenStatus, + mojom::TokenPinStatusPtr(absl::optional, + const mojom::BlockchainTokenPtr&)); + MOCK_METHOD2(GetLastValidateTime, + absl::optional(absl::optional, + const mojom::BlockchainTokenPtr&)); + MOCK_METHOD2(MarkAsPendingForPinning, + void(const mojom::BlockchainTokenPtr&, + const absl::optional&)); + MOCK_METHOD2(MarkAsPendingForUnpinning, + void(const mojom::BlockchainTokenPtr&, + const absl::optional&)); +}; + +class MockBraveWalletService : public BraveWalletService { + public: + MOCK_METHOD3(GetUserAssets, + void(const std::string&, + mojom::CoinType, + BraveWalletService::GetUserAssetsCallback)); +}; + +MATCHER_P(TokenPathMatches, path, "") { + return arg == BraveWalletPinService::TokenFromPath(path); +} + +class BraveWalletAutoPinServiceTest : public testing::Test { + public: + BraveWalletAutoPinServiceTest() = default; + + BraveWalletAutoPinService* service() { + return brave_wallet_auto_pin_service_.get(); + } + + protected: + void SetUp() override { + auto* registry = pref_service_.registry(); + registry->RegisterBooleanPref(kAutoPinEnabled, false); + brave_wallet_auto_pin_service_ = + std::make_unique( + GetPrefs(), GetBraveWalletService(), GetBraveWalletPinService()); + } + + PrefService* GetPrefs() { return &pref_service_; } + + testing::NiceMock* GetBraveWalletPinService() { + return &brave_wallet_pin_service_; + } + + testing::NiceMock* GetBraveWalletService() { + return &brave_wallet_service_; + } + + void SetAutoPinEnabled(bool value) {} + + testing::NiceMock brave_wallet_pin_service_; + testing::NiceMock brave_wallet_service_; + + std::unique_ptr brave_wallet_auto_pin_service_; + + TestingPrefServiceSimple pref_service_; + content::BrowserTaskEnvironment task_environment_; +}; + +TEST_F(BraveWalletAutoPinServiceTest, Autopin_WhenTokenAdded) { + service()->SetAutoPinEnabled(true); + + ON_CALL(*GetBraveWalletPinService(), AddPin(_, _, _)) + .WillByDefault( + ::testing::Invoke([](BlockchainTokenPtr token, + const absl::optional& service, + BraveWalletPinService::AddPinCallback callback) { + std::move(callback).Run(true, nullptr); + })); + EXPECT_CALL(*GetBraveWalletPinService(), AddPin(_, _, _)).Times(3); + + { + mojom::BlockchainTokenPtr token = BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + service()->OnTokenAdded(std::move(token)); + } + + { + mojom::BlockchainTokenPtr token = BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"); + service()->OnTokenAdded(std::move(token)); + } + + { + mojom::BlockchainTokenPtr token = BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"); + service()->OnTokenAdded(std::move(token)); + } +} + +TEST_F(BraveWalletAutoPinServiceTest, TokenRemoved) { + ON_CALL(*GetBraveWalletPinService(), AddPin(_, _, _)) + .WillByDefault( + ::testing::Invoke([](BlockchainTokenPtr token, + const absl::optional& service, + BraveWalletPinService::AddPinCallback callback) { + std::move(callback).Run(true, nullptr); + })); +} + +TEST_F(BraveWalletAutoPinServiceTest, UnpinUnknownTokens_WhenRestore) { + std::set known_tokens; + + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"); + + ON_CALL(*GetBraveWalletPinService(), + GetTokenStatus(testing::Eq(absl::nullopt), _)) + .WillByDefault( + ::testing::Invoke([](absl::optional service, + const mojom::BlockchainTokenPtr& token) + -> mojom::TokenPinStatusPtr { + mojom::TokenPinStatusPtr status = mojom::TokenPinStatus::New(); + status->code = mojom::TokenPinStatusCode::STATUS_PINNED; + status->validate_time = base::Time::Now(); + return status; + })); + ON_CALL(*GetBraveWalletPinService(), GetTokens(_)) + .WillByDefault(::testing::Return(known_tokens)); + ON_CALL(*GetBraveWalletService(), GetUserAssets(_, _, _)) + .WillByDefault(::testing::Invoke([](const std::string& chain_id, + mojom::CoinType coin, + BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2")); + std::move(callback).Run(std::move(result)); + })); + + EXPECT_CALL(*GetBraveWalletPinService(), + RemovePin(TokenPathMatches( + "nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"), + testing::Eq(absl::nullopt), _)) + .Times(1); + + BraveWalletAutoPinService auto_pin_service( + GetPrefs(), GetBraveWalletService(), GetBraveWalletPinService()); +} + +TEST_F(BraveWalletAutoPinServiceTest, ValidateOldTokens_WhenRestore) { + std::set known_tokens; + + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x4"); + ON_CALL(*GetBraveWalletPinService(), + GetTokenStatus(testing::Eq(absl::nullopt), _)) + .WillByDefault( + ::testing::Invoke([](absl::optional service, + const mojom::BlockchainTokenPtr& token) + -> mojom::TokenPinStatusPtr { + mojom::TokenPinStatusPtr status = mojom::TokenPinStatus::New(); + + if ("0x1" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_PINNED; + status->validate_time = base::Time::Now() - base::Days(20); + } else if ("0x2" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_PINNED; + } else if ("0x3" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_PINNED; + status->validate_time = base::Time::Now() + base::Days(20); + } else if ("0x4" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_NOT_PINNED; + } + return status; + })); + ON_CALL(*GetBraveWalletPinService(), GetTokens(_)) + .WillByDefault(::testing::Return(known_tokens)); + ON_CALL(*GetBraveWalletService(), GetUserAssets(_, _, _)) + .WillByDefault(::testing::Invoke([](const std::string& chain_id, + mojom::CoinType coin, + BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x4")); + std::move(callback).Run(std::move(result)); + })); + + ON_CALL(*GetBraveWalletPinService(), Validate(_, _, _)) + .WillByDefault(::testing::Invoke( + [](BlockchainTokenPtr token, + const absl::optional& service, + BraveWalletPinService::ValidateCallback callback) { + std::move(callback).Run(true, nullptr); + })); + + EXPECT_CALL(*GetBraveWalletPinService(), + Validate(TokenPathMatches( + "nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"), + testing::Eq(absl::nullopt), _)) + .Times(1); + EXPECT_CALL(*GetBraveWalletPinService(), + Validate(TokenPathMatches( + "nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"), + testing::Eq(absl::nullopt), _)) + .Times(1); + EXPECT_CALL(*GetBraveWalletPinService(), + Validate(TokenPathMatches( + "nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"), + testing::Eq(absl::nullopt), _)) + .Times(1); + EXPECT_CALL(*GetBraveWalletPinService(), + Validate(TokenPathMatches( + "nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x4"), + testing::Eq(absl::nullopt), _)) + .Times(0); + + BraveWalletAutoPinService auto_pin_service( + GetPrefs(), GetBraveWalletService(), GetBraveWalletPinService()); +} + +TEST_F(BraveWalletAutoPinServiceTest, PinContinue_WhenRestore) { + std::set known_tokens; + + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"); + + ON_CALL(*GetBraveWalletPinService(), + GetTokenStatus(testing::Eq(absl::nullopt), _)) + .WillByDefault( + ::testing::Invoke([](absl::optional service, + const mojom::BlockchainTokenPtr& token) + -> mojom::TokenPinStatusPtr { + mojom::TokenPinStatusPtr status = mojom::TokenPinStatus::New(); + if ("0x1" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_PINNING_FAILED; + } else if ("0x2" == token->token_id) { + status->code = + mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS; + } else if ("0x3" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_PINNING_PENDING; + } + return status; + })); + ON_CALL(*GetBraveWalletPinService(), GetTokens(_)) + .WillByDefault(::testing::Return(known_tokens)); + ON_CALL(*GetBraveWalletService(), GetUserAssets(_, _, _)) + .WillByDefault(::testing::Invoke([](const std::string& chain_id, + mojom::CoinType coin, + BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3")); + std::move(callback).Run(std::move(result)); + })); + + ON_CALL(*GetBraveWalletPinService(), AddPin(_, _, _)) + .WillByDefault( + ::testing::Invoke([](BlockchainTokenPtr token, + const absl::optional& service, + BraveWalletPinService::AddPinCallback callback) { + std::move(callback).Run(true, nullptr); + })); + + EXPECT_CALL( + *GetBraveWalletPinService(), + AddPin(TokenPathMatches("nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"), + testing::Eq(absl::nullopt), _)) + .Times(1); + EXPECT_CALL( + *GetBraveWalletPinService(), + AddPin(TokenPathMatches("nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"), + testing::Eq(absl::nullopt), _)) + .Times(1); + EXPECT_CALL( + *GetBraveWalletPinService(), + AddPin(TokenPathMatches("nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"), + testing::Eq(absl::nullopt), _)) + .Times(1); + + BraveWalletAutoPinService auto_pin_service( + GetPrefs(), GetBraveWalletService(), GetBraveWalletPinService()); +} + +TEST_F(BraveWalletAutoPinServiceTest, UnpinContinue_WhenRestore) { + std::set known_tokens; + + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"); + + ON_CALL(*GetBraveWalletPinService(), + GetTokenStatus(testing::Eq(absl::nullopt), _)) + .WillByDefault( + ::testing::Invoke([](absl::optional service, + const mojom::BlockchainTokenPtr& token) + -> mojom::TokenPinStatusPtr { + mojom::TokenPinStatusPtr status = mojom::TokenPinStatus::New(); + if ("0x1" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED; + } else if ("0x2" == token->token_id) { + status->code = + mojom::TokenPinStatusCode::STATUS_UNPINNING_IN_PROGRESS; + } else if ("0x3" == token->token_id) { + status->code = + mojom::TokenPinStatusCode::STATUS_UNPINNING_PENDING; + } + return status; + })); + ON_CALL(*GetBraveWalletPinService(), GetTokens(_)) + .WillByDefault(::testing::Return(known_tokens)); + ON_CALL(*GetBraveWalletService(), GetUserAssets(_, _, _)) + .WillByDefault(::testing::Invoke([](const std::string& chain_id, + mojom::CoinType coin, + BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2")); + std::move(callback).Run(std::move(result)); + })); + + ON_CALL(*GetBraveWalletPinService(), RemovePin(_, _, _)) + .WillByDefault(::testing::Invoke( + [](BlockchainTokenPtr token, + const absl::optional& service, + BraveWalletPinService::RemovePinCallback callback) { + std::move(callback).Run(true, nullptr); + })); + + EXPECT_CALL(*GetBraveWalletPinService(), + RemovePin(TokenPathMatches( + "nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"), + testing::Eq(absl::nullopt), _)) + .Times(1); + EXPECT_CALL(*GetBraveWalletPinService(), + RemovePin(TokenPathMatches( + "nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"), + testing::Eq(absl::nullopt), _)) + .Times(1); + + BraveWalletAutoPinService auto_pin_service( + GetPrefs(), GetBraveWalletService(), GetBraveWalletPinService()); +} + +TEST_F(BraveWalletAutoPinServiceTest, DoNotAutoPin_WhenAutoPinDisabled) { + service()->SetAutoPinEnabled(false); + + ON_CALL(*GetBraveWalletPinService(), AddPin(_, _, _)) + .WillByDefault( + ::testing::Invoke([](BlockchainTokenPtr token, + const absl::optional& service, + BraveWalletPinService::AddPinCallback callback) { + std::move(callback).Run(true, nullptr); + })); + EXPECT_CALL(*GetBraveWalletPinService(), AddPin(_, _, _)).Times(0); + + { + mojom::BlockchainTokenPtr token = BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + service()->OnTokenAdded(std::move(token)); + } +} + +TEST_F(BraveWalletAutoPinServiceTest, PinOldTokens_WhenAutoPinEnabled) { + std::set known_tokens; + + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + known_tokens.insert( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"); + + ON_CALL(*GetBraveWalletPinService(), + GetTokenStatus(testing::Eq(absl::nullopt), _)) + .WillByDefault( + ::testing::Invoke([](absl::optional service, + const mojom::BlockchainTokenPtr& token) + -> mojom::TokenPinStatusPtr { + mojom::TokenPinStatusPtr status = mojom::TokenPinStatus::New(); + if ("0x1" == token->token_id) { + return nullptr; + } else if ("0x2" == token->token_id) { + status->code = mojom::TokenPinStatusCode::STATUS_NOT_PINNED; + } + return status; + })); + ON_CALL(*GetBraveWalletPinService(), GetTokens(_)) + .WillByDefault(::testing::Return(known_tokens)); + ON_CALL(*GetBraveWalletService(), GetUserAssets(_, _, _)) + .WillByDefault(::testing::Invoke([](const std::string& chain_id, + mojom::CoinType coin, + BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(BraveWalletPinService::TokenFromPath( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2")); + std::move(callback).Run(std::move(result)); + })); + + ON_CALL(*GetBraveWalletPinService(), AddPin(_, _, _)) + .WillByDefault( + ::testing::Invoke([](BlockchainTokenPtr token, + const absl::optional& service, + BraveWalletPinService::AddPinCallback callback) { + std::move(callback).Run(true, nullptr); + })); + + EXPECT_CALL( + *GetBraveWalletPinService(), + AddPin(TokenPathMatches("nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"), + testing::Eq(absl::nullopt), _)) + .Times(1); + EXPECT_CALL( + *GetBraveWalletPinService(), + AddPin(TokenPathMatches("nft.local.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"), + testing::Eq(absl::nullopt), _)) + .Times(1); + + BraveWalletAutoPinService auto_pin_service( + GetPrefs(), GetBraveWalletService(), GetBraveWalletPinService()); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/brave_wallet_p3a.cc b/components/brave_wallet/browser/brave_wallet_p3a.cc index f4ea187f858f..8751b0d847d8 100644 --- a/components/brave_wallet/browser/brave_wallet_p3a.cc +++ b/components/brave_wallet/browser/brave_wallet_p3a.cc @@ -83,6 +83,8 @@ BraveWalletP3A::BraveWalletP3A(BraveWalletService* wallet_service, AddObservers(); } +BraveWalletP3A::BraveWalletP3A() {} + BraveWalletP3A::~BraveWalletP3A() = default; void BraveWalletP3A::AddObservers() { diff --git a/components/brave_wallet/browser/brave_wallet_p3a.h b/components/brave_wallet/browser/brave_wallet_p3a.h index 0c62a338345a..38d16411f74c 100644 --- a/components/brave_wallet/browser/brave_wallet_p3a.h +++ b/components/brave_wallet/browser/brave_wallet_p3a.h @@ -41,6 +41,8 @@ class BraveWalletP3A : public mojom::BraveWalletServiceObserver, BraveWalletP3A(BraveWalletService* wallet_service, KeyringService* keyring_service, PrefService* pref_service); + // For testing + BraveWalletP3A(); ~BraveWalletP3A() override; BraveWalletP3A(const BraveWalletP3A&) = delete; diff --git a/components/brave_wallet/browser/brave_wallet_pin_service.cc b/components/brave_wallet/browser/brave_wallet_pin_service.cc new file mode 100644 index 000000000000..169f1bf99041 --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_pin_service.cc @@ -0,0 +1,671 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" + +#include + +#include "base/strings/strcat.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/task/bind_post_task.h" +#include "base/task/thread_pool.h" +#include "base/values.h" +#include "brave/components/brave_wallet/browser/brave_wallet_utils.h" +#include "brave/components/brave_wallet/browser/json_rpc_response_parser.h" +#include "brave/components/brave_wallet/browser/pref_names.h" +#include "brave/components/ipfs/ipfs_constants.h" +#include "brave/components/ipfs/ipfs_utils.h" +#include "components/prefs/scoped_user_pref_update.h" + +using brave_wallet::mojom::BlockchainToken; +using brave_wallet::mojom::BlockchainTokenPtr; + +namespace brave_wallet { + +const char kAssetStatus[] = "status"; +const char kValidateTimestamp[] = "validate_timestamp"; +const char kError[] = "error"; +const char kErrorCode[] = "error_code"; +const char kErrorMessage[] = "error_message"; +const char kAssetUrlListKey[] = "cids"; + +namespace { + +const char kNftPart[] = "nft"; +const char kLocalService[] = "local"; + +absl::optional StringToStatus( + const std::string& status) { + if (status == "not_pinned") { + return mojom::TokenPinStatusCode::STATUS_NOT_PINNED; + } else if (status == "pining_failed") { + return mojom::TokenPinStatusCode::STATUS_PINNING_FAILED; + } else if (status == "pinned") { + return mojom::TokenPinStatusCode::STATUS_PINNED; + } else if (status == "pinning_in_progress") { + return mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS; + } else if (status == "unpining_in_progress") { + return mojom::TokenPinStatusCode::STATUS_UNPINNING_IN_PROGRESS; + } else if (status == "unpining_failed") { + return mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED; + } else if (status == "pinning_pendig") { + return mojom::TokenPinStatusCode::STATUS_PINNING_PENDING; + } else if (status == "unpinning_pendig") { + return mojom::TokenPinStatusCode::STATUS_UNPINNING_PENDING; + } + return absl::nullopt; +} + +absl::optional StringToErrorCode( + const std::string& error) { + if (error == "ERR_WRONG_TOKEN") { + return mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN; + } else if (error == "ERR_NON_IPFS_TOKEN_URL") { + return mojom::WalletPinServiceErrorCode::ERR_NON_IPFS_TOKEN_URL; + } else if (error == "ERR_FETCH_METADATA_FAILED") { + return mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED; + } else if (error == "ERR_WRONG_METADATA_FORMAT") { + return mojom::WalletPinServiceErrorCode::ERR_WRONG_METADATA_FORMAT; + } else if (error == "ERR_ALREADY_PINNED") { + return mojom::WalletPinServiceErrorCode::ERR_ALREADY_PINNED; + } else if (error == "ERR_NOT_PINNED") { + return mojom::WalletPinServiceErrorCode::ERR_NOT_PINNED; + } else if (error == "ERR_PINNING_FAILED") { + return mojom::WalletPinServiceErrorCode::ERR_PINNING_FAILED; + } + return absl::nullopt; +} + +absl::optional ExtractCID(const std::string& ipfs_url) { + GURL gurl = GURL(ipfs_url); + + if (!gurl.SchemeIs(ipfs::kIPFSScheme)) { + return absl::nullopt; + } + + std::vector result = base::SplitString( + gurl.path(), "/", base::WhitespaceHandling::KEEP_WHITESPACE, + base::SplitResult::SPLIT_WANT_NONEMPTY); + + if (result.size() == 0) { + return absl::nullopt; + } + + if (!ipfs::IsValidCID(result.at(0))) { + return absl::nullopt; + } + + return result.at(0); +} + +} // namespace + +std::string StatusToString(const mojom::TokenPinStatusCode& status) { + switch (status) { + case mojom::TokenPinStatusCode::STATUS_NOT_PINNED: + return "not_pinned"; + case mojom::TokenPinStatusCode::STATUS_PINNED: + return "pinned"; + case mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS: + return "pinning_in_progress"; + case mojom::TokenPinStatusCode::STATUS_UNPINNING_IN_PROGRESS: + return "unpining_in_progress"; + case mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED: + return "unpining_failed"; + case mojom::TokenPinStatusCode::STATUS_PINNING_FAILED: + return "pining_failed"; + case mojom::TokenPinStatusCode::STATUS_PINNING_PENDING: + return "pinning_pendig"; + case mojom::TokenPinStatusCode::STATUS_UNPINNING_PENDING: + return "unpinning_pendig"; + } + NOTREACHED(); + return ""; +} + +std::string ErrorCodeToString( + const mojom::WalletPinServiceErrorCode& error_code) { + switch (error_code) { + case mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN: + return "ERR_WRONG_TOKEN"; + case mojom::WalletPinServiceErrorCode::ERR_NON_IPFS_TOKEN_URL: + return "ERR_NON_IPFS_TOKEN_URL"; + case mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED: + return "ERR_FETCH_METADATA_FAILED"; + case mojom::WalletPinServiceErrorCode::ERR_WRONG_METADATA_FORMAT: + return "ERR_WRONG_METADATA_FORMAT"; + case mojom::WalletPinServiceErrorCode::ERR_ALREADY_PINNED: + return "ERR_ALREADY_PINNED"; + case mojom::WalletPinServiceErrorCode::ERR_NOT_PINNED: + return "ERR_NOT_PINNED"; + case mojom::WalletPinServiceErrorCode::ERR_PINNING_FAILED: + return "ERR_PINNING_FAILED"; + } + NOTREACHED(); + return ""; +} + +BraveWalletPinService::BraveWalletPinService( + PrefService* prefs, + JsonRpcService* service, + ipfs::IpfsLocalPinService* local_pin_service) + : prefs_(prefs), + json_rpc_service_(service), + local_pin_service_(local_pin_service) {} + +BraveWalletPinService::~BraveWalletPinService() {} + +mojo::PendingRemote +BraveWalletPinService::MakeRemote() { + mojo::PendingRemote remote; + receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver()); + return remote; +} + +void BraveWalletPinService::Bind( + mojo::PendingReceiver receiver) { + receivers_.Add(this, std::move(receiver)); +} + +void BraveWalletPinService::AddObserver( + ::mojo::PendingRemote observer) { + observers_.Add(std::move(observer)); +} + +// static +std::string BraveWalletPinService::GetPath( + const absl::optional& service, + const BlockchainTokenPtr& token) { + return base::StrCat({kNftPart, ".", service.value_or(kLocalService), ".", + base::NumberToString(static_cast(token->coin)), ".", + token->chain_id, ".", token->contract_address, ".", + token->token_id}); +} + +// static +BlockchainTokenPtr BraveWalletPinService::TokenFromPath( + const std::string& path) { + std::vector parts = + base::SplitString(path, ".", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + if (parts.size() != 6) { + return nullptr; + } + mojom::BlockchainTokenPtr token = mojom::BlockchainToken::New(); + int32_t coin; + if (!base::StringToInt(parts.at(2), &coin)) { + return nullptr; + } + token->coin = static_cast(coin); + token->chain_id = parts.at(3); + token->contract_address = parts.at(4); + token->token_id = parts.at(5); + token->is_erc721 = true; + token->is_nft = true; + return token; +} + +// static +absl::optional BraveWalletPinService::ServiceFromPath( + const std::string& path) { + std::vector parts = + base::SplitString(path, ".", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + if (parts.size() != 6) { + return nullptr; + } + if (parts.at(1) == kLocalService) { + return absl::nullopt; + } else { + return parts.at(1); + } +} + +void BraveWalletPinService::Validate(BlockchainTokenPtr token, + const absl::optional& service, + ValidateCallback callback) { + mojom::TokenPinStatusPtr status = GetTokenStatus(service, token); + if (!status) { + std::move(callback).Run(false, nullptr); + return; + } + if (status->code != mojom::TokenPinStatusCode::STATUS_PINNED) { + std::move(callback).Run(false, nullptr); + return; + } + + absl::optional> cids = + ResolvePinItems(service, token); + + if (!cids) { + SetTokenStatus(service, std::move(token), + mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS, + nullptr); + std::move(callback).Run(true, nullptr); + return; + } + + if (!service) { + local_pin_service_->ValidatePins( + GetPath(absl::nullopt, token), cids.value(), + base::BindOnce(&BraveWalletPinService::OnTokenValidated, + base::Unretained(this), service, std::move(callback), + std::move(token))); + } +} + +void BraveWalletPinService::MarkAsPendingForPinning( + const mojom::BlockchainTokenPtr& token, + const absl::optional& service) { + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_PINNING_PENDING, nullptr); +} + +void BraveWalletPinService::MarkAsPendingForUnpinning( + const mojom::BlockchainTokenPtr& token, + const absl::optional& service) { + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_UNPINNING_PENDING, nullptr); +} + +void BraveWalletPinService::AddPin(BlockchainTokenPtr token, + const absl::optional& service, + AddPinCallback callback) { + if (!token->is_erc721) { + auto pin_error = + mojom::PinError::New(mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN, + "Token is not erc721"); + + VLOG(1) << "Token is not erc721"; + FinishAddingWithResult(service, token, false, std::move(pin_error), + std::move(callback)); + return; + } + + auto token_status = GetTokenStatus(service, token); + if (token_status && + token_status->code == mojom::TokenPinStatusCode::STATUS_PINNED) { + auto pin_error = mojom::PinError::New( + mojom::WalletPinServiceErrorCode::ERR_ALREADY_PINNED, "Already pinned"); + + FinishAddingWithResult(service, token, true, std::move(pin_error), + std::move(callback)); + return; + } + + json_rpc_service_->GetERC721Metadata( + token->contract_address, token->token_id, token->chain_id, + base::BindOnce(&BraveWalletPinService::OnTokenMetaDataReceived, + base::Unretained(this), service, std::move(callback), + token.Clone())); +} + +void BraveWalletPinService::RemovePin( + BlockchainTokenPtr token, + const absl::optional& service, + RemovePinCallback callback) { + auto token_status = GetTokenStatus(service, token); + if (!token_status) { + std::move(callback).Run(true, nullptr); + return; + } + + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_UNPINNING_IN_PROGRESS, + nullptr); + + local_pin_service_->RemovePins( + GetPath(service, token), + base::BindOnce(&BraveWalletPinService::OnPinsRemoved, + base::Unretained(this), service, std::move(callback), + std::move(token))); +} + +void BraveWalletPinService::GetTokenStatus(BlockchainTokenPtr token, + GetTokenStatusCallback callback) { + mojom::TokenPinOverviewPtr result = mojom::TokenPinOverview::New(); + result->local = GetTokenStatus(absl::nullopt, token); + std::move(callback).Run(std::move(result), nullptr); +} + +void BraveWalletPinService::OnPinsRemoved(absl::optional service, + RemovePinCallback callback, + mojom::BlockchainTokenPtr token, + bool result) { + if (result) { + RemoveToken(service, token); + } else { + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED, nullptr); + } + + std::move(callback).Run(result, nullptr); +} + +void BraveWalletPinService::OnTokenMetaDataReceived( + absl::optional service, + AddPinCallback callback, + mojom::BlockchainTokenPtr token, + const std::string& token_url, + const std::string& result, + mojom::ProviderError error, + const std::string& error_message) { + if (error != mojom::ProviderError::kSuccess) { + auto pin_error = mojom::PinError::New( + mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED, + "Failed to obtain token metadata"); + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, pin_error); + FinishAddingWithResult(service, token, false, std::move(pin_error), + std::move(callback)); + return; + } + + GURL token_gurl = GURL(token_url); + if (!token_gurl.SchemeIs(ipfs::kIPFSScheme)) { + auto pin_error = mojom::PinError::New( + mojom::WalletPinServiceErrorCode::ERR_NON_IPFS_TOKEN_URL, + "Metadata has non-ipfs url"); + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, pin_error); + FinishAddingWithResult(service, token, false, std::move(pin_error), + std::move(callback)); + return; + } + + absl::optional parsed_result = base::JSONReader::Read( + result, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + if (!parsed_result || !parsed_result->is_dict()) { + auto pin_error = mojom::PinError::New( + mojom::WalletPinServiceErrorCode::ERR_WRONG_METADATA_FORMAT, + "Wrong metadata format"); + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, pin_error); + FinishAddingWithResult(service, token, false, std::move(pin_error), + std::move(callback)); + return; + } + + std::vector cids; + + cids.push_back(ExtractCID(token_url).value()); + auto* image = parsed_result->FindStringKey("image"); + if (image) { + cids.push_back(ExtractCID(*image).value()); + } + + CreateToken(service, token, cids); + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS, + nullptr); + + if (!service) { + local_pin_service_->AddPins( + GetPath(service, token), cids, + base::BindOnce(&BraveWalletPinService::OnTokenPinned, + base::Unretained(this), absl::nullopt, + std::move(callback), std::move(token))); + } +} + +void BraveWalletPinService::OnTokenPinned(absl::optional service, + AddPinCallback callback, + mojom::BlockchainTokenPtr token, + bool result) { + auto error = + !result ? mojom::PinError::New( + mojom::WalletPinServiceErrorCode::ERR_WRONG_METADATA_FORMAT, + "Wrong metadata format") + : nullptr; + SetTokenStatus(service, token, + result ? mojom::TokenPinStatusCode::STATUS_PINNED + : mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, + error); + + FinishAddingWithResult(service, token, result, std::move(error), + std::move(callback)); +} + +void BraveWalletPinService::OnTokenValidated( + absl::optional service, + ValidateCallback callback, + mojom::BlockchainTokenPtr token, + absl::optional result) { + if (!result.has_value()) { + std::move(callback).Run(false, nullptr); + return; + } + + if (!result.value()) { + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS, + nullptr); + } else { + // Also updates verification timestamp + SetTokenStatus(service, token, mojom::TokenPinStatusCode::STATUS_PINNED, + nullptr); + } + + std::move(callback).Run(true, nullptr); +} + +void BraveWalletPinService::CreateToken(absl::optional service, + const mojom::BlockchainTokenPtr& token, + const std::vector& cids) { + DictionaryPrefUpdate update(prefs_, kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict token_data; + base::Value::List cids_list; + + for (const auto& cid : cids) { + cids_list.Append(cid); + } + + token_data.Set(kAssetUrlListKey, std::move(cids_list)); + token_data.Set(kAssetStatus, + StatusToString(mojom::TokenPinStatusCode::STATUS_NOT_PINNED)); + + update_dict.SetByDottedPath(GetPath(service, token), std::move(token_data)); +} + +void BraveWalletPinService::RemoveToken( + absl::optional service, + const mojom::BlockchainTokenPtr& token) { + DictionaryPrefUpdate update(prefs_, kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + update_dict.RemoveByDottedPath(GetPath(service, token)); +} + +void BraveWalletPinService::SetTokenStatus( + absl::optional service, + const mojom::BlockchainTokenPtr& token, + mojom::TokenPinStatusCode status, + const mojom::PinErrorPtr& error) { + { + DictionaryPrefUpdate update(prefs_, kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + update_dict.SetByDottedPath(GetPath(service, token) + "." + kAssetStatus, + StatusToString(status)); + if (error) { + base::Value::Dict error_dict; + error_dict.Set(kErrorCode, ErrorCodeToString(error->error_code)); + error_dict.Set(kErrorMessage, error->message); + update_dict.SetByDottedPath(GetPath(service, token) + "." + kError, + std::move(error_dict)); + } else { + update_dict.RemoveByDottedPath(GetPath(service, token) + "." + kError); + } + + if (status == mojom::TokenPinStatusCode::STATUS_PINNED) { + update_dict.SetByDottedPath( + GetPath(service, token) + "." + kValidateTimestamp, + base::NumberToString(base::Time::Now().ToInternalValue())); + } else { + update_dict.RemoveByDottedPath(GetPath(service, token) + "." + + kValidateTimestamp); + } + } + for (const auto& observer : observers_) { + observer->OnTokenStatusChanged(service, token.Clone(), + GetTokenStatus(service, token)); + } +} + +absl::optional> BraveWalletPinService::ResolvePinItems( + const absl::optional& service, + const BlockchainTokenPtr& token) { + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedErc721Assets); + + const std::string& path = GetPath(service, token); + + auto* token_data_as_dict = pinned_assets_pref.FindDictByDottedPath(path); + if (!token_data_as_dict) { + return absl::nullopt; + } + + auto* cids = token_data_as_dict->FindList(kAssetUrlListKey); + if (!cids) { + return absl::nullopt; + } + + std::vector result; + for (const base::Value& item : *cids) { + result.push_back(item.GetString()); + } + + return result; +} + +mojom::TokenPinStatusPtr BraveWalletPinService::GetTokenStatus( + absl::optional service, + const mojom::BlockchainTokenPtr& token) { + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedErc721Assets); + + const std::string& path = GetPath(service, token); + + auto* token_data_as_dict = pinned_assets_pref.FindDictByDottedPath(path); + if (!token_data_as_dict) { + return nullptr; + } + + auto* status = token_data_as_dict->FindString(kAssetStatus); + if (!status) { + return mojom::TokenPinStatus::New( + mojom::TokenPinStatusCode::STATUS_NOT_PINNED, nullptr, absl::nullopt); + } + + auto pin_status = StringToStatus(*status).value_or( + mojom::TokenPinStatusCode::STATUS_NOT_PINNED); + absl::optional validate_timestamp; + mojom::PinErrorPtr error; + + { + auto* validate_timestamp_str = + token_data_as_dict->FindString(kValidateTimestamp); + uint64_t validate_timestamp_internal_value; + if (validate_timestamp_str && + base::StringToUint64(*validate_timestamp_str, + &validate_timestamp_internal_value)) { + validate_timestamp = + base::Time::FromInternalValue(validate_timestamp_internal_value); + } + + auto* error_dict = token_data_as_dict->FindDict(kError); + if (error_dict) { + auto* error_message = error_dict->FindString(kErrorMessage); + auto* error_code = error_dict->FindString(kErrorCode); + if (error_code && error_message) { + error = mojom::PinError::New( + StringToErrorCode(*error_code) + .value_or(mojom::WalletPinServiceErrorCode::ERR_PINNING_FAILED), + *error_message); + } + } + } + + return mojom::TokenPinStatus::New(pin_status, std::move(error), + validate_timestamp); +} + +absl::optional BraveWalletPinService::GetLastValidateTime( + absl::optional service, + const mojom::BlockchainTokenPtr& token) { + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedErc721Assets); + + const std::string& path = GetPath(service, token); + + auto* token_data_as_dict = pinned_assets_pref.FindDictByDottedPath(path); + if (!token_data_as_dict) { + return absl::nullopt; + } + + auto* time = token_data_as_dict->FindString(kValidateTimestamp); + int64_t value; + if (time && base::StringToInt64(*time, &value)) { + return base::Time::FromInternalValue(value); + } + return absl::nullopt; +} + +void BraveWalletPinService::FinishAddingWithResult( + absl::optional service, + const mojom::BlockchainTokenPtr& token, + bool result, + mojom::PinErrorPtr error, + AddPinCallback callback) { + std::move(callback).Run(result, std::move(error)); +} + +std::set BraveWalletPinService::GetTokens( + const absl::optional& service) { + std::set result; + + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedErc721Assets); + + const base::Value::Dict* service_dict = + pinned_assets_pref.FindDictByDottedPath( + base::StrCat({kNftPart, ".", service.value_or(kLocalService)})); + if (!service_dict) { + return result; + } + + for (auto coin_it : *service_dict) { + std::string current_coin = coin_it.first; + auto* network_dict = coin_it.second.GetIfDict(); + if (!network_dict) { + continue; + } + for (auto network_it : *network_dict) { + std::string current_network = network_it.first; + const base::Value::Dict* contract_dict = network_it.second.GetIfDict(); + if (!contract_dict) { + continue; + } + for (auto contract_it : *contract_dict) { + std::string current_contract = contract_it.first; + const base::Value::Dict* id_dict = contract_it.second.GetIfDict(); + if (!id_dict) { + continue; + } + for (auto token_id : *id_dict) { + result.insert( + base::StrCat({kNftPart, ".", service.value_or(kLocalService), ".", + current_coin, ".", current_network, ".", + current_contract, ".", token_id.first})); + } + } + } + } + + return result; +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/brave_wallet_pin_service.h b/components/brave_wallet/browser/brave_wallet_pin_service.h new file mode 100644 index 000000000000..9c90f47d3f46 --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_pin_service.h @@ -0,0 +1,136 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_PIN_SERVICE_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_PIN_SERVICE_H_ + +#include +#include +#include + +#include "base/memory/scoped_refptr.h" +#include "base/task/sequenced_task_runner.h" +#include "brave/components/brave_wallet/browser/brave_wallet_service.h" +#include "brave/components/brave_wallet/browser/json_rpc_service.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "brave/components/ipfs/ipfs_service.h" +#include "brave/components/ipfs/pin/ipfs_local_pin_service.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver_set.h" +#include "mojo/public/cpp/bindings/remote_set.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +using brave_wallet::mojom::BlockchainTokenPtr; + +namespace brave_wallet { + +std::string StatusToString(const mojom::TokenPinStatusCode& status); +std::string ErrorCodeToString( + const mojom::WalletPinServiceErrorCode& error_code); + +class BraveWalletPinService : public KeyedService, + public brave_wallet::mojom::WalletPinService { + public: + BraveWalletPinService(PrefService* prefs, + JsonRpcService* service, + ipfs::IpfsLocalPinService* local_pin_service); + ~BraveWalletPinService() override; + + mojo::PendingRemote MakeRemote(); + void Bind(mojo::PendingReceiver receiver); + + static std::string GetPath(const absl::optional& service, + const BlockchainTokenPtr& token); + static BlockchainTokenPtr TokenFromPath(const std::string& path); + static absl::optional ServiceFromPath(const std::string& path); + + // WalletPinService + void AddObserver(::mojo::PendingRemote + observer) override; + void AddPin(BlockchainTokenPtr token, + const absl::optional& service, + AddPinCallback callback) override; + void RemovePin(BlockchainTokenPtr token, + const absl::optional& service, + RemovePinCallback callback) override; + void GetTokenStatus(BlockchainTokenPtr token, + GetTokenStatusCallback callback) override; + void Validate(BlockchainTokenPtr token, + const absl::optional& service, + ValidateCallback callback) override; + + virtual void MarkAsPendingForPinning( + const mojom::BlockchainTokenPtr& token, + const absl::optional& service); + virtual void MarkAsPendingForUnpinning( + const mojom::BlockchainTokenPtr& token, + const absl::optional& service); + + virtual mojom::TokenPinStatusPtr GetTokenStatus( + absl::optional service, + const mojom::BlockchainTokenPtr& token); + virtual absl::optional GetLastValidateTime( + absl::optional service, + const mojom::BlockchainTokenPtr& token); + virtual std::set GetTokens( + const absl::optional& service); + + private: + void CreateToken(absl::optional service, + const mojom::BlockchainTokenPtr& token, + const std::vector& cids); + void RemoveToken(absl::optional service, + const mojom::BlockchainTokenPtr& token); + void SetTokenStatus(absl::optional service, + const mojom::BlockchainTokenPtr& token, + mojom::TokenPinStatusCode, + const mojom::PinErrorPtr& error); + + void FinishAddingWithResult(absl::optional service, + const mojom::BlockchainTokenPtr& token, + bool result, + mojom::PinErrorPtr error, + AddPinCallback callback); + + absl::optional> ResolvePinItems( + const absl::optional& service, + const BlockchainTokenPtr& token); + + void OnPinsRemoved(absl::optional service, + RemovePinCallback callback, + mojom::BlockchainTokenPtr token, + bool result); + void OnTokenPinned(absl::optional service, + AddPinCallback callback, + mojom::BlockchainTokenPtr, + bool result); + void OnTokenValidated(absl::optional service, + ValidateCallback callback, + mojom::BlockchainTokenPtr, + absl::optional result); + + void OnTokenMetaDataReceived(absl::optional service, + AddPinCallback callback, + mojom::BlockchainTokenPtr token, + const std::string& token_url, + const std::string& result, + mojom::ProviderError error, + const std::string& error_message); + + mojo::ReceiverSet receivers_; + mojo::RemoteSet observers_; + + // Prefs service is used to store list of pinned items + PrefService* prefs_; + + // JsonRpcService is used to fetch token metadata + JsonRpcService* json_rpc_service_; + ipfs::IpfsLocalPinService* local_pin_service_; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_PIN_SERVICE_H_ diff --git a/components/brave_wallet/browser/brave_wallet_pin_service_unittest.cc b/components/brave_wallet/browser/brave_wallet_pin_service_unittest.cc new file mode 100644 index 000000000000..ccf5d9fc447b --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_pin_service_unittest.cc @@ -0,0 +1,586 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" + +#include +#include +#include +#include + +#include "base/json/json_reader.h" +#include "base/test/bind.h" +#include "base/time/time_override.h" +#include "brave/components/brave_wallet/browser/brave_wallet_prefs.h" +#include "brave/components/brave_wallet/browser/json_rpc_service.h" +#include "brave/components/brave_wallet/browser/pref_names.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "brave/components/ipfs/pin/ipfs_local_pin_service.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/prefs/testing_pref_service.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::subtle::ScopedTimeClockOverrides; +using testing::_; + +namespace brave_wallet { +namespace { +const char kMonkey1Path[] = + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"; +const char kMonkey2Path[] = + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"; +const char kMonkey3Path[] = + "nft.nftstorage.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"; + +const char kMonkey1Url[] = + "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/2413"; +const char kMonkey1[] = + R"({"image":"ipfs://Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg", + "attributes":[{"trait_type":"Mouth","value":"Bored Cigarette"}, + {"trait_type":"Fur","value":"Zombie"},{"trait_type":"Background","value":"Purple"}, + {"trait_type":"Eyes","value":"Closed"},{"trait_type":"Clothes","value":"Toga"}, + {"trait_type":"Hat","value":"Cowboy Hat"}]})"; + +base::Time g_overridden_now; +std::unique_ptr OverrideWithTimeNow( + const base::Time& overridden_now) { + g_overridden_now = overridden_now; + return std::make_unique( + []() { return g_overridden_now; }, nullptr, nullptr); +} + +} // namespace + +class MockIpfsLocalPinService : public ipfs::IpfsLocalPinService { + public: + MockIpfsLocalPinService() {} + + ~MockIpfsLocalPinService() override {} + + MOCK_METHOD3(AddPins, + void(const std::string& prefix, + const std::vector& cids, + ipfs::AddPinCallback callback)); + MOCK_METHOD2(RemovePins, + void(const std::string& prefix, + ipfs::RemovePinCallback callback)); + MOCK_METHOD3(ValidatePins, + void(const std::string& prefix, + const std::vector& cids, + ipfs::ValidatePinsCallback callback)); +}; + +class MockJsonRpcService : public JsonRpcService { + public: + MockJsonRpcService() {} + + MOCK_METHOD4(GetERC721Metadata, + void(const std::string& contract_address, + const std::string& token_id, + const std::string& chain_id, + GetTokenMetadataCallback callback)); + + ~MockJsonRpcService() override {} +}; + +class BraveWalletPinServiceTest : public testing::Test { + public: + BraveWalletPinServiceTest() = default; + + BraveWalletPinService* service() { return brave_wallet_pin_service_.get(); } + + protected: + void SetUp() override { + auto* registry = pref_service_.registry(); + registry->RegisterDictionaryPref(kPinnedErc721Assets); + brave_wallet_pin_service_ = std::make_unique( + GetPrefs(), GetJsonRpcService(), GetIpfsLocalPinService()); + } + + PrefService* GetPrefs() { return &pref_service_; } + + testing::NiceMock* GetJsonRpcService() { + return &json_rpc_service_; + } + + testing::NiceMock* GetIpfsLocalPinService() { + return &ipfs_local_pin_service_; + } + + testing::NiceMock ipfs_local_pin_service_; + testing::NiceMock json_rpc_service_; + + std::unique_ptr brave_wallet_pin_service_; + TestingPrefServiceSimple pref_service_; + content::BrowserTaskEnvironment task_environment_; +}; + +TEST_F(BraveWalletPinServiceTest, AddPin) { + { + ON_CALL(*GetJsonRpcService(), GetERC721Metadata(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& contract_address, const std::string& token_id, + const std::string& chain_id, + MockJsonRpcService::GetTokenMetadataCallback callback) { + EXPECT_EQ("0x1", chain_id); + EXPECT_EQ("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", + contract_address); + EXPECT_EQ("0x1", token_id); + std::move(callback).Run(kMonkey1Url, kMonkey1, + mojom::ProviderError::kSuccess, ""); + })); + ON_CALL(*GetIpfsLocalPinService(), AddPins(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& prefix, const std::vector& cids, + ipfs::AddPinCallback callback) { + EXPECT_EQ(kMonkey1Path, prefix); + EXPECT_EQ("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq", + cids.at(0)); + EXPECT_EQ("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg", + cids.at(1)); + std::move(callback).Run(true); + })); + + auto scoped_override = + OverrideWithTimeNow(base::Time::FromInternalValue(123u)); + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + absl::optional call_status; + service()->AddPin( + std::move(token), absl::nullopt, + base::BindLambdaForTesting( + [&call_status](bool result, mojom::PinErrorPtr error) { + call_status = result; + EXPECT_FALSE(error); + })); + EXPECT_TRUE(call_status.value()); + + const base::Value::Dict* token_record = + GetPrefs() + ->GetDict(kPinnedErc721Assets) + .FindDictByDottedPath(kMonkey1Path); + + base::Value::List expected_cids; + expected_cids.Append("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq"); + expected_cids.Append("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg"); + + EXPECT_EQ(StatusToString(mojom::TokenPinStatusCode::STATUS_PINNED), + *(token_record->FindString("status"))); + EXPECT_EQ(nullptr, token_record->FindDict("error")); + EXPECT_EQ(expected_cids, *(token_record->FindList("cids"))); + EXPECT_EQ("123", *(token_record->FindString("validate_timestamp"))); + } + + { + ON_CALL(*GetJsonRpcService(), GetERC721Metadata(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& contract_address, const std::string& token_id, + const std::string& chain_id, + MockJsonRpcService::GetTokenMetadataCallback callback) { + EXPECT_EQ("0x1", chain_id); + EXPECT_EQ("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", + contract_address); + EXPECT_EQ("0x2", token_id); + std::move(callback).Run( + "", "", mojom::ProviderError::kParsingError, "Parsing error"); + })); + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey2Path); + + absl::optional call_status; + service()->AddPin( + std::move(token), absl::nullopt, + base::BindLambdaForTesting( + [&call_status](bool result, mojom::PinErrorPtr error) { + call_status = result; + EXPECT_TRUE(error); + })); + + EXPECT_FALSE(call_status.value()); + + const base::Value::Dict* token_record = + GetPrefs() + ->GetDict(kPinnedErc721Assets) + .FindDictByDottedPath(kMonkey2Path); + + EXPECT_EQ(StatusToString(mojom::TokenPinStatusCode::STATUS_PINNING_FAILED), + *(token_record->FindString("status"))); + EXPECT_EQ(ErrorCodeToString( + mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED), + token_record->FindByDottedPath("error.error_code")->GetString()); + } +} + +TEST_F(BraveWalletPinServiceTest, RemovePin) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + item.Set("validation_timestamp", "123"); + base::Value::List cids; + cids.Append("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq"); + cids.Append("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg"); + + update_dict.SetByDottedPath(kMonkey1Path, std::move(item)); + } + + { + ON_CALL(*GetIpfsLocalPinService(), RemovePins(_, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& prefix, ipfs::RemovePinCallback callback) { + EXPECT_EQ(kMonkey1Path, prefix); + std::move(callback).Run(false); + })); + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + absl::optional remove_status; + service()->RemovePin( + std::move(token), absl::nullopt, + base::BindLambdaForTesting( + [&remove_status](bool status, mojom::PinErrorPtr error) { + remove_status = status; + })); + EXPECT_FALSE(remove_status.value()); + EXPECT_EQ( + StatusToString(mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED), + *(GetPrefs() + ->GetDict(kPinnedErc721Assets) + .FindByDottedPath(kMonkey1Path) + ->FindStringKey("status"))); + } + + { + ON_CALL(*GetIpfsLocalPinService(), RemovePins(_, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& prefix, ipfs::RemovePinCallback callback) { + EXPECT_EQ(kMonkey1Path, prefix); + std::move(callback).Run(true); + })); + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + absl::optional remove_status; + service()->RemovePin( + std::move(token), absl::nullopt, + base::BindLambdaForTesting( + [&remove_status](bool status, mojom::PinErrorPtr error) { + remove_status = status; + })); + EXPECT_TRUE(remove_status.value()); + EXPECT_EQ(nullptr, GetPrefs() + ->GetDict(kPinnedErc721Assets) + .FindDictByDottedPath(kMonkey1Path)); + } +} + +TEST_F(BraveWalletPinServiceTest, ValidatePin) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + item.Set("validate_timestamp", "123"); + base::Value::List cids; + cids.Append("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq"); + cids.Append("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg"); + item.Set("cids", std::move(cids)); + + update_dict.SetByDottedPath(kMonkey1Path, std::move(item)); + } + + { + auto scoped_override = + OverrideWithTimeNow(base::Time::FromInternalValue(345u)); + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + ON_CALL(*GetIpfsLocalPinService(), ValidatePins(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& prefix, const std::vector& cids, + ipfs::ValidatePinsCallback callback) { + EXPECT_EQ("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq", + cids.at(0)); + EXPECT_EQ("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg", + cids.at(1)); + EXPECT_EQ(kMonkey1Path, prefix); + std::move(callback).Run(true); + })); + + absl::optional validate_status; + service()->Validate( + std::move(token), absl::nullopt, + base::BindLambdaForTesting( + [&validate_status](bool status, mojom::PinErrorPtr error) { + validate_status = status; + })); + EXPECT_TRUE(validate_status.value()); + + const base::Value::Dict* token_record = + GetPrefs() + ->GetDict(kPinnedErc721Assets) + .FindDictByDottedPath(kMonkey1Path); + EXPECT_EQ("345", *(token_record->FindString("validate_timestamp"))); + } + + { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + ON_CALL(*GetIpfsLocalPinService(), ValidatePins(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& prefix, const std::vector& cids, + ipfs::ValidatePinsCallback callback) { + std::move(callback).Run(absl::nullopt); + })); + + absl::optional validate_status; + service()->Validate( + std::move(token), absl::nullopt, + base::BindLambdaForTesting( + [&validate_status](bool status, mojom::PinErrorPtr error) { + validate_status = status; + })); + + EXPECT_FALSE(validate_status.value()); + + const base::Value::Dict* token_record = + GetPrefs() + ->GetDict(kPinnedErc721Assets) + .FindDictByDottedPath(kMonkey1Path); + EXPECT_EQ("345", *(token_record->FindString("validate_timestamp"))); + } + + { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + ON_CALL(*GetIpfsLocalPinService(), ValidatePins(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& prefix, const std::vector& cids, + ipfs::ValidatePinsCallback callback) { + std::move(callback).Run(false); + })); + + absl::optional validate_status; + service()->Validate( + std::move(token), absl::nullopt, + base::BindLambdaForTesting( + [&validate_status](bool status, mojom::PinErrorPtr error) { + validate_status = status; + })); + + EXPECT_TRUE(validate_status.value()); + + const base::Value::Dict* token_record = + GetPrefs() + ->GetDict(kPinnedErc721Assets) + .FindDictByDottedPath(kMonkey1Path); + + EXPECT_EQ(nullptr, token_record->FindString("validate_timestamp")); + } +} + +TEST_F(BraveWalletPinServiceTest, GetTokenStatus) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + item.Set("validate_timestamp", "123"); + base::Value::List cids; + cids.Append("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq"); + cids.Append("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg"); + item.Set("cids", std::move(cids)); + + update_dict.SetByDottedPath(kMonkey1Path, std::move(item)); + } + + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", + StatusToString(mojom::TokenPinStatusCode::STATUS_PINNING_FAILED)); + base::Value::Dict error; + error.Set("error_code", + ErrorCodeToString( + mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED)); + error.Set("error_message", "Fail to fetch metadata"); + item.Set("error", std::move(error)); + + update_dict.SetByDottedPath(kMonkey2Path, std::move(item)); + } + + mojom::BlockchainTokenPtr token1 = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + { + mojom::TokenPinStatusPtr status = + service()->GetTokenStatus(absl::nullopt, token1); + EXPECT_EQ(mojom::TokenPinStatusCode::STATUS_PINNED, status->code); + EXPECT_TRUE(status->error.is_null()); + EXPECT_EQ(base::Time::FromInternalValue(123u), + status->validate_time.value()); + } + + { + mojom::TokenPinStatusPtr status = + service()->GetTokenStatus("nft.storage", token1); + EXPECT_TRUE(status.is_null()); + } + + mojom::BlockchainTokenPtr token2 = + BraveWalletPinService::TokenFromPath(kMonkey2Path); + + { + mojom::TokenPinStatusPtr status = + service()->GetTokenStatus(absl::nullopt, token2); + EXPECT_EQ(mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, status->code); + EXPECT_EQ(mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED, + status->error->error_code); + EXPECT_FALSE(status->validate_time.has_value()); + } +} + +TEST_F(BraveWalletPinServiceTest, GetLastValidateTime) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + item.Set("validate_timestamp", "123"); + base::Value::List cids; + cids.Append("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq"); + cids.Append("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg"); + item.Set("cids", std::move(cids)); + + update_dict.SetByDottedPath(kMonkey1Path, std::move(item)); + } + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + + { + base::Time last_validate_time = + service()->GetLastValidateTime(absl::nullopt, token).value(); + EXPECT_EQ(base::Time::FromInternalValue(123u), last_validate_time); + } + + { + EXPECT_FALSE( + service()->GetLastValidateTime("nft.storage", token).has_value()); + } +} + +TEST_F(BraveWalletPinServiceTest, TokenFromPath) { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPath(kMonkey1Path); + EXPECT_TRUE(token->is_erc721); + EXPECT_EQ(mojom::CoinType::ETH, static_cast(token->coin)); + EXPECT_EQ("0x1", token->chain_id); + EXPECT_EQ("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", + token->contract_address); + EXPECT_EQ("0x1", token->token_id); +} + +TEST_F(BraveWalletPinServiceTest, ServiceFromPath) { + EXPECT_FALSE( + BraveWalletPinService::ServiceFromPath(kMonkey1Path).has_value()); + + EXPECT_EQ("nftstorage", BraveWalletPinService::ServiceFromPath( + "nft.nftstorage.60.0x1." + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1") + .value()); +} + +TEST_F(BraveWalletPinServiceTest, GetPath) { + { + mojom::BlockchainTokenPtr token = mojom::BlockchainToken::New(); + token->coin = mojom::CoinType::ETH; + token->contract_address = "abc"; + token->token_id = "0x2"; + token->chain_id = "mainnet"; + auto path = BraveWalletPinService::GetPath(absl::nullopt, token); + EXPECT_EQ("nft.local.60.mainnet.abc.0x2", path); + } + + { + mojom::BlockchainTokenPtr token = mojom::BlockchainToken::New(); + token->coin = mojom::CoinType::ETH; + token->contract_address = "abc"; + token->token_id = "0x2"; + token->chain_id = "mainnet"; + auto path = BraveWalletPinService::GetPath("nftstorage", token); + EXPECT_EQ("nft.nftstorage.60.mainnet.abc.0x2", path); + } +} + +TEST_F(BraveWalletPinServiceTest, GetTokens) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + + update_dict.SetByDottedPath(kMonkey1Path, std::move(item)); + } + + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinning_failed"); + + update_dict.SetByDottedPath(kMonkey2Path, std::move(item)); + } + + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedErc721Assets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + + update_dict.SetByDottedPath(kMonkey3Path, std::move(item)); + } + + { + auto tokens = service()->GetTokens(absl::nullopt); + EXPECT_EQ(2u, tokens.size()); + EXPECT_TRUE(tokens.contains(kMonkey1Path)); + EXPECT_TRUE(tokens.contains(kMonkey2Path)); + } + + { + auto tokens = service()->GetTokens("nftstorage"); + EXPECT_EQ(1u, tokens.size()); + EXPECT_TRUE(tokens.contains(kMonkey3Path)); + } + + { + auto tokens = service()->GetTokens("non_existing_storage"); + EXPECT_EQ(0u, tokens.size()); + } +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/brave_wallet_prefs.cc b/components/brave_wallet/browser/brave_wallet_prefs.cc index 8d489d0d7be6..4fad28a61d5f 100644 --- a/components/brave_wallet/browser/brave_wallet_prefs.cc +++ b/components/brave_wallet/browser/brave_wallet_prefs.cc @@ -88,6 +88,8 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { registry->RegisterDictionaryPref(kBraveWalletLastTransactionSentTimeDict); registry->RegisterDictionaryPref(kBraveWalletNextAssetDiscoveryFromBlocks); registry->RegisterTimePref(kBraveWalletLastDiscoveredAssetsAt, base::Time()); + registry->RegisterDictionaryPref(kPinnedErc721Assets); + registry->RegisterBooleanPref(kAutoPinEnabled, true); } void RegisterProfilePrefsForMigration( @@ -154,6 +156,7 @@ void ClearBraveWalletServicePrefs(PrefService* prefs) { prefs->ClearPref(kBraveWalletUserAssets); prefs->ClearPref(kDefaultBaseCurrency); prefs->ClearPref(kDefaultBaseCryptocurrency); + prefs->ClearPref(kPinnedErc721Assets); } void MigrateObsoleteProfilePrefs(PrefService* prefs) { diff --git a/components/brave_wallet/browser/brave_wallet_service.cc b/components/brave_wallet/browser/brave_wallet_service.cc index 65b1312ab394..a9e92729e764 100644 --- a/components/brave_wallet/browser/brave_wallet_service.cc +++ b/components/brave_wallet/browser/brave_wallet_service.cc @@ -214,6 +214,8 @@ BraveWalletService::BraveWalletService( OnP3ATimerFired(); // Also call on startup } +BraveWalletService::BraveWalletService() : weak_ptr_factory_(this) {} + BraveWalletService::~BraveWalletService() = default; mojo::PendingRemote @@ -368,6 +370,7 @@ bool BraveWalletService::AddUserAsset(mojom::BlockchainTokenPtr token, value.Set("coingecko_id", token->coingecko_id); user_assets_list->Append(std::move(value)); + return true; } @@ -380,7 +383,15 @@ void BraveWalletService::GetUserAssets(const std::string& chain_id, } bool BraveWalletService::AddUserAsset(mojom::BlockchainTokenPtr token) { - return BraveWalletService::AddUserAsset(std::move(token), prefs_); + mojom::BlockchainTokenPtr clone = token.Clone(); + bool result = BraveWalletService::AddUserAsset(std::move(token), prefs_); + + if (result) { + for (const auto& observer : token_observers_) { + observer->OnTokenAdded(clone.Clone()); + } + } + return result; } void BraveWalletService::AddUserAsset(mojom::BlockchainTokenPtr token, @@ -417,6 +428,11 @@ bool BraveWalletService::RemoveUserAsset(mojom::BlockchainTokenPtr token) { FindAsset(user_assets_list, *address, token->token_id, token->is_erc721); if (it != user_assets_list->end()) user_assets_list->erase(it); + + for (const auto& observer : token_observers_) { + observer->OnTokenRemoved(token.Clone()); + } + return true; } @@ -1109,6 +1125,11 @@ void BraveWalletService::AddObserver( observers_.Add(std::move(observer)); } +void BraveWalletService::AddTokenObserver( + ::mojo::PendingRemote observer) { + token_observers_.Add(std::move(observer)); +} + void BraveWalletService::OnActiveOriginChanged( const mojom::OriginInfoPtr& origin_info) { for (const auto& observer : observers_) { diff --git a/components/brave_wallet/browser/brave_wallet_service.h b/components/brave_wallet/browser/brave_wallet_service.h index 6194a55ecc38..02427cfc8c0c 100644 --- a/components/brave_wallet/browser/brave_wallet_service.h +++ b/components/brave_wallet/browser/brave_wallet_service.h @@ -64,6 +64,8 @@ class BraveWalletService : public KeyedService, JsonRpcService* json_rpc_service, TxService* tx_service, PrefService* prefs); + // For tests + BraveWalletService(); ~BraveWalletService() override; BraveWalletService(const BraveWalletService&) = delete; @@ -89,6 +91,9 @@ class BraveWalletService : public KeyedService, // mojom::BraveWalletService: void AddObserver(::mojo::PendingRemote observer) override; + void AddTokenObserver( + ::mojo::PendingRemote observer) + override; void GetUserAssets(const std::string& chain_id, mojom::CoinType coin, @@ -312,6 +317,7 @@ class BraveWalletService : public KeyedService, decrypt_callbacks_; base::flat_map decrypt_ids_; mojo::RemoteSet observers_; + mojo::RemoteSet token_observers_; std::unique_ptr delegate_; raw_ptr keyring_service_ = nullptr; raw_ptr json_rpc_service_ = nullptr; diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 6dfdcb68d278..92e220bac89f 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -190,6 +190,8 @@ JsonRpcService::JsonRpcService( : JsonRpcService(std::move(url_loader_factory), std::move(prefs), nullptr) { } +JsonRpcService::JsonRpcService() : weak_ptr_factory_(this) {} + void JsonRpcService::SetAPIRequestHelperForTesting( scoped_refptr url_loader_factory) { api_request_helper_ = std::make_unique( @@ -2039,14 +2041,14 @@ void JsonRpcService::GetTokenMetadata(const std::string& contract_address, auto network_url = GetNetworkURL(prefs_, chain_id, mojom::CoinType::ETH); if (!network_url.is_valid()) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + "", "", mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } if (!EthAddress::IsValidAddress(contract_address)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + "", "", mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } @@ -2054,7 +2056,7 @@ void JsonRpcService::GetTokenMetadata(const std::string& contract_address, uint256_t token_id_uint = 0; if (!HexValueToUint256(token_id, &token_id_uint)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + "", "", mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } @@ -2063,21 +2065,21 @@ void JsonRpcService::GetTokenMetadata(const std::string& contract_address, if (interface_id == kERC721MetadataInterfaceId) { if (!erc721::TokenUri(token_id_uint, &function_signature)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + "", "", mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } } else if (interface_id == kERC1155MetadataInterfaceId) { if (!erc1155::Uri(token_id_uint, &function_signature)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + "", "", mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } } else { // Unknown inteface ID std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + "", "", mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } @@ -2100,13 +2102,13 @@ void JsonRpcService::OnGetSupportsInterfaceTokenMetadata( mojom::ProviderError error, const std::string& error_message) { if (error != mojom::ProviderError::kSuccess) { - std::move(callback).Run("", error, error_message); + std::move(callback).Run("", "", error, error_message); return; } if (!is_supported) { std::move(callback).Run( - "", mojom::ProviderError::kMethodNotSupported, + "", "", mojom::ProviderError::kMethodNotSupported, l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); return; } @@ -2124,7 +2126,7 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, APIRequestResult api_request_result) { if (!api_request_result.Is2XXResponseCode()) { std::move(callback).Run( - "", mojom::ProviderError::kInternalError, + "", "", mojom::ProviderError::kInternalError, l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } @@ -2136,9 +2138,10 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, std::string error_message; ParseErrorResult(api_request_result.body(), &error, &error_message); - std::move(callback).Run("", error, error_message); + std::move(callback).Run("", "", error, error_message); return; } + std::string token_url_spec = url.spec(); // Obtain JSON from the URL depending on the scheme. // IPFS, HTTPS, and data URIs are supported. @@ -2152,7 +2155,7 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, if (scheme != url::kDataScheme && scheme != url::kHttpsScheme) { #endif std::move(callback).Run( - "", mojom::ProviderError::kMethodNotSupported, + "", "", mojom::ProviderError::kMethodNotSupported, l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); return; } @@ -2160,7 +2163,7 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, if (scheme == url::kDataScheme) { if (!eth::ParseDataURIAndExtractJSON(url, &metadata_json)) { std::move(callback).Run( - "", mojom::ProviderError::kParsingError, + "", "", mojom::ProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } @@ -2169,7 +2172,8 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, data_decoder::JsonSanitizer::Sanitize( std::move(metadata_json), base::BindOnce(&JsonRpcService::OnSanitizeTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); + weak_ptr_factory_.GetWeakPtr(), token_url_spec, + std::move(callback))); return; } @@ -2177,7 +2181,7 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, if (scheme == ipfs::kIPFSScheme && !ipfs::TranslateIPFSURI(url, &url, ipfs::GetDefaultNFTIPFSGateway(prefs_), false)) { - std::move(callback).Run("", mojom::ProviderError::kParsingError, + std::move(callback).Run("", "", mojom::ProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } @@ -2185,17 +2189,19 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, auto internal_callback = base::BindOnce(&JsonRpcService::OnGetTokenMetadataPayload, - weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + weak_ptr_factory_.GetWeakPtr(), std::move(callback), + std::move(token_url_spec)); api_request_helper_->Request("GET", url, "", "", true, std::move(internal_callback)); } void JsonRpcService::OnSanitizeTokenMetadata( + const std::string& token_url, GetTokenMetadataCallback callback, data_decoder::JsonSanitizer::Result result) { if (result.error) { VLOG(1) << "Data URI JSON validation error:" << *result.error; - std::move(callback).Run("", mojom::ProviderError::kParsingError, + std::move(callback).Run("", "", mojom::ProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } @@ -2205,27 +2211,29 @@ void JsonRpcService::OnSanitizeTokenMetadata( metadata_json = result.value.value(); } - std::move(callback).Run(metadata_json, mojom::ProviderError::kSuccess, ""); + std::move(callback).Run(token_url, metadata_json, + mojom::ProviderError::kSuccess, ""); } void JsonRpcService::OnGetTokenMetadataPayload( GetTokenMetadataCallback callback, + const std::string& token_url, APIRequestResult api_request_result) { if (!api_request_result.Is2XXResponseCode()) { std::move(callback).Run( - "", mojom::ProviderError::kInternalError, + token_url, "", mojom::ProviderError::kInternalError, l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } // Invalid JSON becomes an empty string after sanitization if (api_request_result.body().empty()) { - std::move(callback).Run("", mojom::ProviderError::kParsingError, + std::move(callback).Run(token_url, "", mojom::ProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } - std::move(callback).Run(api_request_result.body(), + std::move(callback).Run(token_url, api_request_result.body(), mojom::ProviderError::kSuccess, ""); } diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index a695c3248a17..aa734c87d15e 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -54,6 +54,7 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { JsonRpcService( scoped_refptr url_loader_factory, PrefService* prefs); + JsonRpcService(); ~JsonRpcService() override; static void MigrateMultichainNetworks(PrefService* prefs); @@ -300,7 +301,8 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { GetERC721TokenBalanceCallback callback) override; using GetTokenMetadataCallback = - base::OnceCallback; @@ -535,9 +537,11 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const APIRequestResult api_request_result); void OnGetTokenMetadataPayload(GetTokenMetadataCallback callback, + const std::string& token_url, APIRequestResult api_request_result); - void OnSanitizeTokenMetadata(GetTokenMetadataCallback callback, + void OnSanitizeTokenMetadata(const std::string& token_url, + GetTokenMetadataCallback callback, data_decoder::JsonSanitizer::Result result); void OnGetSupportsInterface(GetSupportsInterfaceCallback callback, diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index 1e4ecbeafee0..ed3872cd48f9 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -1073,14 +1073,14 @@ class JsonRpcServiceUnitTest : public testing::Test { base::RunLoop run_loop; json_rpc_service_->GetERC721Metadata( contract, token_id, chain_id, - base::BindLambdaForTesting([&](const std::string& response, - mojom::ProviderError error, - const std::string& error_message) { - EXPECT_EQ(response, expected_response); - EXPECT_EQ(error, expected_error); - EXPECT_EQ(error_message, expected_error_message); - run_loop.Quit(); - })); + base::BindLambdaForTesting( + [&](const std::string& token_url, const std::string& response, + mojom::ProviderError error, const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + run_loop.Quit(); + })); run_loop.Run(); } @@ -1093,14 +1093,14 @@ class JsonRpcServiceUnitTest : public testing::Test { base::RunLoop run_loop; json_rpc_service_->GetERC1155Metadata( contract, token_id, chain_id, - base::BindLambdaForTesting([&](const std::string& response, - mojom::ProviderError error, - const std::string& error_message) { - EXPECT_EQ(response, expected_response); - EXPECT_EQ(error, expected_error); - EXPECT_EQ(error_message, expected_error_message); - run_loop.Quit(); - })); + base::BindLambdaForTesting( + [&](const std::string& token_url, const std::string& response, + mojom::ProviderError error, const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + run_loop.Quit(); + })); run_loop.Run(); } @@ -1114,14 +1114,14 @@ class JsonRpcServiceUnitTest : public testing::Test { base::RunLoop run_loop; json_rpc_service_->GetTokenMetadata( contract, token_id, chain_id, interface_id, - base::BindLambdaForTesting([&](const std::string& response, - mojom::ProviderError error, - const std::string& error_message) { - EXPECT_EQ(response, expected_response); - EXPECT_EQ(error, expected_error); - EXPECT_EQ(error_message, expected_error_message); - run_loop.Quit(); - })); + base::BindLambdaForTesting( + [&](const std::string& token_url, const std::string& response, + mojom::ProviderError error, const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + run_loop.Quit(); + })); run_loop.Run(); } diff --git a/components/brave_wallet/browser/pref_names.cc b/components/brave_wallet/browser/pref_names.cc index 054ea7533a15..5f083ae56db0 100644 --- a/components/brave_wallet/browser/pref_names.cc +++ b/components/brave_wallet/browser/pref_names.cc @@ -71,3 +71,5 @@ const char kBraveWalletCurrentChainId[] = const char kBraveWalletUserAssetsDeprecated[] = "brave.wallet.user_assets"; const char kBraveWalletUserAssetsAddPreloadingNetworksMigratedDeprecated[] = "brave.wallet.user.assets.add_preloading_networks_migrated"; +const char kPinnedErc721Assets[] = "brave.wallet.user_pin_data"; +const char kAutoPinEnabled[] = "brave.wallet.auto_pin_enabled"; diff --git a/components/brave_wallet/browser/pref_names.h b/components/brave_wallet/browser/pref_names.h index e7f3f1d7318c..2c00cf795bfe 100644 --- a/components/brave_wallet/browser/pref_names.h +++ b/components/brave_wallet/browser/pref_names.h @@ -59,5 +59,7 @@ extern const char kBraveWalletCurrentChainId[]; extern const char kBraveWalletUserAssetsDeprecated[]; extern const char kBraveWalletUserAssetsAddPreloadingNetworksMigratedDeprecated[]; +extern const char kPinnedErc721Assets[]; +extern const char kAutoPinEnabled[]; #endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_PREF_NAMES_H_ diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index 8619002c08bc..2b6568adf391 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -13,6 +13,8 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/asset_ratio_service_unittest.cc", "//brave/components/brave_wallet/browser/blockchain_list_parser_unittest.cc", "//brave/components/brave_wallet/browser/blockchain_registry_unittest.cc", + "//brave/components/brave_wallet/browser/brave_wallet_auto_pin_service_unittest.cc", + "//brave/components/brave_wallet/browser/brave_wallet_pin_service_unittest.cc", "//brave/components/brave_wallet/browser/brave_wallet_utils_unittest.cc", "//brave/components/brave_wallet/browser/eip1559_transaction_unittest.cc", "//brave/components/brave_wallet/browser/eip2930_transaction_unittest.cc", diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index 6df4469f01ac..618edcd180b3 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -207,6 +207,62 @@ interface SolanaProvider { mojo_base.mojom.DictionaryValue result); }; +enum WalletPinServiceErrorCode { + ERR_WRONG_TOKEN = 1, + ERR_NON_IPFS_TOKEN_URL = 2, + ERR_FETCH_METADATA_FAILED = 3, + ERR_WRONG_METADATA_FORMAT = 4, + ERR_ALREADY_PINNED = 5, + ERR_NOT_PINNED = 6, + ERR_PINNING_FAILED = 7 +}; + +enum TokenPinStatusCode { + STATUS_NOT_PINNED = 1, + STATUS_PINNED = 2, + STATUS_PINNING_IN_PROGRESS = 3, + STATUS_PINNING_FAILED = 4, + STATUS_UNPINNING_IN_PROGRESS = 5, + STATUS_UNPINNING_FAILED = 6, + STATUS_PINNING_PENDING = 7, + STATUS_UNPINNING_PENDING = 8 +}; + +struct PinError { + WalletPinServiceErrorCode error_code; + string message; +}; + +struct TokenPinStatus { + TokenPinStatusCode code; + PinError? error; + mojo_base.mojom.Time? validate_time; +}; + +struct TokenPinOverview { + TokenPinStatus local; + map remotes; +}; + +interface BraveWalletPinServiceObserver { + OnTokenStatusChanged(string? service, BlockchainToken token, TokenPinStatus status); +}; + +interface WalletPinService { + AddObserver(pending_remote observer); + + AddPin(BlockchainToken token, string? service) => (bool result, PinError? error); + RemovePin(BlockchainToken token, string? service) => (bool result, PinError? response); + GetTokenStatus(BlockchainToken token) => (TokenPinOverview? status, PinError? error); + Validate(BlockchainToken token, string? service) => (bool result, PinError? error); +}; + +interface WalletAutoPinService { + SetAutoPinEnabled(bool enabled); + IsAutoPinEnabled() => (bool enabled); + PostPinToken(BlockchainToken token) => (bool result); + PostUnpinToken(BlockchainToken token) => (bool result); +}; // Used by the WebUI page to bootstrap bidirectional communication. interface PanelHandlerFactory { @@ -242,8 +298,9 @@ interface PageHandlerFactory { pending_receiver solana_tx_manager_proxy, pending_receiver fil_tx_manager_proxy, pending_receiver brave_wallet_service, - pending_receiver brave_wallet_p3a); - + pending_receiver brave_wallet_p3a, + pending_receiver brave_wallet_pin_service, + pending_receiver brave_wallet_auto_pin_service); }; // Browser-side handler for requests from WebUI page. @@ -900,10 +957,10 @@ interface JsonRpcService { string owner_address, string spender_address) => (string allowance, ProviderError error, string error_message); // Obtains the metadata JSON for a token ID of an ERC721 contract - GetERC721Metadata(string contract, string token_id, string chain_id) => (string response, ProviderError error, string error_message); + GetERC721Metadata(string contract, string token_id, string chain_id) => (string token_url, string response, ProviderError error, string error_message); // Obtains the metadata JSON for a token ID of an ERC1155 contract - GetERC1155Metadata(string contract, string token_id, string chain_id) => (string response, ProviderError error, string error_message); + GetERC1155Metadata(string contract, string token_id, string chain_id) => (string token_url, string response, ProviderError error, string error_message); // ENS lookups EnsGetEthAddr(string domain, EnsOffchainLookupOptions? options) => (string address, bool require_offchain_consent, ProviderError error, string error_message); @@ -1146,6 +1203,11 @@ interface BraveWalletServiceObserver { OnDiscoverAssetsCompleted(array discovered_assets); }; +interface BraveWalletServiceTokenObserver { + OnTokenAdded(BlockchainToken token); + OnTokenRemoved(BlockchainToken token); +}; + struct SignMessageRequest { OriginInfo origin_info; int32 id; @@ -1202,6 +1264,8 @@ interface BraveWalletService { // Adds an observer for BraveWalletService AddObserver(pending_remote observer); + AddTokenObserver(pending_remote observer); + // Obtains the user assets for the specified chain ID and coin type. GetUserAssets(string chain_id, CoinType coin) => (array tokens); diff --git a/components/brave_wallet_ui/common/actions/wallet_actions.ts b/components/brave_wallet_ui/common/actions/wallet_actions.ts index 82b5fa677ffe..b5933aaf93ea 100644 --- a/components/brave_wallet_ui/common/actions/wallet_actions.ts +++ b/components/brave_wallet_ui/common/actions/wallet_actions.ts @@ -96,5 +96,6 @@ unlocked, updateUnapprovedTransactionGasFields, updateUnapprovedTransactionNonce, - updateUnapprovedTransactionSpendAllowance + updateUnapprovedTransactionSpendAllowance, + updateTokenPinStatus } = WalletActions diff --git a/components/brave_wallet_ui/common/slices/wallet.slice.ts b/components/brave_wallet_ui/common/slices/wallet.slice.ts index fecb0208b298..db9eb8e7cf64 100644 --- a/components/brave_wallet_ui/common/slices/wallet.slice.ts +++ b/components/brave_wallet_ui/common/slices/wallet.slice.ts @@ -182,7 +182,8 @@ export const WalletAsyncActions = { addAccount: createAction('addAccount'), // alias for keyringService.addAccount addFilecoinAccount: createAction('addFilecoinAccount'), // alias for keyringService.addFilecoinAccount getOnRampCurrencies: createAction('getOnRampCurrencies'), - autoLockMinutesChanged: createAction('autoLockMinutesChanged') // No reducer or API logic for this (UNUSED) + autoLockMinutesChanged: createAction('autoLockMinutesChanged'), // No reducer or API logic for this (UNUSED) + updateTokenPinStatus: createAction('updateTokenPinStatus') } // slice diff --git a/components/brave_wallet_ui/common/wallet_api_proxy.ts b/components/brave_wallet_ui/common/wallet_api_proxy.ts index a3d6cff07bf2..358aa8e1aeb1 100644 --- a/components/brave_wallet_ui/common/wallet_api_proxy.ts +++ b/components/brave_wallet_ui/common/wallet_api_proxy.ts @@ -24,6 +24,8 @@ export class WalletApiProxy { filTxManagerProxy = new BraveWallet.FilTxManagerProxyRemote() braveWalletService = new BraveWallet.BraveWalletServiceRemote() braveWalletP3A = new BraveWallet.BraveWalletP3ARemote() + braveWalletPinService = new BraveWallet.WalletPinServiceRemote() + braveWalletAutoPinService = new BraveWallet.WalletAutoPinServiceRemote() addJsonRpcServiceObserver (store: Store) { const jsonRpcServiceObserverReceiver = new BraveWallet.JsonRpcServiceObserverReceiver({ @@ -125,6 +127,14 @@ export class WalletApiProxy { }) this.braveWalletService.addObserver(braveWalletServiceObserverReceiver.$.bindNewPipeAndPassRemote()) } + + addBraveWalletPinServiceObserver (store: Store) { + const braveWalletServiceObserverReceiver = new BraveWallet.BraveWalletPinServiceObserverReceiver({ + onTokenStatusChanged: function (service, token, status) { + } + }) + this.braveWalletPinService.addObserver(braveWalletServiceObserverReceiver.$.bindNewPipeAndPassRemote()) + } } export default WalletApiProxy diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index 99939b9b6b6f..f2b49ea767bc 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -287,6 +287,7 @@ export interface PageState { isFetchingNFTMetadata: boolean nftMetadata: NFTMetadataReturnType | undefined nftMetadataError: string | undefined + pinStatusOverview: BraveWallet.TokenPinOverview | undefined selectedAssetFiatPrice: BraveWallet.AssetPrice | undefined selectedAssetCryptoPrice: BraveWallet.AssetPrice | undefined selectedAssetPriceHistory: GetPriceHistoryReturnInfo[] diff --git a/components/brave_wallet_ui/page/actions/wallet_page_actions.ts b/components/brave_wallet_ui/page/actions/wallet_page_actions.ts index 19407bb0cd7d..a42726d3addb 100644 --- a/components/brave_wallet_ui/page/actions/wallet_page_actions.ts +++ b/components/brave_wallet_ui/page/actions/wallet_page_actions.ts @@ -42,5 +42,7 @@ export const { updateSelectedAsset, walletBackupComplete, walletCreated, - walletSetupComplete + walletSetupComplete, + updateNFTPinStatus, + getPinStatus } = PageActions diff --git a/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts b/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts index f8a8facf6ab8..b5642364e67d 100644 --- a/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts +++ b/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts @@ -143,6 +143,7 @@ handler.on(WalletPageActions.selectAsset.type, async (store: Store, payload: Upd if (payload.asset.isErc721) { store.dispatch(WalletPageActions.getNFTMetadata(payload.asset)) + store.dispatch(WalletPageActions.getPinStatus(payload.asset)) } } else { store.dispatch(WalletPageActions.updatePriceInfo({ priceHistory: undefined, defaultFiatPrice: undefined, defaultCryptoPrice: undefined, timeFrame: payload.timeFrame })) @@ -305,4 +306,14 @@ handler.on(WalletPageActions.getNFTMetadata.type, async (store, payload: BraveWa store.dispatch(WalletPageActions.setIsFetchingNFTMetadata(false)) }) +handler.on(WalletPageActions.getPinStatus.type, async (store, payload: BraveWallet.BlockchainToken) => { + const braveWalletPinService = getWalletPageApiProxy().braveWalletPinService + const result = await braveWalletPinService.getTokenStatus(payload) + if (result.status) { + store.dispatch(WalletPageActions.updateNFTPinStatus(result.status)) + } else { + store.dispatch(WalletPageActions.updateNFTPinStatus(undefined)) + } +}) + export default handler.middleware diff --git a/components/brave_wallet_ui/page/reducers/page_reducer.ts b/components/brave_wallet_ui/page/reducers/page_reducer.ts index 861009666e11..82b582b0e2a1 100644 --- a/components/brave_wallet_ui/page/reducers/page_reducer.ts +++ b/components/brave_wallet_ui/page/reducers/page_reducer.ts @@ -41,6 +41,7 @@ const defaultState: PageState = { isFetchingNFTMetadata: true, nftMetadata: undefined, nftMetadataError: undefined, + pinStatusOverview: undefined, selectedAssetFiatPrice: undefined, selectedAssetCryptoPrice: undefined, selectedAssetPriceHistory: [], @@ -71,7 +72,9 @@ export const WalletPageAsyncActions = { removeImportedAccount: createAction('removeImportedAccount'), restoreWallet: createAction('restoreWallet'), selectAsset: createAction('selectAsset'), - updateAccountName: createAction('updateAccountName') + updateAccountName: createAction('updateAccountName'), + updateNFTPinStatus: createAction('updateNFTPinStatus'), + getPinStatus: createAction('getPinStatus') } export const createPageSlice = (initialState: PageState = defaultState) => { @@ -177,6 +180,10 @@ export const createPageSlice = (initialState: PageState = defaultState) => { // complete setup unless explicitly halted state.setupStillInProgress = !action?.payload state.mnemonic = undefined + }, + + updateNFTPinStatus (state, { payload }: PayloadAction) { + state.pinStatusOverview = payload } } }) diff --git a/components/brave_wallet_ui/page/selectors/page-selectors.ts b/components/brave_wallet_ui/page/selectors/page-selectors.ts index 8987495b76d8..4b17c8c43b29 100644 --- a/components/brave_wallet_ui/page/selectors/page-selectors.ts +++ b/components/brave_wallet_ui/page/selectors/page-selectors.ts @@ -33,6 +33,7 @@ export const nftMetadataError = ({ page }: State) => page.nftMetadataError export const portfolioPriceHistory = ({ page }: State) => page.portfolioPriceHistory export const selectedAsset = ({ page }: State) => page.selectedAsset +export const pinStatusOverview = ({ page }: State) => page.pinStatusOverview export const selectedAssetCryptoPrice = ({ page }: State) => page.selectedAssetCryptoPrice export const selectedAssetFiatPrice = ({ page }: State) => page.selectedAssetFiatPrice export const selectedAssetPriceHistory = ({ page }: State) => page.selectedAssetPriceHistory diff --git a/components/brave_wallet_ui/page/store.ts b/components/brave_wallet_ui/page/store.ts index 183b489aac7a..97c5b297d1e6 100644 --- a/components/brave_wallet_ui/page/store.ts +++ b/components/brave_wallet_ui/page/store.ts @@ -41,6 +41,7 @@ proxy.addJsonRpcServiceObserver(store) proxy.addKeyringServiceObserver(store) proxy.addTxServiceObserver(store) proxy.addBraveWalletServiceObserver(store) +proxy.addBraveWalletPinServiceObserver(store) export const walletPageApiProxy = proxy diff --git a/components/brave_wallet_ui/page/wallet_page_api_proxy.ts b/components/brave_wallet_ui/page/wallet_page_api_proxy.ts index 09d4ec92e596..6a870a905106 100644 --- a/components/brave_wallet_ui/page/wallet_page_api_proxy.ts +++ b/components/brave_wallet_ui/page/wallet_page_api_proxy.ts @@ -29,7 +29,9 @@ class WalletPageApiProxy extends WalletApiProxy { this.solanaTxManagerProxy.$.bindNewPipeAndPassReceiver(), this.filTxManagerProxy.$.bindNewPipeAndPassReceiver(), this.braveWalletService.$.bindNewPipeAndPassReceiver(), - this.braveWalletP3A.$.bindNewPipeAndPassReceiver()) + this.braveWalletP3A.$.bindNewPipeAndPassReceiver(), + this.braveWalletPinService.$.bindNewPipeAndPassReceiver(), + this.braveWalletAutoPinService.$.bindNewPipeAndPassReceiver()) } } diff --git a/components/ipfs/BUILD.gn b/components/ipfs/BUILD.gn index 51b520e65dc8..6bc43e8daaaf 100644 --- a/components/ipfs/BUILD.gn +++ b/components/ipfs/BUILD.gn @@ -73,6 +73,12 @@ static_library("ipfs") { "ipfs_onboarding_page.h", "keys/ipns_keys_manager.cc", "keys/ipns_keys_manager.h", + "pin/ipfs_base_pin_service.cc", + "pin/ipfs_base_pin_service.h", + "pin/ipfs_local_pin_service.cc", + "pin/ipfs_local_pin_service.h", + "pin/ipfs_pin_rpc_types.cc", + "pin/ipfs_pin_rpc_types.h", ] deps += [ "//brave/components/l10n/common", diff --git a/components/ipfs/ipfs_constants.cc b/components/ipfs/ipfs_constants.cc index 042a820b6c22..99da29342948 100644 --- a/components/ipfs/ipfs_constants.cc +++ b/components/ipfs/ipfs_constants.cc @@ -45,4 +45,10 @@ const char kFileValueName[] = "file"; const char kFileMimeType[] = "application/octet-stream"; const char kDirectoryMimeType[] = "application/x-directory"; const char kIPFSImportTextMimeType[] = "application/octet-stream"; + +// Local pins +const char kAddPinPath[] = "api/v0/pin/add"; +const char kRemovePinPath[] = "api/v0/pin/rm"; +const char kGetPinsPath[] = "api/v0/pin/ls"; + } // namespace ipfs diff --git a/components/ipfs/ipfs_constants.h b/components/ipfs/ipfs_constants.h index ac201a4d3194..ade282d953e9 100644 --- a/components/ipfs/ipfs_constants.h +++ b/components/ipfs/ipfs_constants.h @@ -42,6 +42,12 @@ extern const char kFileValueName[]; extern const char kFileMimeType[]; extern const char kDirectoryMimeType[]; extern const char kIPFSImportTextMimeType[]; +extern const char kNodeInfoPath[]; + +// Local pins +extern const char kAddPinPath[]; +extern const char kRemovePinPath[]; +extern const char kGetPinsPath[]; // Keep it synced with IPFSResolveMethodTypes in // browser/resources/settings/brave_ipfs_page/brave_ipfs_page.js diff --git a/components/ipfs/ipfs_json_parser.cc b/components/ipfs/ipfs_json_parser.cc index 483ef1ca5036..a21c7816ad56 100644 --- a/components/ipfs/ipfs_json_parser.cc +++ b/components/ipfs/ipfs_json_parser.cc @@ -40,6 +40,132 @@ bool RemoveValueFromList(base::Value::List* root, const T& value_to_remove) { } // namespace +// static +// Response format /api/v0/pin/add +// { +// "Pins": [ +// "" +// ], +// "Progress": "" +// } +bool IPFSJSONParser::GetAddPinsResultFromJSON( + const std::string& json, + ipfs::AddPinResult* add_pin_result) { + absl::optional records_v = + base::JSONReader::Read(json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + + if (!records_v) { + VLOG(1) << "Invalid response, could not parse JSON, JSON is: " << json; + return false; + } + + const base::Value* pins_arr = records_v->FindKey("Pins"); + if (!pins_arr || !pins_arr->is_list()) { + VLOG(1) << "Invalid response, can not find Pins array."; + return false; + } + + auto progress = records_v->FindIntKey("Progress"); + if (progress) { + add_pin_result->progress = *progress; + } else { + add_pin_result->progress = -1; + } + + for (const base::Value& val : pins_arr->GetList()) { + add_pin_result->pins.push_back(val.GetString()); + } + return true; +} + +// static +// Response format /api/v0/pin/rm +// { +// "Pins": [ +// "" +// ] +// } +bool IPFSJSONParser::GetRemovePinsResultFromJSON( + const std::string& json, + ipfs::RemovePinResult* remove_pin_result) { + absl::optional records_v = + base::JSONReader::Read(json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + + if (!records_v) { + VLOG(1) << "Invalid response, could not parse JSON, JSON is: " << json; + return false; + } + + const base::Value* pins_arr = records_v->FindKey("Pins"); + if (!pins_arr || !pins_arr->is_list()) { + VLOG(1) << "Invalid response, can not find Pins array."; + return false; + } + + ipfs::RemovePinResult result; + for (const base::Value& val : pins_arr->GetList()) { + auto* val_as_str = val.GetIfString(); + if (!val_as_str) { + return false; + } + result.push_back(*val_as_str); + } + *remove_pin_result = result; + return true; +} + +// static +// Response format /api/v0/pin/ls +// { +// "PinLsList": { +// "Keys": { +// "": { +// "Type": "" +// } +// } +// }, +// "PinLsObject": { +// "Cid": "", +// "Type": "" +// } +// } +bool IPFSJSONParser::GetGetPinsResultFromJSON( + const std::string& json, + ipfs::GetPinsResult* get_pins_result) { + DCHECK(get_pins_result); + absl::optional records_v = + base::JSONReader::Read(json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + + if (!records_v) { + VLOG(1) << "Invalid response, could not parse JSON, JSON is: " << json; + return false; + } + + const base::Value* keys = records_v->FindKey("Keys"); + if (!keys || !keys->is_dict()) { + VLOG(1) << "Invalid response, can not find Keys in PinLsList dict."; + return false; + } + + for (const auto it : keys->GetDict()) { + if (!it.second.is_dict()) { + VLOG(1) << "Missing Type for " << it.first; + return false; + } + const std::string* type = it.second.FindStringKey("Type"); + if (!type) { + VLOG(1) << "Missing Type for " << it.first; + return false; + } + (*get_pins_result)[it.first] = *type; + } + + return true; +} + // static // Response Format for /api/v0/swarm/peers // { diff --git a/components/ipfs/ipfs_json_parser.h b/components/ipfs/ipfs_json_parser.h index a24f1aa39b31..ce2bd3708a1e 100644 --- a/components/ipfs/ipfs_json_parser.h +++ b/components/ipfs/ipfs_json_parser.h @@ -13,6 +13,7 @@ #include "brave/components/ipfs/addresses_config.h" #include "brave/components/ipfs/import/imported_data.h" #include "brave/components/ipfs/node_info.h" +#include "brave/components/ipfs/pin/ipfs_pin_rpc_types.h" #include "brave/components/ipfs/repo_stats.h" class IPFSJSONParser { @@ -42,6 +43,14 @@ class IPFSJSONParser { static std::string RemovePeerFromConfigJSON(const std::string& json, const std::string& peer_id, const std::string& address); + // Local pins + static bool GetAddPinsResultFromJSON(const std::string& json, + ipfs::AddPinResult* add_pin_result); + static bool GetGetPinsResultFromJSON(const std::string& json, + ipfs::GetPinsResult* result); + static bool GetRemovePinsResultFromJSON( + const std::string& json, + ipfs::RemovePinResult* add_pin_result); }; #endif // BRAVE_COMPONENTS_IPFS_IPFS_JSON_PARSER_H_ diff --git a/components/ipfs/ipfs_json_parser_unittest.cc b/components/ipfs/ipfs_json_parser_unittest.cc index ffb238a3b379..8b802ea793b3 100644 --- a/components/ipfs/ipfs_json_parser_unittest.cc +++ b/components/ipfs/ipfs_json_parser_unittest.cc @@ -378,3 +378,109 @@ TEST_F(IPFSJSONParserTest, RemovePeerFromConfigJSONTest) { "{\"Peering\":{\"Peers\":" "[{\"Addrs\":[\"/b\"],\"ID\":\"QmA\"}]}}"); } + +TEST_F(IPFSJSONParserTest, GetGetPinsResultFromJSONTest) { + { + std::string json = R"({})"; + ipfs::GetPinsResult result; + EXPECT_FALSE(IPFSJSONParser::GetGetPinsResultFromJSON(json, &result)); + } + + { + std::string json = R"({"Keys":{"QmA" : {"Type" : "Recursive"}}})"; + ipfs::GetPinsResult result; + EXPECT_TRUE(IPFSJSONParser::GetGetPinsResultFromJSON(json, &result)); + EXPECT_EQ(result.at("QmA"), "Recursive"); + } + + { + std::string json = R"({"Keys":{}})"; + ipfs::GetPinsResult result; + EXPECT_TRUE(IPFSJSONParser::GetGetPinsResultFromJSON(json, &result)); + EXPECT_TRUE(result.empty()); + } + + { + std::string json = R"({"Keys":[]})"; + ipfs::GetPinsResult result; + EXPECT_FALSE(IPFSJSONParser::GetGetPinsResultFromJSON(json, &result)); + } + + { + std::string json = + R"({"Keys":{"QmA" : {"Type" :"Recursive"}, "QmB" : {"Type" :"Direct"}}})"; + ipfs::GetPinsResult result; + EXPECT_TRUE(IPFSJSONParser::GetGetPinsResultFromJSON(json, &result)); + EXPECT_EQ(result.at("QmA"), "Recursive"); + EXPECT_EQ(result.at("QmB"), "Direct"); + } +} + +TEST_F(IPFSJSONParserTest, GetRemovePinsResultFromJSONTest) { + { + std::string json = R"({})"; + ipfs::RemovePinResult result; + EXPECT_FALSE(IPFSJSONParser::GetRemovePinsResultFromJSON(json, &result)); + } + + { + std::string json = R"({"Pins" : {}})"; + ipfs::RemovePinResult result; + EXPECT_FALSE(IPFSJSONParser::GetRemovePinsResultFromJSON(json, &result)); + } + + { + std::string json = R"({"Pins" : []})"; + ipfs::RemovePinResult result; + EXPECT_TRUE(IPFSJSONParser::GetRemovePinsResultFromJSON(json, &result)); + EXPECT_TRUE(result.empty()); + } + + { + std::string json = R"({"Pins" : ["QmA", "QmB"]})"; + ipfs::RemovePinResult result; + EXPECT_TRUE(IPFSJSONParser::GetRemovePinsResultFromJSON(json, &result)); + EXPECT_EQ(result.at(0), "QmA"); + EXPECT_EQ(result.at(1), "QmB"); + } +} + +TEST_F(IPFSJSONParserTest, GetAddPinsResultFromJSONTest) { + { + std::string json = R"({})"; + ipfs::AddPinResult result; + EXPECT_FALSE(IPFSJSONParser::GetAddPinsResultFromJSON(json, &result)); + } + + { + std::string json = R"({"Pins" : {}})"; + ipfs::AddPinResult result; + EXPECT_FALSE(IPFSJSONParser::GetAddPinsResultFromJSON(json, &result)); + } + + { + std::string json = R"({"Pins" : []})"; + ipfs::AddPinResult result; + EXPECT_TRUE(IPFSJSONParser::GetAddPinsResultFromJSON(json, &result)); + EXPECT_TRUE(result.pins.empty()); + EXPECT_EQ(result.progress, -1); + } + + { + std::string json = R"({"Pins" : ["QmA", "QmB"]})"; + ipfs::AddPinResult result; + EXPECT_TRUE(IPFSJSONParser::GetAddPinsResultFromJSON(json, &result)); + EXPECT_EQ(result.pins.at(0), "QmA"); + EXPECT_EQ(result.pins.at(1), "QmB"); + EXPECT_EQ(result.progress, -1); + } + + { + std::string json = R"({"Pins" : ["QmA", "QmB"], "Progress" : 10})"; + ipfs::AddPinResult result; + EXPECT_TRUE(IPFSJSONParser::GetAddPinsResultFromJSON(json, &result)); + EXPECT_EQ(result.pins.at(0), "QmA"); + EXPECT_EQ(result.pins.at(1), "QmB"); + EXPECT_EQ(result.progress, 10); + } +} diff --git a/components/ipfs/ipfs_service.cc b/components/ipfs/ipfs_service.cc index 582b7f847fac..65b0c8ac2f62 100644 --- a/components/ipfs/ipfs_service.cc +++ b/components/ipfs/ipfs_service.cc @@ -96,6 +96,8 @@ std::pair LoadConfigFileOnFileTaskRunner( namespace ipfs { +IpfsService::IpfsService() : ipfs_p3a_(nullptr, nullptr), weak_factory_(this) {} + IpfsService::IpfsService( PrefService* prefs, scoped_refptr url_loader_factory, @@ -141,7 +143,9 @@ IpfsService::~IpfsService() { ipfs_client_updater_->RemoveObserver(this); } #if BUILDFLAG(ENABLE_IPFS_LOCAL_NODE) - RemoveObserver(ipns_keys_manager_.get()); + if (observers_.HasObserver(ipns_keys_manager_.get())) { + RemoveObserver(ipns_keys_manager_.get()); + } #endif Shutdown(); } @@ -162,6 +166,7 @@ void IpfsService::RegisterProfilePrefs(PrefRegistrySimple* registry) { registry->RegisterStringPref(kIPFSPublicNFTGatewayAddress, kDefaultIPFSNFTGateway); registry->RegisterFilePathPref(kIPFSBinaryPath, base::FilePath()); + registry->RegisterDictionaryPref(kIPFSPinnedCids); } base::FilePath IpfsService::GetIpfsExecutablePath() const { @@ -371,6 +376,91 @@ void IpfsService::NotifyIpnsKeysLoaded(bool result) { } } +// Local pinning +void IpfsService::AddPin(const std::vector& cids, + bool recursive, + AddPinCallback callback) { + if (!IsDaemonLaunched()) { + std::move(callback).Run(false, absl::nullopt); + return; + } + + GURL gurl = server_endpoint_.Resolve(kAddPinPath); + for (const auto& cid : cids) { + gurl = net::AppendQueryParameter(gurl, kArgQueryParam, cid); + } + gurl = net::AppendQueryParameter(gurl, "recursive", + recursive ? "true" : "false"); + + auto url_loader = std::make_unique( + GetIpfsNetworkTrafficAnnotationTag(), url_loader_factory_); + auto iter = + requests_list_.insert(requests_list_.begin(), std::move(url_loader)); + + iter->get()->Request( + "POST", gurl, std::string(), std::string(), false, + base::BindOnce(&IpfsService::OnPinAddResult, base::Unretained(this), iter, + std::move(callback)), + {{net::HttpRequestHeaders::kOrigin, + url::Origin::Create(gurl).Serialize()}}); +} + +void IpfsService::RemovePin(const std::vector& cids, + RemovePinCallback callback) { + if (!IsDaemonLaunched()) { + std::move(callback).Run(false, absl::nullopt); + return; + } + + GURL gurl = server_endpoint_.Resolve(kRemovePinPath); + for (const auto& cid : cids) { + gurl = net::AppendQueryParameter(gurl, kArgQueryParam, cid); + } + LOG(ERROR) << "XXZZZ " << gurl.spec(); + + auto url_loader = std::make_unique( + GetIpfsNetworkTrafficAnnotationTag(), url_loader_factory_); + auto iter = + requests_list_.insert(requests_list_.begin(), std::move(url_loader)); + + iter->get()->Request( + "POST", gurl, std::string(), std::string(), false, + base::BindOnce(&IpfsService::OnPinRemoveResult, base::Unretained(this), + iter, std::move(callback)), + {{net::HttpRequestHeaders::kOrigin, + url::Origin::Create(gurl).Serialize()}}); +} + +void IpfsService::GetPins(const absl::optional>& cids, + const std::string& type, + bool quiet, + GetPinsCallback callback) { + if (!IsDaemonLaunched()) { + std::move(callback).Run(false, absl::nullopt); + return; + } + + GURL gurl = server_endpoint_.Resolve(kGetPinsPath); + if (cids) { + for (const auto& cid : cids.value()) { + gurl = net::AppendQueryParameter(gurl, kArgQueryParam, cid); + } + } + gurl = net::AppendQueryParameter(gurl, "type", type); + gurl = net::AppendQueryParameter(gurl, "quiet", quiet ? "true" : "false"); + + auto url_loader = std::make_unique( + GetIpfsNetworkTrafficAnnotationTag(), url_loader_factory_); + auto iter = + requests_list_.insert(requests_list_.begin(), std::move(url_loader)); + iter->get()->Request( + "POST", gurl, std::string(), std::string(), false, + base::BindOnce(&IpfsService::OnGetPinsResult, base::Unretained(this), + iter, std::move(callback)), + {{net::HttpRequestHeaders::kOrigin, + url::Origin::Create(gurl).Serialize()}}); +} + void IpfsService::ImportFileToIpfs(const base::FilePath& path, const std::string& key, ipfs::ImportCompletedCallback callback) { @@ -881,6 +971,100 @@ void IpfsService::OnPreWarmComplete( std::move(prewarm_callback_for_testing_).Run(); } +//{ +// "PinLsList": { +// "Keys": { +// "": { +// "Type": "" +// } +// } +// }, +// "PinLsObject": { +// "Cid": "", +// "Type": "" +// } +//} +void IpfsService::OnGetPinsResult( + APIRequestList::iterator iter, + GetPinsCallback callback, + api_request_helper::APIRequestResult response) { + int response_code = response.response_code(); + requests_list_.erase(iter); + + bool success = response.Is2XXResponseCode(); + + if (!success) { + VLOG(1) << "Fail to get pins, response_code = " << response_code; + std::move(callback).Run(false, absl::nullopt); + return; + } + + ipfs::GetPinsResult result; + bool parse_result = + IPFSJSONParser::GetGetPinsResultFromJSON(response.body(), &result); + if (!parse_result) { + VLOG(1) << "Fail to get pins, wrong format"; + std::move(callback).Run(false, absl::nullopt); + return; + } + + std::move(callback).Run(true, result); +} + +void IpfsService::OnPinAddResult( + APIRequestList::iterator iter, + AddPinCallback callback, + api_request_helper::APIRequestResult response) { + int response_code = response.response_code(); + requests_list_.erase(iter); + + bool success = response.Is2XXResponseCode(); + + if (!success) { + VLOG(1) << "Fail to add pin, response_code = " << response_code; + std::move(callback).Run(false, absl::nullopt); + return; + } + + ipfs::AddPinResult result; + bool parse_result = + IPFSJSONParser::GetAddPinsResultFromJSON(response.body(), &result); + if (!parse_result) { + VLOG(1) << "Fail to add pin service, wrong format"; + std::move(callback).Run(false, absl::nullopt); + return; + } + + std::move(callback).Run(true, result); +} + +void IpfsService::OnPinRemoveResult( + APIRequestList::iterator iter, + RemovePinCallback callback, + api_request_helper::APIRequestResult response) { + int response_code = response.response_code(); + requests_list_.erase(iter); + + bool success = response.Is2XXResponseCode(); + + if (!success) { + VLOG(1) << "Fail to remove pin, response_code = " << response_code; + std::move(callback).Run(false, absl::nullopt); + return; + } + + ipfs::RemovePinResult result; + bool parse_result = + IPFSJSONParser::GetRemovePinsResultFromJSON(response.body(), &result); + if (!parse_result) { + VLOG(1) << "Fail to remove pin, wrong response format"; + std::move(callback).Run(false, absl::nullopt); + return; + } + + std::move(callback).Run(true, result); +} + void IpfsService::ValidateGateway(const GURL& url, BoolCallback callback) { GURL::Replacements replacements; std::string path = "/ipfs/"; diff --git a/components/ipfs/ipfs_service.h b/components/ipfs/ipfs_service.h index 64462cc499db..3a5897b23304 100644 --- a/components/ipfs/ipfs_service.h +++ b/components/ipfs/ipfs_service.h @@ -27,6 +27,7 @@ #include "brave/components/ipfs/ipfs_dns_resolver.h" #include "brave/components/ipfs/ipfs_p3a.h" #include "brave/components/ipfs/node_info.h" +#include "brave/components/ipfs/pin/ipfs_pin_rpc_types.h" #include "brave/components/ipfs/repo_stats.h" #include "brave/components/services/ipfs/public/mojom/ipfs_service.mojom.h" #include "components/keyed_service/core/keyed_service.h" @@ -81,6 +82,13 @@ class IpfsService : public KeyedService, base::OnceCallback; using GarbageCollectionCallback = base::OnceCallback; + // Local pins + using AddPinCallback = + base::OnceCallback)>; + using RemovePinCallback = + base::OnceCallback)>; + using GetPinsCallback = + base::OnceCallback)>; using BoolCallback = base::OnceCallback; using GetConfigCallback = base::OnceCallback; @@ -112,6 +120,17 @@ class IpfsService : public KeyedService, virtual void PreWarmShareableLink(const GURL& url); #if BUILDFLAG(ENABLE_IPFS_LOCAL_NODE) + // Local pins + virtual void AddPin(const std::vector& cids, + bool recursive, + AddPinCallback callback); + virtual void RemovePin(const std::vector& cid, + RemovePinCallback callback); + virtual void GetPins(const absl::optional>& cid, + const std::string& type, + bool quiet, + GetPinsCallback callback); + virtual void ImportFileToIpfs(const base::FilePath& path, const std::string& key, ipfs::ImportCompletedCallback callback); @@ -131,12 +150,12 @@ class IpfsService : public KeyedService, const base::FilePath& target_path, BoolCallback callback); #endif - void GetConnectedPeers(GetConnectedPeersCallback callback, - int retries = kPeersDefaultRetries); + virtual void GetConnectedPeers(GetConnectedPeersCallback callback, + int retries = kPeersDefaultRetries); void GetAddressesConfig(GetAddressesConfigCallback callback); virtual void LaunchDaemon(BoolCallback callback); void ShutdownDaemon(BoolCallback callback); - void StartDaemonAndLaunch(base::OnceCallback callback); + virtual void StartDaemonAndLaunch(base::OnceCallback callback); void GetConfig(GetConfigCallback); void GetRepoStats(GetRepoStatsCallback callback); void GetNodeInfo(GetNodeInfoCallback callback); @@ -158,6 +177,7 @@ class IpfsService : public KeyedService, IpnsKeysManager* GetIpnsKeysManager() { return ipns_keys_manager_.get(); } #endif protected: + IpfsService(); void OnConfigLoaded(GetConfigCallback, const std::pair&); private: @@ -189,6 +209,18 @@ class IpfsService : public KeyedService, BoolCallback callback); #endif base::TimeDelta CalculatePeersRetryTime(); + + // Local pins + void OnGetPinsResult(APIRequestList::iterator iter, + GetPinsCallback callback, + api_request_helper::APIRequestResult response); + void OnPinAddResult(APIRequestList::iterator iter, + AddPinCallback callback, + api_request_helper::APIRequestResult response); + void OnPinRemoveResult(APIRequestList::iterator iter, + RemovePinCallback callback, + api_request_helper::APIRequestResult response); + void OnGatewayValidationComplete(SimpleURLLoaderList::iterator iter, BoolCallback callback, const GURL& initial_url, @@ -212,6 +244,7 @@ class IpfsService : public KeyedService, api_request_helper::APIRequestResult responsey); void OnPreWarmComplete(APIRequestList::iterator iter, api_request_helper::APIRequestResult response); + std::string GetStorageSize(); void OnDnsConfigChanged(absl::optional dns_server); diff --git a/components/ipfs/pin/ipfs_base_pin_service.cc b/components/ipfs/pin/ipfs_base_pin_service.cc new file mode 100644 index 000000000000..50618cfaeb94 --- /dev/null +++ b/components/ipfs/pin/ipfs_base_pin_service.cc @@ -0,0 +1,99 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/ipfs/pin/ipfs_base_pin_service.h" + +#include "brave/components/ipfs/ipfs_utils.h" +#include "brave/components/ipfs/pref_names.h" + +namespace ipfs { + +IpfsBaseJob::IpfsBaseJob() {} + +IpfsBaseJob::~IpfsBaseJob() {} + +IpfsBasePinService::IpfsBasePinService(PrefService* pref_service, + IpfsService* ipfs_service) + : pref_service_(pref_service), ipfs_service_(ipfs_service) { + ipfs_service_->AddObserver(this); + pref_change_registrar_.Init(pref_service_); + pref_change_registrar_.Add( + kIPFSResolveMethod, + base::BindRepeating(&IpfsBasePinService::MaybeStartDaemon, + base::Unretained(this))); +} + +IpfsBasePinService::IpfsBasePinService() {} + +IpfsBasePinService::~IpfsBasePinService() {} + +// For unit tests +void IpfsBasePinService::RemovePrefListenersForTests() { + pref_change_registrar_.RemoveAll(); +} + +void IpfsBasePinService::OnIpfsShutdown() { + daemon_ready_ = false; +} + +void IpfsBasePinService::OnGetConnectedPeers( + bool success, + const std::vector& peers) { + if (success) { + daemon_ready_ = true; + DoNextJob(); + } +} + +void IpfsBasePinService::AddJob(std::unique_ptr job) { + jobs_.push(std::move(job)); + if (!current_job_) { + DoNextJob(); + } +} + +void IpfsBasePinService::DoNextJob() { + if (jobs_.empty()) { + return; + } + + if (!IsDaemonReady()) { + MaybeStartDaemon(); + return; + } + + current_job_ = std::move(jobs_.front()); + jobs_.pop(); + + current_job_->Start(); +} + +void IpfsBasePinService::OnJobDone(bool result) { + current_job_.reset(); + DoNextJob(); +} + +bool IpfsBasePinService::IsDaemonReady() { + return daemon_ready_; +} + +void IpfsBasePinService::MaybeStartDaemon() { + if (daemon_ready_) { + return; + } + + if (!ipfs::IsLocalGatewayConfigured(pref_service_)) { + return; + } + + ipfs_service_->StartDaemonAndLaunch(base::BindOnce( + &IpfsBasePinService::OnDaemonStarted, base::Unretained(this))); +} + +void IpfsBasePinService::OnDaemonStarted() { + ipfs_service_->GetConnectedPeers(base::NullCallback(), 2); +} + +} // namespace ipfs diff --git a/components/ipfs/pin/ipfs_base_pin_service.h b/components/ipfs/pin/ipfs_base_pin_service.h new file mode 100644 index 000000000000..bf46354819f9 --- /dev/null +++ b/components/ipfs/pin/ipfs_base_pin_service.h @@ -0,0 +1,62 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_IPFS_PIN_IPFS_BASE_PIN_SERVICE_H_ +#define BRAVE_COMPONENTS_IPFS_PIN_IPFS_BASE_PIN_SERVICE_H_ + +#include +#include +#include +#include +#include + +#include "brave/components/ipfs/ipfs_service.h" +#include "components/prefs/pref_change_registrar.h" +#include "components/prefs/pref_service.h" + +namespace ipfs { + +class IpfsBaseJob { + public: + IpfsBaseJob(); + virtual ~IpfsBaseJob(); + virtual void Start() = 0; +}; + +class IpfsBasePinService : public IpfsServiceObserver { + public: + IpfsBasePinService(PrefService* pref_service, IpfsService* service); + ~IpfsBasePinService() override; + + virtual void AddJob(std::unique_ptr job); + void OnJobDone(bool result); + + void OnIpfsShutdown() override; + void OnGetConnectedPeers(bool succes, + const std::vector& peers) override; + + void RemovePrefListenersForTests(); + + protected: + // For testing + IpfsBasePinService(); + + private: + bool IsDaemonReady(); + void MaybeStartDaemon(); + void OnDaemonStarted(); + void DoNextJob(); + + bool daemon_ready_ = false; + PrefService* pref_service_; + IpfsService* ipfs_service_; + PrefChangeRegistrar pref_change_registrar_; + std::unique_ptr current_job_; + std::queue> jobs_; +}; + +} // namespace ipfs + +#endif // BRAVE_COMPONENTS_IPFS_PIN_IPFS_BASE_PIN_SERVICE_H_ diff --git a/components/ipfs/pin/ipfs_base_pin_service_unittest.cc b/components/ipfs/pin/ipfs_base_pin_service_unittest.cc new file mode 100644 index 000000000000..77811a2b5314 --- /dev/null +++ b/components/ipfs/pin/ipfs_base_pin_service_unittest.cc @@ -0,0 +1,112 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/ipfs/pin/ipfs_base_pin_service.h" + +#include +#include + +#include "base/test/bind.h" +#include "brave/components/ipfs/ipfs_service.h" +#include "brave/components/ipfs/pref_names.h" +#include "components/prefs/testing_pref_service.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; + +namespace ipfs { + +class MockIpfsService : public IpfsService { + public: + MockIpfsService() = default; + + ~MockIpfsService() override = default; + + MOCK_METHOD1(StartDaemonAndLaunch, void(base::OnceCallback)); + MOCK_METHOD2(GetConnectedPeers, + void(IpfsService::GetConnectedPeersCallback, int)); +}; + +class IpfsBasePinServiceTest : public testing::Test { + public: + IpfsBasePinServiceTest() = default; + + void SetUp() override { + auto* registry = pref_service_.registry(); + IpfsService::RegisterProfilePrefs(registry); + ipfs_base_pin_service_ = + std::make_unique(GetPrefs(), GetIpfsService()); + } + + void TearDown() override { + ipfs_base_pin_service_->RemovePrefListenersForTests(); + } + + void SetLocalNodeEnabled(bool enabled) { + GetPrefs()->SetInteger( + kIPFSResolveMethod, + static_cast(enabled ? IPFSResolveMethodTypes::IPFS_LOCAL + : IPFSResolveMethodTypes::IPFS_DISABLED)); + } + + PrefService* GetPrefs() { return &pref_service_; } + + testing::NiceMock* GetIpfsService() { + return &ipfs_service_; + } + + IpfsBasePinService* service() { return ipfs_base_pin_service_.get(); } + + private: + std::unique_ptr ipfs_base_pin_service_; + testing::NiceMock ipfs_service_; + TestingPrefServiceSimple pref_service_; + content::BrowserTaskEnvironment task_environment_; +}; + +class MockJob : public IpfsBaseJob { + public: + explicit MockJob(base::OnceCallback callback) { + callback_ = std::move(callback); + } + + void Start() override { + if (callback_) { + std::move(callback_).Run(); + } + } + + private: + base::OnceCallback callback_; +}; + +TEST_F(IpfsBasePinServiceTest, TasksExecuted) { + service()->OnGetConnectedPeers(true, {}); + absl::optional method_called; + std::unique_ptr first_job = std::make_unique( + base::BindLambdaForTesting([&method_called]() { method_called = true; })); + service()->AddJob(std::move(first_job)); + EXPECT_TRUE(method_called.value()); + + absl::optional second_method_called; + std::unique_ptr second_job = + std::make_unique(base::BindLambdaForTesting( + [&second_method_called]() { second_method_called = true; })); + service()->AddJob(std::move(second_job)); + EXPECT_FALSE(second_method_called.has_value()); + + service()->OnJobDone(true); + EXPECT_TRUE(second_method_called.value()); +} + +TEST_F(IpfsBasePinServiceTest, LaunchDaemon_AfterSettingChange) { + SetLocalNodeEnabled(false); + EXPECT_CALL(*GetIpfsService(), StartDaemonAndLaunch(_)).Times(1); + SetLocalNodeEnabled(true); +} + +} // namespace ipfs diff --git a/components/ipfs/pin/ipfs_local_pin_service.cc b/components/ipfs/pin/ipfs_local_pin_service.cc new file mode 100644 index 000000000000..3e02edf6e4b3 --- /dev/null +++ b/components/ipfs/pin/ipfs_local_pin_service.cc @@ -0,0 +1,284 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/ipfs/pin/ipfs_local_pin_service.h" + +#include +#include +#include + +#include "brave/components/ipfs/pref_names.h" +#include "components/prefs/scoped_user_pref_update.h" + +namespace ipfs { + +namespace { +const char kRecursiveMode[] = "recursive"; +} // namespace + +AddLocalPinJob::AddLocalPinJob(PrefService* prefs_service, + IpfsService* ipfs_service, + const std::string& prefix, + const std::vector& cids, + AddPinCallback callback) + : prefs_service_(prefs_service), + ipfs_service_(ipfs_service), + prefix_(prefix), + cids_(cids), + callback_(std::move(callback)) {} + +AddLocalPinJob::~AddLocalPinJob() {} + +void AddLocalPinJob::Start() { + ipfs_service_->AddPin( + cids_, true, + base::BindOnce(&AddLocalPinJob::OnAddPinResult, base::Unretained(this))); +} + +void AddLocalPinJob::OnAddPinResult(bool status, + absl::optional result) { + if (status && result) { + for (const auto& cid : cids_) { + if (std::find(result->pins.begin(), result->pins.end(), cid) == + result->pins.end()) { + std::move(callback_).Run(false); + return; + } + } + + { + DictionaryPrefUpdate update(prefs_service_, kIPFSPinnedCids); + base::Value::Dict& update_dict = update->GetDict(); + + for (const auto& cid : cids_) { + base::Value::List* list = update_dict.FindList(cid); + if (!list) { + update_dict.Set(cid, base::Value::List()); + list = update_dict.FindList(cid); + } + DCHECK(list); + list->EraseValue(base::Value(prefix_)); + list->Append(base::Value(prefix_)); + } + } + std::move(callback_).Run(true); + } else { + std::move(callback_).Run(false); + } +} + +RemoveLocalPinJob::RemoveLocalPinJob(PrefService* prefs_service, + const std::string& prefix, + RemovePinCallback callback) + : prefs_service_(prefs_service), + prefix_(prefix), + callback_(std::move(callback)) {} + +RemoveLocalPinJob::~RemoveLocalPinJob() {} + +void RemoveLocalPinJob::Start() { + { + DictionaryPrefUpdate update(prefs_service_, kIPFSPinnedCids); + base::Value::Dict& update_dict = update->GetDict(); + + std::vector remove_list; + for (auto pair : update_dict) { + base::Value::List* list = pair.second.GetIfList(); + if (list) { + list->EraseValue(base::Value(prefix_)); + if (list->empty()) { + remove_list.push_back(pair.first); + } + } + } + for (const auto& cid : remove_list) { + update_dict.Remove(cid); + } + } + std::move(callback_).Run(true); +} + +VerifyLocalPinJob::VerifyLocalPinJob(PrefService* prefs_service, + IpfsService* ipfs_service, + const std::string& prefix, + const std::vector& cids, + ValidatePinsCallback callback) + : prefs_service_(prefs_service), + ipfs_service_(ipfs_service), + prefix_(prefix), + cids_(cids), + callback_(std::move(callback)) {} + +VerifyLocalPinJob::~VerifyLocalPinJob() {} + +void VerifyLocalPinJob::Start() { + ipfs_service_->GetPins(absl::nullopt, kRecursiveMode, true, + base::BindOnce(&VerifyLocalPinJob::OnGetPinsResult, + base::Unretained(this))); +} + +void VerifyLocalPinJob::OnGetPinsResult(bool status, + absl::optional result) { + if (status && result) { + DictionaryPrefUpdate update(prefs_service_, kIPFSPinnedCids); + base::Value::Dict& update_dict = update->GetDict(); + + bool verification_pased = true; + for (const auto& cid : cids_) { + base::Value::List* list = update_dict.FindList(cid); + if (!list) { + verification_pased = false; + } else { + if (result->find(cid) != result->end()) { + list->EraseValue(base::Value(prefix_)); + list->Append(base::Value(prefix_)); + } else { + verification_pased = false; + list->EraseValue(base::Value(prefix_)); + } + if (list->empty()) { + update_dict.Remove(cid); + } + } + } + std::move(callback_).Run(verification_pased); + } else { + std::move(callback_).Run(absl::nullopt); + } +} + +GcJob::GcJob(PrefService* prefs_service, + IpfsService* ipfs_service, + GcCallback callback) + : prefs_service_(prefs_service), + ipfs_service_(ipfs_service), + callback_(std::move(callback)) {} +GcJob::~GcJob() {} + +void GcJob::Start() { + ipfs_service_->GetPins( + absl::nullopt, kRecursiveMode, true, + base::BindOnce(&GcJob::OnGetPinsResult, base::Unretained(this))); +} + +void GcJob::OnGetPinsResult(bool status, absl::optional result) { + std::vector cids_to_delete; + if (status && result) { + const base::Value::Dict& dict = prefs_service_->GetDict(kIPFSPinnedCids); + for (const auto& pair : result.value()) { + const base::Value::List* list = dict.FindList(pair.first); + if (!list || list->empty()) { + cids_to_delete.push_back(pair.first); + } + } + + if (!cids_to_delete.empty()) { + ipfs_service_->RemovePin( + cids_to_delete, + base::BindOnce(&GcJob::OnPinsRemovedResult, base::Unretained(this))); + } else { + std::move(callback_).Run(true); + } + } else { + std::move(callback_).Run(false); + } +} + +void GcJob::OnPinsRemovedResult(bool status, + absl::optional result) { + if (status && result) { + std::move(callback_).Run(true); + } else { + std::move(callback_).Run(false); + } +} + +IpfsLocalPinService::IpfsLocalPinService(PrefService* prefs_service, + IpfsService* ipfs_service) + : prefs_service_(prefs_service), ipfs_service_(ipfs_service) { + ipfs_base_pin_service_ = + std::make_unique(prefs_service_, ipfs_service_); + base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&IpfsLocalPinService::AddGcTask, base::Unretained(this)), + base::Minutes(1)); +} + +IpfsLocalPinService::IpfsLocalPinService() {} + +void IpfsLocalPinService::SetIpfsBasePinServiceForTesting( + std::unique_ptr service) { + ipfs_base_pin_service_ = std::move(service); +} + +IpfsLocalPinService::~IpfsLocalPinService() {} + +void IpfsLocalPinService::AddPins(const std::string& prefix, + const std::vector& cids, + AddPinCallback callback) { + ipfs_base_pin_service_->AddJob(std::make_unique( + prefs_service_, ipfs_service_, prefix, cids, + base::BindOnce(&IpfsLocalPinService::OnAddJobFinished, + base::Unretained(this), std::move(callback)))); +} + +void IpfsLocalPinService::RemovePins(const std::string& prefix, + RemovePinCallback callback) { + ipfs_base_pin_service_->AddJob(std::make_unique( + prefs_service_, prefix, + base::BindOnce(&IpfsLocalPinService::OnRemovePinsFinished, + base::Unretained(this), std::move(callback)))); +} + +void IpfsLocalPinService::ValidatePins(const std::string& prefix, + const std::vector& cids, + ValidatePinsCallback callback) { + ipfs_base_pin_service_->AddJob(std::make_unique( + prefs_service_, ipfs_service_, prefix, cids, + base::BindOnce(&IpfsLocalPinService::OnValidateJobFinished, + base::Unretained(this), std::move(callback)))); +} + +void IpfsLocalPinService::OnRemovePinsFinished(RemovePinCallback callback, + bool status) { + std::move(callback).Run(status); + if (status) { + base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&IpfsLocalPinService::AddGcTask, base::Unretained(this)), + base::Minutes(1)); + } + ipfs_base_pin_service_->OnJobDone(status); +} + +void IpfsLocalPinService::OnAddJobFinished(AddPinCallback callback, + bool status) { + std::move(callback).Run(status); + ipfs_base_pin_service_->OnJobDone(status); +} + +void IpfsLocalPinService::OnValidateJobFinished(ValidatePinsCallback callback, + absl::optional status) { + std::move(callback).Run(status); + ipfs_base_pin_service_->OnJobDone(status.value_or(false)); +} + +void IpfsLocalPinService::AddGcTask() { + if (gc_task_posted_) { + return; + } + gc_task_posted_ = true; + ipfs_base_pin_service_->AddJob(std::make_unique( + prefs_service_, ipfs_service_, + base::BindOnce(&IpfsLocalPinService::OnGcFinishedCallback, + base::Unretained(this)))); +} + +void IpfsLocalPinService::OnGcFinishedCallback(bool status) { + gc_task_posted_ = false; + ipfs_base_pin_service_->OnJobDone(status); +} + +} // namespace ipfs diff --git a/components/ipfs/pin/ipfs_local_pin_service.h b/components/ipfs/pin/ipfs_local_pin_service.h new file mode 100644 index 000000000000..826673b8192d --- /dev/null +++ b/components/ipfs/pin/ipfs_local_pin_service.h @@ -0,0 +1,135 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_IPFS_PIN_IPFS_LOCAL_PIN_SERVICE_H_ +#define BRAVE_COMPONENTS_IPFS_PIN_IPFS_LOCAL_PIN_SERVICE_H_ + +#include +#include +#include + +#include "brave/components/ipfs/ipfs_service.h" +#include "brave/components/ipfs/pin/ipfs_base_pin_service.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +using ipfs::IpfsService; + +namespace ipfs { + +using AddPinCallback = base::OnceCallback; +using RemovePinCallback = base::OnceCallback; +using ValidatePinsCallback = base::OnceCallback)>; +using GcCallback = base::OnceCallback; + +class AddLocalPinJob : public IpfsBaseJob { + public: + AddLocalPinJob(PrefService* prefs_service, + IpfsService* ipfs_service, + const std::string& prefix, + const std::vector& cids, + AddPinCallback callback); + ~AddLocalPinJob() override; + + void Start() override; + + private: + void OnAddPinResult(bool status, absl::optional result); + + PrefService* prefs_service_; + IpfsService* ipfs_service_; + std::string prefix_; + std::vector cids_; + AddPinCallback callback_; +}; + +class RemoveLocalPinJob : public IpfsBaseJob { + public: + RemoveLocalPinJob(PrefService* prefs_service, + const std::string& prefix, + RemovePinCallback callback); + ~RemoveLocalPinJob() override; + + void Start() override; + + private: + PrefService* prefs_service_; + std::string prefix_; + RemovePinCallback callback_; +}; + +class VerifyLocalPinJob : public IpfsBaseJob { + public: + VerifyLocalPinJob(PrefService* prefs_service, + IpfsService* ipfs_service, + const std::string& prefix, + const std::vector& cids, + ValidatePinsCallback callback); + ~VerifyLocalPinJob() override; + + void Start() override; + + private: + void OnGetPinsResult(bool status, absl::optional result); + + PrefService* prefs_service_; + IpfsService* ipfs_service_; + std::string prefix_; + std::vector cids_; + ValidatePinsCallback callback_; +}; + +class GcJob : public IpfsBaseJob { + public: + GcJob(PrefService* prefs_service, + IpfsService* ipfs_service, + GcCallback callback); + ~GcJob() override; + + void Start() override; + + private: + void OnGetPinsResult(bool status, absl::optional result); + void OnPinsRemovedResult(bool status, absl::optional result); + + PrefService* prefs_service_; + IpfsService* ipfs_service_; + GcCallback callback_; +}; + +class IpfsLocalPinService : public KeyedService { + public: + IpfsLocalPinService(PrefService* prefs_service, IpfsService* ipfs_service); + // For testing + IpfsLocalPinService(); + ~IpfsLocalPinService() override; + + virtual void AddPins(const std::string& prefix, + const std::vector& cids, + AddPinCallback callback); + virtual void RemovePins(const std::string& prefix, + RemovePinCallback callback); + virtual void ValidatePins(const std::string& prefix, + const std::vector& cids, + ValidatePinsCallback callback); + + void SetIpfsBasePinServiceForTesting(std::unique_ptr); + + private: + void AddGcTask(); + void OnRemovePinsFinished(RemovePinCallback callback, bool status); + void OnAddJobFinished(AddPinCallback callback, bool status); + void OnValidateJobFinished(ValidatePinsCallback callback, + absl::optional status); + void OnGcFinishedCallback(bool status); + + bool gc_task_posted_ = false; + std::unique_ptr ipfs_base_pin_service_; + PrefService* prefs_service_; + IpfsService* ipfs_service_; +}; + +} // namespace ipfs + +#endif // BRAVE_COMPONENTS_IPFS_PIN_IPFS_LOCAL_PIN_SERVICE_H_ diff --git a/components/ipfs/pin/ipfs_local_pin_service_unittest.cc b/components/ipfs/pin/ipfs_local_pin_service_unittest.cc new file mode 100644 index 000000000000..f28fa02fd2c0 --- /dev/null +++ b/components/ipfs/pin/ipfs_local_pin_service_unittest.cc @@ -0,0 +1,457 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/ipfs/pin/ipfs_local_pin_service.h" + +#include +#include + +#include "base/json/json_reader.h" +#include "base/test/bind.h" +#include "brave/components/ipfs/ipfs_service.h" +#include "brave/components/ipfs/pref_names.h" +#include "components/prefs/testing_pref_service.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; + +namespace ipfs { + +class MockIpfsService : public IpfsService { + public: + MockIpfsService() = default; + + ~MockIpfsService() override = default; + + MOCK_METHOD3(AddPin, + void(const std::vector& cids, + bool recursive, + IpfsService::AddPinCallback callback)); + MOCK_METHOD2(RemovePin, + void(const std::vector& cid, + IpfsService::RemovePinCallback callback)); + MOCK_METHOD4(GetPins, + void(const absl::optional>& cid, + const std::string& type, + bool quiet, + IpfsService::GetPinsCallback callback)); +}; + +class MockIpfsBasePinService : public IpfsBasePinService { + public: + MockIpfsBasePinService() = default; + void AddJob(std::unique_ptr job) override { + std::move(job)->Start(); + } +}; + +class IpfsLocalPinServiceTest : public testing::Test { + public: + IpfsLocalPinServiceTest() = default; + + IpfsLocalPinService* service() { return ipfs_local_pin_service_.get(); } + + protected: + void SetUp() override { + auto* registry = pref_service_.registry(); + IpfsService::RegisterProfilePrefs(registry); + ipfs_local_pin_service_ = + std::make_unique(GetPrefs(), GetIpfsService()); + ipfs_local_pin_service_->SetIpfsBasePinServiceForTesting( + std::make_unique()); + } + + PrefService* GetPrefs() { return &pref_service_; } + + testing::NiceMock* GetIpfsService() { + return &ipfs_service_; + } + + std::unique_ptr ipfs_local_pin_service_; + testing::NiceMock ipfs_service_; + TestingPrefServiceSimple pref_service_; + content::BrowserTaskEnvironment task_environment_; +}; + +TEST_F(IpfsLocalPinServiceTest, AddLocalPinJobTest) { + { + ON_CALL(*GetIpfsService(), AddPin(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::vector& cids, bool recursive, + IpfsService::AddPinCallback callback) { + AddPinResult result; + result.pins = cids; + std::move(callback).Run(true, result); + })); + + absl::optional success; + service()->AddPins("a", {"Qma", "Qmb", "Qmc"}, + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + std::string expected = R"({ + "Qma" : ["a"], + "Qmb" : ["a"], + "Qmc" : ["a"] + })"; + absl::optional expected_value = base::JSONReader::Read( + expected, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + EXPECT_EQ(expected_value.value(), GetPrefs()->GetDict(kIPFSPinnedCids)); + EXPECT_TRUE(success.value()); + } + + { + ON_CALL(*GetIpfsService(), AddPin(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::vector& cids, bool recursive, + IpfsService::AddPinCallback callback) { + AddPinResult result; + result.pins = cids; + std::move(callback).Run(true, result); + })); + + absl::optional success; + service()->AddPins("b", {"Qma", "Qmb", "Qmc", "Qmd"}, + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + std::string expected = R"({ + "Qma" : ["a", "b"], + "Qmb" : ["a", "b"], + "Qmc" : ["a", "b"], + "Qmd" : ["b"] + })"; + absl::optional expected_value = base::JSONReader::Read( + expected, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + EXPECT_EQ(expected_value.value(), GetPrefs()->GetDict(kIPFSPinnedCids)); + EXPECT_TRUE(success.value()); + } + + { + ON_CALL(*GetIpfsService(), AddPin(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::vector& cids, bool recursive, + IpfsService::AddPinCallback callback) { + AddPinResult result; + result.pins = {"Qma", "Qmb", "Qmc"}; + std::move(callback).Run(true, result); + })); + + absl::optional success; + service()->AddPins("c", {"Qma", "Qmb", "Qmc", "Qmd", "Qme"}, + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + std::string expected = R"({ + "Qma" : ["a", "b"], + "Qmb" : ["a", "b"], + "Qmc" : ["a", "b"], + "Qmd" : ["b"] + })"; + absl::optional expected_value = base::JSONReader::Read( + expected, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + EXPECT_EQ(expected_value.value(), GetPrefs()->GetDict(kIPFSPinnedCids)); + EXPECT_FALSE(success.value()); + } + + { + ON_CALL(*GetIpfsService(), AddPin(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::vector& cids, bool recursive, + IpfsService::AddPinCallback callback) { + std::move(callback).Run(false, absl::nullopt); + })); + + absl::optional success; + service()->AddPins("c", {"Qma", "Qmb", "Qmc", "Qmd", "Qme"}, + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + std::string expected = R"({ + "Qma" : ["a", "b"], + "Qmb" : ["a", "b"], + "Qmc" : ["a", "b"], + "Qmd" : ["b"] + })"; + absl::optional expected_value = base::JSONReader::Read( + expected, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + EXPECT_EQ(expected_value.value(), GetPrefs()->GetDict(kIPFSPinnedCids)); + EXPECT_FALSE(success.value()); + } +} + +TEST_F(IpfsLocalPinServiceTest, RemoveLocalPinJobTest) { + { + std::string base = R"({ + "Qma" : ["a", "b"], + "Qmb" : ["a", "b"], + "Qmc" : ["a", "b"], + "Qmd" : ["b"] + })"; + absl::optional base_value = base::JSONReader::Read( + base, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + GetPrefs()->SetDict(kIPFSPinnedCids, base_value.value().GetDict().Clone()); + } + + { + absl::optional success; + service()->RemovePins("a", + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + std::string expected = R"({ + "Qma" : ["b"], + "Qmb" : ["b"], + "Qmc" : ["b"], + "Qmd" : ["b"] + })"; + absl::optional expected_value = base::JSONReader::Read( + expected, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + EXPECT_EQ(expected_value.value(), GetPrefs()->GetDict(kIPFSPinnedCids)); + EXPECT_TRUE(success.value()); + } + + { + absl::optional success; + service()->RemovePins("c", + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + std::string expected = R"({ + "Qma" : ["b"], + "Qmb" : ["b"], + "Qmc" : ["b"], + "Qmd" : ["b"] + })"; + absl::optional expected_value = base::JSONReader::Read( + expected, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + EXPECT_EQ(expected_value.value(), GetPrefs()->GetDict(kIPFSPinnedCids)); + EXPECT_TRUE(success.value()); + } + + { + absl::optional success; + service()->RemovePins("b", + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + std::string expected = R"({ + })"; + absl::optional expected_value = base::JSONReader::Read( + expected, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + EXPECT_EQ(expected_value.value(), GetPrefs()->GetDict(kIPFSPinnedCids)); + EXPECT_TRUE(success.value()); + } +} + +TEST_F(IpfsLocalPinServiceTest, VerifyLocalPinJobTest) { + { + std::string base = R"({ + "Qma" : ["a", "b"], + "Qmb" : ["a", "b"], + "Qmc" : ["a", "b"], + "Qmd" : ["b"] + })"; + absl::optional base_value = base::JSONReader::Read( + base, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + GetPrefs()->SetDict(kIPFSPinnedCids, base_value.value().GetDict().Clone()); + } + + { + ON_CALL(*GetIpfsService(), GetPins(_, _, _, _)) + .WillByDefault(::testing::Invoke([](const absl::optional< + std::vector>& cid, + const std::string& type, bool quiet, + IpfsService::GetPinsCallback + callback) { + GetPinsResult result = {{"Qma", "Recursive"}, {"Qmb", "Recursive"}}; + std::move(callback).Run(true, result); + })); + + absl::optional success; + service()->ValidatePins( + "a", {"Qma", "Qmb", "Qmc"}, + base::BindLambdaForTesting([&success](absl::optional result) { + success = result.value(); + })); + + EXPECT_TRUE(success.has_value()); + EXPECT_FALSE(success.value()); + } + + { + ON_CALL(*GetIpfsService(), GetPins(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const absl::optional>& cid, + const std::string& type, bool quiet, + IpfsService::GetPinsCallback callback) { + GetPinsResult result = {{"Qma", "Recursive"}, + {"Qmb", "Recursive"}, + {"Qmc", "Recursive"}}; + std::move(callback).Run(true, result); + })); + + absl::optional success; + service()->ValidatePins( + "a", {"Qma", "Qmb", "Qmc"}, + base::BindLambdaForTesting([&success](absl::optional result) { + success = result.value(); + })); + EXPECT_TRUE(success.has_value()); + EXPECT_TRUE(success.value()); + } + + { + ON_CALL(*GetIpfsService(), GetPins(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const absl::optional>& cid, + const std::string& type, bool quiet, + IpfsService::GetPinsCallback callback) { + GetPinsResult result = {}; + std::move(callback).Run(true, result); + })); + + absl::optional success = false; + service()->ValidatePins( + "b", {"Qma", "Qmb", "Qmc", "Qmd"}, + base::BindLambdaForTesting([&success](absl::optional result) { + success = result.value(); + })); + + EXPECT_TRUE(success.has_value()); + + EXPECT_FALSE(success.value()); + } + + { + absl::optional success; + VerifyLocalPinJob job( + GetPrefs(), GetIpfsService(), "b", {"Qma", "Qmb", "Qmc", "Qmd"}, + base::BindLambdaForTesting( + [&success](absl::optional result) { success = result; })); + + ON_CALL(*GetIpfsService(), GetPins(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const absl::optional>& cid, + const std::string& type, bool quiet, + IpfsService::GetPinsCallback callback) { + std::move(callback).Run(false, absl::nullopt); + })); + + job.Start(); + + EXPECT_FALSE(success.has_value()); + } +} + +TEST_F(IpfsLocalPinServiceTest, GcJobTest) { + { + std::string base = R"({ + "Qma" : ["a", "b"], + "Qmb" : ["a", "b"], + "Qmc" : ["a", "b"], + "Qmd" : ["b"] + })"; + absl::optional base_value = base::JSONReader::Read( + base, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + GetPrefs()->SetDict(kIPFSPinnedCids, base_value.value().GetDict().Clone()); + } + + { + absl::optional success; + GcJob job(GetPrefs(), GetIpfsService(), + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + ON_CALL(*GetIpfsService(), GetPins(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const absl::optional>& cid, + const std::string& type, bool quiet, + IpfsService::GetPinsCallback callback) { + EXPECT_FALSE(cid.has_value()); + EXPECT_TRUE(quiet); + GetPinsResult result = {{"Qma", "Recursive"}, + {"Qmb", "Recursive"}, + {"Qmc", "Recursive"}}; + std::move(callback).Run(true, result); + })); + + EXPECT_CALL(*GetIpfsService(), RemovePin(_, _)).Times(0); + + job.Start(); + + EXPECT_TRUE(success.value()); + } + + { + absl::optional success; + GcJob job(GetPrefs(), GetIpfsService(), + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + ON_CALL(*GetIpfsService(), GetPins(_, _, _, _)) + .WillByDefault(::testing::Invoke([](const absl::optional< + std::vector>& cid, + const std::string& type, bool quiet, + IpfsService::GetPinsCallback + callback) { + EXPECT_FALSE(cid.has_value()); + EXPECT_TRUE(quiet); + GetPinsResult result = {{"Qm1", "Recursive"}, {"Qm2", "Recursive"}}; + std::move(callback).Run(true, result); + })); + EXPECT_CALL(*GetIpfsService(), RemovePin(_, _)).Times(1); + + ON_CALL(*GetIpfsService(), RemovePin(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::vector& cid, + IpfsService::RemovePinCallback callback) { + EXPECT_EQ(cid.size(), 2u); + EXPECT_EQ(cid[0], "Qm1"); + EXPECT_EQ(cid[1], "Qm2"); + RemovePinResult result = cid; + std::move(callback).Run(true, result); + })); + + job.Start(); + + EXPECT_TRUE(success.value()); + } + + { + absl::optional success; + GcJob job(GetPrefs(), GetIpfsService(), + base::BindLambdaForTesting( + [&success](bool result) { success = result; })); + + ON_CALL(*GetIpfsService(), GetPins(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const absl::optional>& cid, + const std::string& type, bool quiet, + IpfsService::GetPinsCallback callback) { + std::move(callback).Run(false, absl::nullopt); + })); + + EXPECT_CALL(*GetIpfsService(), RemovePin(_, _)).Times(0); + + job.Start(); + + EXPECT_FALSE(success.value()); + } +} + +} // namespace ipfs diff --git a/components/ipfs/pin/ipfs_pin_rpc_types.cc b/components/ipfs/pin/ipfs_pin_rpc_types.cc new file mode 100644 index 000000000000..b7707013080e --- /dev/null +++ b/components/ipfs/pin/ipfs_pin_rpc_types.cc @@ -0,0 +1,16 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/ipfs/pin/ipfs_pin_rpc_types.h" + +namespace ipfs { + +AddPinResult::AddPinResult() {} + +AddPinResult::~AddPinResult() {} + +AddPinResult::AddPinResult(const AddPinResult& other) = default; + +} // namespace ipfs diff --git a/components/ipfs/pin/ipfs_pin_rpc_types.h b/components/ipfs/pin/ipfs_pin_rpc_types.h new file mode 100644 index 000000000000..2ad0b362e107 --- /dev/null +++ b/components/ipfs/pin/ipfs_pin_rpc_types.h @@ -0,0 +1,29 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_IPFS_PIN_IPFS_PIN_RPC_TYPES_H_ +#define BRAVE_COMPONENTS_IPFS_PIN_IPFS_PIN_RPC_TYPES_H_ + +#include +#include +#include + +namespace ipfs { + +struct AddPinResult { + AddPinResult(); + ~AddPinResult(); + AddPinResult(const AddPinResult&); + std::vector pins; + int progress; +}; + +using GetPinsResult = std::map; + +using RemovePinResult = std::vector; + +} // namespace ipfs + +#endif // BRAVE_COMPONENTS_IPFS_PIN_IPFS_PIN_RPC_TYPES_H_ diff --git a/components/ipfs/pref_names.cc b/components/ipfs/pref_names.cc index a9fb7ee14422..b5f9ca819f96 100644 --- a/components/ipfs/pref_names.cc +++ b/components/ipfs/pref_names.cc @@ -44,3 +44,6 @@ const char kIPFSPublicGatewayAddress[] = "brave.ipfs.public_gateway_address"; // Stores IPFS public gateway address to be used when translating IPFS NFT URLs. const char kIPFSPublicNFTGatewayAddress[] = "brave.ipfs.public_nft_gateway_address"; + +// Stores list of CIDs that are pinned localy +const char kIPFSPinnedCids[] = "brave.ipfs.local_pinned_cids"; diff --git a/components/ipfs/pref_names.h b/components/ipfs/pref_names.h index f0b31b9f0641..3173d879bd68 100644 --- a/components/ipfs/pref_names.h +++ b/components/ipfs/pref_names.h @@ -17,5 +17,6 @@ extern const char kIPFSPublicGatewayAddress[]; extern const char kIPFSPublicNFTGatewayAddress[]; extern const char kIPFSResolveMethod[]; extern const char kIpfsStorageMax[]; +extern const char kIPFSPinnedCids[]; #endif // BRAVE_COMPONENTS_IPFS_PREF_NAMES_H_ diff --git a/components/ipfs/test/BUILD.gn b/components/ipfs/test/BUILD.gn index 65b150155d76..a63b54a9c20b 100644 --- a/components/ipfs/test/BUILD.gn +++ b/components/ipfs/test/BUILD.gn @@ -17,6 +17,8 @@ source_set("brave_ipfs_unit_tests") { "//brave/components/ipfs/ipfs_ports_unittest.cc", "//brave/components/ipfs/ipfs_utils_unittest.cc", "//brave/components/ipfs/keys/ipns_keys_manager_unittest.cc", + "//brave/components/ipfs/pin/ipfs_base_pin_service_unittest.cc", + "//brave/components/ipfs/pin/ipfs_local_pin_service_unittest.cc", ] deps = [