diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/util/AsyncUtils.java b/android/java/org/chromium/chrome/browser/crypto_wallet/util/AsyncUtils.java index 90f4d198cfa3..07116be2fb5c 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/util/AsyncUtils.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/util/AsyncUtils.java @@ -423,7 +423,8 @@ public GetNftErc721MetadataContext(Runnable responseCompleteCallback) { } @Override - public void call(String erc721Metadata, Integer errorCode, String errorMessage) { + public void call( + String tokenUrl, String erc721Metadata, Integer errorCode, String errorMessage) { this.tokenMetadata = erc721Metadata; this.errorCode = errorCode; this.errorMessage = errorMessage; diff --git a/browser/brave_wallet/BUILD.gn b/browser/brave_wallet/BUILD.gn index d471f53014bf..b3315b061960 100644 --- a/browser/brave_wallet/BUILD.gn +++ b/browser/brave_wallet/BUILD.gn @@ -1,3 +1,8 @@ +# Copyright (c) 2019 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + import("//brave/browser/ethereum_remote_client/buildflags/buildflags.gni") import("//extensions/buildflags/buildflags.gni") import("//testing/test.gni") @@ -8,8 +13,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..e9954c3aba51 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_auto_pin_service_factory.cc @@ -0,0 +1,98 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/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" +// TODO(cypt4) : Refactor brave/browser/ipfs into separate component (#27486) +#include "brave/browser/ipfs/ipfs_service_factory.h" // nogncheck + +#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(); + } + + auto* service = GetServiceForContext(context); + + if (!service) { + return mojo::PendingRemote(); + } + + return service->MakeRemote(); +} + +// static +BraveWalletAutoPinService* +BraveWalletAutoPinServiceFactory::GetServiceForContext( + content::BrowserContext* context) { + if (!IsAllowedForContext(context)) { + return nullptr; + } + if (!ipfs::IpfsServiceFactory::IsIpfsEnabled(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..c2778d2771c8 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h @@ -0,0 +1,54 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_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..f49edf983367 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_pin_service_factory.cc @@ -0,0 +1,93 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/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" +// TODO(cypt4) : Refactor brave/browser into separate component (#27486) +#include "brave/browser/ipfs/ipfs_local_pin_service_factory.h" // nogncheck +#include "brave/browser/ipfs/ipfs_service_factory.h" // nogncheck +#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(); + } + + auto* service = GetServiceForContext(context); + if (!service) { + return mojo::PendingRemote(); + } + + return service->MakeRemote(); +} + +// static +BraveWalletPinService* BraveWalletPinServiceFactory::GetServiceForContext( + content::BrowserContext* context) { + if (!IsAllowedForContext(context)) { + return nullptr; + } + if (!ipfs::IpfsServiceFactory::IsIpfsEnabled(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()); + DependsOn(ipfs::IpfsServiceFactory::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), + ipfs::IpfsServiceFactory::GetForContext(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..35759571f282 --- /dev/null +++ b/browser/brave_wallet/brave_wallet_pin_service_factory.h @@ -0,0 +1,50 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_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/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/browser_context_keyed_service_factories.cc b/browser/browser_context_keyed_service_factories.cc index b2c278c72079..273d1ccb3f7b 100644 --- a/browser/browser_context_keyed_service_factories.cc +++ b/browser/browser_context_keyed_service_factories.cc @@ -53,6 +53,8 @@ #endif #if BUILDFLAG(ENABLE_IPFS) +#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/ipfs/ipfs_service_factory.h" #endif @@ -107,6 +109,8 @@ void EnsureBrowserContextKeyedServiceFactoriesBuilt() { #endif #if BUILDFLAG(ENABLE_IPFS) + brave_wallet::BraveWalletAutoPinServiceFactory::GetInstance(); + brave_wallet::BraveWalletPinServiceFactory::GetInstance(); ipfs::IpfsServiceFactory::GetInstance(); #endif 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..aea731930338 --- /dev/null +++ b/browser/ipfs/ipfs_local_pin_service_factory.cc @@ -0,0 +1,55 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/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) { + if (!ipfs::IpfsServiceFactory::IsIpfsEnabled(context)) { + return nullptr; + } + 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..1ddcad2840c9 --- /dev/null +++ b/browser/ipfs/ipfs_local_pin_service_factory.h @@ -0,0 +1,42 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_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/ipfs/test/ipfs_service_browsertest.cc b/browser/ipfs/test/ipfs_service_browsertest.cc index 613768eae021..2b18727c9b09 100644 --- a/browser/ipfs/test/ipfs_service_browsertest.cc +++ b/browser/ipfs/test/ipfs_service_browsertest.cc @@ -707,7 +707,8 @@ IN_PROC_BROWSER_TEST_F(IpfsServiceBrowserTest, GetConnectedPeers) { base::Unretained(this))); ipfs_service()->GetConnectedPeers( base::BindOnce(&IpfsServiceBrowserTest::OnGetConnectedPeersSuccess, - base::Unretained(this))); + base::Unretained(this)), + absl::nullopt); WaitForRequest(); } @@ -717,7 +718,8 @@ IN_PROC_BROWSER_TEST_F(IpfsServiceBrowserTest, GetConnectedPeersServerError) { base::Unretained(this))); ipfs_service()->GetConnectedPeers( base::BindOnce(&IpfsServiceBrowserTest::OnGetConnectedPeersFail, - base::Unretained(this))); + base::Unretained(this)), + absl::nullopt); WaitForRequest(); } diff --git a/browser/profiles/brave_profile_manager.cc b/browser/profiles/brave_profile_manager.cc index 8deec5d924ed..2a4ec849d719 100644 --- a/browser/profiles/brave_profile_manager.cc +++ b/browser/profiles/brave_profile_manager.cc @@ -44,6 +44,7 @@ #endif #if BUILDFLAG(ENABLE_IPFS) +#include "brave/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h" #include "brave/browser/ipfs/ipfs_service_factory.h" #endif @@ -97,10 +98,11 @@ void BraveProfileManager::DoFinalInitForServices(Profile* profile, return; brave_ads::AdsServiceFactory::GetForProfile(profile); brave_rewards::RewardsServiceFactory::GetForProfile(profile); - brave_wallet::BraveWalletServiceFactory::GetServiceForContext(profile); #if BUILDFLAG(ENABLE_IPFS) + brave_wallet::BraveWalletAutoPinServiceFactory::GetServiceForContext(profile); ipfs::IpfsServiceFactory::GetForContext(profile); #endif + brave_wallet::BraveWalletServiceFactory::GetServiceForContext(profile); #if !BUILDFLAG(USE_GCM_FROM_PLATFORM) gcm::BraveGCMChannelStatus* status = gcm::BraveGCMChannelStatus::GetForProfile(profile); diff --git a/browser/ui/webui/brave_wallet/wallet_page_ui.cc b/browser/ui/webui/brave_wallet/wallet_page_ui.cc index 6b9648688689..ace82b4575f4 100644 --- a/browser/ui/webui/brave_wallet/wallet_page_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_page_ui.cc @@ -21,6 +21,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" @@ -42,6 +43,11 @@ #include "ui/base/accelerators/accelerator.h" #include "ui/base/webui/web_ui_util.h" +#if BUILDFLAG(ENABLE_IPFS) +#include "brave/browser/brave_wallet/brave_wallet_auto_pin_service_factory.h" +#include "brave/browser/brave_wallet/brave_wallet_pin_service_factory.h" +#endif + WalletPageUI::WalletPageUI(content::WebUI* web_ui) : ui::MojoWebUIController(web_ui, true /* Needed for webui browser tests */) { @@ -111,7 +117,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); @@ -143,6 +153,13 @@ void WalletPageUI::CreatePageHandler( wallet_service->GetBraveWalletP3A()->Bind( std::move(brave_wallet_p3a_receiver)); +#if BUILDFLAG(ENABLE_IPFS) + 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)); +#endif + auto* blockchain_registry = brave_wallet::BlockchainRegistry::GetInstance(); if (blockchain_registry) { blockchain_registry->Bind(std::move(blockchain_registry_receiver)); 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/browser/ui/webui/ipfs_ui.cc b/browser/ui/webui/ipfs_ui.cc index 1138bbae3231..b0523b25fe07 100644 --- a/browser/ui/webui/ipfs_ui.cc +++ b/browser/ui/webui/ipfs_ui.cc @@ -127,8 +127,10 @@ void IPFSDOMHandler::HandleGetConnectedPeers(const base::Value::List& args) { if (!service) { return; } - service->GetConnectedPeers(base::BindOnce( - &IPFSDOMHandler::OnGetConnectedPeers, weak_ptr_factory_.GetWeakPtr())); + service->GetConnectedPeers( + base::BindOnce(&IPFSDOMHandler::OnGetConnectedPeers, + weak_ptr_factory_.GetWeakPtr()), + absl::nullopt); } void IPFSDOMHandler::OnGetConnectedPeers( diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index 93ac3479bccc..75dee59a939a 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright (c) 2023 The Brave Authors. All rights reserved. +# Copyright (c) 2019 The Brave Authors. All rights reserved. # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at https://mozilla.org/MPL/2.0/. @@ -32,10 +32,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..5e5dd6f371b2 --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_auto_pin_service.cc @@ -0,0 +1,261 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/brave_wallet/browser/brave_wallet_auto_pin_service.h" + +#include "brave/components/brave_wallet/browser/pref_names.h" + +namespace brave_wallet { + +BraveWalletAutoPinService::IntentData::IntentData( + const BlockchainTokenPtr& token, + Operation operation, + absl::optional service) + : token(token.Clone()), operation(operation), service(std::move(service)) {} + +BraveWalletAutoPinService::IntentData::~IntentData() = default; + +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) { + DCHECK(brave_wallet_service); + Restore(); + brave_wallet_service->AddTokenObserver( + token_observer_.BindNewPipeAndPassRemote()); +} + +BraveWalletAutoPinService::~BraveWalletAutoPinService() {} + +void BraveWalletAutoPinService::Bind( + mojo::PendingReceiver receiver) { + receivers_.Add(this, std::move(receiver)); +} + +mojo::PendingRemote +BraveWalletAutoPinService::MakeRemote() { + mojo::PendingRemote remote; + receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver()); + return remote; +} + +void BraveWalletAutoPinService::OnTokenAdded(BlockchainTokenPtr token) { + if (!BraveWalletPinService::IsTokenSupportedForPinning(token)) { + return; + } + if (!IsAutoPinEnabled()) { + return; + } + PostPinToken(std::move(token)); +} + +void BraveWalletAutoPinService::OnTokenRemoved(BlockchainTokenPtr token) { + if (!BraveWalletPinService::IsTokenSupportedForPinning(token)) { + return; + } + base::EraseIf(queue_, [&token](const std::unique_ptr& intent) { + return intent->token == token; + }); + PostUnpinToken(std::move(token)); +} + +void BraveWalletAutoPinService::Restore() { + brave_wallet_service_->GetAllUserAssets( + base::BindOnce(&BraveWalletAutoPinService::OnTokenListResolved, + weak_ptr_factory_.GetWeakPtr())); +} + +void BraveWalletAutoPinService::OnTokenListResolved( + std::vector token_list) { + bool autopin_enabled = IsAutoPinEnabled(); + // Resolves list of user tokens. + // Check whether they are pinned or not and posts corresponding tasks. + std::set known_tokens = + brave_wallet_pin_service_->GetTokens(absl::nullopt); + for (const auto& token : token_list) { + if (!BraveWalletPinService::IsTokenSupportedForPinning(token)) { + continue; + } + auto current_token_path = + BraveWalletPinService::GetTokenPrefPath(absl::nullopt, token); + if (!current_token_path) { + continue; + } + + known_tokens.erase(current_token_path.value()); + + 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::kAdd, + 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::kAdd, 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::kDelete, + absl::nullopt)); + } else if (status->code == mojom::TokenPinStatusCode::STATUS_PINNED) { + // Pinned tokens should be verified for entirety time to time. + // We should check that related CIDs are still pinned. + auto t1 = status->validate_time; + if ((base::Time::Now() - t1) > base::Days(1) || t1 > base::Time::Now()) { + AddOrExecute(std::make_unique(token, Operation::kValidate, + absl::nullopt)); + } + } + } + + // Tokens that were previously pinned but not listed in the wallet should be + // unpinned. + for (const auto& t : known_tokens) { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPrefPath(t); + if (token) { + AddOrExecute(std::make_unique(token, Operation::kDelete, + absl::nullopt)); + } + } + + CheckQueue(); +} + +void BraveWalletAutoPinService::PostPinToken(BlockchainTokenPtr token) { + queue_.push_back( + std::make_unique(token, Operation::kAdd, absl::nullopt)); + CheckQueue(); +} + +void BraveWalletAutoPinService::PostUnpinToken(BlockchainTokenPtr token) { + queue_.push_back( + std::make_unique(token, Operation::kDelete, 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, + weak_ptr_factory_.GetWeakPtr())); +} + +void BraveWalletAutoPinService::PinToken( + const std::unique_ptr& data) { + brave_wallet_pin_service_->AddPin( + data->token->Clone(), data->service, + base::BindOnce(&BraveWalletAutoPinService::OnTaskFinished, + weak_ptr_factory_.GetWeakPtr())); +} + +void BraveWalletAutoPinService::UnpinToken( + const std::unique_ptr& data) { + brave_wallet_pin_service_->RemovePin( + data->token->Clone(), data->service, + base::BindOnce(&BraveWalletAutoPinService::OnTaskFinished, + weak_ptr_factory_.GetWeakPtr())); +} + +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::kAdd) { + brave_wallet_pin_service_->MarkAsPendingForPinning(data->token, + data->service); + } else if (data->operation == Operation::kDelete) { + 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, + weak_ptr_factory_.GetWeakPtr(), 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::kAdd) { + PinToken(current_); + } else if (current_->operation == Operation::kDelete) { + UnpinToken(current_); + } else if (current_->operation == Operation::kValidate) { + 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::kAdd, + 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..e34ab3b94dbf --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_auto_pin_service.h @@ -0,0 +1,106 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_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 { + +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; + + // BraveWalletServiceTokenObserver + void OnTokenAdded(mojom::BlockchainTokenPtr token) override; + void OnTokenRemoved(mojom::BlockchainTokenPtr token) override; + + private: + enum Operation { kAdd = 0, kDelete = 1, kValidate = 2 }; + + struct IntentData { + BlockchainTokenPtr token; + Operation operation; + absl::optional service; + size_t attempt = 0; + IntentData(const BlockchainTokenPtr& token, + Operation operation, + absl::optional service); + ~IntentData(); + }; + + void PostPinToken(BlockchainTokenPtr token); + void PostUnpinToken(BlockchainTokenPtr token); + + // Iterates through user tokens and manages their pin statuses. + 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_; + + raw_ptr pref_service_; + raw_ptr brave_wallet_service_; + raw_ptr brave_wallet_pin_service_; + + std::unique_ptr current_; + std::deque> queue_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // 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..77e49b90039a --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_auto_pin_service_unittest.cc @@ -0,0 +1,524 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/brave_wallet/browser/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 { + +namespace { + +mojom::BlockchainTokenPtr GetErc721Token(const std::string& pref_path) { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPrefPath(pref_path); + token->is_erc721 = true; + return token; +} + +class MockBraveWalletPinService : public BraveWalletPinService { + public: + MockBraveWalletPinService() : BraveWalletPinService() {} + 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(const absl::optional&, + const mojom::BlockchainTokenPtr&)); + MOCK_METHOD2(GetLastValidateTime, + absl::optional(const 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_METHOD1(GetAllUserAssets, + void(BraveWalletService::GetUserAssetsCallback)); +}; + +MATCHER_P(TokenPathMatches, path, "") { + auto token = GetErc721Token(path); + return token->coin == arg->coin && token->chain_id == arg->chain_id && + token->contract_address == arg->contract_address && + token->token_id == arg->token_id; +} + +} // namespace + +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, true); + 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 = GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1"); + token->is_erc721 = true; + service()->OnTokenAdded(std::move(token)); + } + + { + mojom::BlockchainTokenPtr token = GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2"); + token->is_erc721 = true; + service()->OnTokenAdded(std::move(token)); + } + + { + mojom::BlockchainTokenPtr token = GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3"); + token->is_erc721 = true; + 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(), GetAllUserAssets(_)) + .WillByDefault(::testing::Invoke([](BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(GetErc721Token( + "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(), GetAllUserAssets(_)) + .WillByDefault(::testing::Invoke([](BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2")); + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3")); + result.push_back(GetErc721Token( + "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(), GetAllUserAssets(_)) + .WillByDefault(::testing::Invoke([](BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2")); + result.push_back(GetErc721Token( + "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(), GetAllUserAssets(_)) + .WillByDefault(::testing::Invoke([](BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(GetErc721Token( + "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); + 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, 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 = GetErc721Token( + "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(), GetAllUserAssets(_)) + .WillByDefault(::testing::Invoke([](BraveWalletService:: + GetUserAssetsCallback callback) { + std::vector result; + result.push_back(GetErc721Token( + "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1")); + result.push_back(GetErc721Token( + "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 e754c4210baf..b87690364ced 100644 --- a/components/brave_wallet/browser/brave_wallet_p3a.cc +++ b/components/brave_wallet/browser/brave_wallet_p3a.cc @@ -108,6 +108,8 @@ BraveWalletP3A::BraveWalletP3A(BraveWalletService* wallet_service, base::Unretained(this), true)); } +BraveWalletP3A::BraveWalletP3A() = default; + 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 be7fc74bdaef..c9946b09209a 100644 --- a/components/brave_wallet/browser/brave_wallet_p3a.h +++ b/components/brave_wallet/browser/brave_wallet_p3a.h @@ -47,6 +47,9 @@ class BraveWalletP3A : public mojom::BraveWalletServiceObserver, PrefService* profile_prefs, PrefService* local_state); + // For testing + BraveWalletP3A(); + ~BraveWalletP3A() override; BraveWalletP3A(const BraveWalletP3A&) = delete; BraveWalletP3A& operator=(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..443afc5e46ea --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_pin_service.cc @@ -0,0 +1,819 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" + +#include + +#include "base/json/values_util.h" +#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" + +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"; +/** + * Service name used in prefs for local pinning service. + * Use absl::nullopt in methods to perform operations on + * the local pinning service. + */ +const char kLocalService[] = "local"; + +absl::optional StringToStatus( + const std::string& status) { + if (status == "not_pinned") { + return mojom::TokenPinStatusCode::STATUS_NOT_PINNED; + } else if (status == "pinning_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 == "unpinning_in_progress") { + return mojom::TokenPinStatusCode::STATUS_UNPINNING_IN_PROGRESS; + } else if (status == "unpinning_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 + +// static +std::string BraveWalletPinService::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 "unpinning_in_progress"; + case mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED: + return "unpinning_failed"; + case mojom::TokenPinStatusCode::STATUS_PINNING_FAILED: + return "pinning_failed"; + case mojom::TokenPinStatusCode::STATUS_PINNING_PENDING: + return "pinning_pendig"; + case mojom::TokenPinStatusCode::STATUS_UNPINNING_PENDING: + return "unpinning_pendig"; + } + NOTREACHED(); + return ""; +} + +// static +std::string BraveWalletPinService::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 ""; +} + +// static +bool BraveWalletPinService::IsTokenSupportedForPinning( + const mojom::BlockchainTokenPtr& token) { + return token->is_erc721; +} + +/** + * Structure of kPinnedNFTAssets prefs: + * // Type of pinned content + * "nft" : { + * // List of services + * "local" : { + * // Coin type + * "60": { + * // Chain id + * "0x1": { + * // Contract + * "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d" : { + * // Token id + * "3139" : { + * "status": , + * "validate_timestamp": , + * "error": { + * "error_code": , + * "error_message": , + * }, + * cids: [ + * // List of related CIDs + * "bafy..", + * ... + * ] + * } + * ... + * } + * ... + * } + * ... + * } + * ... + * }, + * // Remote service + * nftstorage : { + * ... + * } + * } + */ +BraveWalletPinService::BraveWalletPinService( + PrefService* prefs, + JsonRpcService* service, + ipfs::IpfsLocalPinService* local_pin_service, + IpfsService* ipfs_service) + : prefs_(prefs), + json_rpc_service_(service), + local_pin_service_(local_pin_service), + ipfs_service_(ipfs_service) { + ipfs_service_->AddObserver(this); +} + +// For testing +BraveWalletPinService::BraveWalletPinService() = default; + +BraveWalletPinService::~BraveWalletPinService() { + if (ipfs_service_) { + ipfs_service_->RemoveObserver(this); + } +} + +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 +absl::optional BraveWalletPinService::GetTokenPrefPath( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token) { + if (service && base::ContainsOnlyChars(service.value(), ".")) { + return absl::nullopt; + } + if (base::ContainsOnlyChars(token->contract_address, ".")) { + return absl::nullopt; + } + if (base::ContainsOnlyChars(token->token_id, ".")) { + return absl::nullopt; + } + DCHECK(!base::ContainsOnlyChars(token->chain_id, ".")); + return base::StrCat({kNftPart, ".", service.value_or(kLocalService), ".", + base::NumberToString(static_cast(token->coin)), ".", + token->chain_id, ".", token->contract_address, ".", + token->token_id}); +} + +// static +mojom::BlockchainTokenPtr BraveWalletPinService::TokenFromPrefPath( + 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_nft = true; + return token; +} + +// static +absl::optional BraveWalletPinService::ServiceFromPrefPath( + 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(mojom::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; + } + + auto path = GetTokenPrefPath(absl::nullopt, token); + if (!path) { + std::move(callback).Run( + false, + mojom::PinError::New(mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN, + "Wrong token data")); + return; + } + + if (!service) { + local_pin_service_->ValidatePins( + path.value(), cids.value(), + base::BindOnce(&BraveWalletPinService::OnTokenValidated, + weak_ptr_factory_.GetWeakPtr(), service, + std::move(callback), std::move(token))); + } else { + // Remote pinning not implemented yet + std::move(callback).Run(false, nullptr); + } +} + +void BraveWalletPinService::IsLocalNodeRunning( + IsLocalNodeRunningCallback callback) { + std::move(callback).Run(ipfs_service_->IsDaemonLaunched()); +} + +void BraveWalletPinService::IsTokenSupported( + mojom::BlockchainTokenPtr token, + IsTokenSupportedCallback callback) { + std::move(callback).Run(IsTokenSupportedForPinning(token)); +} + +void BraveWalletPinService::OnIpfsLaunched(bool result, int64_t pid) { + for (const auto& observer : observers_) { + observer->OnLocalNodeStatusChanged(result); + } +} + +void BraveWalletPinService::OnIpfsShutdown() { + for (const auto& observer : observers_) { + observer->OnLocalNodeStatusChanged(false); + } +} + +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(mojom::BlockchainTokenPtr token, + const absl::optional& service, + AddPinCallback callback) { + if (!IsTokenSupportedForPinning(token)) { + auto pin_error = + mojom::PinError::New(mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN, + "Token pinning is not supported"); + std::move(callback).Run(false, std::move(pin_error)); + 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"); + std::move(callback).Run(true, std::move(pin_error)); + return; + } + + json_rpc_service_->GetERC721Metadata( + token->contract_address, token->token_id, token->chain_id, + base::BindOnce(&BraveWalletPinService::OnTokenMetaDataReceived, + weak_ptr_factory_.GetWeakPtr(), service, + std::move(callback), token.Clone())); +} + +void BraveWalletPinService::RemovePin( + mojom::BlockchainTokenPtr token, + const absl::optional& service, + RemovePinCallback callback) { + auto token_status = GetTokenStatus(service, token); + if (!token_status) { + std::move(callback).Run(true, nullptr); + return; + } + + auto path = GetTokenPrefPath(absl::nullopt, token); + if (!path) { + std::move(callback).Run( + false, + mojom::PinError::New(mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN, + "Wrong token data")); + return; + } + + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_UNPINNING_IN_PROGRESS, + nullptr); + + if (!service) { + local_pin_service_->RemovePins( + path.value(), base::BindOnce(&BraveWalletPinService::OnPinsRemoved, + weak_ptr_factory_.GetWeakPtr(), service, + std::move(callback), std::move(token))); + } else { + // Remote pinning not implemented yet + std::move(callback).Run(false, nullptr); + } +} + +void BraveWalletPinService::GetTokenStatus(mojom::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); + std::move(callback).Run(false, std::move(pin_error)); + 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); + std::move(callback).Run(false, std::move(pin_error)); + 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); + std::move(callback).Run(false, std::move(pin_error)); + return; + } + + std::vector cids; + + cids.push_back(ExtractCID(token_url).value()); + auto* image = parsed_result->FindStringKey("image"); + if (image) { + auto image_cid = ExtractCID(*image); + if (image_cid) { + cids.push_back(image_cid.value()); + } + } + + auto path = GetTokenPrefPath(service, token); + if (!path) { + std::move(callback).Run( + false, + mojom::PinError::New(mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN, + "Wrong token data")); + return; + } + + AddToken(service, token, cids); + SetTokenStatus(service, token, + mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS, + nullptr); + + if (!service) { + local_pin_service_->AddPins( + path.value(), cids, + base::BindOnce(&BraveWalletPinService::OnTokenPinned, + weak_ptr_factory_.GetWeakPtr(), absl::nullopt, + std::move(callback), std::move(token))); + } else { + // Remote pinning not implemented yet + std::move(callback).Run(false, nullptr); + } +} + +void BraveWalletPinService::OnTokenPinned(absl::optional service, + AddPinCallback callback, + mojom::BlockchainTokenPtr token, + bool result) { + auto error = !result + ? mojom::PinError::New( + mojom::WalletPinServiceErrorCode::ERR_PINNING_FAILED, + "Pinning failed") + : nullptr; + SetTokenStatus(service, token, + result ? mojom::TokenPinStatusCode::STATUS_PINNED + : mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, + error); + + std::move(callback).Run(result, std::move(error)); +} + +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); +} + +bool BraveWalletPinService::AddToken(const absl::optional& service, + const mojom::BlockchainTokenPtr& token, + const std::vector& cids) { + auto path = GetTokenPrefPath(service, token); + if (!path) { + return false; + } + + { + DictionaryPrefUpdate update(prefs_, kPinnedNFTAssets); + 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(path.value(), std::move(token_data)); + } + return true; +} + +bool BraveWalletPinService::RemoveToken( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token) { + auto path = GetTokenPrefPath(service, token); + if (!path) { + return false; + } + + { + DictionaryPrefUpdate update(prefs_, kPinnedNFTAssets); + base::Value::Dict& update_dict = update->GetDict(); + update_dict.RemoveByDottedPath(path.value()); + } + for (const auto& observer : observers_) { + observer->OnTokenStatusChanged(service, token.Clone(), + GetTokenStatus(service, token)); + } + return true; +} + +bool BraveWalletPinService::SetTokenStatus( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token, + mojom::TokenPinStatusCode status, + const mojom::PinErrorPtr& error) { + auto path = GetTokenPrefPath(service, token); + if (!path) { + return false; + } + + { + DictionaryPrefUpdate update(prefs_, kPinnedNFTAssets); + base::Value::Dict& update_dict = update->GetDict(); + + update_dict.SetByDottedPath(path.value() + "." + 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(path.value() + "." + kError, + std::move(error_dict)); + } else { + update_dict.RemoveByDottedPath(path.value() + "." + kError); + } + + if (status == mojom::TokenPinStatusCode::STATUS_PINNED) { + update_dict.SetByDottedPath(path.value() + "." + kValidateTimestamp, + base::TimeToValue(base::Time::Now())); + } else { + update_dict.RemoveByDottedPath(path.value() + "." + kValidateTimestamp); + } + } + for (const auto& observer : observers_) { + observer->OnTokenStatusChanged(service, token.Clone(), + GetTokenStatus(service, token)); + } + return true; +} + +absl::optional> BraveWalletPinService::ResolvePinItems( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token) { + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedNFTAssets); + + auto path = GetTokenPrefPath(service, token); + if (!path) { + return absl::nullopt; + } + + auto* token_data_as_dict = + pinned_assets_pref.FindDictByDottedPath(path.value()); + 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( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token) { + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedNFTAssets); + + auto path = GetTokenPrefPath(service, token); + if (!path) { + return nullptr; + } + + auto* token_data_as_dict = + pinned_assets_pref.FindDictByDottedPath(path.value()); + if (!token_data_as_dict) { + return mojom::TokenPinStatus::New( + mojom::TokenPinStatusCode::STATUS_NOT_PINNED, nullptr, base::Time()); + } + + auto* status = token_data_as_dict->FindString(kAssetStatus); + if (!status) { + return mojom::TokenPinStatus::New( + mojom::TokenPinStatusCode::STATUS_NOT_PINNED, nullptr, base::Time()); + } + + auto pin_status = StringToStatus(*status).value_or( + mojom::TokenPinStatusCode::STATUS_NOT_PINNED); + base::Time validate_timestamp; + mojom::PinErrorPtr error; + + { + auto* validate_timestamp_value = + token_data_as_dict->Find(kValidateTimestamp); + if (validate_timestamp_value) { + validate_timestamp = + base::ValueToTime(validate_timestamp_value).value_or(base::Time()); + } + + 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( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token) { + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedNFTAssets); + + auto path = GetTokenPrefPath(service, token); + if (!path) { + return absl::nullopt; + } + + auto* token_data_as_dict = + pinned_assets_pref.FindDictByDottedPath(path.value()); + if (!token_data_as_dict) { + return absl::nullopt; + } + + auto* time = token_data_as_dict->Find(kValidateTimestamp); + return base::ValueToTime(time); +} + +std::set BraveWalletPinService::GetTokens( + const absl::optional& service) { + std::set result; + + const base::Value::Dict& pinned_assets_pref = + prefs_->GetDict(kPinnedNFTAssets); + + 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..6d87692a8add --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_pin_service.h @@ -0,0 +1,156 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_PIN_SERVICE_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_PIN_SERVICE_H_ + +#include +#include +#include + +#include "base/containers/cxx20_erase_deque.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.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" + +namespace brave_wallet { + +/** + * At the moment only local pinning is supported so use absl::nullopt + * for optional service argument. + */ +class BraveWalletPinService : public KeyedService, + public brave_wallet::mojom::WalletPinService, + public ipfs::IpfsServiceObserver { + public: + BraveWalletPinService(PrefService* prefs, + JsonRpcService* service, + ipfs::IpfsLocalPinService* local_pin_service, + IpfsService* ipfs_service); + ~BraveWalletPinService() override; + + mojo::PendingRemote MakeRemote(); + void Bind(mojo::PendingReceiver receiver); + + static absl::optional GetTokenPrefPath( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token); + static mojom::BlockchainTokenPtr TokenFromPrefPath(const std::string& path); + static absl::optional ServiceFromPrefPath( + const std::string& path); + static std::string StatusToString(const mojom::TokenPinStatusCode& status); + static std::string ErrorCodeToString( + const mojom::WalletPinServiceErrorCode& error_code); + static bool IsTokenSupportedForPinning( + const mojom::BlockchainTokenPtr& token); + + // WalletPinService + void AddObserver(::mojo::PendingRemote + observer) override; + void AddPin(mojom::BlockchainTokenPtr token, + const absl::optional& service, + AddPinCallback callback) override; + void RemovePin(mojom::BlockchainTokenPtr token, + const absl::optional& service, + RemovePinCallback callback) override; + void GetTokenStatus(mojom::BlockchainTokenPtr token, + GetTokenStatusCallback callback) override; + void Validate(mojom::BlockchainTokenPtr token, + const absl::optional& service, + ValidateCallback callback) override; + void IsLocalNodeRunning(IsLocalNodeRunningCallback callback) override; + void IsTokenSupported(mojom::BlockchainTokenPtr token, + IsTokenSupportedCallback 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( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token); + virtual absl::optional GetLastValidateTime( + const absl::optional& service, + const mojom::BlockchainTokenPtr& token); + // Returns list of known tokens for the provided pinning service. + // Tokens are returned in the format of string path. + // See BraveWalletPinService::GetTokenPrefPath. + virtual std::set GetTokens( + const absl::optional& service); + + protected: + // For testing + BraveWalletPinService(); + + private: + bool AddToken(const absl::optional& service, + const mojom::BlockchainTokenPtr& token, + const std::vector& cids); + bool RemoveToken(const absl::optional& service, + const mojom::BlockchainTokenPtr& token); + bool SetTokenStatus(const absl::optional& service, + const mojom::BlockchainTokenPtr& token, + mojom::TokenPinStatusCode, + const mojom::PinErrorPtr& error); + + absl::optional> ResolvePinItems( + const absl::optional& service, + const mojom::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); + + // ipfs::IpfsServiceObserver + void OnIpfsLaunched(bool result, int64_t pid) override; + void OnIpfsShutdown() override; + + mojo::ReceiverSet receivers_; + mojo::RemoteSet observers_; + + // Prefs service is used to store list of pinned items + raw_ptr prefs_ = nullptr; + + // JsonRpcService is used to fetch token metadata + raw_ptr json_rpc_service_ = nullptr; + raw_ptr local_pin_service_ = nullptr; + raw_ptr ipfs_service_ = nullptr; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // 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..93c8417230c7 --- /dev/null +++ b/components/brave_wallet/browser/brave_wallet_pin_service_unittest.cc @@ -0,0 +1,609 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/brave_wallet/browser/brave_wallet_pin_service.h" + +#include +#include +#include +#include + +#include "base/json/json_reader.h" +#include "base/json/values_util.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 "services/network/public/cpp/shared_url_loader_factory.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); +} + +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() : JsonRpcService() {} + + MOCK_METHOD4(GetERC721Metadata, + void(const std::string& contract_address, + const std::string& token_id, + const std::string& chain_id, + GetERC721MetadataCallback callback)); + + ~MockJsonRpcService() override {} +}; + +class MockIpfsService : public IpfsService { + public: + MockIpfsService() = default; + ~MockIpfsService() override = default; + + MOCK_METHOD1(AddObserver, void(ipfs::IpfsServiceObserver* observer)); + MOCK_METHOD0(IsDaemonLaunched, bool()); +}; + +} // namespace + +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(kPinnedNFTAssets); + brave_wallet_pin_service_ = std::make_unique( + GetPrefs(), GetJsonRpcService(), GetIpfsLocalPinService(), + GetIpfsService()); + } + + PrefService* GetPrefs() { return &pref_service_; } + + testing::NiceMock* GetJsonRpcService() { + return &json_rpc_service_; + } + + testing::NiceMock* GetIpfsLocalPinService() { + return &ipfs_local_pin_service_; + } + + testing::NiceMock* GetIpfsService() { + return &ipfs_service_; + } + + testing::NiceMock ipfs_local_pin_service_; + testing::NiceMock json_rpc_service_; + testing::NiceMock ipfs_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::GetERC721MetadataCallback 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::FromTimeT(123u)); + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPrefPath(kMonkey1Path); + token->is_erc721 = true; + 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(kPinnedNFTAssets) + .FindDictByDottedPath(kMonkey1Path); + + base::Value::List expected_cids; + expected_cids.Append("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq"); + expected_cids.Append("Qmcyc7tm9sZB9JnvLgejPTwdzjjNjDMiRWCUvaZAfp6cUg"); + + EXPECT_EQ(BraveWalletPinService::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(base::Time::FromTimeT(123u), + base::ValueToTime(token_record->Find("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::GetERC721MetadataCallback 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::TokenFromPrefPath(kMonkey2Path); + token->is_erc721 = true; + 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(kPinnedNFTAssets) + .FindDictByDottedPath(kMonkey2Path); + + EXPECT_EQ(BraveWalletPinService::StatusToString( + mojom::TokenPinStatusCode::STATUS_PINNING_FAILED), + *(token_record->FindString("status"))); + EXPECT_EQ(BraveWalletPinService::ErrorCodeToString( + mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED), + token_record->FindByDottedPath("error.error_code")->GetString()); + } +} + +TEST_F(BraveWalletPinServiceTest, RemovePin) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedNFTAssets); + 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::TokenFromPrefPath(kMonkey1Path); + token->is_erc721 = true; + 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(BraveWalletPinService::StatusToString( + mojom::TokenPinStatusCode::STATUS_UNPINNING_FAILED), + *(GetPrefs() + ->GetDict(kPinnedNFTAssets) + .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::TokenFromPrefPath(kMonkey1Path); + token->is_erc721 = true; + 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(kPinnedNFTAssets) + .FindDictByDottedPath(kMonkey1Path)); + } +} + +TEST_F(BraveWalletPinServiceTest, ValidatePin) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedNFTAssets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + item.Set("validate_timestamp", base::TimeToValue(base::Time::Now())); + 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::FromTimeT(345u)); + + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPrefPath(kMonkey1Path); + token->is_erc721 = true; + 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(kPinnedNFTAssets) + .FindDictByDottedPath(kMonkey1Path); + EXPECT_EQ(base::Time::FromTimeT(345u), + base::ValueToTime(token_record->Find("validate_timestamp"))); + } + + { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPrefPath(kMonkey1Path); + token->is_erc721 = true; + 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(kPinnedNFTAssets) + .FindDictByDottedPath(kMonkey1Path); + EXPECT_EQ(base::Time::FromTimeT(345u), + base::ValueToTime(token_record->Find("validate_timestamp"))); + } + + { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPrefPath(kMonkey1Path); + token->is_erc721 = true; + 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(kPinnedNFTAssets) + .FindDictByDottedPath(kMonkey1Path); + + EXPECT_EQ(nullptr, token_record->Find("validate_timestamp")); + } +} + +TEST_F(BraveWalletPinServiceTest, GetTokenStatus) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedNFTAssets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + item.Set("validate_timestamp", + base::TimeToValue(base::Time::FromTimeT(123u))); + 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(), kPinnedNFTAssets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", BraveWalletPinService::StatusToString( + mojom::TokenPinStatusCode::STATUS_PINNING_FAILED)); + base::Value::Dict error; + error.Set("error_code", + BraveWalletPinService::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::TokenFromPrefPath(kMonkey1Path); + token1->is_erc721 = true; + { + 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::FromTimeT(123u), status->validate_time); + } + + { + mojom::TokenPinStatusPtr status = + service()->GetTokenStatus("nft.storage", token1); + EXPECT_EQ(mojom::TokenPinStatusCode::STATUS_NOT_PINNED, status->code); + } + + mojom::BlockchainTokenPtr token2 = + BraveWalletPinService::TokenFromPrefPath(kMonkey2Path); + token2->is_erc721 = true; + { + 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_EQ(base::Time(), status->validate_time); + } +} + +TEST_F(BraveWalletPinServiceTest, GetLastValidateTime) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedNFTAssets); + base::Value::Dict& update_dict = update->GetDict(); + + base::Value::Dict item; + item.Set("status", "pinned"); + item.Set("validate_timestamp", + base::TimeToValue(base::Time::FromTimeT(123u))); + 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::TokenFromPrefPath(kMonkey1Path); + token->is_erc721 = true; + { + base::Time last_validate_time = + service()->GetLastValidateTime(absl::nullopt, token).value(); + EXPECT_EQ(base::Time::FromTimeT(123u), last_validate_time); + } + + { + EXPECT_FALSE( + service()->GetLastValidateTime("nft.storage", token).has_value()); + } +} + +TEST_F(BraveWalletPinServiceTest, TokenFromPrefPath) { + mojom::BlockchainTokenPtr token = + BraveWalletPinService::TokenFromPrefPath(kMonkey1Path); + 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::ServiceFromPrefPath(kMonkey1Path).has_value()); + + EXPECT_EQ("nftstorage", BraveWalletPinService::ServiceFromPrefPath( + "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::GetTokenPrefPath(absl::nullopt, token); + EXPECT_EQ("nft.local.60.mainnet.abc.0x2", path.value()); + } + + { + 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::GetTokenPrefPath("nftstorage", token); + EXPECT_EQ("nft.nftstorage.60.mainnet.abc.0x2", path.value()); + } +} + +TEST_F(BraveWalletPinServiceTest, GetTokens) { + { + DictionaryPrefUpdate update(GetPrefs(), kPinnedNFTAssets); + 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(), kPinnedNFTAssets); + 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(), kPinnedNFTAssets); + 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 724d803edf2c..3f2f5cd7d019 100644 --- a/components/brave_wallet/browser/brave_wallet_prefs.cc +++ b/components/brave_wallet/browser/brave_wallet_prefs.cc @@ -126,6 +126,9 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { registry->RegisterTimePref(kBraveWalletP3ALastReportTime, base::Time()); registry->RegisterTimePref(kBraveWalletP3AFirstReportTime, base::Time()); registry->RegisterListPref(kBraveWalletP3AWeeklyStorage); + + registry->RegisterDictionaryPref(kPinnedNFTAssets); + registry->RegisterBooleanPref(kAutoPinEnabled, false); } void RegisterProfilePrefsForMigration( @@ -195,6 +198,7 @@ void ClearBraveWalletServicePrefs(PrefService* prefs) { prefs->ClearPref(kBraveWalletUserAssets); prefs->ClearPref(kDefaultBaseCurrency); prefs->ClearPref(kDefaultBaseCryptocurrency); + prefs->ClearPref(kPinnedNFTAssets); } 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 cf632e3b941c..6c71f9907542 100644 --- a/components/brave_wallet/browser/brave_wallet_service.cc +++ b/components/brave_wallet/browser/brave_wallet_service.cc @@ -162,10 +162,11 @@ BraveWalletService::BraveWalletService( tx_service_(tx_service), profile_prefs_(profile_prefs), brave_wallet_p3a_(this, keyring_service, profile_prefs, local_state), - asset_discovery_manager_(this, - json_rpc_service, - keyring_service, - profile_prefs), + asset_discovery_manager_( + std::make_unique(this, + json_rpc_service, + keyring_service, + profile_prefs)), weak_ptr_factory_(this) { if (delegate_) delegate_->AddObserver(this); @@ -207,6 +208,8 @@ BraveWalletService::BraveWalletService( weak_ptr_factory_.GetWeakPtr())); } +BraveWalletService::BraveWalletService() : weak_ptr_factory_(this) {} + BraveWalletService::~BraveWalletService() = default; mojo::PendingRemote @@ -274,6 +277,36 @@ absl::optional BraveWalletService::GetChecksumAddress( return eth_addr.ToChecksumAddress(chain); } +// static +std::vector BraveWalletService::GetUserAssets( + PrefService* profile_prefs) { + std::vector result; + const auto& user_assets_dict = profile_prefs->GetDict(kBraveWalletUserAssets); + for (auto coin_it : user_assets_dict) { + auto coin = GetCoinTypeFromPrefKey(coin_it.first); + if (!coin) + continue; + + for (auto network_it : coin_it.second.GetDict()) { + auto chain_id = GetChainId(profile_prefs, coin.value(), network_it.first); + + if (!chain_id) + continue; + + for (const auto& item : network_it.second.GetList()) { + const auto* token = item.GetIfDict(); + if (!token) + continue; + mojom::BlockchainTokenPtr tokenPtr = + ValueToBlockchainToken(*token, chain_id.value(), coin.value()); + if (tokenPtr) + result.push_back(std::move(tokenPtr)); + } + } + } + return result; +} + // static std::vector BraveWalletService::GetUserAssets( const std::string& chain_id, @@ -361,6 +394,7 @@ bool BraveWalletService::AddUserAsset(mojom::BlockchainTokenPtr token, value.Set("coingecko_id", token->coingecko_id); user_assets_list->Append(std::move(value)); + return true; } @@ -372,8 +406,22 @@ void BraveWalletService::GetUserAssets(const std::string& chain_id, std::move(callback).Run(std::move(result)); } +void BraveWalletService::GetAllUserAssets(GetUserAssetsCallback callback) { + std::vector result = GetUserAssets(profile_prefs_); + std::move(callback).Run(std::move(result)); +} + bool BraveWalletService::AddUserAsset(mojom::BlockchainTokenPtr token) { - return BraveWalletService::AddUserAsset(std::move(token), profile_prefs_); + mojom::BlockchainTokenPtr clone = token.Clone(); + bool result = + BraveWalletService::AddUserAsset(std::move(token), profile_prefs_); + + if (result) { + for (const auto& observer : token_observers_) { + observer->OnTokenAdded(clone.Clone()); + } + } + return result; } void BraveWalletService::AddUserAsset(mojom::BlockchainTokenPtr token, @@ -410,6 +458,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; } @@ -1026,6 +1079,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_) { @@ -1392,7 +1450,8 @@ void BraveWalletService::DiscoverAssetsOnAllSupportedChains() { addresses[mojom::CoinType::SOL] = std::move(sol_account_addresses); // Discover assets owned by the SOL and ETH addresses on all supported chains - asset_discovery_manager_.DiscoverAssetsOnAllSupportedChainsRefresh(addresses); + asset_discovery_manager_->DiscoverAssetsOnAllSupportedChainsRefresh( + addresses); } void BraveWalletService::CancelAllSuggestedTokenCallbacks() { diff --git a/components/brave_wallet/browser/brave_wallet_service.h b/components/brave_wallet/browser/brave_wallet_service.h index 1023459885bf..f2f8c18d4b00 100644 --- a/components/brave_wallet/browser/brave_wallet_service.h +++ b/components/brave_wallet/browser/brave_wallet_service.h @@ -66,6 +66,7 @@ class BraveWalletService : public KeyedService, TxService* tx_service, PrefService* profile_prefs, PrefService* local_state); + ~BraveWalletService() override; BraveWalletService(const BraveWalletService&) = delete; @@ -86,6 +87,8 @@ class BraveWalletService : public KeyedService, const std::string& chain_id, mojom::CoinType coin, PrefService* profile_prefs); + static std::vector GetUserAssets( + PrefService* profile_prefs); static base::Value::Dict GetDefaultEthereumAssets(); static base::Value::Dict GetDefaultSolanaAssets(); static base::Value::Dict GetDefaultFilecoinAssets(); @@ -93,10 +96,14 @@ 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, GetUserAssetsCallback callback) override; + void GetAllUserAssets(GetUserAssetsCallback callback) override; void AddUserAsset(mojom::BlockchainTokenPtr token, AddUserAssetCallback callback) override; void RemoveUserAsset(mojom::BlockchainTokenPtr token, @@ -227,6 +234,10 @@ class BraveWalletService : public KeyedService, BraveWalletP3A* GetBraveWalletP3A(); + protected: + // For tests + BraveWalletService(); + private: friend class EthereumProviderImplUnitTest; friend class SolanaProviderImplUnitTest; @@ -306,13 +317,14 @@ 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; raw_ptr tx_service_ = nullptr; raw_ptr profile_prefs_ = nullptr; BraveWalletP3A brave_wallet_p3a_; - AssetDiscoveryManager asset_discovery_manager_; + std::unique_ptr asset_discovery_manager_; mojo::ReceiverSet receivers_; PrefChangeRegistrar pref_change_registrar_; base::WeakPtrFactory weak_ptr_factory_; diff --git a/components/brave_wallet/browser/brave_wallet_utils.cc b/components/brave_wallet/browser/brave_wallet_utils.cc index 038d8b6f24bf..f682a025b68a 100644 --- a/components/brave_wallet/browser/brave_wallet_utils.cc +++ b/components/brave_wallet/browser/brave_wallet_utils.cc @@ -1157,6 +1157,19 @@ std::string GetNetworkId(PrefService* prefs, return base::ToLowerASCII(id); } +absl::optional GetChainId(PrefService* prefs, + const mojom::CoinType& coin, + const std::string& network_id) { + std::vector networks = GetAllChains(prefs, coin); + for (const auto& network : networks) { + std::string id = GetKnownNetworkId(coin, network->chain_id); + if (id == network_id) { + return network->chain_id; + } + } + return absl::nullopt; +} + mojom::DefaultWallet GetDefaultEthereumWallet(PrefService* prefs) { return static_cast( prefs->GetInteger(kDefaultEthereumWallet)); @@ -1376,6 +1389,18 @@ std::string GetPrefKeyForCoinType(mojom::CoinType coin) { return ""; } +absl::optional GetCoinTypeFromPrefKey(const std::string& key) { + if (key == kEthereumPrefKey) { + return mojom::CoinType::ETH; + } else if (key == kFilecoinPrefKey) { + return mojom::CoinType::FIL; + } else if (key == kSolanaPrefKey) { + return mojom::CoinType::SOL; + } + NOTREACHED(); + return absl::nullopt; +} + std::string eTLDPlusOne(const url::Origin& origin) { return net::registry_controlled_domains::GetDomainAndRegistry( origin, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); diff --git a/components/brave_wallet/browser/brave_wallet_utils.h b/components/brave_wallet/browser/brave_wallet_utils.h index 9eecf96e76e4..0f45c440441a 100644 --- a/components/brave_wallet/browser/brave_wallet_utils.h +++ b/components/brave_wallet/browser/brave_wallet_utils.h @@ -151,6 +151,14 @@ std::string GetCurrentChainId(PrefService* prefs, mojom::CoinType coin); std::string GetPrefKeyForCoinType(mojom::CoinType coin); +// Converts string representation of CoinType to enum. +absl::optional GetCoinTypeFromPrefKey(const std::string& key); + +// Resolves chain_id from network_id. +absl::optional GetChainId(PrefService* prefs, + const mojom::CoinType& coin, + const std::string& network_id); + // Returns a string used for web3_clientVersion in the form of // BraveWallet/v[chromium-version]. Note that we expose only the Chromium // version and not the Brave version because that way no extra entropy diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 9da7cae7bbed..3004959eee89 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -187,6 +187,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( diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index 6d86743ba8e4..95a33bb3589b 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -55,6 +55,8 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { JsonRpcService( scoped_refptr url_loader_factory, PrefService* prefs); + // For testing: + JsonRpcService(); ~JsonRpcService() override; static void MigrateMultichainNetworks(PrefService* prefs); @@ -543,6 +545,7 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& owner_address, mojom::ProviderError error, const std::string& error_message); + void OnGetEthTokenUri(GetEthTokenUriCallback callback, const APIRequestResult api_request_result); diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index 6b5ee3f825b9..3761affed11e 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -1108,14 +1108,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(); } @@ -1128,14 +1128,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(); } diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.cc b/components/brave_wallet/browser/nft_metadata_fetcher.cc index f59ba33a9c73..76f69c953df4 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher.cc +++ b/components/brave_wallet/browser/nft_metadata_fetcher.cc @@ -94,14 +94,14 @@ void NftMetadataFetcher::GetEthTokenMetadata( 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; } @@ -125,13 +125,13 @@ void NftMetadataFetcher::OnGetSupportsInterface( 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; } @@ -149,20 +149,20 @@ void NftMetadataFetcher::OnGetEthTokenUri(GetEthTokenMetadataCallback callback, 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 (!uri.is_valid()) { std::move(callback).Run( - "", mojom::ProviderError::kInternalError, + "", "", mojom::ProviderError::kInternalError, l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } auto internal_callback = base::BindOnce(&NftMetadataFetcher::CompleteGetEthTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + weak_ptr_factory_.GetWeakPtr(), std::move(callback), uri); FetchMetadata(uri, std::move(internal_callback)); } @@ -263,13 +263,14 @@ void NftMetadataFetcher::OnGetTokenMetadataPayload( void NftMetadataFetcher::CompleteGetEthTokenMetadata( GetEthTokenMetadataCallback callback, + const GURL& uri, const std::string& response, int error, const std::string& error_message) { mojom::ProviderError mojo_err = static_cast(error); if (!mojom::IsKnownEnumValue(mojo_err)) mojo_err = mojom::ProviderError::kUnknown; - std::move(callback).Run(response, mojo_err, error_message); + std::move(callback).Run(uri.spec(), response, mojo_err, error_message); } void NftMetadataFetcher::GetSolTokenMetadata( diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.h b/components/brave_wallet/browser/nft_metadata_fetcher.h index 2f5bde01eabf..56c74f48d93a 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher.h +++ b/components/brave_wallet/browser/nft_metadata_fetcher.h @@ -36,7 +36,8 @@ class NftMetadataFetcher { using APIRequestHelper = api_request_helper::APIRequestHelper; using APIRequestResult = api_request_helper::APIRequestResult; using GetEthTokenMetadataCallback = - base::OnceCallback; void GetEthTokenMetadata(const std::string& contract_address, @@ -83,6 +84,7 @@ class NftMetadataFetcher { mojom::SolanaProviderError error, const std::string& error_message); void CompleteGetEthTokenMetadata(GetEthTokenMetadataCallback callback, + const GURL& uri, const std::string& response, int error, const std::string& error_message); diff --git a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc index 62e003a29770..57f7aa1540e8 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc +++ b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc @@ -148,14 +148,14 @@ class NftMetadataFetcherUnitTest : public testing::Test { base::RunLoop run_loop; nft_metadata_fetcher_->GetEthTokenMetadata( contract, token_id, chain_id, interface_id, - base::BindLambdaForTesting([&](const std::string& response, - mojom::ProviderError error, - const std::string& error_message) { - CompareJSON(response, expected_response); - EXPECT_EQ(error, expected_error); - EXPECT_EQ(error_message, expected_error_message); - run_loop.Quit(); - })); + base::BindLambdaForTesting( + [&](const std::string& url, const std::string& response, + mojom::ProviderError error, const std::string& error_message) { + CompareJSON(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 cbeffc5e55b4..a7da02ebf79d 100644 --- a/components/brave_wallet/browser/pref_names.cc +++ b/components/brave_wallet/browser/pref_names.cc @@ -72,3 +72,5 @@ const char kBraveWalletCurrentChainId[] = const char kBraveWalletUserAssetsDeprecated[] = "brave.wallet.user_assets"; const char kBraveWalletUserAssetsAddPreloadingNetworksMigratedDeprecated[] = "brave.wallet.user.assets.add_preloading_networks_migrated"; +const char kPinnedNFTAssets[] = "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 87977c67eb53..555074102924 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 kPinnedNFTAssets[]; +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 28aa45b91de4..f795c4e6bc41 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 88469debe067..a9bfd0925027 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -213,6 +213,93 @@ interface SolanaProvider { }; +enum WalletPinServiceErrorCode { + // Token not supported for pinning + ERR_WRONG_TOKEN = 1, + // Token metadata has non-ipfs url + ERR_NON_IPFS_TOKEN_URL = 2, + // Fetching metadata has failed + 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 { + // Actual token status + TokenPinStatusCode code; + // Token error related to the pin status. May be null. + PinError? error; + // Last time token was validated(checked that data is pinned correctly) + mojo_base.mojom.Time validate_time; +}; + +struct TokenPinOverview { + // Token status for the local pin backend. + TokenPinStatus? local; + // Token statuses for remote pin backends. + map remotes; +}; + +// Observers state of the BraveWalletPinService +interface BraveWalletPinServiceObserver { + OnTokenStatusChanged(string? service, BlockchainToken token, + TokenPinStatus status); + OnLocalNodeStatusChanged(bool running); +}; + +// Low-level interface for token pinning. +// String service argument is used to select on which pinning +// service operation should be performed. +// At the moment we have only local pinning, so use null. +interface WalletPinService { + AddObserver(pending_remote observer); + + // Launches pinning for provided token. + // At the moment only local pinning is supported so use null "service" argument. + AddPin(BlockchainToken token, string? service) =>(bool result, + PinError? error); + // Unpins token. + RemovePin(BlockchainToken token, string? service) => (bool result, + PinError? response); + // Returns overview for provided token. + GetTokenStatus(BlockchainToken token) => (TokenPinOverview? status, + PinError? error); + // Checks whether token is pinned correctly. + Validate(BlockchainToken token, string? service) => (bool result, + PinError? error); + // Returns whether IPFS localnode is currently running. + IsLocalNodeRunning() => (bool result); + + // Returns whether token is supported for pinning. + // Note: You should manually check token metadata url to have ipfs:// url. + IsTokenSupported(BlockchainToken token) => (bool result); +}; + +// Listens for added user tokens and automatically pins them. +interface WalletAutoPinService { + // Enables autopinning, so old and new user tokens may be pinned. + SetAutoPinEnabled(bool enabled); + IsAutoPinEnabled() => (bool enabled); +}; + // Used by the WebUI page to bootstrap bidirectional communication. interface PanelHandlerFactory { // The WebUI calls this method when the page is first initialized. @@ -247,8 +334,10 @@ 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. @@ -930,10 +1019,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 EnableEnsOffchainLookup(); @@ -1183,6 +1272,11 @@ interface BraveWalletServiceObserver { OnDiscoverAssetsCompleted(array discovered_assets); }; +interface BraveWalletServiceTokenObserver { + OnTokenAdded(BlockchainToken token); + OnTokenRemoved(BlockchainToken token); +}; + struct SignMessageRequest { OriginInfo origin_info; int32 id; @@ -1239,6 +1333,11 @@ interface BraveWalletService { // Adds an observer for BraveWalletService AddObserver(pending_remote observer); + AddTokenObserver(pending_remote observer); + + // Obtains all the user assets. + GetAllUserAssets() => (array tokens); + // 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 9383e1826d0e..d8dac2598734 100644 --- a/components/brave_wallet_ui/common/actions/wallet_actions.ts +++ b/components/brave_wallet_ui/common/actions/wallet_actions.ts @@ -97,5 +97,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 dc8420081358..2620057dfb8d 100644 --- a/components/brave_wallet_ui/common/slices/wallet.slice.ts +++ b/components/brave_wallet_ui/common/slices/wallet.slice.ts @@ -235,7 +235,8 @@ export const WalletAsyncActions = { addFilecoinAccount: createAction('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 d384a05271ec..2432d58121ca 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({ @@ -127,6 +129,16 @@ export class WalletApiProxy { }) this.braveWalletService.addObserver(braveWalletServiceObserverReceiver.$.bindNewPipeAndPassRemote()) } + + addBraveWalletPinServiceObserver (store: Store) { + const braveWalletServiceObserverReceiver = new BraveWallet.BraveWalletPinServiceObserverReceiver({ + onTokenStatusChanged: function (service, token, status) { + }, + onLocalNodeStatusChanged: function (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 5a64d0cc2529..678c5d9212a8 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -295,6 +295,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 b23ff2d5760f..2860e40a13cf 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 @@ -144,6 +144,7 @@ handler.on(WalletPageActions.selectAsset.type, async (store: Store, payload: Upd if (payload.asset.isErc721 || payload.asset.isNft) { 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 })) @@ -308,4 +309,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 5c9fd2889bc5..c5d4ce147dfb 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 46d0137cb1d3..1df99c1f0c7b 100644 --- a/components/brave_wallet_ui/page/store.ts +++ b/components/brave_wallet_ui/page/store.ts @@ -42,6 +42,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/brave_wallet_ui/stories/mock-data/mock-page-state.ts b/components/brave_wallet_ui/stories/mock-data/mock-page-state.ts index fff129de6a1b..05e8d9068354 100644 --- a/components/brave_wallet_ui/stories/mock-data/mock-page-state.ts +++ b/components/brave_wallet_ui/stories/mock-data/mock-page-state.ts @@ -22,6 +22,7 @@ export const mockPageState: PageState = { isMetaMaskInitialized: false, portfolioPriceHistory: [], selectedAsset: undefined, + pinStatusOverview: undefined, selectedAssetCryptoPrice: undefined, selectedAssetFiatPrice: undefined, selectedAssetPriceHistory: [], diff --git a/components/ipfs/BUILD.gn b/components/ipfs/BUILD.gn index 51b520e65dc8..8dee6455e938 100644 --- a/components/ipfs/BUILD.gn +++ b/components/ipfs/BUILD.gn @@ -1,3 +1,8 @@ +# Copyright (c) 2020 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + import("//brave/components/ipfs/buildflags/buildflags.gni") static_library("ipfs") { @@ -73,6 +78,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 41677495ef44..15547af8b954 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 4d069d81efc9..2ddd7d7ba3c8 100644 --- a/components/ipfs/ipfs_json_parser.cc +++ b/components/ipfs/ipfs_json_parser.cc @@ -40,6 +40,125 @@ bool RemoveValueFromList(base::Value::List* root, const T& value_to_remove) { } // namespace +// static +// Response format /api/v0/pin/add +// { +// "Pins": [ +// "" +// ], +// "Progress": "" +// } +absl::optional IPFSJSONParser::GetAddPinsResultFromJSON( + const base::Value& value) { + auto* auto_dict = value.GetIfDict(); + if (!auto_dict) { + VLOG(1) << "Invalid response, wrong format."; + return absl::nullopt; + } + const base::Value::List* pins_list = auto_dict->FindList("Pins"); + if (!pins_list) { + VLOG(1) << "Invalid response, can not find Pins array."; + return absl::nullopt; + } + + ipfs::AddPinResult result; + + auto progress = auto_dict->FindInt("Progress"); + if (progress) { + result.progress = *progress; + } else { + result.progress = -1; + } + + for (const base::Value& val : *pins_list) { + auto* val_as_str = val.GetIfString(); + if (!val_as_str) { + VLOG(1) << "Invalid response, wrong format."; + return absl::nullopt; + } + result.pins.push_back(*val_as_str); + } + return result; +} + +// static +// Response format /api/v0/pin/rm +// { +// "Pins": [ +// "" +// ] +// } +absl::optional +IPFSJSONParser::GetRemovePinsResultFromJSON(const base::Value& value) { + auto* auto_dict = value.GetIfDict(); + if (!auto_dict) { + VLOG(1) << "Invalid response, wrong format."; + return absl::nullopt; + } + const base::Value::List* pins_list = auto_dict->FindList("Pins"); + if (!pins_list) { + VLOG(1) << "Invalid response, can not find Pins array."; + return absl::nullopt; + } + + ipfs::RemovePinResult result; + for (const base::Value& val : *pins_list) { + auto* val_as_str = val.GetIfString(); + if (!val_as_str) { + return absl::nullopt; + } + result.push_back(*val_as_str); + } + return result; +} + +// static +// Response format /api/v0/pin/ls +// { +// "PinLsList": { +// "Keys": { +// "": { +// "Type": "" +// } +// } +// }, +// "PinLsObject": { +// "Cid": "", +// "Type": "" +// } +// } +absl::optional IPFSJSONParser::GetGetPinsResultFromJSON( + const base::Value& value) { + auto* auto_dict = value.GetIfDict(); + if (!auto_dict) { + VLOG(1) << "Invalid response, wrong format."; + return absl::nullopt; + } + + const base::Value::Dict* keys = auto_dict->FindDict("Keys"); + if (!keys) { + VLOG(1) << "Invalid response, can not find Keys in PinLsList dict."; + return absl::nullopt; + } + + ipfs::GetPinsResult result; + for (auto it : *keys) { + auto* it_dict = it.second.GetIfDict(); + if (!it_dict) { + VLOG(1) << "Missing Type for " << it.first; + return absl::nullopt; + } + const std::string* type = it_dict->FindString("Type"); + if (!type) { + VLOG(1) << "Missing Type for " << it.first; + return absl::nullopt; + } + result[it.first] = *type; + } + + return result; +} + // 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 8ac6fb5cc42a..49fe7c840652 100644 --- a/components/ipfs/ipfs_json_parser.h +++ b/components/ipfs/ipfs_json_parser.h @@ -14,6 +14,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 { @@ -46,6 +47,13 @@ class IPFSJSONParser { static std::string RemovePeerFromConfigJSON(const std::string& json, const std::string& peer_id, const std::string& address); + // Local pins + static absl::optional GetAddPinsResultFromJSON( + const base::Value& value); + static absl::optional GetGetPinsResultFromJSON( + const base::Value& value); + static absl::optional GetRemovePinsResultFromJSON( + const base::Value& value); }; #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 90473b782d40..18ca0f3f561e 100644 --- a/components/ipfs/ipfs_json_parser_unittest.cc +++ b/components/ipfs/ipfs_json_parser_unittest.cc @@ -382,3 +382,137 @@ TEST_F(IPFSJSONParserTest, RemovePeerFromConfigJSONTest) { "{\"Peering\":{\"Peers\":" "[{\"Addrs\":[\"/b\"],\"ID\":\"QmA\"}]}}"); } + +TEST_F(IPFSJSONParserTest, GetGetPinsResultFromJSONTest) { + { + std::string json = R"({})"; + auto result = + IPFSJSONParser::GetGetPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = R"({"Keys":{"QmA" : {"Type" : "Recursive"}}})"; + auto result = + IPFSJSONParser::GetGetPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->at("QmA"), "Recursive"); + } + + { + std::string json = R"({"Keys":{}})"; + auto result = + IPFSJSONParser::GetGetPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(result->empty()); + } + + { + std::string json = R"({"Keys":[]})"; + auto result = + IPFSJSONParser::GetGetPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = + R"({"Keys":{"QmA" : {"Type" :"Recursive"}, "QmB" : {"Type" :"Direct"}}})"; + auto result = + IPFSJSONParser::GetGetPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->at("QmA"), "Recursive"); + EXPECT_EQ(result->at("QmB"), "Direct"); + } +} + +TEST_F(IPFSJSONParserTest, GetRemovePinsResultFromJSONTest) { + { + std::string json = R"({})"; + auto result = IPFSJSONParser::GetRemovePinsResultFromJSON( + base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = R"({"Pins" : {}})"; + auto result = IPFSJSONParser::GetRemovePinsResultFromJSON( + base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = R"({"Pins" : []})"; + auto result = IPFSJSONParser::GetRemovePinsResultFromJSON( + base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(result.value().empty()); + } + + { + std::string json = R"({"Pins" : [{}, 123]})"; + auto result = IPFSJSONParser::GetRemovePinsResultFromJSON( + base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = R"({"Pins" : ["QmA", "QmB"]})"; + auto result = IPFSJSONParser::GetRemovePinsResultFromJSON( + base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->at(0), "QmA"); + EXPECT_EQ(result->at(1), "QmB"); + } +} + +TEST_F(IPFSJSONParserTest, GetAddPinsResultFromJSONTest) { + { + std::string json = R"({})"; + auto result = + IPFSJSONParser::GetAddPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = R"({"Pins" : {}})"; + auto result = + IPFSJSONParser::GetAddPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = R"({"Pins" : []})"; + auto result = + IPFSJSONParser::GetAddPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + EXPECT_TRUE(result->pins.empty()); + EXPECT_EQ(result->progress, -1); + } + + { + std::string json = R"({"Pins" : ["QmA", "QmB"]})"; + auto result = + IPFSJSONParser::GetAddPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result->pins.at(0), "QmA"); + EXPECT_EQ(result->pins.at(1), "QmB"); + EXPECT_EQ(result->progress, -1); + } + + { + std::string json = R"({"Pins" : [{}, 123]})"; + auto result = + IPFSJSONParser::GetAddPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_FALSE(result.has_value()); + } + + { + std::string json = R"({"Pins" : ["QmA", "QmB"], "Progress" : 10})"; + auto result = + IPFSJSONParser::GetAddPinsResultFromJSON(base::test::ParseJson(json)); + EXPECT_TRUE(result.has_value()); + 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_navigation_throttle.cc b/components/ipfs/ipfs_navigation_throttle.cc index 29b5feac87a9..f8f0d8b15c8c 100644 --- a/components/ipfs/ipfs_navigation_throttle.cc +++ b/components/ipfs/ipfs_navigation_throttle.cc @@ -145,7 +145,8 @@ IpfsNavigationThrottle::WillFailRequest() { void IpfsNavigationThrottle::GetConnectedPeers() { ipfs_service_->GetConnectedPeers( base::BindOnce(&IpfsNavigationThrottle::OnGetConnectedPeers, - weak_ptr_factory_.GetWeakPtr())); + weak_ptr_factory_.GetWeakPtr()), + absl::nullopt); } void IpfsNavigationThrottle::OnGetConnectedPeers( diff --git a/components/ipfs/ipfs_onboarding_page.cc b/components/ipfs/ipfs_onboarding_page.cc index 7f28eb8a3aca..ee31f9f3f78c 100644 --- a/components/ipfs/ipfs_onboarding_page.cc +++ b/components/ipfs/ipfs_onboarding_page.cc @@ -133,7 +133,7 @@ void IPFSOnboardingPage::ReportDaemonStopped() { } void IPFSOnboardingPage::GetConnectedPeers() { - ipfs_service_->GetConnectedPeers(base::NullCallback()); + ipfs_service_->GetConnectedPeers(base::NullCallback(), absl::nullopt); } bool IPFSOnboardingPage::IsLocalNodeMode() { diff --git a/components/ipfs/ipfs_service.cc b/components/ipfs/ipfs_service.cc index 92fdbf8e6259..c47b21c266fd 100644 --- a/components/ipfs/ipfs_service.cc +++ b/components/ipfs/ipfs_service.cc @@ -92,10 +92,17 @@ std::pair LoadConfigFileOnFileTaskRunner( return result; } +base::flat_map GetHeaders(const GURL& url) { + return { + {net::HttpRequestHeaders::kOrigin, url::Origin::Create(url).Serialize()}}; +} + } // namespace namespace ipfs { +IpfsService::IpfsService() : ipfs_p3a_(nullptr, nullptr), weak_factory_(this) {} + IpfsService::IpfsService( PrefService* prefs, scoped_refptr url_loader_factory, @@ -141,7 +148,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 +171,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 +381,87 @@ void IpfsService::NotifyIpnsKeysLoaded(bool result) { } } +// Local pinning +void IpfsService::AddPin(const std::vector& cids, + bool recursive, + AddPinCallback callback) { + if (!IsDaemonLaunched()) { + std::move(callback).Run(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)), + GetHeaders(gurl)); +} + +void IpfsService::RemovePin(const std::vector& cids, + RemovePinCallback callback) { + if (!IsDaemonLaunched()) { + std::move(callback).Run(absl::nullopt); + return; + } + + GURL gurl = server_endpoint_.Resolve(kRemovePinPath); + for (const auto& cid : cids) { + gurl = net::AppendQueryParameter(gurl, kArgQueryParam, cid); + } + + 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)), + GetHeaders(gurl)); +} + +void IpfsService::GetPins(const absl::optional>& cids, + const std::string& type, + bool quiet, + GetPinsCallback callback) { + if (!IsDaemonLaunched()) { + std::move(callback).Run(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)), + GetHeaders(gurl)); +} + void IpfsService::ImportFileToIpfs(const base::FilePath& path, const std::string& key, ipfs::ImportCompletedCallback callback) { @@ -491,8 +582,9 @@ void IpfsService::OnImportFinished(ipfs::ImportCompletedCallback callback, importers_.erase(key); } #endif + void IpfsService::GetConnectedPeers(GetConnectedPeersCallback callback, - int retries) { + absl::optional retries) { if (!IsDaemonLaunched()) { if (callback) std::move(callback).Run(false, std::vector{}); @@ -514,9 +606,9 @@ void IpfsService::GetConnectedPeers(GetConnectedPeersCallback callback, iter->get()->Request( "POST", gurl, std::string(), std::string(), false, base::BindOnce(&IpfsService::OnGetConnectedPeers, base::Unretained(this), - iter, std::move(callback), retries), - {{net::HttpRequestHeaders::kOrigin, - url::Origin::Create(gurl).Serialize()}}); + iter, std::move(callback), + retries.value_or(kPeersDefaultRetries)), + GetHeaders(gurl)); } base::TimeDelta IpfsService::CalculatePeersRetryTime() { @@ -581,8 +673,7 @@ void IpfsService::GetAddressesConfig(GetAddressesConfigCallback callback) { "POST", gurl, std::string(), std::string(), false, base::BindOnce(&IpfsService::OnGetAddressesConfig, base::Unretained(this), iter, std::move(callback)), - {{net::HttpRequestHeaders::kOrigin, - url::Origin::Create(gurl).Serialize()}}); + GetHeaders(gurl)); } void IpfsService::OnGetAddressesConfig( @@ -754,8 +845,7 @@ void IpfsService::GetRepoStats(GetRepoStatsCallback callback) { "POST", gurl, std::string(), std::string(), false, base::BindOnce(&IpfsService::OnRepoStats, base::Unretained(this), iter, std::move(callback)), - {{net::HttpRequestHeaders::kOrigin, - url::Origin::Create(gurl).Serialize()}}); + GetHeaders(gurl)); } void IpfsService::OnRepoStats(APIRequestList::iterator iter, @@ -795,8 +885,7 @@ void IpfsService::GetNodeInfo(GetNodeInfoCallback callback) { "POST", gurl, std::string(), std::string(), false, base::BindOnce(&IpfsService::OnNodeInfo, base::Unretained(this), iter, std::move(callback)), - {{net::HttpRequestHeaders::kOrigin, - url::Origin::Create(gurl).Serialize()}}); + GetHeaders(gurl)); } void IpfsService::OnNodeInfo(APIRequestList::iterator iter, @@ -836,8 +925,7 @@ void IpfsService::RunGarbageCollection(GarbageCollectionCallback callback) { "POST", gurl, std::string(), std::string(), false, base::BindOnce(&IpfsService::OnGarbageCollection, base::Unretained(this), iter, std::move(callback)), - {{net::HttpRequestHeaders::kOrigin, - url::Origin::Create(gurl).Serialize()}}); + GetHeaders(gurl)); } void IpfsService::OnGarbageCollection( @@ -869,8 +957,7 @@ void IpfsService::PreWarmShareableLink(const GURL& url) { iter->get()->Request("HEAD", url, std::string(), std::string(), false, base::BindOnce(&IpfsService::OnPreWarmComplete, base::Unretained(this), iter), - {{net::HttpRequestHeaders::kOrigin, - url::Origin::Create(url).Serialize()}}); + GetHeaders(url)); } void IpfsService::OnPreWarmComplete( @@ -881,6 +968,91 @@ 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); + + if (!response.Is2XXResponseCode()) { + VLOG(1) << "Fail to get pins, response_code = " << response_code; + std::move(callback).Run(absl::nullopt); + return; + } + + auto parse_result = + IPFSJSONParser::GetGetPinsResultFromJSON(response.value_body()); + if (!parse_result) { + VLOG(1) << "Fail to get pins, wrong format"; + std::move(callback).Run(absl::nullopt); + return; + } + + std::move(callback).Run(std::move(parse_result)); +} + +void IpfsService::OnPinAddResult( + APIRequestList::iterator iter, + AddPinCallback callback, + api_request_helper::APIRequestResult response) { + int response_code = response.response_code(); + requests_list_.erase(iter); + + if (!response.Is2XXResponseCode()) { + VLOG(1) << "Fail to add pin, response_code = " << response_code; + std::move(callback).Run(absl::nullopt); + return; + } + + auto parse_result = + IPFSJSONParser::GetAddPinsResultFromJSON(response.value_body()); + if (!parse_result) { + VLOG(1) << "Fail to add pin service, wrong format"; + std::move(callback).Run(absl::nullopt); + return; + } + + std::move(callback).Run(std::move(parse_result)); +} + +void IpfsService::OnPinRemoveResult( + APIRequestList::iterator iter, + RemovePinCallback callback, + api_request_helper::APIRequestResult response) { + int response_code = response.response_code(); + requests_list_.erase(iter); + + if (!response.Is2XXResponseCode()) { + VLOG(1) << "Fail to remove pin, response_code = " << response_code; + std::move(callback).Run(absl::nullopt); + return; + } + + auto parse_result = + IPFSJSONParser::GetRemovePinsResultFromJSON(response.value_body()); + if (!parse_result) { + VLOG(1) << "Fail to remove pin, wrong response format"; + std::move(callback).Run(absl::nullopt); + return; + } + + std::move(callback).Run(std::move(parse_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..42f307d5df8c 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,12 @@ 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 +119,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 +149,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, + absl::optional retries); 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 +176,7 @@ class IpfsService : public KeyedService, IpnsKeysManager* GetIpnsKeysManager() { return ipns_keys_manager_.get(); } #endif protected: + IpfsService(); void OnConfigLoaded(GetConfigCallback, const std::pair&); private: @@ -189,6 +208,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 +243,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..ea55664b0508 --- /dev/null +++ b/components/ipfs/pin/ipfs_base_pin_service.cc @@ -0,0 +1,87 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/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() = default; + +IpfsBaseJob::~IpfsBaseJob() = default; + +IpfsBasePinService::IpfsBasePinService(IpfsService* ipfs_service) + : ipfs_service_(ipfs_service) { + ipfs_service_->AddObserver(this); +} + +IpfsBasePinService::IpfsBasePinService() = default; + +IpfsBasePinService::~IpfsBasePinService() = default; + +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; + } + + DCHECK(!current_job_); + + 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; + } + + ipfs_service_->StartDaemonAndLaunch(base::BindOnce( + &IpfsBasePinService::OnDaemonStarted, weak_ptr_factory_.GetWeakPtr())); +} + +void IpfsBasePinService::OnDaemonStarted() { + // Ensure that daemon service is fully initialized + 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..5037c6abe2b1 --- /dev/null +++ b/components/ipfs/pin/ipfs_base_pin_service.h @@ -0,0 +1,61 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_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_service.h" + +namespace ipfs { + +class IpfsBaseJob { + public: + IpfsBaseJob(); + virtual ~IpfsBaseJob(); + virtual void Start() = 0; +}; + +// Manages a queue of IpfsService-related tasks. +// Launches IPFS daemon if needed. +class IpfsBasePinService : public IpfsServiceObserver { + public: + explicit IpfsBasePinService(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; + + protected: + // For testing + IpfsBasePinService(); + + private: + bool IsDaemonReady(); + void MaybeStartDaemon(); + void OnDaemonStarted(); + void DoNextJob(); + + bool daemon_ready_ = false; + raw_ptr ipfs_service_; + std::unique_ptr current_job_; + std::queue> jobs_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // 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..8814bb08bb2c --- /dev/null +++ b/components/ipfs/pin/ipfs_base_pin_service_unittest.cc @@ -0,0 +1,95 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/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 { + +namespace { + +class MockIpfsService : public IpfsService { + public: + MockIpfsService() = default; + + ~MockIpfsService() override = default; + + MOCK_METHOD1(StartDaemonAndLaunch, void(base::OnceCallback)); + MOCK_METHOD2(GetConnectedPeers, + void(IpfsService::GetConnectedPeersCallback, + absl::optional)); +}; + +} // namespace + +class IpfsBasePinServiceTest : public testing::Test { + public: + IpfsBasePinServiceTest() = default; + + void SetUp() override { + ipfs_base_pin_service_ = + std::make_unique(GetIpfsService()); + } + + 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_; + 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()); +} + +} // 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..b80cbc8b9ee2 --- /dev/null +++ b/components/ipfs/pin/ipfs_local_pin_service.cc @@ -0,0 +1,275 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/ipfs/pin/ipfs_local_pin_service.h" + +#include +#include +#include + +#include "base/containers/contains.h" +#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& key, + const std::vector& cids, + AddPinCallback callback) + : prefs_service_(prefs_service), + ipfs_service_(ipfs_service), + key_(key), + cids_(cids), + callback_(std::move(callback)) {} + +AddLocalPinJob::~AddLocalPinJob() = default; + +void AddLocalPinJob::Start() { + ipfs_service_->AddPin(cids_, true, + base::BindOnce(&AddLocalPinJob::OnAddPinResult, + weak_ptr_factory_.GetWeakPtr())); +} + +void AddLocalPinJob::OnAddPinResult(absl::optional result) { + if (!result) { + std::move(callback_).Run(false); + return; + } + + for (const auto& cid : cids_) { + if (!base::Contains(result->pins, cid)) { + 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.EnsureList(cid); + list->EraseValue(base::Value(key_)); + list->Append(base::Value(key_)); + } + } + std::move(callback_).Run(true); +} + +RemoveLocalPinJob::RemoveLocalPinJob(PrefService* prefs_service, + const std::string& key, + RemovePinCallback callback) + : prefs_service_(prefs_service), + key_(key), + callback_(std::move(callback)) {} + +RemoveLocalPinJob::~RemoveLocalPinJob() = default; + +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(key_)); + 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& key, + const std::vector& cids, + ValidatePinsCallback callback) + : prefs_service_(prefs_service), + ipfs_service_(ipfs_service), + key_(key), + cids_(cids), + callback_(std::move(callback)) {} + +VerifyLocalPinJob::~VerifyLocalPinJob() = default; + +void VerifyLocalPinJob::Start() { + ipfs_service_->GetPins(absl::nullopt, kRecursiveMode, true, + base::BindOnce(&VerifyLocalPinJob::OnGetPinsResult, + weak_ptr_factory_.GetWeakPtr())); +} + +void VerifyLocalPinJob::OnGetPinsResult(absl::optional result) { + if (!result) { + std::move(callback_).Run(absl::nullopt); + return; + } + DictionaryPrefUpdate update(prefs_service_, kIPFSPinnedCids); + base::Value::Dict& update_dict = update->GetDict(); + + bool verification_passed = true; + for (const auto& cid : cids_) { + base::Value::List* list = update_dict.FindList(cid); + if (!list) { + verification_passed = false; + } else { + if (result->find(cid) != result->end()) { + list->EraseValue(base::Value(key_)); + list->Append(base::Value(key_)); + } else { + verification_passed = false; + list->EraseValue(base::Value(key_)); + } + if (list->empty()) { + update_dict.Remove(cid); + } + } + } + std::move(callback_).Run(verification_passed); +} + +GcJob::GcJob(PrefService* prefs_service, + IpfsService* ipfs_service, + GcCallback callback) + : prefs_service_(prefs_service), + ipfs_service_(ipfs_service), + callback_(std::move(callback)) {} + +GcJob::~GcJob() = default; + +void GcJob::Start() { + ipfs_service_->GetPins( + absl::nullopt, kRecursiveMode, true, + base::BindOnce(&GcJob::OnGetPinsResult, weak_ptr_factory_.GetWeakPtr())); +} + +void GcJob::OnGetPinsResult(absl::optional result) { + if (!result) { + std::move(callback_).Run(false); + return; + } + std::vector cids_to_delete; + 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, + weak_ptr_factory_.GetWeakPtr())); + } else { + std::move(callback_).Run(true); + } +} + +void GcJob::OnPinsRemovedResult(absl::optional result) { + std::move(callback_).Run(result.has_value()); +} + +IpfsLocalPinService::IpfsLocalPinService(PrefService* prefs_service, + IpfsService* ipfs_service) + : prefs_service_(prefs_service), ipfs_service_(ipfs_service) { + ipfs_base_pin_service_ = std::make_unique(ipfs_service_); + base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&IpfsLocalPinService::AddGcTask, + weak_ptr_factory_.GetWeakPtr()), + base::Minutes(1)); +} + +IpfsLocalPinService::IpfsLocalPinService() = default; + +void IpfsLocalPinService::SetIpfsBasePinServiceForTesting( + std::unique_ptr service) { + ipfs_base_pin_service_ = std::move(service); +} + +IpfsLocalPinService::~IpfsLocalPinService() = default; + +void IpfsLocalPinService::AddPins(const std::string& key, + const std::vector& cids, + AddPinCallback callback) { + ipfs_base_pin_service_->AddJob(std::make_unique( + prefs_service_, ipfs_service_, key, cids, + base::BindOnce(&IpfsLocalPinService::OnAddJobFinished, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)))); +} + +void IpfsLocalPinService::RemovePins(const std::string& key, + RemovePinCallback callback) { + ipfs_base_pin_service_->AddJob(std::make_unique( + prefs_service_, key, + base::BindOnce(&IpfsLocalPinService::OnRemovePinsFinished, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)))); +} + +void IpfsLocalPinService::ValidatePins(const std::string& key, + const std::vector& cids, + ValidatePinsCallback callback) { + ipfs_base_pin_service_->AddJob(std::make_unique( + prefs_service_, ipfs_service_, key, cids, + base::BindOnce(&IpfsLocalPinService::OnValidateJobFinished, + weak_ptr_factory_.GetWeakPtr(), 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, + weak_ptr_factory_.GetWeakPtr()), + 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, + weak_ptr_factory_.GetWeakPtr()))); +} + +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..9f80a94a5def --- /dev/null +++ b/components/ipfs/pin/ipfs_local_pin_service.h @@ -0,0 +1,163 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_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; + +/** + * Pins provided cids and writes record to kIPFSPinnedCids: + * { + * // List of all pinned CIDs + * "Qme1": [ + * // List of tokens that contain this CID + * "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1" + * "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2" + * ], + * "Qme2": [ + * "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x1" + * ], + * "Qme3": [ + * "nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2" + * ] + * } + */ +class AddLocalPinJob : public IpfsBaseJob { + public: + AddLocalPinJob(PrefService* prefs_service, + IpfsService* ipfs_service, + const std::string& key, + const std::vector& cids, + AddPinCallback callback); + ~AddLocalPinJob() override; + + void Start() override; + + private: + void OnAddPinResult(absl::optional result); + + raw_ptr prefs_service_; + raw_ptr ipfs_service_; + std::string key_; + std::vector cids_; + AddPinCallback callback_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +// Removes records related to the key and launches GC task. +class RemoveLocalPinJob : public IpfsBaseJob { + public: + RemoveLocalPinJob(PrefService* prefs_service, + const std::string& key, + RemovePinCallback callback); + ~RemoveLocalPinJob() override; + + void Start() override; + + private: + raw_ptr prefs_service_; + std::string key_; + RemovePinCallback callback_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +// Verifies that cids are actually pinned +class VerifyLocalPinJob : public IpfsBaseJob { + public: + VerifyLocalPinJob(PrefService* prefs_service, + IpfsService* ipfs_service, + const std::string& key, + const std::vector& cids, + ValidatePinsCallback callback); + ~VerifyLocalPinJob() override; + + void Start() override; + + private: + void OnGetPinsResult(absl::optional result); + + raw_ptr prefs_service_; + raw_ptr ipfs_service_; + std::string key_; + std::vector cids_; + ValidatePinsCallback callback_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +// Unpins cids that don't have kIPFSPinnedCids record +class GcJob : public IpfsBaseJob { + public: + GcJob(PrefService* prefs_service, + IpfsService* ipfs_service, + GcCallback callback); + ~GcJob() override; + + void Start() override; + + private: + void OnGetPinsResult(absl::optional result); + void OnPinsRemovedResult(absl::optional result); + + raw_ptr prefs_service_; + raw_ptr ipfs_service_; + GcCallback callback_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +class IpfsLocalPinService : public KeyedService { + public: + IpfsLocalPinService(PrefService* prefs_service, IpfsService* ipfs_service); + // For testing + IpfsLocalPinService(); + ~IpfsLocalPinService() override; + + // Pins provided cids and stores related record in the prefs. + virtual void AddPins(const std::string& key, + const std::vector& cids, + AddPinCallback callback); + // Unpins all cids related to the key. + virtual void RemovePins(const std::string& key, RemovePinCallback callback); + // Checks that all cids related to the key are pinned. + virtual void ValidatePins(const std::string& key, + 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_; + raw_ptr prefs_service_; + raw_ptr ipfs_service_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // 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..76c0c9d11f8b --- /dev/null +++ b/components/ipfs/pin/ipfs_local_pin_service_unittest.cc @@ -0,0 +1,461 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/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 { + +namespace { + +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(); + } +}; + +} // namespace + +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(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(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(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(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(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(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(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(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(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(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(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(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..0a4deca1c27d --- /dev/null +++ b/components/ipfs/pin/ipfs_pin_rpc_types.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/ipfs/pin/ipfs_pin_rpc_types.h" + +namespace ipfs { + +AddPinResult::AddPinResult() = default; + +AddPinResult::~AddPinResult() = default; + +AddPinResult::AddPinResult(const AddPinResult& other) = default; + +AddPinResult& AddPinResult::operator=(const AddPinResult&) = 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..ba0b9c732886 --- /dev/null +++ b/components/ipfs/pin/ipfs_pin_rpc_types.h @@ -0,0 +1,30 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_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&); + AddPinResult& operator=(const AddPinResult&); + std::vector pins; + int progress = 0; +}; + +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 = [