From 1dacb3966f36482b2cfdf5db0fcc1f195d35c40e Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 12 Nov 2021 00:57:01 +0300 Subject: [PATCH 1/2] Signing Transaction with Trezor accounts (#10902) --- .../brave_wallet/brave_wallet_tab_helper.cc | 6 +- .../brave_wallet/brave_wallet_tab_helper.h | 4 +- .../brave_wallet_tab_helper_browsertest.cc | 128 ++++++ .../wallet_bubble_manager_delegate.h | 2 + .../ui/wallet_bubble_manager_delegate_impl.cc | 79 +++- .../ui/wallet_bubble_manager_delegate_impl.h | 2 + .../panel_handler/wallet_panel_handler.cc | 12 +- .../panel_handler/wallet_panel_handler.h | 7 +- .../ui/webui/brave_wallet/wallet_common_ui.cc | 27 ++ .../ui/webui/brave_wallet/wallet_common_ui.h | 16 + .../ui/webui/brave_wallet/wallet_panel_ui.cc | 26 +- .../ui/webui/brave_wallet/wallet_panel_ui.h | 6 + .../chrome/browser/ui/views/bubble/DEPS | 4 + .../views/bubble/bubble_contents_wrapper.cc | 43 ++ .../ui/views/bubble/bubble_contents_wrapper.h | 38 ++ .../permission_bubble/chooser_bubble_ui.cc | 4 +- .../browser/brave_wallet_constants.h | 12 +- .../brave_wallet/browser/eth_transaction.cc | 7 +- .../browser/eth_transaction_unittest.cc | 24 +- .../brave_wallet/browser/eth_tx_controller.cc | 56 ++- .../brave_wallet/browser/eth_tx_controller.h | 22 +- .../browser/eth_tx_controller_unittest.cc | 97 +++-- .../brave_wallet/common/brave_wallet.mojom | 7 +- .../brave_wallet_ui/common/async/handlers.ts | 13 - .../common/async/hardware.test.ts | 187 ++++++++ .../brave_wallet_ui/common/async/hardware.ts | 59 +++ .../brave_wallet_ui/common/async/lib.ts | 2 - .../brave_wallet_ui/common/constants/mocks.ts | 36 ++ .../common/hardware_operations.ts | 16 + .../ledgerjs/eth_ledger_bridge_keyring.ts | 8 +- .../common/trezor/trezor-messages.ts | 31 +- .../trezor/trezor_bridge_keyring.test.ts | 108 +++-- .../common/trezor/trezor_bridge_keyring.ts | 71 ++- components/brave_wallet_ui/constants/types.ts | 20 + components/brave_wallet_ui/page/container.tsx | 1 + .../panel/actions/wallet_panel_actions.ts | 4 +- .../panel/async/wallet_panel_async_handler.ts | 26 +- .../brave_wallet_ui/panel/container.tsx | 23 +- .../panel/wallet_panel_api_proxy.d.ts | 1 + components/brave_wallet_ui/trezor/trezor.ts | 16 +- .../brave_wallet_ui/utils/address-utils.ts | 8 + components/resources/wallet_strings.grdp | 5 + package-lock.json | 404 ++++++------------ package.json | 2 +- test/data/brave-wallet/popup.html | 5 + 45 files changed, 1194 insertions(+), 481 deletions(-) create mode 100644 chromium_src/chrome/browser/ui/views/bubble/DEPS create mode 100644 chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.cc create mode 100644 chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.h create mode 100644 components/brave_wallet_ui/common/async/hardware.test.ts create mode 100644 components/brave_wallet_ui/common/async/hardware.ts create mode 100644 components/brave_wallet_ui/common/constants/mocks.ts create mode 100644 components/brave_wallet_ui/common/hardware_operations.ts create mode 100644 test/data/brave-wallet/popup.html diff --git a/browser/brave_wallet/brave_wallet_tab_helper.cc b/browser/brave_wallet/brave_wallet_tab_helper.cc index ec56f0f2aae2..488ad246f999 100644 --- a/browser/brave_wallet/brave_wallet_tab_helper.cc +++ b/browser/brave_wallet/brave_wallet_tab_helper.cc @@ -34,7 +34,7 @@ BraveWalletTabHelper::~BraveWalletTabHelper() { } #if !defined(OS_ANDROID) && !defined(OS_IOS) -void BraveWalletTabHelper::ClosePanelOnDeactivate(bool close) { +void BraveWalletTabHelper::SetCloseOnDeactivate(bool close) { if (wallet_bubble_manager_delegate_) wallet_bubble_manager_delegate_->CloseOnDeactivate(close); close_on_deactivate_for_testing_ = close; @@ -125,6 +125,10 @@ content::WebContents* BraveWalletTabHelper::GetBubbleWebContentsForTesting() { return wallet_bubble_manager_delegate_->GetWebContentsForTesting(); } +const std::vector& BraveWalletTabHelper::GetPopupIdsForTesting() { + return wallet_bubble_manager_delegate_->GetPopupIdsForTesting(); +} + GURL BraveWalletTabHelper::GetApproveBubbleURL() { GURL webui_url = GURL(kBraveUIWalletPanelURL); url::Replacements replacements; diff --git a/browser/brave_wallet/brave_wallet_tab_helper.h b/browser/brave_wallet/brave_wallet_tab_helper.h index 54a1a3c41e87..a495e681e57e 100644 --- a/browser/brave_wallet/brave_wallet_tab_helper.h +++ b/browser/brave_wallet/brave_wallet_tab_helper.h @@ -8,6 +8,7 @@ #include #include +#include #include "content/public/browser/web_contents_user_data.h" #include "url/gurl.h" @@ -33,9 +34,10 @@ class BraveWalletTabHelper void ShowApproveWalletBubble(); void CloseBubble(); bool IsShowingBubble(); - void ClosePanelOnDeactivate(bool close); + void SetCloseOnDeactivate(bool close); bool IsBubbleClosedForTesting(); content::WebContents* GetBubbleWebContentsForTesting(); + const std::vector& GetPopupIdsForTesting(); void SetShowBubbleCallbackForTesting(base::OnceClosure callback) { show_bubble_callback_for_testing_ = std::move(callback); } diff --git a/browser/brave_wallet/brave_wallet_tab_helper_browsertest.cc b/browser/brave_wallet/brave_wallet_tab_helper_browsertest.cc index 6605098608a2..f64082a7aaf6 100644 --- a/browser/brave_wallet/brave_wallet_tab_helper_browsertest.cc +++ b/browser/brave_wallet/brave_wallet_tab_helper_browsertest.cc @@ -10,6 +10,7 @@ #include "base/test/thread_test_helper.h" #include "brave/browser/brave_wallet/brave_wallet_tab_helper.h" #include "brave/browser/brave_wallet/rpc_controller_factory.h" +#include "brave/browser/ui/webui/brave_wallet/wallet_common_ui.h" #include "brave/common/brave_paths.h" #include "brave/common/webui_url_constants.h" #include "brave/components/brave_wallet/common/features.h" @@ -22,9 +23,11 @@ #include "chrome/test/base/ui_test_utils.h" #include "components/network_session_configurator/common/network_switches.h" #include "components/permissions/fake_usb_chooser_controller.h" +#include "components/sessions/content/session_tab_helper.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" +#include "content/public/test/test_navigation_observer.h" #include "content/public/test/web_contents_tester.h" #include "net/dns/mock_host_resolver.h" @@ -39,6 +42,50 @@ base::OnceClosure ShowChooserBubble( std::move(controller)); } +void ExecuteScriptToOpenPopup(content::WebContents* web_contents, + const GURL& url) { + content::TestNavigationObserver popup_waiter(nullptr, 1); + popup_waiter.StartWatchingNewWebContents(); + bool result = false; + CHECK(content::ExecuteScriptAndExtractBool( + web_contents, + content::JsReplace( + "window.domAutomationController.send(!!window.open($1));", url), + &result)); + ASSERT_TRUE(result); + popup_waiter.Wait(); +} + +int32_t OpenNonPanelPopup(const GURL& url, + Browser* browser, + content::WebContents* web_contents) { + EXPECT_EQ(1, browser->tab_strip_model()->count()); + content::TestNavigationObserver popup_waiter(nullptr, 1); + popup_waiter.StartWatchingNewWebContents(); + ExecuteScriptToOpenPopup(web_contents, url); + popup_waiter.Wait(); + EXPECT_EQ(2, browser->tab_strip_model()->count()); + content::WebContents* popup = + browser->tab_strip_model()->GetActiveWebContents(); + auto popup_id = sessions::SessionTabHelper::IdForTab(popup).id(); + auto* child_popup = brave_wallet::GetWebContentsFromTabId(nullptr, popup_id); + EXPECT_EQ(child_popup->GetVisibleURL(), url); + return popup_id; +} + +int32_t OpenPanelPopup(const GURL& url, + content::WebContents* panel_contents, + brave_wallet::BraveWalletTabHelper* tab_helper) { + auto current_size = tab_helper->GetPopupIdsForTesting().size(); + ExecuteScriptToOpenPopup(panel_contents, url); + auto popup_ids = tab_helper->GetPopupIdsForTesting(); + EXPECT_EQ(popup_ids.size(), current_size + 1); + auto popup_id = popup_ids.back(); + auto* child_popup = brave_wallet::GetWebContentsFromTabId(nullptr, popup_id); + EXPECT_EQ(child_popup->GetVisibleURL(), url); + return popup_id; +} + } // namespace class BraveWalletTabHelperBrowserTest : public InProcessBrowserTest { @@ -126,3 +173,84 @@ IN_PROC_BROWSER_TEST_F(BraveWalletTabHelperBrowserTest, base::RunLoop().RunUntilIdle(); ASSERT_FALSE(tab_helper->IsShowingBubble()); } + +IN_PROC_BROWSER_TEST_F(BraveWalletTabHelperBrowserTest, + ClosePopupsWhenSwitchTabs) { + auto blank_url = https_server()->GetURL("c.com", "/popup.html"); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), blank_url)); + content::WebContents* active_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + WaitForLoadStop(active_contents); + auto non_panel_popup_id = + OpenNonPanelPopup(blank_url, browser(), active_contents); + browser()->tab_strip_model()->ActivateTabAt(0); + auto* tab_helper = + brave_wallet::BraveWalletTabHelper::FromWebContents(active_contents); + ASSERT_TRUE( + brave_wallet::GetWebContentsFromTabId(nullptr, non_panel_popup_id)); + tab_helper->ShowApproveWalletBubble(); + EXPECT_TRUE(tab_helper->IsShowingBubble()); + auto* panel_contents = tab_helper->GetBubbleWebContentsForTesting(); + WaitForLoadStop(panel_contents); + tab_helper->SetCloseOnDeactivate(false); + auto popup1_id = + OpenPanelPopup(https_server()->GetURL("a.com", "/popup.html"), + panel_contents, tab_helper); + auto popup2_id = + OpenPanelPopup(https_server()->GetURL("b.com", "/popup.html"), + panel_contents, tab_helper); + EXPECT_TRUE(tab_helper->IsShowingBubble()); + chrome::NewTab(browser()); + base::RunLoop().RunUntilIdle(); + ASSERT_FALSE(tab_helper->IsShowingBubble()); + Browser* target_browser = nullptr; + ASSERT_FALSE( + brave_wallet::GetWebContentsFromTabId(&target_browser, popup1_id)); + EXPECT_EQ(target_browser, nullptr); + target_browser = nullptr; + ASSERT_FALSE( + brave_wallet::GetWebContentsFromTabId(&target_browser, popup2_id)); + EXPECT_EQ(target_browser, nullptr); + EXPECT_EQ(tab_helper->GetPopupIdsForTesting().size(), 0u); + target_browser = nullptr; + ASSERT_TRUE(brave_wallet::GetWebContentsFromTabId(&target_browser, + non_panel_popup_id)); + EXPECT_EQ(target_browser, browser()); +} + +IN_PROC_BROWSER_TEST_F(BraveWalletTabHelperBrowserTest, ClosePopupsWithBubble) { + auto blank_url = https_server()->GetURL("c.com", "/popup.html"); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), blank_url)); + content::WebContents* active_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + WaitForLoadStop(active_contents); + auto non_panel_popup_id = + OpenNonPanelPopup(blank_url, browser(), active_contents); + browser()->tab_strip_model()->ActivateTabAt(0); + auto* tab_helper = + brave_wallet::BraveWalletTabHelper::FromWebContents(active_contents); + Browser* target_browser = nullptr; + ASSERT_TRUE(brave_wallet::GetWebContentsFromTabId(&target_browser, + non_panel_popup_id)); + EXPECT_EQ(browser(), target_browser); + tab_helper->ShowApproveWalletBubble(); + EXPECT_TRUE(tab_helper->IsShowingBubble()); + auto* panel_contents = tab_helper->GetBubbleWebContentsForTesting(); + WaitForLoadStop(panel_contents); + tab_helper->SetCloseOnDeactivate(false); + auto popup1_id = + OpenPanelPopup(https_server()->GetURL("a.com", "/popup.html"), + panel_contents, tab_helper); + auto popup2_id = + OpenPanelPopup(https_server()->GetURL("b.com", "/popup.html"), + panel_contents, tab_helper); + EXPECT_TRUE(tab_helper->IsShowingBubble()); + tab_helper->CloseBubble(); + base::RunLoop().RunUntilIdle(); + ASSERT_FALSE(tab_helper->IsShowingBubble()); + ASSERT_FALSE(brave_wallet::GetWebContentsFromTabId(nullptr, popup1_id)); + ASSERT_FALSE(brave_wallet::GetWebContentsFromTabId(nullptr, popup2_id)); + EXPECT_EQ(tab_helper->GetPopupIdsForTesting().size(), 0u); + ASSERT_TRUE( + brave_wallet::GetWebContentsFromTabId(nullptr, non_panel_popup_id)); +} diff --git a/browser/ui/brave_wallet/wallet_bubble_manager_delegate.h b/browser/ui/brave_wallet/wallet_bubble_manager_delegate.h index d3e65b4f525b..b6bd0a2cc29c 100644 --- a/browser/ui/brave_wallet/wallet_bubble_manager_delegate.h +++ b/browser/ui/brave_wallet/wallet_bubble_manager_delegate.h @@ -7,6 +7,7 @@ #define BRAVE_BROWSER_UI_BRAVE_WALLET_WALLET_BUBBLE_MANAGER_DELEGATE_H_ #include +#include class GURL; @@ -33,6 +34,7 @@ class WalletBubbleManagerDelegate { virtual bool IsShowingBubble() = 0; virtual bool IsBubbleClosedForTesting() = 0; virtual content::WebContents* GetWebContentsForTesting() = 0; + virtual const std::vector& GetPopupIdsForTesting() = 0; virtual void CloseOnDeactivate(bool close) = 0; protected: diff --git a/browser/ui/wallet_bubble_manager_delegate_impl.cc b/browser/ui/wallet_bubble_manager_delegate_impl.cc index 14ecdc9ca5be..2b2bb9c03fb9 100644 --- a/browser/ui/wallet_bubble_manager_delegate_impl.cc +++ b/browser/ui/wallet_bubble_manager_delegate_impl.cc @@ -6,8 +6,11 @@ #include "brave/browser/ui/wallet_bubble_manager_delegate_impl.h" #include +#include +#include "base/callback.h" #include "brave/browser/ui/views/frame/brave_browser_view.h" +#include "brave/browser/ui/webui/brave_wallet/wallet_common_ui.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/browser_list.h" @@ -35,22 +38,81 @@ template class BraveWebUIBubbleManagerT : public WebUIBubbleManagerT { public: BraveWebUIBubbleManagerT(views::View* anchor_view, - Profile* profile, + Browser* browser, const GURL& webui_url, int task_manager_string_id, bool enable_extension_apis = false) : WebUIBubbleManagerT(anchor_view, - profile, + browser->profile(), webui_url, task_manager_string_id, - enable_extension_apis) {} + enable_extension_apis), + browser_(browser) {} base::WeakPtr CreateWebUIBubbleDialog() override { auto bubble_view = WebUIBubbleManagerT::CreateWebUIBubbleDialog(); bubble_view_ = bubble_view.get(); web_ui_contents_for_testing_ = bubble_view_->web_view()->GetWebContents(); + // Checking if we create WalletPanelUI instance of WebUI and + // extracting BubbleContentsWrapper class to pass real browser delegate + // into it to redirect popups to be opened as separate windows. + // Set a callback to be possible to activate/deactivate wallet panel from + // typescript side + auto contents_wrapper = WebUIBubbleManagerT::cached_contents_wrapper(); + if (!contents_wrapper || !contents_wrapper->web_contents()) { + return std::move(bubble_view); + } + content::WebUI* const webui = contents_wrapper->web_contents()->GetWebUI(); + if (!webui || !webui->GetController()) { + return std::move(bubble_view); + } + WalletPanelUI* wallet_panel = + webui->GetController()->template GetAs(); + if (!wallet_panel || !browser_ || !browser_->GetDelegateWeakPtr()) { + return std::move(bubble_view); + } + // Set Browser delegate to redirect popups to be opened as Popup window + contents_wrapper->SetWebContentsAddNewContentsDelegate( + browser_->GetDelegateWeakPtr()); + // Pass deactivation callback for wallet panel api calls + // The bubble disappears by default when Trezor opens a popup window + // from the wallet panel bubble. In order to prevent it we set a callback + // to modify panel deactivation flag from api calls in SetCloseOnDeactivate + // inside wallet_panel_handler.cc when necessary. + wallet_panel->SetDeactivationCallback( + base::BindRepeating(&BraveWebUIBubbleManagerT::SetCloseOnDeactivate, + weak_factory_.GetWeakPtr())); + return std::move(bubble_view); } + void CloseOpenedPopups() { + auto contents_wrapper = WebUIBubbleManagerT::cached_contents_wrapper(); + if (!contents_wrapper) + return; + for (auto tab_id : contents_wrapper->GetPopupIds()) { + Browser* popup_browser = nullptr; + content::WebContents* popup_contents = + brave_wallet::GetWebContentsFromTabId(&popup_browser, tab_id); + if (!popup_contents || !popup_browser) + continue; + auto delegate = popup_browser->GetDelegateWeakPtr(); + if (!delegate) + continue; + delegate->CloseContents(popup_contents); + } + contents_wrapper->GetPopupIds().clear(); + } + + const std::vector& GetPopupIdsForTesting() { + auto contents_wrapper = WebUIBubbleManagerT::cached_contents_wrapper(); + return contents_wrapper->GetPopupIds(); + } + + void OnWidgetDestroying(views::Widget* widget) override { + CloseOpenedPopups(); + WebUIBubbleManagerT::OnWidgetDestroying(widget); + } + void SetCloseOnDeactivate(bool close) { if (bubble_view_) { bubble_view_->set_close_on_deactivate(close); @@ -62,8 +124,10 @@ class BraveWebUIBubbleManagerT : public WebUIBubbleManagerT { } private: + Browser* browser_ = nullptr; WebUIBubbleDialogView* bubble_view_ = nullptr; content::WebContents* web_ui_contents_for_testing_ = nullptr; + base::WeakPtrFactory> weak_factory_{this}; }; // static @@ -87,8 +151,8 @@ WalletBubbleManagerDelegateImpl::WalletBubbleManagerDelegateImpl( webui_bubble_manager_ = std::make_unique>( - anchor_view, browser->profile(), webui_url_, - IDS_ACCNAME_BRAVE_WALLET_BUTTON, true); + anchor_view, browser, webui_url_, IDS_ACCNAME_BRAVE_WALLET_BUTTON, + true); } WalletBubbleManagerDelegateImpl::~WalletBubbleManagerDelegateImpl() { @@ -112,6 +176,11 @@ content::WebContents* WalletBubbleManagerDelegateImpl::GetWebContentsForTesting() { return webui_bubble_manager_->GetWebContentsForTesting(); } + +const std::vector& +WalletBubbleManagerDelegateImpl::GetPopupIdsForTesting() { + return webui_bubble_manager_->GetPopupIdsForTesting(); +} void WalletBubbleManagerDelegateImpl::CloseBubble() { webui_bubble_manager_->CloseBubble(); } diff --git a/browser/ui/wallet_bubble_manager_delegate_impl.h b/browser/ui/wallet_bubble_manager_delegate_impl.h index 7093e7e5edcf..c0cb7e53947e 100644 --- a/browser/ui/wallet_bubble_manager_delegate_impl.h +++ b/browser/ui/wallet_bubble_manager_delegate_impl.h @@ -9,6 +9,7 @@ #include "brave/browser/ui/brave_wallet/wallet_bubble_manager_delegate.h" #include +#include #include "brave/browser/ui/webui/brave_wallet/wallet_panel_ui.h" #include "chrome/browser/ui/views/bubble/webui_bubble_manager.h" @@ -35,6 +36,7 @@ class WalletBubbleManagerDelegateImpl : public WalletBubbleManagerDelegate { void CloseOnDeactivate(bool close) override; bool IsBubbleClosedForTesting() override; content::WebContents* GetWebContentsForTesting() override; + const std::vector& GetPopupIdsForTesting() override; private: content::WebContents* web_contents_; diff --git a/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.cc b/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.cc index e4b0611e85ed..ff4f4e52472d 100644 --- a/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.cc +++ b/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.cc @@ -7,15 +7,18 @@ #include +#include "base/callback.h" #include "brave/components/permissions/contexts/brave_ethereum_permission_context.h" WalletPanelHandler::WalletPanelHandler( mojo::PendingReceiver receiver, ui::MojoBubbleWebUIController* webui_controller, - GetWebContentsForTabCallback get_web_contents_for_tab) + GetWebContentsForTabCallback get_web_contents_for_tab, + PanelCloseOnDeactivationCallback close_on_deactivation) : receiver_(this, std::move(receiver)), webui_controller_(webui_controller), - get_web_contents_for_tab_(std::move(get_web_contents_for_tab)) {} + get_web_contents_for_tab_(std::move(get_web_contents_for_tab)), + close_on_deactivation_(std::move(close_on_deactivation)) {} WalletPanelHandler::~WalletPanelHandler() {} @@ -52,3 +55,8 @@ void WalletPanelHandler::CancelConnectToSite(const std::string& origin, permissions::BraveEthereumPermissionContext::Cancel(contents); } + +void WalletPanelHandler::SetCloseOnDeactivate(bool close) { + if (close_on_deactivation_) + close_on_deactivation_.Run(close); +} diff --git a/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.h b/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.h index f6959f4dd79e..7d0e17b275d1 100644 --- a/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.h +++ b/browser/ui/webui/brave_wallet/panel_handler/wallet_panel_handler.h @@ -22,11 +22,12 @@ class WalletPanelHandler : public brave_wallet::mojom::PanelHandler { public: using GetWebContentsForTabCallback = base::RepeatingCallback; - + using PanelCloseOnDeactivationCallback = base::RepeatingCallback; WalletPanelHandler( mojo::PendingReceiver receiver, ui::MojoBubbleWebUIController* webui_controller, - GetWebContentsForTabCallback get_web_contents_for_tab); + GetWebContentsForTabCallback get_web_contents_for_tab, + PanelCloseOnDeactivationCallback close_on_deactivation); WalletPanelHandler(const WalletPanelHandler&) = delete; WalletPanelHandler& operator=(const WalletPanelHandler&) = delete; @@ -35,6 +36,7 @@ class WalletPanelHandler : public brave_wallet::mojom::PanelHandler { // brave_wallet::mojom::PanelHandler: void ShowUI() override; void CloseUI() override; + void SetCloseOnDeactivate(bool close) override; void ConnectToSite(const std::vector& accounts, const std::string& origin, int32_t tab_id) override; @@ -44,6 +46,7 @@ class WalletPanelHandler : public brave_wallet::mojom::PanelHandler { mojo::Receiver receiver_; ui::MojoBubbleWebUIController* const webui_controller_; const GetWebContentsForTabCallback get_web_contents_for_tab_; + const PanelCloseOnDeactivationCallback close_on_deactivation_; }; #endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_PANEL_HANDLER_WALLET_PANEL_HANDLER_H_ diff --git a/browser/ui/webui/brave_wallet/wallet_common_ui.cc b/browser/ui/webui/brave_wallet/wallet_common_ui.cc index d43473e03e4e..355eae6b1ace 100644 --- a/browser/ui/webui/brave_wallet/wallet_common_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_common_ui.cc @@ -9,11 +9,16 @@ #include "base/version.h" #include "brave/browser/brave_wallet/erc_token_images_source.h" +#include "brave/common/webui_url_constants.h" #include "brave/components/brave_wallet/browser/brave_wallet_constants.h" #include "brave/components/brave_wallet/browser/wallet_data_files_installer.h" #include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/webui/favicon_source.h" #include "components/favicon_base/favicon_url_parser.h" +#include "components/sessions/content/session_tab_helper.h" #include "third_party/abseil-cpp/absl/types/optional.h" namespace brave_wallet { @@ -29,4 +34,26 @@ void AddERCTokenImageSource(Profile* profile) { profile, std::make_unique(path)); } +bool IsBraveWalletOrigin(const url::Origin& origin) { + return origin == url::Origin::Create(GURL(kBraveUIWalletPanelURL)) || + origin == url::Origin::Create(GURL(kBraveUIWalletPageURL)); +} + +content::WebContents* GetWebContentsFromTabId(Browser** browser, + int32_t tab_id) { + for (auto* target_browser : *BrowserList::GetInstance()) { + TabStripModel* tab_strip_model = target_browser->tab_strip_model(); + for (int index = 0; index < tab_strip_model->count(); ++index) { + content::WebContents* contents = tab_strip_model->GetWebContentsAt(index); + if (sessions::SessionTabHelper::IdForTab(contents).id() == tab_id) { + if (browser) + *browser = target_browser; + return contents; + } + } + } + + return nullptr; +} + } // namespace brave_wallet diff --git a/browser/ui/webui/brave_wallet/wallet_common_ui.h b/browser/ui/webui/brave_wallet/wallet_common_ui.h index 70fc82d85ff1..563f35e327cc 100644 --- a/browser/ui/webui/brave_wallet/wallet_common_ui.h +++ b/browser/ui/webui/brave_wallet/wallet_common_ui.h @@ -6,12 +6,28 @@ #ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_WALLET_COMMON_UI_H_ #define BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_WALLET_COMMON_UI_H_ +#include + class Profile; +class Browser; + +namespace url { +class Origin; +} // namespace url + +namespace content { +class WebContents; +} // namespace content namespace brave_wallet { void AddERCTokenImageSource(Profile* profile); +bool IsBraveWalletOrigin(const url::Origin& origin); + +content::WebContents* GetWebContentsFromTabId(Browser** browser, + int32_t tab_id); + } // namespace brave_wallet #endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_WALLET_COMMON_UI_H_ diff --git a/browser/ui/webui/brave_wallet/wallet_panel_ui.cc b/browser/ui/webui/brave_wallet/wallet_panel_ui.cc index b112a78f7c4b..557f885923e8 100644 --- a/browser/ui/webui/brave_wallet/wallet_panel_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_panel_ui.cc @@ -41,24 +41,6 @@ #include "ui/base/accelerators/accelerator.h" #include "ui/base/webui/web_ui_util.h" -namespace { - -content::WebContents* GetWebContentsFromTabId(int32_t tab_id) { - for (auto* browser : *BrowserList::GetInstance()) { - TabStripModel* tab_strip_model = browser->tab_strip_model(); - for (int index = 0; index < tab_strip_model->count(); ++index) { - content::WebContents* contents = tab_strip_model->GetWebContentsAt(index); - if (sessions::SessionTabHelper::IdForTab(contents).id() == tab_id) { - return contents; - } - } - } - - return nullptr; -} - -} // namespace - WalletPanelUI::WalletPanelUI(content::WebUI* web_ui) : ui::MojoBubbleWebUIController(web_ui, true /* Needed for webui browser tests */) { @@ -89,6 +71,11 @@ void WalletPanelUI::BindInterface( panel_factory_receiver_.Bind(std::move(receiver)); } +void WalletPanelUI::SetDeactivationCallback( + base::RepeatingCallback deactivation_callback) { + deactivation_callback_ = std::move(deactivation_callback); +} + void WalletPanelUI::CreatePanelHandler( mojo::PendingRemote page, mojo::PendingReceiver panel_receiver, @@ -113,7 +100,8 @@ void WalletPanelUI::CreatePanelHandler( panel_handler_ = std::make_unique( std::move(panel_receiver), this, - base::BindRepeating(&GetWebContentsFromTabId)); + base::BindRepeating(&brave_wallet::GetWebContentsFromTabId, nullptr), + std::move(deactivation_callback_)); wallet_handler_ = std::make_unique(std::move(wallet_receiver), profile); diff --git a/browser/ui/webui/brave_wallet/wallet_panel_ui.h b/browser/ui/webui/brave_wallet/wallet_panel_ui.h index 8bf2a35f003d..a007822dfe21 100644 --- a/browser/ui/webui/brave_wallet/wallet_panel_ui.h +++ b/browser/ui/webui/brave_wallet/wallet_panel_ui.h @@ -33,6 +33,11 @@ class WalletPanelUI : public ui::MojoBubbleWebUIController, // interface passing the pending receiver that will be internally bound. void BindInterface( mojo::PendingReceiver receiver); + // The bubble disappears by default when Trezor opens a popup window + // from the wallet panel bubble. In order to prevent it we set a callback + // to modify panel deactivation flag when necessary. + void SetDeactivationCallback( + base::RepeatingCallback deactivation_callback); private: // brave_wallet::mojom::PanelHandlerFactory: @@ -58,6 +63,7 @@ class WalletPanelUI : public ui::MojoBubbleWebUIController, std::unique_ptr panel_handler_; std::unique_ptr wallet_handler_; + base::RepeatingCallback deactivation_callback_; mojo::Receiver panel_factory_receiver_{this}; diff --git a/chromium_src/chrome/browser/ui/views/bubble/DEPS b/chromium_src/chrome/browser/ui/views/bubble/DEPS new file mode 100644 index 000000000000..2ccaf7d7874b --- /dev/null +++ b/chromium_src/chrome/browser/ui/views/bubble/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+../../../../../../../chrome/browser/ui/views/bubble", + "+components/sessions", +] diff --git a/chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.cc b/chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.cc new file mode 100644 index 000000000000..cf85f767bed1 --- /dev/null +++ b/chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.cc @@ -0,0 +1,43 @@ +// Copyright (c) 2021 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include "chrome/browser/ui/views/bubble/bubble_contents_wrapper.h" +#include "components/sessions/content/session_tab_helper.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/public/browser/web_contents_observer.h" + +// The buble delegate doesnt allow to open popups and we use +// the Browser window delegate to redirect opening new popup content +// to the Browser delegate instead of default one. +// In order to close all popups we also save tab ids of each opened popup window +// and close all with the bubble together. +#define RenderViewHostChanged \ + SetWebContentsAddNewContentsDelegate( \ + base::WeakPtr browser_delegate) { \ + browser_delegate_ = std::move(browser_delegate); \ + } \ + void BubbleContentsWrapper::AddNewContents( \ + content::WebContents* source, \ + std::unique_ptr new_contents, \ + const GURL& target_url, WindowOpenDisposition disposition, \ + const gfx::Rect& initial_rect, bool user_gesture, bool* was_blocked) { \ + if (!browser_delegate_) { \ + return; \ + } \ + auto* raw_popup_contents = new_contents.get(); \ + browser_delegate_->AddNewContents( \ + source, std::move(new_contents), target_url, \ + WindowOpenDisposition::NEW_POPUP, initial_rect, user_gesture, \ + was_blocked); \ + auto tab_id = \ + sessions::SessionTabHelper::IdForTab(raw_popup_contents).id(); \ + popup_ids_.push_back(tab_id); \ + } \ + void BubbleContentsWrapper::RenderViewHostChanged +#include "../../../../../../chrome/browser/ui/views/bubble/bubble_contents_wrapper.cc" +#undef RenderViewHostChanged diff --git a/chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.h b/chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.h new file mode 100644 index 000000000000..e636419ab36d --- /dev/null +++ b/chromium_src/chrome/browser/ui/views/bubble/bubble_contents_wrapper.h @@ -0,0 +1,38 @@ +// Copyright (c) 2021 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_VIEWS_BUBBLE_BUBBLE_CONTENTS_WRAPPER_H_ +#define BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_VIEWS_BUBBLE_BUBBLE_CONTENTS_WRAPPER_H_ + +#include + +#include "chrome/browser/ui/views/bubble/bubble_contents_wrapper.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/public/browser/web_contents_observer.h" +#include "ui/base/window_open_disposition.h" +#include "url/gurl.h" + +#define RenderViewHostChanged \ + SetWebContentsAddNewContentsDelegate( \ + base::WeakPtr browser_delegate); \ + void AddNewContents(content::WebContents* source, \ + std::unique_ptr new_contents, \ + const GURL& target_url, \ + WindowOpenDisposition disposition, \ + const gfx::Rect& initial_rect, bool user_gesture, \ + bool* was_blocked) override; \ + std::vector& GetPopupIds() { return popup_ids_; } \ + void RenderViewHostChanged + +#define webui_resizes_host_ \ + webui_resizes_host_; \ + std::vector popup_ids_; \ + base::WeakPtr browser_delegate_ +#include "../../../../../../../chrome/browser/ui/views/bubble/bubble_contents_wrapper.h" +#undef webui_resizes_host_ +#undef RenderViewHostChanged + +#endif // BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_VIEWS_BUBBLE_BUBBLE_CONTENTS_WRAPPER_H_ diff --git a/chromium_src/chrome/browser/ui/views/permission_bubble/chooser_bubble_ui.cc b/chromium_src/chrome/browser/ui/views/permission_bubble/chooser_bubble_ui.cc index ac39ec2ddce2..92b0c7e2f0f1 100644 --- a/chromium_src/chrome/browser/ui/views/permission_bubble/chooser_bubble_ui.cc +++ b/chromium_src/chrome/browser/ui/views/permission_bubble/chooser_bubble_ui.cc @@ -42,7 +42,7 @@ class BraveBubbleDialogDelegateView : public views::BubbleDialogDelegateView { auto* tab_helper = brave_wallet::BraveWalletTabHelper::FromWebContents(active); if (tab_helper) - tab_helper->ClosePanelOnDeactivate(true); + tab_helper->SetCloseOnDeactivate(true); } }; @@ -62,7 +62,7 @@ Browser* FindBrowserAndAdjustBubbleForBraveWalletPanel( auto* tab_helper = brave_wallet::BraveWalletTabHelper::FromWebContents(active); if (tab_helper) - tab_helper->ClosePanelOnDeactivate(false); + tab_helper->SetCloseOnDeactivate(false); return browser; } diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index d1e6b4f639dd..8d36f5c74df7 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -484,8 +484,16 @@ constexpr webui::LocalizedString kLocalizedStrings[] = { IDS_BRAVE_WALLET_HARDWARE_ACCOUNT_NOT_FOUND_ERROR}, {"braveWalletCreateBridgeError", IDS_BRAVE_WALLET_HARDWARE_CREATE_BRIDGE_ERROR}, - {"braveWalletImportingAccountsError", - IDS_BRAVE_WALLET_HARDWARE_ACCOUNTS_IMPORT_ERROR}}; + {"braveWalletProcessTransactionError", + IDS_BRAVE_WALLET_HARDWARE_PROCESS_TRANSACTION_ERROR}, + {"braveWalletApproveTransactionError", + IDS_BRAVE_WALLET_HARDWARE_APPROVE_TRANSACTION_ERROR}, + {"braveWalletTransactionNotFoundSignError", + IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_NOT_FOUND_ERROR}, + {"braveWalletSignOnDeviceError", + IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_DEVICE_ERROR}, + {"braveWalletNoMessageToSignError", + IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_NO_MESSAGE_TO_SIGN_ERROR}}; const char kRopstenSwapBaseAPIURL[] = "https://ropsten.api.0x.org/"; const char kRopstenBuyTokenPercentageFee[] = "0.00875"; diff --git a/components/brave_wallet/browser/eth_transaction.cc b/components/brave_wallet/browser/eth_transaction.cc index ec7f275e4a81..7972335c853b 100644 --- a/components/brave_wallet/browser/eth_transaction.cc +++ b/components/brave_wallet/browser/eth_transaction.cc @@ -190,6 +190,9 @@ std::string EthTransaction::GetSignedTransaction() const { bool EthTransaction::ProcessVRS(const std::string& v, const std::string& r, const std::string& s) { + if (!base::StartsWith(v, "0x") || !base::StartsWith(r, "0x") || + !base::StartsWith(s, "0x")) + return false; uint256_t v_decoded; if (!HexValueToUint256(v, &v_decoded)) { LOG(ERROR) << "Unable to decode v param"; @@ -197,12 +200,12 @@ bool EthTransaction::ProcessVRS(const std::string& v, } std::vector r_decoded; - if (!base::HexStringToBytes(r, &r_decoded)) { + if (!base::HexStringToBytes(r.substr(2), &r_decoded)) { LOG(ERROR) << "Unable to decode r param"; return false; } std::vector s_decoded; - if (!base::HexStringToBytes(s, &s_decoded)) { + if (!base::HexStringToBytes(s.substr(2), &s_decoded)) { LOG(ERROR) << "Unable to decode s param"; return false; } diff --git a/components/brave_wallet/browser/eth_transaction_unittest.cc b/components/brave_wallet/browser/eth_transaction_unittest.cc index 7e67c7fbbbe5..68a17b583240 100644 --- a/components/brave_wallet/browser/eth_transaction_unittest.cc +++ b/components/brave_wallet/browser/eth_transaction_unittest.cc @@ -295,13 +295,13 @@ TEST(EthTransactionUnitTest, ProcessVRS) { tx.set_nonce(0u); std::string r = - "93b9121e82df014428924df439ff044f89c205dd76a194f8b11f50d2eade744e"; + "0x93b9121e82df014428924df439ff044f89c205dd76a194f8b11f50d2eade744e"; std::string s = - "7aa705c9144742836b7fbbd0745c57f67b60df7b8d1790fe59f91ed8d2bfc11d"; + "0x7aa705c9144742836b7fbbd0745c57f67b60df7b8d1790fe59f91ed8d2bfc11d"; ASSERT_TRUE(tx.ProcessVRS("0x00", r, s)); EXPECT_EQ(tx.v(), (uint256_t)0); - EXPECT_EQ(base::ToLowerASCII(base::HexEncode(tx.r())), r); - EXPECT_EQ(base::ToLowerASCII(base::HexEncode(tx.s())), s); + EXPECT_EQ(base::ToLowerASCII(base::HexEncode(tx.r())), r.substr(2)); + EXPECT_EQ(base::ToLowerASCII(base::HexEncode(tx.s())), s.substr(2)); EXPECT_EQ( tx.GetSignedTransaction(), @@ -310,4 +310,20 @@ TEST(EthTransactionUnitTest, ProcessVRS) { "d2bfc11d"); } +TEST(EthTransactionUnitTest, ProcessVRSFail) { + EthTransaction tx; + ASSERT_FALSE(tx.ProcessVRS("", "", "")); + ASSERT_FALSE(tx.ProcessVRS("00", "aefrwr", "342fds")); + EXPECT_EQ(tx.v(), (uint256_t)0); + ASSERT_TRUE(tx.r().empty()); + ASSERT_TRUE(tx.s().empty()); + tx.set_nonce(0u); + + std::string r = + "93b9121e82df014428924df439ff044f89c205dd76a194f8b11f50d2eade744e"; + std::string s = + "7aa705c9144742836b7fbbd0745c57f67b60df7b8d1790fe59f91ed8d2bfc11d"; + ASSERT_FALSE(tx.ProcessVRS("0x00", r, s)); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/eth_tx_controller.cc b/components/brave_wallet/browser/eth_tx_controller.cc index a1b2e02a6cd8..38ad0da0d085 100644 --- a/components/brave_wallet/browser/eth_tx_controller.cc +++ b/components/brave_wallet/browser/eth_tx_controller.cc @@ -360,14 +360,14 @@ void EthTxController::OnGetGasOracle( } } -void EthTxController::ApproveHardwareTransaction( +void EthTxController::GetNonceForHardwareTransaction( const std::string& tx_meta_id, - ApproveHardwareTransactionCallback callback) { + GetNonceForHardwareTransactionCallback callback) { std::unique_ptr meta = tx_state_manager_->GetTx(tx_meta_id); if (!meta) { LOG(ERROR) << "No transaction found"; - std::move(callback).Run(false, ""); + std::move(callback).Run(absl::nullopt); return; } if (!meta->tx->nonce()) { @@ -383,52 +383,68 @@ void EthTxController::ApproveHardwareTransaction( } } +void EthTxController::GetTransactionMessageToSign( + const std::string& tx_meta_id, + GetTransactionMessageToSignCallback callback) { + std::unique_ptr meta = + tx_state_manager_->GetTx(tx_meta_id); + if (!meta) { + VLOG(1) << __FUNCTION__ << "No transaction found with id:" << tx_meta_id; + std::move(callback).Run(absl::nullopt); + return; + } + uint256_t chain_id = 0; + if (!HexValueToUint256(rpc_controller_->GetChainId(), &chain_id)) { + std::move(callback).Run(absl::nullopt); + return; + } + auto message = meta->tx->GetMessageToSign(chain_id, false); + auto encoded = brave_wallet::ToHex(message); + std::move(callback).Run(encoded); +} + void EthTxController::OnGetNextNonceForHardware( std::unique_ptr meta, - ApproveHardwareTransactionCallback callback, + GetNonceForHardwareTransactionCallback callback, bool success, uint256_t nonce) { if (!success) { meta->status = mojom::TransactionStatus::Error; tx_state_manager_->AddOrUpdateTx(*meta); - LOG(ERROR) << "GetNextNonce failed"; - std::move(callback).Run(false, ""); + VLOG(1) << __FUNCTION__ + << "GetNextNonce failed for tx with meta:" << meta->id; + std::move(callback).Run(absl::nullopt); return; } meta->tx->set_nonce(nonce); - meta->status = mojom::TransactionStatus::Approved; tx_state_manager_->AddOrUpdateTx(*meta); - uint256_t chain_id = 0; - if (!HexValueToUint256(rpc_controller_->GetChainId(), &chain_id)) { - std::move(callback).Run(false, ""); - return; - } - - auto message = meta->tx->GetMessageToSign(chain_id, false); - auto encoded = brave_wallet::ToHex(message); - std::move(callback).Run(true, encoded); + std::move(callback).Run(Uint256ValueToHex(nonce)); } -void EthTxController::ProcessLedgerSignature( +void EthTxController::ProcessHardwareSignature( const std::string& tx_meta_id, const std::string& v, const std::string& r, const std::string& s, - ProcessLedgerSignatureCallback callback) { + ProcessHardwareSignatureCallback callback) { std::unique_ptr meta = tx_state_manager_->GetTx(tx_meta_id); if (!meta) { - LOG(ERROR) << "No transaction found"; + VLOG(1) << __FUNCTION__ << "No transaction found with id" << tx_meta_id; std::move(callback).Run(false); return; } if (!meta->tx->ProcessVRS(v, r, s)) { - LOG(ERROR) << "Could not initialize a transaction with v,r,s"; + VLOG(1) << __FUNCTION__ + << "Could not initialize a transaction with v,r,s for id:" + << tx_meta_id; meta->status = mojom::TransactionStatus::Error; tx_state_manager_->AddOrUpdateTx(*meta); std::move(callback).Run(false); return; } + meta->status = mojom::TransactionStatus::Approved; + tx_state_manager_->AddOrUpdateTx(*meta); auto data = meta->tx->GetSignedTransaction(); PublishTransaction(tx_meta_id, data); std::move(callback).Run(true); diff --git a/components/brave_wallet/browser/eth_tx_controller.h b/components/brave_wallet/browser/eth_tx_controller.h index 8b06ee00118a..ea4dbf671681 100644 --- a/components/brave_wallet/browser/eth_tx_controller.h +++ b/components/brave_wallet/browser/eth_tx_controller.h @@ -98,22 +98,24 @@ class EthTxController : public KeyedService, const std::string& tx_meta_id, const std::vector& data, SetDataForUnapprovedTransactionCallback callback) override; - void ApproveHardwareTransaction( + void GetNonceForHardwareTransaction( const std::string& tx_meta_id, - ApproveHardwareTransactionCallback callback) override; - void ProcessLedgerSignature(const std::string& tx_meta_id, - const std::string& v, - const std::string& r, - const std::string& s, - ProcessLedgerSignatureCallback callback) override; - + GetNonceForHardwareTransactionCallback callback) override; void SpeedupOrCancelTransaction( const std::string& tx_meta_id, bool cancel, SpeedupOrCancelTransactionCallback callback) override; void RetryTransaction(const std::string& tx_meta_id, RetryTransactionCallback callback) override; - + void ProcessHardwareSignature( + const std::string& tx_meta_id, + const std::string& v, + const std::string& r, + const std::string& s, + ProcessHardwareSignatureCallback callback) override; + void GetTransactionMessageToSign( + const std::string& tx_meta_id, + GetTransactionMessageToSignCallback callback) override; void AddObserver( ::mojo::PendingRemote observer) override; @@ -137,7 +139,7 @@ class EthTxController : public KeyedService, uint256_t nonce); void OnGetNextNonceForHardware( std::unique_ptr meta, - ApproveHardwareTransactionCallback callback, + GetNonceForHardwareTransactionCallback callback, bool success, uint256_t nonce); void PublishTransaction(const std::string& tx_meta_id, diff --git a/components/brave_wallet/browser/eth_tx_controller_unittest.cc b/components/brave_wallet/browser/eth_tx_controller_unittest.cc index 55374a69e86e..d5526d4ab2e9 100644 --- a/components/brave_wallet/browser/eth_tx_controller_unittest.cc +++ b/components/brave_wallet/browser/eth_tx_controller_unittest.cc @@ -732,7 +732,7 @@ TEST_F(EthTxControllerUnitTest, ValidateTxData1559) { &error_message)); } -TEST_F(EthTxControllerUnitTest, ProcessLedgerSignature) { +TEST_F(EthTxControllerUnitTest, ProcessHardwareSignature) { auto tx_data = mojom::TxData::New("0x06", "" /* gas_price */, "" /* gas_limit */, "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c", @@ -744,28 +744,29 @@ TEST_F(EthTxControllerUnitTest, ProcessLedgerSignature) { tx_data.Clone(), from(), base::BindOnce(&AddUnapprovedTransactionSuccessCallback, &callback_called, &tx_meta_id)); - TestEthTxControllerObserver observer("", ""); + TestEthTxControllerObserver observer("", "", "", "", std::vector(), + mojom::TransactionStatus::Approved); eth_tx_controller_->AddObserver(observer.GetReceiver()); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(callback_called); callback_called = false; - eth_tx_controller_->ProcessLedgerSignature( + eth_tx_controller_->ProcessHardwareSignature( tx_meta_id, "0x00", - "93b9121e82df014428924df439ff044f89c205dd76a194f8b11f50d2eade744e", - "7aa705c9144742836b7fbbd0745c57f67b60df7b8d1790fe59f91ed8d2bfc11d", + "0x93b9121e82df014428924df439ff044f89c205dd76a194f8b11f50d2eade744e", + "0x7aa705c9144742836b7fbbd0745c57f67b60df7b8d1790fe59f91ed8d2bfc11d", base::BindLambdaForTesting([&](bool success) { EXPECT_TRUE(success); auto tx_meta = eth_tx_controller_->GetTxForTesting(tx_meta_id); EXPECT_TRUE(tx_meta); - EXPECT_EQ(tx_meta->status, mojom::TransactionStatus::Unapproved); + EXPECT_EQ(tx_meta->status, mojom::TransactionStatus::Approved); callback_called = true; })); base::RunLoop().RunUntilIdle(); ASSERT_TRUE(callback_called); - ASSERT_FALSE(observer.TxStatusChanged()); + ASSERT_TRUE(observer.TxStatusChanged()); } -TEST_F(EthTxControllerUnitTest, ProcessLedgerSignatureFail) { +TEST_F(EthTxControllerUnitTest, ProcessHardwareSignatureFail) { auto tx_data = mojom::TxData::New("0x06", "" /* gas_price */, "" /* gas_limit */, "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c", @@ -783,7 +784,7 @@ TEST_F(EthTxControllerUnitTest, ProcessLedgerSignatureFail) { base::RunLoop().RunUntilIdle(); EXPECT_TRUE(callback_called); callback_called = false; - eth_tx_controller_->ProcessLedgerSignature( + eth_tx_controller_->ProcessHardwareSignature( tx_meta_id, "0x00", "9ff044f89c205dd76a194f8b11f50d2eade744e", "", base::BindLambdaForTesting([&](bool success) { EXPECT_FALSE(success); @@ -797,7 +798,7 @@ TEST_F(EthTxControllerUnitTest, ProcessLedgerSignatureFail) { ASSERT_TRUE(observer.TxStatusChanged()); observer.Reset(); callback_called = false; - eth_tx_controller_->ProcessLedgerSignature( + eth_tx_controller_->ProcessHardwareSignature( "-1", "0x00", "9ff044f89c205dd76a194f8b11f50d2eade744e", "", base::BindLambdaForTesting([&](bool success) { EXPECT_FALSE(success); @@ -808,7 +809,7 @@ TEST_F(EthTxControllerUnitTest, ProcessLedgerSignatureFail) { ASSERT_FALSE(observer.TxStatusChanged()); } -TEST_F(EthTxControllerUnitTest, ApproveHardwareTransaction) { +TEST_F(EthTxControllerUnitTest, GetNonceForHardwareTransaction) { auto tx_data = mojom::TxData::New("", "" /* gas_price */, "" /* gas_limit */, "0xbe862ad9abfe6f22bcb087716c7d89a26051f74c", @@ -824,21 +825,33 @@ TEST_F(EthTxControllerUnitTest, ApproveHardwareTransaction) { base::RunLoop().RunUntilIdle(); EXPECT_TRUE(callback_called); TestEthTxControllerObserver observer("", "", "", "", std::vector(), - mojom::TransactionStatus::Approved); + mojom::TransactionStatus::Unapproved); eth_tx_controller_->AddObserver(observer.GetReceiver()); callback_called = false; - eth_tx_controller_->ApproveHardwareTransaction( + eth_tx_controller_->GetNonceForHardwareTransaction( tx_meta_id, - base::BindLambdaForTesting([&](bool success, const std::string& result) { - EXPECT_TRUE(success); + base::BindLambdaForTesting([&](const absl::optional& nonce) { + EXPECT_TRUE(nonce); + EXPECT_FALSE(nonce->empty()); + auto tx_meta = eth_tx_controller_->GetTxForTesting(tx_meta_id); + EXPECT_TRUE(tx_meta); + EXPECT_EQ(tx_meta->status, mojom::TransactionStatus::Unapproved); + EXPECT_EQ(Uint256ValueToHex(tx_meta->tx->nonce().value()), nonce); + callback_called = true; + })); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(callback_called); + + callback_called = false; + eth_tx_controller_->GetTransactionMessageToSign( + tx_meta_id, + base::BindLambdaForTesting([&](const absl::optional& + result) { EXPECT_EQ(result, "0xf873808517fcf1832182960494be862ad9abfe6f22bcb087716c7d89a2" "6051f74c88016345785d8a0000b844095ea7b30000000000000000000000" "00bfb30a082f650c2a15d0632f0e87be4f8e64460f000000000000000000" "0000000000000000000000000000003fffffffffffffff8205398080"); - auto tx_meta = eth_tx_controller_->GetTxForTesting(tx_meta_id); - EXPECT_TRUE(tx_meta); - EXPECT_EQ(tx_meta->status, mojom::TransactionStatus::Approved); callback_called = true; })); base::RunLoop().RunUntilIdle(); @@ -846,7 +859,7 @@ TEST_F(EthTxControllerUnitTest, ApproveHardwareTransaction) { ASSERT_TRUE(observer.TxStatusChanged()); } -TEST_F(EthTxControllerUnitTest, ApproveHardwareTransaction1559) { +TEST_F(EthTxControllerUnitTest, GetNonceForHardwareTransaction1559) { auto tx_data = mojom::TxData1559::New( mojom::TxData::New("0x00", "", "0x01", "0x0101010101010101010101010101010101010101", "0x00", @@ -864,20 +877,31 @@ TEST_F(EthTxControllerUnitTest, ApproveHardwareTransaction1559) { base::RunLoop().RunUntilIdle(); EXPECT_TRUE(callback_called); TestEthTxControllerObserver observer("", "", "", "", std::vector(), - mojom::TransactionStatus::Approved); + mojom::TransactionStatus::Unapproved); eth_tx_controller_->AddObserver(observer.GetReceiver()); callback_called = false; - eth_tx_controller_->ApproveHardwareTransaction( + eth_tx_controller_->GetNonceForHardwareTransaction( tx_meta_id, - base::BindLambdaForTesting([&](bool success, const std::string& result) { - EXPECT_TRUE(success); - EXPECT_EQ( - result, - "0x02dd04800101019401010101010101010101010101010101010101018080c0"); + base::BindLambdaForTesting([&](const absl::optional& nonce) { + EXPECT_TRUE(nonce); + EXPECT_FALSE(nonce->empty()); auto tx_meta = eth_tx_controller_->GetTxForTesting(tx_meta_id); EXPECT_TRUE(tx_meta); - EXPECT_EQ(tx_meta->status, mojom::TransactionStatus::Approved); + EXPECT_EQ(tx_meta->status, mojom::TransactionStatus::Unapproved); + EXPECT_EQ(Uint256ValueToHex(tx_meta->tx->nonce().value()), nonce); + callback_called = true; + })); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(callback_called); + callback_called = false; + eth_tx_controller_->GetTransactionMessageToSign( + tx_meta_id, + base::BindLambdaForTesting([&](const absl::optional& + result) { + EXPECT_EQ( + result, + "0x02dd04800101019401010101010101010101010101010101010101018080c0"); callback_called = true; })); base::RunLoop().RunUntilIdle(); @@ -885,19 +909,28 @@ TEST_F(EthTxControllerUnitTest, ApproveHardwareTransaction1559) { ASSERT_TRUE(observer.TxStatusChanged()); } -TEST_F(EthTxControllerUnitTest, ApproveHardwareTransactionFail) { +TEST_F(EthTxControllerUnitTest, GetNonceForHardwareTransactionFail) { bool callback_called = false; TestEthTxControllerObserver observer("", ""); eth_tx_controller_->AddObserver(observer.GetReceiver()); - eth_tx_controller_->ApproveHardwareTransaction( + eth_tx_controller_->GetNonceForHardwareTransaction( std::string(), - base::BindLambdaForTesting([&](bool success, const std::string& result) { - EXPECT_FALSE(success); - ASSERT_TRUE(result.empty()); + base::BindLambdaForTesting([&](const absl::optional& nonce) { + EXPECT_FALSE(nonce); callback_called = true; })); base::RunLoop().RunUntilIdle(); ASSERT_TRUE(callback_called); + + callback_called = false; + eth_tx_controller_->GetTransactionMessageToSign( + std::string(), base::BindLambdaForTesting( + [&](const absl::optional& result) { + ASSERT_FALSE(result); + callback_called = true; + })); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(callback_called); ASSERT_FALSE(observer.TxStatusChanged()); } diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index 1f9b38929d6c..2f2e0096ade0 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -68,6 +68,7 @@ interface PanelHandler { ConnectToSite(array accounts, string origin, int32 tab_id); CancelConnectToSite(string origin, int32 tab_id); + SetCloseOnDeactivate(bool close); }; // Browser-side handler for requests from WebUI page. @@ -403,11 +404,11 @@ interface EthTxController { // This returns different data depending on which network is currently selected in EthJsonRpcController GetAllTransactionInfo(string from) => (array transaction_infos); AddObserver(pending_remote observer); - ApproveHardwareTransaction(string tx_meta_id) => (bool success, string message); - ProcessLedgerSignature(string tx_meta_id, string v, string r, string s) => (bool status); - SpeedupOrCancelTransaction(string tx_meta_id, bool cancel) => (bool success, string tx_meta_id, string error_message); RetryTransaction(string tx_meta_id) => (bool success, string tx_meta_id, string error_message); + GetNonceForHardwareTransaction(string tx_meta_id) => (string? nonce); + GetTransactionMessageToSign(string tx_meta_id) => (string? message); + ProcessHardwareSignature(string tx_meta_id, string v, string r, string s) => (bool status); }; interface BraveWalletServiceObserver { diff --git a/components/brave_wallet_ui/common/async/handlers.ts b/components/brave_wallet_ui/common/async/handlers.ts index 12c4296d7dfc..0bfd84c3924f 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -40,7 +40,6 @@ import getSwapConfig from '../../constants/swap.config' import { hexStrToNumberArray } from '../../utils/hex-utils' import getAPIProxy from './bridge' import { - findHardwareAccountInfo, refreshKeyringInfo, refreshNetworkInfo, refreshTokenPriceHistory, @@ -398,18 +397,6 @@ handler.on(WalletActions.approveERC20Allowance.getType(), async (store: Store, p handler.on(WalletActions.approveTransaction.getType(), async (store: Store, txInfo: TransactionInfo) => { const apiProxy = await getAPIProxy() - const hardwareAccount = await findHardwareAccountInfo(txInfo.fromAddress) - if (hardwareAccount && hardwareAccount.hardware) { - const { success, message } = await apiProxy.ethTxController.approveHardwareTransaction(txInfo.id) - if (success) { - let deviceKeyring = await apiProxy.getKeyringsByType(hardwareAccount.hardware.vendor) - const { v, r, s } = await deviceKeyring.signTransaction(hardwareAccount.hardware.path, message.replace('0x', '')) - await apiProxy.ethTxController.processLedgerSignature(txInfo.id, '0x' + v, r, s) - await refreshWalletInfo(store) - } - return - } - await apiProxy.ethTxController.approveTransaction(txInfo.id) await refreshWalletInfo(store) }) diff --git a/components/brave_wallet_ui/common/async/hardware.test.ts b/components/brave_wallet_ui/common/async/hardware.test.ts new file mode 100644 index 000000000000..6b1dee0ce0e6 --- /dev/null +++ b/components/brave_wallet_ui/common/async/hardware.test.ts @@ -0,0 +1,187 @@ +/* Copyright (c) 2021 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { EthereumSignedTx } from 'trezor-connect/lib/typescript' +import { +GetNonceForHardwareTransactionReturnInfo, +GetTransactionMessageToSignReturnInfo, +SignatureVRS, +ProcessHardwareSignatureReturnInfo, +TransactionInfo, +kTrezorHardwareVendor, +kLedgerHardwareVendor +} from '../../constants/types' +import { +signTrezorTransaction, +signLedgerTransaction +} from '../../common/async/hardware' +import { getLocale } from '../../../common/locale' +import { Success, Unsuccessful } from 'trezor-connect' +import { getMockedTransactionInfo } from '../constants/mocks' +import { SignHardwareTransactionOperationResult, SignHardwareTransactionType } from '../hardware_operations' +import { APIProxyControllers } from 'components/brave_wallet_ui/constants/types' + +const getMockedLedgerKeyring = (expectedPath: string, expectedData: string | TransactionInfo, signed?: SignHardwareTransactionOperationResult) => { + return { + type: () => { + return kLedgerHardwareVendor + }, + signTransaction: async (path: string, data: string): Promise => { + expect(path).toStrictEqual(expectedPath) + expect(data).toStrictEqual(expectedData) + if (signed) { + return Promise.resolve(signed) + } + return Promise.resolve({ success: false }) + }, + signed: () => { + if (!signed) { + return + } + const { v, r, s } = signed.payload as EthereumSignedTx + return { + v: '0x' + v, + r: r, + s: s + } + } + } +} + +const getMockedTrezorKeyring = (expectedDevicePath: string, expectedData: string | TransactionInfo, signed?: Success | Unsuccessful) => { + return { + type: () => { + return kTrezorHardwareVendor + }, + signTransaction: async (path: string, data: string): Promise | Unsuccessful | undefined> => { + expect(path).toStrictEqual(expectedDevicePath) + expect(data).toStrictEqual(expectedData) + return Promise.resolve(signed) + }, + signed: () => { + if (!signed) { + return + } + const { v, r, s } = signed.payload as EthereumSignedTx + return { + v: v, + r: r, + s: s + } + } + } +} + +const getMockedProxyControllers = (expectedId: string, + nonce?: GetNonceForHardwareTransactionReturnInfo, + messageToSign?: GetTransactionMessageToSignReturnInfo | undefined, + keyring?: any, + hardwareSignature?: ProcessHardwareSignatureReturnInfo) => { + return { + ethJsonRpcController: { + getChainId: async () => { + return '0x123' + } + }, + ethTxController: { + getNonceForHardwareTransaction: (id: string): GetNonceForHardwareTransactionReturnInfo | undefined => { + expect(id).toStrictEqual(expectedId) + return nonce + }, + getTransactionMessageToSign: (id: string): GetTransactionMessageToSignReturnInfo | undefined => { + expect(id).toStrictEqual(expectedId) + return messageToSign + }, + processHardwareSignature: (id: string, v: string, r: string, s: string): ProcessHardwareSignatureReturnInfo | undefined => { + expect(id).toStrictEqual(expectedId) + expect(v.startsWith('0x')).toStrictEqual(true) + expect(r.startsWith('0x')).toStrictEqual(true) + expect(s.startsWith('0x')).toStrictEqual(true) + return hardwareSignature + } + }, + getKeyringsByType (type: string) { + expect(type).toStrictEqual(keyring.type()) + return keyring + } + } +} + +const signTransactionWithLedger = (vrs?: SignatureVRS, signatureResponse?: boolean): Promise => { + const txInfo = getMockedTransactionInfo() + const expectedData = 'raw_message_to_sign' + const messageToSign = { message: expectedData } + const expectedPath = 'path' + const signTransactionResult = vrs ? { success: true, payload: vrs } : { success: false } + const mockedKeyring = getMockedLedgerKeyring(expectedPath, expectedData, signTransactionResult as SignHardwareTransactionOperationResult) + const signed = signatureResponse ? { status: signatureResponse } : undefined + const apiProxy = getMockedProxyControllers(txInfo.id, { nonce: '0x1' }, messageToSign, + mockedKeyring, signed) + return signLedgerTransaction(apiProxy as unknown as APIProxyControllers, expectedPath, txInfo) +} + +const hardwareTransactionErrorResponse = (errorId: string): SignHardwareTransactionType => { + return { success: false, error: getLocale(errorId) } +} + +const signTransactionWithTrezor = (signed: Success | Unsuccessful, signatureResponse?: ProcessHardwareSignatureReturnInfo) => { + const txInfo = getMockedTransactionInfo() + const expectedPath = 'path' + const mockedKeyring = getMockedTrezorKeyring(expectedPath, txInfo, signed) + const apiProxy = getMockedProxyControllers(txInfo.id, { nonce: '0x1' }, undefined, mockedKeyring, signatureResponse) + return signTrezorTransaction(apiProxy as unknown as APIProxyControllers, + expectedPath, txInfo) +} + +test('Test sign Ledger transaction, nonce failed', () => { + const txInfo = getMockedTransactionInfo() + const apiProxy = getMockedProxyControllers(txInfo.id, { nonce: '' }) + return expect(signLedgerTransaction(apiProxy as unknown as APIProxyControllers, + 'path', txInfo)).resolves.toStrictEqual(hardwareTransactionErrorResponse('braveWalletApproveTransactionError')) +}) + +test('Test sign Ledger transaction, approved, no message to sign', () => { + const txInfo = getMockedTransactionInfo() + const apiProxy = getMockedProxyControllers(txInfo.id, { nonce: '0x1' }) + return expect(signLedgerTransaction(apiProxy as unknown as APIProxyControllers, + 'path', txInfo)).resolves.toStrictEqual(hardwareTransactionErrorResponse('braveWalletNoMessageToSignError')) +}) + +test('Test sign Ledger transaction, approved, device error', () => { + return expect(signTransactionWithLedger()).resolves.toStrictEqual( + hardwareTransactionErrorResponse('braveWalletSignOnDeviceError')) +}) + +test('Test sign Ledger transaction, approved, processing error', () => { + return expect(signTransactionWithLedger({ v: 1, r: 'R', s: 'S' })).resolves.toStrictEqual( + hardwareTransactionErrorResponse('braveWalletProcessTransactionError')) +}) + +test('Test sign Ledger transaction, approved, processed', () => { + return expect(signTransactionWithLedger({ v: 1, r: 'R', s: 'S' }, true)).resolves.toStrictEqual({ success: true }) +}) + +test('Test sign Trezor transaction, approve failed', () => { + const txInfo = getMockedTransactionInfo() + const apiProxy = getMockedProxyControllers(txInfo.id, { nonce: '' }) + return expect(signTrezorTransaction(apiProxy as unknown as WalletApiProxy, + 'path', txInfo)).resolves.toStrictEqual( + hardwareTransactionErrorResponse('braveWalletApproveTransactionError')) +}) + +test('Test sign Trezor transaction, approved, device error', () => { + return expect(signTransactionWithTrezor({ success: false, payload: { error: 'error', code: '111' } })) + .resolves.toStrictEqual(hardwareTransactionErrorResponse('braveWalletSignOnDeviceError')) +}) + +test('Test sign Trezor transaction, approved, processing error', () => { + return expect(signTransactionWithTrezor({ id: 1, success: true, payload: { v: '0xV', r: '0xR', s: '0xS' } }, { status: false })).resolves.toStrictEqual( + hardwareTransactionErrorResponse('braveWalletProcessTransactionError')) +}) + +test('Test sign Trezor transaction, approved, processed', () => { + return expect(signTransactionWithTrezor({ id: 1, success: true, payload: { v: '0xV', r: '0xR', s: '0xS' } }, { status: true })).resolves.toStrictEqual( + { success: true }) +}) diff --git a/components/brave_wallet_ui/common/async/hardware.ts b/components/brave_wallet_ui/common/async/hardware.ts new file mode 100644 index 000000000000..83626485ed79 --- /dev/null +++ b/components/brave_wallet_ui/common/async/hardware.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2021 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import { SignHardwareTransactionType } from '../hardware_operations' +import { getLocale } from '../../../common/locale' +import LedgerBridgeKeyring from '../../common/ledgerjs/eth_ledger_bridge_keyring' +import TrezorBridgeKeyring from '../../common/trezor/trezor_bridge_keyring' +import { APIProxyControllers } from 'components/brave_wallet_ui/constants/types' +import { + kTrezorHardwareVendor, + kLedgerHardwareVendor, + TransactionInfo +} from '../../constants/types' + +export async function signTrezorTransaction (apiProxy: APIProxyControllers, path: string, txInfo: TransactionInfo): Promise { + const chainId = await apiProxy.ethJsonRpcController.getChainId() + const nonce = await apiProxy.ethTxController.getNonceForHardwareTransaction(txInfo.id) + if (!nonce || !nonce.nonce) { + return { success: false, error: getLocale('braveWalletApproveTransactionError') } + } + txInfo.txData.baseData.nonce = nonce.nonce + const deviceKeyring = apiProxy.getKeyringsByType(kTrezorHardwareVendor) as TrezorBridgeKeyring + const signed = await deviceKeyring.signTransaction(path, txInfo, chainId.chainId) + if (!signed || !signed.success || !signed.payload) { + return { success: false, error: getLocale('braveWalletSignOnDeviceError') } + } + const { v, r, s } = signed.payload + const result = + await apiProxy.ethTxController.processHardwareSignature(txInfo.id, v, r, s) + if (!result.status) { + return { success: false, error: getLocale('braveWalletProcessTransactionError') } + } + return { success: result.status } +} + +export async function signLedgerTransaction (apiProxy: APIProxyControllers, path: string, txInfo: TransactionInfo): Promise { + const nonce = await apiProxy.ethTxController.getNonceForHardwareTransaction(txInfo.id) + if (!nonce || !nonce.nonce) { + return { success: false, error: getLocale('braveWalletApproveTransactionError') } + } + const data = await apiProxy.ethTxController.getTransactionMessageToSign(txInfo.id) + if (!data || !data.message) { + return { success: false, error: getLocale('braveWalletNoMessageToSignError') } + } + const deviceKeyring = apiProxy.getKeyringsByType(kLedgerHardwareVendor) as LedgerBridgeKeyring + const signed = await deviceKeyring.signTransaction(path, data.message.replace('0x', '')) + if (!signed || !signed.success || !signed.payload) { + const error = signed && signed.error ? signed.error : getLocale('braveWalletSignOnDeviceError') + return { success: false, error: error } + } + const { v, r, s } = signed.payload + const result = await apiProxy.ethTxController.processHardwareSignature(txInfo.id, '0x' + v, '0x' + r, '0x' + s) + if (!result || !result.status) { + return { success: false, error: getLocale('braveWalletProcessTransactionError') } + } + return { success: result.status } +} diff --git a/components/brave_wallet_ui/common/async/lib.ts b/components/brave_wallet_ui/common/async/lib.ts index fbba5289cc5a..5b28d1ed3728 100644 --- a/components/brave_wallet_ui/common/async/lib.ts +++ b/components/brave_wallet_ui/common/async/lib.ts @@ -8,7 +8,6 @@ import { HardwareWalletConnectOpts } from '../../components/desktop/popup-modals/add-account-modal/hardware-wallet-connect/types' import { formatBalance } from '../../utils/format-balances' - import { AccountTransactions, AssetPriceTimeframe, @@ -18,7 +17,6 @@ import { } from '../../constants/types' import * as WalletActions from '../actions/wallet_actions' import { GetNetworkInfo } from '../../utils/network-utils' - import getAPIProxy from './bridge' import { Dispatch, State } from './types' diff --git a/components/brave_wallet_ui/common/constants/mocks.ts b/components/brave_wallet_ui/common/constants/mocks.ts new file mode 100644 index 000000000000..83be539ec92c --- /dev/null +++ b/components/brave_wallet_ui/common/constants/mocks.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2021 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import { + TransactionInfo +} from '../../constants/types' + +export const getMockedTransactionInfo = (): TransactionInfo => { + return { + id: '1', + fromAddress: '0x8b52c24d6e2600bdb8dbb6e8da849ed38ab7e81f', + txHash: '', + txData: { + baseData: { + to: '0x8b52c24d6e2600bdb8dbb6e8da849ed38ab7e81f', + value: '0x01706a99bf354000', + data: new Uint8Array(0), + nonce: '0x03', + gasLimit: '0x5208', + gasPrice: '0x22ecb25c00' + }, + chainId: '1337', + maxPriorityFeePerGas: '', + maxFeePerGas: '' + }, + txStatus: 1, + txType: 5, + txParams: [], + txArgs: [], + createdTime: { microseconds: 0 }, + submittedTime: { microseconds: 0 }, + confirmedTime: { microseconds: 0 } + } +} diff --git a/components/brave_wallet_ui/common/hardware_operations.ts b/components/brave_wallet_ui/common/hardware_operations.ts new file mode 100644 index 000000000000..66c98af962a8 --- /dev/null +++ b/components/brave_wallet_ui/common/hardware_operations.ts @@ -0,0 +1,16 @@ +import { EthereumSignedTx } from 'trezor-connect/lib/typescript' + +export interface SignHardwareTransactionType { + success: boolean + error?: string +} + +export type HardwareOperationResult = { + success: boolean + error?: string + code?: string | number +} + +export type SignHardwareTransactionOperationResult = HardwareOperationResult & { + payload?: EthereumSignedTx +} diff --git a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts index 5c40d98f2ea2..3eb5ab5bf2e5 100644 --- a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts +++ b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts @@ -18,6 +18,7 @@ import Eth from '@ledgerhq/hw-app-eth' import TransportWebHID from '@ledgerhq/hw-transport-webhid' import { getLocale } from '../../../common/locale' import { hardwareDeviceIdFromAddress } from '../hardwareDeviceIdFromAddress' +import { SignHardwareTransactionOperationResult } from '../../common/hardware_operations' export default class LedgerBridgeKeyring extends EventEmitter { constructor () { @@ -62,11 +63,12 @@ export default class LedgerBridgeKeyring extends EventEmitter { return this.isUnlocked() } - signTransaction = async (path: string, rawTxHex: string) => { + signTransaction = async (path: string, rawTxHex: string): Promise => { if (!this.isUnlocked() && !(await this.unlock())) { - return new Error(getLocale('braveWalletUnlockError')) + return { success: false, error: getLocale('braveWalletUnlockError') } } - return this.app.signTransaction(path, rawTxHex) + const signed = await this.app.signTransaction(path, rawTxHex) + return { success: true, payload: signed } } signPersonalMessage = async (path: string, address: string, message: string) => { diff --git a/components/brave_wallet_ui/common/trezor/trezor-messages.ts b/components/brave_wallet_ui/common/trezor/trezor-messages.ts index 913d710fdab7..0d345caae1e4 100644 --- a/components/brave_wallet_ui/common/trezor/trezor-messages.ts +++ b/components/brave_wallet_ui/common/trezor/trezor-messages.ts @@ -2,15 +2,16 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. - import { loadTimeData } from '../../../common/loadTimeData' -import { Unsuccessful, Success } from 'trezor-connect' -import { HDNodeResponse } from 'trezor-connect/lib/typescript/trezor/protobuf' +import { Unsuccessful, EthereumSignTransaction, CommonParams, Success } from 'trezor-connect' +import { HDNodeResponse } from 'trezor-connect/lib/typescript' +import { EthereumSignedTx } from 'trezor-connect/lib/typescript/networks/ethereum' export const kTrezorBridgeUrl = loadTimeData.getString('braveWalletTrezorBridgeUrl') export enum TrezorCommand { Unlock = 'trezor-unlock', - GetAccounts = 'trezor-get-accounts' + GetAccounts = 'trezor-get-accounts', + SignTransaction = 'trezor-sign-treansaction' } export type CommandMessage = { command: TrezorCommand @@ -31,21 +32,35 @@ export type UnlockResponse = CommandMessage & { result: Boolean, error?: Unsuccessful } + +export type SignTransactionCommandPayload = CommonParams & EthereumSignTransaction + +export type SignTransactionCommand = CommandMessage & { + command: TrezorCommand.SignTransaction + payload: SignTransactionCommandPayload +} + +export type SignTransactionResponse = Unsuccessful | Success + export type TrezorAccount = { publicKey: string serializedPath: string, fingerprint: number } export type TrezorError = { - error: string, - code: string + error: string + code?: string | number } export type TrezorGetPublicKeyResponse = Unsuccessful | Success export type GetAccountsResponsePayload = CommandMessage & { payload: TrezorGetPublicKeyResponse } -export type TrezorFrameCommand = GetAccountsCommand | UnlockCommand -export type TrezorFrameResponse = UnlockResponse | GetAccountsResponsePayload + +export type SignTransactionResponsePayload = CommandMessage & { + payload: SignTransactionResponse +} +export type TrezorFrameCommand = GetAccountsCommand | UnlockCommand | SignTransactionCommand +export type TrezorFrameResponse = UnlockResponse | GetAccountsResponsePayload | SignTransactionResponsePayload // Trezor library is loaded inside the chrome-untrusted webui page // and communication is going through posting messages between parent window diff --git a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.test.ts b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.test.ts index af01680d7bea..ef87075dd2e0 100644 --- a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.test.ts +++ b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.test.ts @@ -17,7 +17,8 @@ import { TrezorFrameCommand, UnlockResponse, UnlockCommand, - GetAccountsCommand + GetAccountsCommand, + SignTransactionResponse } from '../../common/trezor/trezor-messages' import { kTrezorHardwareVendor @@ -25,6 +26,7 @@ import { import { getLocale } from '../../../common/locale' import { TrezorBridgeTransport } from './trezor-bridge-transport' import { TrezorCommandHandler } from './trezor-command-handler' +import { getMockedTransactionInfo } from '../constants/mocks' let uuid = 0 window.crypto = { @@ -33,9 +35,7 @@ window.crypto = { } } -const createTrezorTransport = (unlock: Boolean, - accounts?: TrezorGetPublicKeyResponse) => { - const hardwareTransport = new TrezorBridgeTransport(kTrezorBridgeUrl) +const createTransport = (url: string, hardwareTransport: TrezorBridgeTransport | TrezorCommandHandler) => { hardwareTransport.windowListeners_ = {} hardwareTransport.getTrezorBridgeOrigin = () => { return 'braveWalletTrezorBridgeUrl' @@ -62,12 +62,20 @@ const createTrezorTransport = (unlock: Boolean, hardwareTransport.postResponse = (data: TrezorFrameResponse) => { for (const value of Object.values(hardwareTransport.windowListeners_)) { (value as Function)( - { type: 'message', - origin: kTrezorBridgeUrl, + { + type: 'message', + origin: url, data: data - }, kTrezorBridgeUrl) + }, url) } } + return hardwareTransport +} + +const createTrezorTransport = (unlock: Boolean, + accounts?: TrezorGetPublicKeyResponse, + signedPayload?: SignTransactionResponse) => { + let hardwareTransport = createTransport(kTrezorBridgeUrl, new TrezorBridgeTransport(kTrezorBridgeUrl)) hardwareTransport.contentWindow = { postMessage: (message: any, command: any) => { expect(command).toStrictEqual(kTrezorBridgeUrl) @@ -85,6 +93,13 @@ const createTrezorTransport = (unlock: Boolean, payload: accounts }) } + if (message.command === TrezorCommand.SignTransaction) { + hardwareTransport.postResponse({ + id: message.id, + command: TrezorCommand.SignTransaction, + payload: signedPayload + }) + } } } hardwareTransport.createBridge = async () => { @@ -95,38 +110,7 @@ const createTrezorTransport = (unlock: Boolean, } const createCommandHandler = () => { - const hardwareTransport = new TrezorCommandHandler() - hardwareTransport.windowListeners_ = {} - hardwareTransport.getTrezorBridgeOrigin = () => { - return 'braveWalletTrezorBridgeUrl' - } - hardwareTransport.addWindowMessageListener = () => { - hardwareTransport.expectWindowMessageSubscribers(0) - - const key = hardwareTransport.onMessageReceived.toString() - hardwareTransport.windowListeners_[key] = hardwareTransport.onMessageReceived - - hardwareTransport.expectWindowMessageSubscribers(1) - } - hardwareTransport.expectWindowMessageSubscribers = (amount: number) => { - const keys = Object.keys(hardwareTransport.windowListeners_) - expect(keys.length).toStrictEqual(amount) - } - hardwareTransport.removeWindowMessageListener = () => { - hardwareTransport.expectWindowMessageSubscribers(1) - const key = hardwareTransport.onMessageReceived.toString() - delete hardwareTransport.windowListeners_[key] - hardwareTransport.expectWindowMessageSubscribers(0) - } - hardwareTransport.postResponse = (data: TrezorFrameResponse) => { - for (const value of Object.values(hardwareTransport.windowListeners_)) { - (value as Function)( - { type: 'message', - origin: kTrezorBridgeUrl, - data: data - }, kTrezorBridgeUrl) - } - } + let hardwareTransport = createTransport(kTrezorBridgeUrl, new TrezorCommandHandler()) hardwareTransport._commands_called = [] return hardwareTransport } @@ -214,9 +198,10 @@ test('isUnlocked', () => { }) const createTrezorKeyringWithTransport = (unlock: Boolean, - accounts?: TrezorGetAccountsResponse) => { + accounts?: TrezorGetAccountsResponse, + signedPayload?: SignTransactionResponse) => { const hardwareKeyring = new TrezorBridgeKeyring() - const transport = createTrezorTransport(unlock, accounts) + const transport = createTrezorTransport(unlock, accounts, signedPayload) hardwareKeyring.sendTrezorCommand = async (command: TrezorFrameCommand, listener: Function) => { return transport.sendCommandToTrezorFrame(command, listener) } @@ -268,7 +253,7 @@ test('Extracting accounts from unlocked device returned fail', () => { .rejects.toStrictEqual(new Error(getLocale('braveWalletCreateBridgeError'))) }) -test('Extracting accounts from unlocked device returned success', () => { +test('Extract accounts from unlocked device returned success', () => { const accounts = [ { publicKey: '3a443d8381a6798a70c6ff9304bdc8cb0163c23211d11628fae52ef9e0dca11a001cf066d56a8156fc201cd5df8a36ef694eecd258903fca7086c1fae7441e1d', @@ -390,3 +375,42 @@ test('Add multiple commands handlers', () => { [ TrezorCommand.Unlock, TrezorCommand.GetAccounts, TrezorCommand.GetAccounts, TrezorCommand.Unlock]]) }) + +test('Sign transaction from unlocked device', () => { + const txInfo = getMockedTransactionInfo() + const signed = { + success: true, + payload: { + v: '0xV', + r: '0xR', + s: '0xS' + } + } + const hardwareKeyring = createTrezorKeyringWithTransport(true, undefined, signed) + hardwareKeyring._getBridge = () => { + return hardwareKeyring as any + } + return expect(hardwareKeyring.signTransaction('m/44\'/60\'/0\'/0', txInfo, '0x539')) + .resolves.toStrictEqual(signed) +}) + +test('Sign transaction failed from unlocked device', () => { + const txInfo = getMockedTransactionInfo() + const signed = { + success: false, + payload: { + error: 'Permissions not granted', + code: 'Method_PermissionsNotGranted' + } + } + const hardwareKeyring = createTrezorKeyringWithTransport(true, undefined, signed) + hardwareKeyring._getBridge = () => { + return hardwareKeyring as any + } + return expect(hardwareKeyring.signTransaction('m/44\'/60\'/0\'/0', txInfo, '0x539')) + .resolves.toStrictEqual({ + error: signed.payload.error, + code: signed.payload.code, + success: false + }) +}) diff --git a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts index dcd3d310dafb..ca788fc833ab 100644 --- a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts +++ b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts @@ -5,23 +5,27 @@ /* global window */ const { EventEmitter } = require('events') -import { publicToAddress, toChecksumAddress } from 'ethereumjs-util' +import { publicToAddress, toChecksumAddress, bufferToHex } from 'ethereumjs-util' import { TrezorDerivationPaths, TrezorBridgeAccountsPayload } from '../../components/desktop/popup-modals/add-account-modal/hardware-wallet-connect/types' import { - kTrezorHardwareVendor + kTrezorHardwareVendor, + TransactionInfo } from '../../constants/types' import { TrezorCommand, UnlockResponse, GetAccountsResponsePayload, TrezorAccount, - TrezorFrameCommand + SignTransactionCommandPayload, + TrezorFrameCommand, + SignTransactionResponsePayload } from '../../common/trezor/trezor-messages' import { sendTrezorCommand } from '../../common/trezor/trezor-bridge-transport' import { getLocale } from '../../../common/locale' import { hardwareDeviceIdFromAddress } from '../hardwareDeviceIdFromAddress' +import { SignHardwareTransactionOperationResult } from '../../common/hardware_operations' export default class TrezorBridgeKeyring extends EventEmitter { constructor () { @@ -57,6 +61,26 @@ export default class TrezorBridgeKeyring extends EventEmitter { return accounts.accounts } + signTransaction = async (path: string, txInfo: TransactionInfo, chainId: string): Promise => { + if (!this.isUnlocked() && !(await this.unlock())) { + return { success: false, error: getLocale('braveWalletUnlockError') } + } + const data = await this.sendTrezorCommand({ + command: TrezorCommand.SignTransaction, + // @ts-expect-error + id: crypto.randomUUID(), + payload: this.prepareTransactionPayload(path, txInfo, chainId), + origin: window.origin + }) + if (!data || !data.payload) { + return { success: false, error: getLocale('braveWalletProcessTransactionError') } + } + if (!data.payload.success) { + return { success: false, error: data.payload.payload.error, code: data.payload.payload.code } + } + return { success: true, payload: data.payload.payload } + } + isUnlocked = () => { return this.unlocked_ } @@ -97,7 +121,46 @@ export default class TrezorBridgeKeyring extends EventEmitter { return '' } - private publicKeyToAddress = (key: string) => { + private prepareTransactionPayload = (path: string, txInfo: TransactionInfo, chainId: string): SignTransactionCommandPayload => { + const isEIP1559Transaction = txInfo.txData.maxPriorityFeePerGas !== '' && txInfo.txData.maxFeePerGas !== '' + if (isEIP1559Transaction) { + return this.createEIP1559TransactionPayload(path, txInfo, chainId) + } + return this.createLegacyTransactionPayload(path, txInfo, chainId) + } + + private createEIP1559TransactionPayload = (path: string, txInfo: TransactionInfo, chainId: string): SignTransactionCommandPayload => { + return { + path: path, + transaction: { + to: txInfo.txData.baseData.to, + value: txInfo.txData.baseData.value, + data: bufferToHex(Buffer.from(txInfo.txData.baseData.data)).toString(), + chainId: parseInt(chainId, 16), + nonce: txInfo.txData.baseData.nonce, + gasLimit: txInfo.txData.baseData.gasLimit, + maxFeePerGas: txInfo.txData.maxFeePerGas, + maxPriorityFeePerGas: txInfo.txData.maxPriorityFeePerGas + } + } + } + + private createLegacyTransactionPayload = (path: string, txInfo: TransactionInfo, chainId: string): SignTransactionCommandPayload => { + return { + path: path, + transaction: { + to: txInfo.txData.baseData.to, + value: txInfo.txData.baseData.value, + data: bufferToHex(Buffer.from(txInfo.txData.baseData.data)).toString(), + chainId: parseInt(chainId, 16), + nonce: txInfo.txData.baseData.nonce, + gasLimit: txInfo.txData.baseData.gasLimit, + gasPrice: txInfo.txData.baseData.gasPrice + } + } + } + + private readonly publicKeyToAddress = (key: string) => { const buffer = Buffer.from(key, 'hex') const address = publicToAddress(buffer, true).toString('hex') return toChecksumAddress(`0x${address}`) diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index eeb2760e9aa0..721bf96e2f2e 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -1,3 +1,8 @@ +// Copyright (c) 2021 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + // url.mojom.Url export interface Url { url: string @@ -661,6 +666,9 @@ export interface EthTxController { processLedgerSignature: (txMetaId: string, v: string, r: string, s: string) => Promise speedupOrCancelTransaction: (txMetaId: string, cancel: boolean) => Promise retryTransaction: (txMetaId: string) => Promise + getNonceForHardwareTransaction: (txMetaId: string) => Promise + processHardwareSignature: (txMetaId: string, v: string, r: string, s: string) => Promise + getTransactionMessageToSign: (txMetaId: string) => Promise } export interface EthJsonRpcController { @@ -800,6 +808,18 @@ export interface MojoTime { microseconds: number } +export interface GetTransactionMessageToSignReturnInfo { + message: string +} + +export interface ProcessHardwareSignatureReturnInfo { + status: boolean +} + +export interface GetNonceForHardwareTransactionReturnInfo { + nonce: string +} + export type BuySendSwapViewTypes = | 'swap' | 'buy' diff --git a/components/brave_wallet_ui/page/container.tsx b/components/brave_wallet_ui/page/container.tsx index 0fe5d0d46151..c7fdef78847f 100644 --- a/components/brave_wallet_ui/page/container.tsx +++ b/components/brave_wallet_ui/page/container.tsx @@ -277,6 +277,7 @@ function Container (props: Props) { if (found) { balance = Number(formatBalance(found.assetBalance, found.asset.decimals)) } + console.log(found, account) return balance }) const grandTotal = amounts.reduce(function (a, b) { diff --git a/components/brave_wallet_ui/panel/actions/wallet_panel_actions.ts b/components/brave_wallet_ui/panel/actions/wallet_panel_actions.ts index e1160fdb52b0..7a00265cd022 100644 --- a/components/brave_wallet_ui/panel/actions/wallet_panel_actions.ts +++ b/components/brave_wallet_ui/panel/actions/wallet_panel_actions.ts @@ -18,7 +18,8 @@ import { SwapErrorResponse, SwapResponse, SignMessageData, - SwitchChainRequest + SwitchChainRequest, + TransactionInfo } from '../../constants/types' import { SwapParamsPayloadType } from '../../common/constants/action_types' @@ -45,3 +46,4 @@ export const signMessage = createAction('signMessage') export const signMessageProcessed = createAction('signMessageProcessed') export const signMessageHardware = createAction('signMessageHardware') export const signMessageHardwareProcessed = createAction('signMessageHardwareProcessed') +export const approveHardwareTransaction = createAction('approveHardwareTransaction') diff --git a/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts b/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts index 40ebf8737ba0..deab8496d33b 100644 --- a/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts +++ b/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts @@ -13,7 +13,10 @@ import { WalletState, TransactionStatus, SignMessageData, - SwitchChainRequest + SwitchChainRequest, + TransactionInfo, + kTrezorHardwareVendor, + kLedgerHardwareVendor } from '../../constants/types' import { AccountPayloadType, @@ -28,7 +31,10 @@ import { import { findHardwareAccountInfo } from '../../common/async/lib' - +import { + signTrezorTransaction, + signLedgerTransaction +} from '../../common/async/hardware' import { fetchSwapQuoteFactory } from '../../common/async/handlers' import { Store } from '../../common/async/types' import { getLocale } from '../../../common/locale' @@ -136,6 +142,22 @@ handler.on(PanelActions.cancelConnectToSite.getType(), async (store: Store, payl apiProxy.closeUI() }) +handler.on(PanelActions.approveHardwareTransaction.getType(), async (store: Store, txInfo: TransactionInfo) => { + const hardwareAccount = await findHardwareAccountInfo(txInfo.fromAddress) + if (!hardwareAccount || !hardwareAccount.hardware) { + return + } + const apiProxy = await getAPIProxy() + apiProxy.setCloseOnDeactivate(false) + if (hardwareAccount.hardware.vendor === kLedgerHardwareVendor) { + await signLedgerTransaction(apiProxy, hardwareAccount.hardware.path, txInfo) + } else if (hardwareAccount.hardware.vendor === kTrezorHardwareVendor) { + await signTrezorTransaction(apiProxy, hardwareAccount.hardware.path, txInfo) + } + apiProxy.setCloseOnDeactivate(true) + apiProxy.showUI() +}) + handler.on(PanelActions.connectToSite.getType(), async (store: Store, payload: AccountPayloadType) => { const state = getPanelState(store) const apiProxy = await getAPIProxy() diff --git a/components/brave_wallet_ui/panel/container.tsx b/components/brave_wallet_ui/panel/container.tsx index df3b88e10c91..b3f4f7adb2dd 100644 --- a/components/brave_wallet_ui/panel/container.tsx +++ b/components/brave_wallet_ui/panel/container.tsx @@ -62,7 +62,7 @@ import { findUnstoppableDomainAddress, getERC20Allowance } from '../common/async/lib' - +import { isHardwareAccount } from '../utils/address-utils' import { useAssets, useBalance, useSwap, useSend, usePreset } from '../common/hooks' type Props = { @@ -353,17 +353,8 @@ function Container (props: Props) { props.walletPanelActions.navigateTo('main') } - const isHardwareAccount = (address: string) => { - for (const account of accounts) { - if (account.deviceId && account.address === address) { - return true - } - } - return false - } - const onCancelSigning = () => { - if (isHardwareAccount(signMessageData[0].address)) { + if (isHardwareAccount(accounts, signMessageData[0].address)) { props.walletPanelActions.signMessageHardwareProcessed({ success: false, id: signMessageData[0].id, @@ -379,7 +370,7 @@ function Container (props: Props) { } const onSignData = () => { - if (isHardwareAccount(signMessageData[0].address)) { + if (isHardwareAccount(accounts, signMessageData[0].address)) { props.walletPanelActions.signMessageHardware(signMessageData[0]) } else { props.walletPanelActions.signMessageProcessed({ @@ -424,9 +415,13 @@ function Container (props: Props) { const onQueueNextTransction = () => { props.walletActions.queueNextTransaction() } - const onConfirmTransaction = () => { - if (selectedPendingTransaction) { + if (!selectedPendingTransaction) { + return + } + if (isHardwareAccount(accounts, selectedPendingTransaction.fromAddress)) { + props.walletPanelActions.approveHardwareTransaction(selectedPendingTransaction) + } else { props.walletActions.approveTransaction(selectedPendingTransaction) } } diff --git a/components/brave_wallet_ui/panel/wallet_panel_api_proxy.d.ts b/components/brave_wallet_ui/panel/wallet_panel_api_proxy.d.ts index a734bab5750c..e38377338d71 100644 --- a/components/brave_wallet_ui/panel/wallet_panel_api_proxy.d.ts +++ b/components/brave_wallet_ui/panel/wallet_panel_api_proxy.d.ts @@ -14,6 +14,7 @@ export default class APIProxy implements APIProxyControllers { closeUI: () => {} connectToSite: (accounts: string[], origin: string, tabId: number) => {} cancelConnectToSite: (origin: string, tabId: number) => {} + setCloseOnDeactivate: (close: boolean) => {} walletHandler: WalletAPIHandler ethJsonRpcController: EthJsonRpcController swapController: SwapController diff --git a/components/brave_wallet_ui/trezor/trezor.ts b/components/brave_wallet_ui/trezor/trezor.ts index ab04f61a6713..88f08aa9a75e 100644 --- a/components/brave_wallet_ui/trezor/trezor.ts +++ b/components/brave_wallet_ui/trezor/trezor.ts @@ -3,15 +3,17 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. -import TrezorConnect, { Unsuccessful } from 'trezor-connect' - +import TrezorConnect, { Unsuccessful, Success } from 'trezor-connect' +import { EthereumSignedTx } from 'trezor-connect/lib/typescript/networks/ethereum' import { TrezorCommand, UnlockCommand, UnlockResponse, GetAccountsCommand, GetAccountsResponsePayload, - TrezorGetPublicKeyResponse + TrezorGetPublicKeyResponse, + SignTransactionCommand, + SignTransactionResponsePayload } from '../common/trezor/trezor-messages' import { addTrezorCommandHandler } from '../common/trezor/trezor-command-handler' @@ -48,3 +50,11 @@ addTrezorCommandHandler(TrezorCommand.GetAccounts, (command: GetAccountsCommand, }) }) }) + +addTrezorCommandHandler(TrezorCommand.SignTransaction, (command: SignTransactionCommand, source: Window): Promise => { + return new Promise(async (resolve) => { + TrezorConnect.ethereumSignTransaction(command.payload).then((result: Unsuccessful | Success) => { + resolve({ id: command.id, command: command.command, payload: result, origin: command.origin }) + }) + }) +}) diff --git a/components/brave_wallet_ui/utils/address-utils.ts b/components/brave_wallet_ui/utils/address-utils.ts index 8cfe47c8c49d..1eda8db80c47 100644 --- a/components/brave_wallet_ui/utils/address-utils.ts +++ b/components/brave_wallet_ui/utils/address-utils.ts @@ -3,6 +3,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { + WalletAccountType +} from '../constants/types' + export function isValidAddress (value: string, length: number): boolean { if (!value.match(/^0x[0-9A-Fa-f]*$/)) { return false @@ -14,3 +18,7 @@ export function isValidAddress (value: string, length: number): boolean { return true } + +export function isHardwareAccount (accounts: WalletAccountType[], address: string) { + return accounts.some(account => account.deviceId && account.address === address) +} diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index 0a7942ce6609..9e2cbc65d50d 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -334,4 +334,9 @@ Internal error Internal error, unable to create iframe bridget Internal error, Unable to fetch accounts + Error processing the transaction + Error processing the transaction + Unable to find hardware transaction, please try again + Received error from hardware device, probably you need to confirm the transaction on your device + Error processing the transaction, please try again diff --git a/package-lock.json b/package-lock.json index 022937b603dd..e9eedb842c5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2451,6 +2451,12 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@socket.io/component-emitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", + "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==", + "dev": true + }, "@storybook/addon-actions": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.1.tgz", @@ -4576,6 +4582,12 @@ } } }, + "@trezor/connect-common": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-0.0.2.tgz", + "integrity": "sha512-b0R/HfA10mH2CR0BoPkKUs/2TC5Bf4MSGBFRF+1Q4tluQfSRzongQEZivg09vbBQEZBD1Ora3Oikn74zqsW4Aw==", + "dev": true + }, "@trezor/rollout": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@trezor/rollout/-/rollout-1.2.0.tgz", @@ -5652,12 +5664,6 @@ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", "integrity": "sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0=" }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -6024,12 +6030,6 @@ "is-string": "^1.0.5" } }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -6179,9 +6179,9 @@ } }, "babel-loader": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", - "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz", + "integrity": "sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==", "dev": true, "requires": { "find-cache-dir": "^3.3.1", @@ -6558,9 +6558,9 @@ "dev": true }, "base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz", + "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==", "dev": true }, "base64-js": { @@ -6575,13 +6575,38 @@ "dev": true }, "bchaddrjs": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/bchaddrjs/-/bchaddrjs-0.4.8.tgz", - "integrity": "sha512-2U2BVogvxIwvTgjNAgM5Znabkyf8opORLq+gPPYekgZoAWk/FenLSoa2p1oB3pUMdFQJm9/TMmMx8Q9o8Q6EAw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/bchaddrjs/-/bchaddrjs-0.5.2.tgz", + "integrity": "sha512-OO7gIn3m7ea4FVx4cT8gdlWQR2+++EquhdpWQJH9BQjK63tJJ6ngB3QMZDO6DiBoXiIGUsTPHjlrHVxPGcGxLQ==", "dev": true, "requires": { - "bs58check": "^2.1.2", - "cashaddrjs": "^0.3.11" + "bs58check": "2.1.2", + "buffer": "^6.0.3", + "cashaddrjs": "0.4.4", + "stream-browserify": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + } } }, "bech32": { @@ -6785,12 +6810,6 @@ "integrity": "sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==", "dev": true }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", - "dev": true - }, "blob-to-buffer": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz", @@ -7458,14 +7477,20 @@ "dev": true }, "cashaddrjs": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/cashaddrjs/-/cashaddrjs-0.3.12.tgz", - "integrity": "sha512-GdjCYMVwd86HXcFcxyEZQLPLFv8a/u0ccYPsO0PpnUW26LhZzHX9l9QA+DjaeUah7tnebwPs33NWDbbUy8iVYQ==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cashaddrjs/-/cashaddrjs-0.4.4.tgz", + "integrity": "sha512-xZkuWdNOh0uq/mxJIng6vYWfTowZLd9F4GMAlp2DwFHlcCqCm91NtuAc47RuV4L7r4PYcY5p6Cr2OKNb4hnkWA==", "dev": true, "requires": { "big-integer": "1.6.36" } }, + "cbor-web": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-7.0.6.tgz", + "integrity": "sha512-A6ZH12jcDJG9PS7StugO78G+ok23SAjxMugGInPBy4IItiH6hYzybi6HQkGjWw9jBEGQpIBkleB2mizxYZIpLw==", + "dev": true + }, "ccount": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", @@ -7971,24 +7996,12 @@ "ipaddr.js": ">= 0.1.5" } }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -9409,58 +9422,37 @@ } }, "engine.io-client": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", - "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.0.2.tgz", + "integrity": "sha512-cAep9lhZV6Q8jMXx3TNSU5cydMzMed8/O7Tz5uzyqZvpNPtQ3WQXrLYGADxlsuaFmOLN7wZLmT7ImiFhUOku8g==", "dev": true, "requires": { - "component-emitter": "~1.3.0", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.2.0", + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", "has-cors": "1.1.0", - "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.6.2", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0", "yeast": "0.1.2" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true } } }, "engine.io-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", - "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.1.tgz", + "integrity": "sha512-j4p3WwJrG2k92VISM0op7wiq60vO92MlF3CRGxhKHy9ywG1/Dkc72g0dXeDQ+//hrcDn8gqQzoEkdO9FN0d9AA==", "dev": true, "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.4", - "blob": "0.0.5", - "has-binary2": "~1.0.2" + "base64-arraybuffer": "~1.0.1" } }, "enhanced-resolve": { @@ -11325,23 +11317,6 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", @@ -11574,104 +11549,23 @@ } }, "hd-wallet": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/hd-wallet/-/hd-wallet-9.0.0.tgz", - "integrity": "sha512-WzzGwljUhA7Tc3a3U8JuAEXdFSowadYmAu6a+BmER0zQTah5bqPfSiVjhRpA57NloTdjFz+c2mJo3+wUaNII4g==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/hd-wallet/-/hd-wallet-9.1.2.tgz", + "integrity": "sha512-IkjLUqAI4kkkVUP9I1caCTdbaiQ1Ma0JbjoBzwL9RCc7/qHrYGh8Zu6b1xfTQNCMr0ctj3tzoXr2XDJ5wsOy1Q==", "dev": true, "requires": { - "@trezor/utxo-lib": "0.1.0", - "bchaddrjs": "^0.3.2", - "bignumber.js": "^9.0.0", - "queue": "^6.0.1", - "socket.io-client": "^2.2.0" + "@trezor/utxo-lib": "0.1.2", + "bchaddrjs": "^0.5.2", + "bignumber.js": "^9.0.1", + "queue": "^6.0.2", + "socket.io-client": "^4.1.2" }, "dependencies": { - "@trezor/utxo-lib": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@trezor/utxo-lib/-/utxo-lib-0.1.0.tgz", - "integrity": "sha512-jkqFRKL++xTga3xUgu5pWw1ArzTfraK0unXuj31V9g6wu+MKPbvcXW1+tMOVh+WNZZ3QUSGSQNNflSyh+qD0Vw==", - "dev": true, - "requires": { - "bech32": "0.0.3", - "bigi": "^1.4.0", - "bip66": "^1.1.0", - "bitcoin-ops": "^1.3.0", - "blake2b": "git+https://github.com/BitGo/blake2b.git#6268e6dd678661e0acc4359e9171b97eb1ebf8ac", - "bs58check": "^2.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.3", - "debug": "~3.1.0", - "ecurve": "^1.0.0", - "merkle-lib": "^2.0.10", - "pushdata-bitcoin": "^1.0.1", - "randombytes": "^2.0.1", - "safe-buffer": "^5.0.1", - "secp256k1": "^3.5.2", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.0.4", - "wif": "^2.0.1" - } - }, - "bchaddrjs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/bchaddrjs/-/bchaddrjs-0.3.2.tgz", - "integrity": "sha512-jpoq2GX6PphcCpuvvrQG4oQmEzn4nGQSm5dT208W72r9GDdbmNPi0hG9TY/dFF3r9sNtdl0qKwNsh8dNL3Q62g==", - "dev": true, - "requires": { - "bs58check": "^2.1.2", - "cashaddrjs": "^0.3.3" - } - }, - "bech32": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-0.0.3.tgz", - "integrity": "sha512-O+K1w8P/aAOLcYwwQ4sbiPYZ51ZIW95lnS4/6nE8Aib/z+OOddQIIPdu2qi94qGDp4HhYy/wJotttXKkak1lXg==", - "dev": true - }, "bignumber.js": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==", "dev": true - }, - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true, - "optional": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "secp256k1": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", - "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "bip66": "^1.1.5", - "bn.js": "^4.11.8", - "create-hash": "^1.2.0", - "drbg.js": "^1.0.1", - "elliptic": "^6.5.2", - "nan": "^2.14.0", - "safe-buffer": "^5.1.2" - } } } }, @@ -12039,12 +11933,6 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -18264,73 +18152,27 @@ } }, "socket.io-client": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", - "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.3.2.tgz", + "integrity": "sha512-2B9LqSunN60yV8F7S84CCEEcgbYNfrn7ejIInZtLZ7ppWtiX8rGZAjvdCvbnC8bqo/9RlCNOUsORLyskxSFP1g==", "dev": true, "requires": { - "backo2": "1.0.2", - "component-bind": "1.0.0", - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "engine.io-client": "~3.5.0", - "has-binary2": "~1.0.2", - "indexof": "0.0.1", - "parseqs": "0.0.6", + "@socket.io/component-emitter": "~3.0.0", + "backo2": "~1.0.2", + "debug": "~4.3.2", + "engine.io-client": "~6.0.1", "parseuri": "0.0.6", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "socket.io-parser": "~4.1.1" } }, "socket.io-parser": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", - "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.1.tgz", + "integrity": "sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA==", "dev": true, "requires": { - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1" } }, "socks": { @@ -19250,12 +19092,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -19366,26 +19202,28 @@ } }, "trezor-connect": { - "version": "8.1.5-extended", - "resolved": "https://registry.npmjs.org/trezor-connect/-/trezor-connect-8.1.5-extended.tgz", - "integrity": "sha512-xTjpgD6j19qQLCSwqUQCSPXC2Rt8pNzgwHGiBfsI+QHdw1CFyKkHgZx3LxByKga6HfgzIVbdUS5Be1txk442Uw==", + "version": "8.2.1-extended", + "resolved": "https://registry.npmjs.org/trezor-connect/-/trezor-connect-8.2.1-extended.tgz", + "integrity": "sha512-U5OKXG2MBrKEvAEyloDYyqpWZpg5nvZrjE3RsAO/Slmk0ZI22VZZbKAiCbQZsAv2s5OV6PNn4uVyPmUztnS6uQ==", "dev": true, "requires": { - "@babel/runtime": "^7.9.2", - "@trezor/blockchain-link": "^1.0.11", - "@trezor/rollout": "^1.0.4", - "@trezor/utxo-lib": "^0.1.0", - "bchaddrjs": "0.4.8", - "bignumber.js": "^9.0.0", - "bowser": "^2.9.0", - "events": "^3.1.0", - "hd-wallet": "9.0.0", - "keccak": "^3.0.0", - "node-fetch": "^2.6.0", - "parse-uri": "^1.0.0", + "@babel/runtime": "^7.12.5", + "@trezor/blockchain-link": "^1.0.17", + "@trezor/connect-common": "^0.0.2", + "@trezor/rollout": "^1.2.0", + "@trezor/utxo-lib": "^0.1.2", + "bchaddrjs": "^0.5.2", + "bignumber.js": "^9.0.1", + "bowser": "^2.11.0", + "cbor-web": "^7.0.6", + "events": "^3.2.0", + "hd-wallet": "9.1.2", + "keccak": "^3.0.1", + "node-fetch": "^2.6.1", + "parse-uri": "^1.0.3", "tiny-worker": "^2.3.0", - "trezor-link": "1.7.0", - "whatwg-fetch": "^3.0.0" + "trezor-link": "1.7.3", + "whatwg-fetch": "^3.5.0" }, "dependencies": { "bignumber.js": { @@ -19397,19 +19235,19 @@ } }, "trezor-link": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/trezor-link/-/trezor-link-1.7.0.tgz", - "integrity": "sha512-JJ4vVp5md8SVn3kLRkjMbxCUbqPTf78MH5fVXPa0yvUhsPKElCP0PjeEmCwJZ4f0tmnorkUE/9+ZlZDoUjLBKQ==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/trezor-link/-/trezor-link-1.7.3.tgz", + "integrity": "sha512-KaVYcK96BLD+cBuYmJmsxr6G9Z0ZeO6VYPhcYQyFxJ7jvc6/UvTmdACZdHjlUD7U5VuJEGMI4G8L3IGxFTFv4Q==", "dev": true, "requires": { "bigi": "^1.4.1", "ecurve": "^1.0.3", "json-stable-stringify": "^1.0.1", - "node-fetch": "^2.6.0", - "object.values": "^1.1.0", + "node-fetch": "^2.6.1", + "object.values": "^1.1.2", "protobufjs-old-fixed-webpack": "3.8.5", "semver-compare": "^1.0.0", - "whatwg-fetch": "^3.0.0" + "whatwg-fetch": "^3.5.0" } }, "trim": { @@ -21585,9 +21423,9 @@ "dev": true }, "xmlhttprequest-ssl": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", - "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "dev": true }, "xtend": { diff --git a/package.json b/package.json index 8b2c795a9420..f95e51bfc4f1 100644 --- a/package.json +++ b/package.json @@ -283,7 +283,6 @@ "enzyme-adapter-react-16": "1.15.6", "ethereum-blockies": "github:brave/blockies#07ebdfa3ed79fceec8234ef8c880ebdefd79bf0a", "ethereumjs-util": "7.0.9", - "trezor-connect": "8.1.5-extended", "file-loader": "1.1.11", "font-awesome": "4.7.0", "fs-extra": "8.1.0", @@ -303,6 +302,7 @@ "sinon": "7.5.0", "style-loader": "0.23.1", "styled-components": "5.3.0", + "trezor-connect": "8.2.1-extended", "ts-jest": "26.5.6", "ts-loader": "6.2.2", "tslint": "5.20.1", diff --git a/test/data/brave-wallet/popup.html b/test/data/brave-wallet/popup.html new file mode 100644 index 000000000000..13c0ab155c19 --- /dev/null +++ b/test/data/brave-wallet/popup.html @@ -0,0 +1,5 @@ + + + + + From bb78c70111d3a9ba6b0f76adf80489bed85c6825 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 12 Nov 2021 10:17:19 +0300 Subject: [PATCH 2/2] Sign messages by Trezor (#10903) (cherry picked from commit a3759580f1872c2b32a9dc3627e9428cba638b90) --- .../browser/brave_wallet_constants.h | 6 ++- .../brave_wallet_ui/common/async/hardware.ts | 12 ++++- .../common/hardware_operations.ts | 10 +++- .../eth_ledger_bridge_keyring.test.ts | 16 ++---- .../ledgerjs/eth_ledger_bridge_keyring.ts | 51 +++++++++---------- .../common/trezor/trezor-messages.ts | 49 ++++++++++-------- .../common/trezor/trezor_bridge_keyring.ts | 31 ++++++++++- components/brave_wallet_ui/constants/types.ts | 6 --- .../panel/async/wallet_panel_async_handler.ts | 36 +++++++------ .../panel/constants/action_types.ts | 4 +- .../panel/wallet_panel_api_proxy.js | 5 ++ components/brave_wallet_ui/trezor/trezor.ts | 13 ++++- components/resources/wallet_strings.grdp | 2 + 13 files changed, 152 insertions(+), 89 deletions(-) diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index 8d36f5c74df7..65810fa32919 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -493,7 +493,11 @@ constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletSignOnDeviceError", IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_DEVICE_ERROR}, {"braveWalletNoMessageToSignError", - IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_NO_MESSAGE_TO_SIGN_ERROR}}; + IDS_BRAVE_WALLET_HARDWARE_TRANSACTION_NO_MESSAGE_TO_SIGN_ERROR}, + {"braveWalletProcessMessageError", + IDS_BRAVE_WALLET_HARDWARE_SIGN_MESSAGE_ERROR}, + {"braveWalletUnknownKeyringError", + IDS_BRAVE_WALLET_HARDWARE_UNKNOWN_KEYRING_ERROR}}; const char kRopstenSwapBaseAPIURL[] = "https://ropsten.api.0x.org/"; const char kRopstenBuyTokenPercentageFee[] = "0.00875"; diff --git a/components/brave_wallet_ui/common/async/hardware.ts b/components/brave_wallet_ui/common/async/hardware.ts index 83626485ed79..bb86fb61843a 100644 --- a/components/brave_wallet_ui/common/async/hardware.ts +++ b/components/brave_wallet_ui/common/async/hardware.ts @@ -3,7 +3,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. -import { SignHardwareTransactionType } from '../hardware_operations' +import { SignHardwareTransactionType, SignHardwareMessageOperationResult } from '../hardware_operations' import { getLocale } from '../../../common/locale' import LedgerBridgeKeyring from '../../common/ledgerjs/eth_ledger_bridge_keyring' import TrezorBridgeKeyring from '../../common/trezor/trezor_bridge_keyring' @@ -57,3 +57,13 @@ export async function signLedgerTransaction (apiProxy: APIProxyControllers, path } return { success: result.status } } + +export async function signMessageWithHardwareKeyring (apiProxy: APIProxyControllers, vendor: string, path: string, address: string, message: string): Promise { + const deviceKeyring = await apiProxy.getKeyringsByType(vendor) + if (deviceKeyring instanceof LedgerBridgeKeyring) { + return deviceKeyring.signPersonalMessage(path, address, message) + } else if (deviceKeyring instanceof TrezorBridgeKeyring) { + return deviceKeyring.signPersonalMessage(path, message) + } + return { success: false, error: getLocale('braveWalletUnknownKeyringError') } +} diff --git a/components/brave_wallet_ui/common/hardware_operations.ts b/components/brave_wallet_ui/common/hardware_operations.ts index 66c98af962a8..0aa300eab147 100644 --- a/components/brave_wallet_ui/common/hardware_operations.ts +++ b/components/brave_wallet_ui/common/hardware_operations.ts @@ -4,7 +4,11 @@ export interface SignHardwareTransactionType { success: boolean error?: string } - +export interface SignatureVRS { + v: number + r: string + s: string +} export type HardwareOperationResult = { success: boolean error?: string @@ -14,3 +18,7 @@ export type HardwareOperationResult = { export type SignHardwareTransactionOperationResult = HardwareOperationResult & { payload?: EthereumSignedTx } + +export type SignHardwareMessageOperationResult = HardwareOperationResult & { + payload?: string +} diff --git a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts index b2f3298b9df5..48f9c1d1b223 100644 --- a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts +++ b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.test.ts @@ -11,9 +11,9 @@ import { } from '../../components/desktop/popup-modals/add-account-modal/hardware-wallet-connect/types' import { - kLedgerHardwareVendor, - SignatureVRS + kLedgerHardwareVendor } from '../../constants/types' +import { SignatureVRS } from '../hardware_operations' class MockApp { signature: SignatureVRS @@ -109,21 +109,15 @@ test('Sign personal message successfully', () => { const ledgerHardwareKeyring = new LedgerBridgeKeyring() ledgerHardwareKeyring.app = new MockApp() ledgerHardwareKeyring.app.signature = { v: 1, r: 'b68983', s: 'r68983' } - ledgerHardwareKeyring._recoverAddressFromSignature = (message: string, signature: string) => { - return '0x111' - } return expect(ledgerHardwareKeyring.signPersonalMessage( 'm/44\'/60\'/0\'/0/0', '0x111', 'message')) - .resolves.toStrictEqual('0xb68983r68983-26') + .resolves.toStrictEqual({ payload: '0xb68983r68983-26', success: true }) }) test('Sign personal message failed', () => { - const ledgerHardwareKeyring = new LedgerBridgeKeyring() + const ledgerHardwareKeyring = createLedgerKeyring() ledgerHardwareKeyring.app = new MockApp() - ledgerHardwareKeyring._recoverAddressFromSignature = (message: string, signature: string) => { - return '0x111' - } return expect(ledgerHardwareKeyring.signPersonalMessage( 'm/44\'/60\'/0\'/0/0', '0x111', 'message')) - .rejects.toThrow() + .resolves.toMatchObject({ success: false }) }) diff --git a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts index 3eb5ab5bf2e5..8f9c0fe4599f 100644 --- a/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts +++ b/components/brave_wallet_ui/common/ledgerjs/eth_ledger_bridge_keyring.ts @@ -5,20 +5,17 @@ /* global window */ const { EventEmitter } = require('events') - +import Eth from '@ledgerhq/hw-app-eth' +import TransportWebHID from '@ledgerhq/hw-transport-webhid' import { LedgerDerivationPaths } from '../../components/desktop/popup-modals/add-account-modal/hardware-wallet-connect/types' - import { - kLedgerHardwareVendor, SignatureVRS + kLedgerHardwareVendor } from '../../constants/types' - -import Eth from '@ledgerhq/hw-app-eth' -import TransportWebHID from '@ledgerhq/hw-transport-webhid' import { getLocale } from '../../../common/locale' import { hardwareDeviceIdFromAddress } from '../hardwareDeviceIdFromAddress' -import { SignHardwareTransactionOperationResult } from '../../common/hardware_operations' +import { SignatureVRS, SignHardwareMessageOperationResult, SignHardwareTransactionOperationResult } from '../../common/hardware_operations' export default class LedgerBridgeKeyring extends EventEmitter { constructor () { @@ -56,7 +53,7 @@ export default class LedgerBridgeKeyring extends EventEmitter { } this.app = new Eth(await TransportWebHID.create()) if (this.app) { - const zeroPath = this._getPathForIndex(0, LedgerDerivationPaths.LedgerLive) + const zeroPath = this.getPathForIndex(0, LedgerDerivationPaths.LedgerLive) const address = await this._getAddress(zeroPath) this.deviceId_ = await hardwareDeviceIdFromAddress(address) } @@ -71,26 +68,24 @@ export default class LedgerBridgeKeyring extends EventEmitter { return { success: true, payload: signed } } - signPersonalMessage = async (path: string, address: string, message: string) => { - return new Promise(async (resolve, reject) => { - try { - if (!this.isUnlocked() && !(await this.unlock())) { - return new Error(getLocale('braveWalletUnlockError')) - } - return this.app.signPersonalMessage(path, - Buffer.from(message)).then((result: SignatureVRS) => { - const signature = this._createMessageSignature(result, message, address) - if (!signature) { - return reject(new Error(getLocale('braveWalletLedgerValidationError'))) - } - resolve(signature) - }).catch(reject) - } catch (e) { - reject(e) + signPersonalMessage = async (path: string, address: string, message: string): Promise => { + if (!this.isUnlocked() && !(await this.unlock())) { + return { success: false, error: getLocale('braveWalletUnlockError') } + } + try { + const data = await this.app.signPersonalMessage(path, + Buffer.from(message)) + const signature = this.createMessageSignature(data) + if (!signature) { + return { success: false, error: getLocale('braveWalletLedgerValidationError') } } - }) + return { success: true, payload: signature } + } catch (e) { + return { success: false, error: e.message } + } } - _createMessageSignature = (result: SignatureVRS, message: string, address: string) => { + + private readonly createMessageSignature = (result: SignatureVRS) => { let v = (result.v - 27).toString() if (v.length < 2) { v = `0${v}` @@ -100,7 +95,7 @@ export default class LedgerBridgeKeyring extends EventEmitter { } /* PRIVATE METHODS */ - _getPathForIndex = (index: number, scheme: string) => { + private readonly getPathForIndex = (index: number, scheme: string) => { if (scheme === LedgerDerivationPaths.LedgerLive) { return `m/44'/60'/${index}'/0/0` } else if (scheme === LedgerDerivationPaths.Legacy) { @@ -120,7 +115,7 @@ export default class LedgerBridgeKeyring extends EventEmitter { _getAccounts = async (from: number, to: number, scheme: string) => { const accounts = [] for (let i = from; i <= to; i++) { - const path = this._getPathForIndex(i, scheme) + const path = this.getPathForIndex(i, scheme) const address = await this._getAddress(path) accounts.push({ address: address.address, diff --git a/components/brave_wallet_ui/common/trezor/trezor-messages.ts b/components/brave_wallet_ui/common/trezor/trezor-messages.ts index 0d345caae1e4..24fe11f1375d 100644 --- a/components/brave_wallet_ui/common/trezor/trezor-messages.ts +++ b/components/brave_wallet_ui/common/trezor/trezor-messages.ts @@ -5,26 +5,21 @@ import { loadTimeData } from '../../../common/loadTimeData' import { Unsuccessful, EthereumSignTransaction, CommonParams, Success } from 'trezor-connect' import { HDNodeResponse } from 'trezor-connect/lib/typescript' -import { EthereumSignedTx } from 'trezor-connect/lib/typescript/networks/ethereum' +import { EthereumSignedTx, EthereumSignMessage } from 'trezor-connect/lib/typescript/networks/ethereum' +import { MessageSignature } from 'trezor-connect/lib/typescript/trezor/protobuf' export const kTrezorBridgeUrl = loadTimeData.getString('braveWalletTrezorBridgeUrl') export enum TrezorCommand { Unlock = 'trezor-unlock', GetAccounts = 'trezor-get-accounts', - SignTransaction = 'trezor-sign-treansaction' + SignTransaction = 'trezor-sign-treansaction', + SignMessage = 'trezor-sign-message' } export type CommandMessage = { command: TrezorCommand id: string origin: string } -export type TrezorAccountPath = { - path: string -} -export type GetAccountsCommand = CommandMessage & { - command: TrezorCommand.GetAccounts, - paths: TrezorAccountPath[] -} export type UnlockCommand = CommandMessage & { command: TrezorCommand.Unlock } @@ -32,16 +27,9 @@ export type UnlockResponse = CommandMessage & { result: Boolean, error?: Unsuccessful } - -export type SignTransactionCommandPayload = CommonParams & EthereumSignTransaction - -export type SignTransactionCommand = CommandMessage & { - command: TrezorCommand.SignTransaction - payload: SignTransactionCommandPayload +export type TrezorAccountPath = { + path: string } - -export type SignTransactionResponse = Unsuccessful | Success - export type TrezorAccount = { publicKey: string serializedPath: string, @@ -55,12 +43,33 @@ export type TrezorGetPublicKeyResponse = Unsuccessful | Success export type SignTransactionResponsePayload = CommandMessage & { payload: SignTransactionResponse } -export type TrezorFrameCommand = GetAccountsCommand | UnlockCommand | SignTransactionCommand -export type TrezorFrameResponse = UnlockResponse | GetAccountsResponsePayload | SignTransactionResponsePayload + +export type SignMessageCommandPayload = CommonParams & EthereumSignMessage +export type SignMessageCommand = CommandMessage & { + command: TrezorCommand.SignMessage + payload: SignMessageCommandPayload +} +export type SignMessageResponse = Unsuccessful | Success +export type SignMessageResponsePayload = CommandMessage & { + payload: SignMessageResponse +} + +export type TrezorFrameCommand = GetAccountsCommand | UnlockCommand | SignTransactionCommand | SignMessageCommand +export type TrezorFrameResponse = UnlockResponse | GetAccountsResponsePayload | SignTransactionResponsePayload | SignMessageResponsePayload // Trezor library is loaded inside the chrome-untrusted webui page // and communication is going through posting messages between parent window diff --git a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts index ca788fc833ab..93c31917acc3 100644 --- a/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts +++ b/components/brave_wallet_ui/common/trezor/trezor_bridge_keyring.ts @@ -20,12 +20,14 @@ import { TrezorAccount, SignTransactionCommandPayload, TrezorFrameCommand, - SignTransactionResponsePayload + SignTransactionResponsePayload, + SignMessageCommandPayload, + SignMessageResponsePayload } from '../../common/trezor/trezor-messages' import { sendTrezorCommand } from '../../common/trezor/trezor-bridge-transport' import { getLocale } from '../../../common/locale' import { hardwareDeviceIdFromAddress } from '../hardwareDeviceIdFromAddress' -import { SignHardwareTransactionOperationResult } from '../../common/hardware_operations' +import { SignHardwareMessageOperationResult, SignHardwareTransactionOperationResult } from '../../common/hardware_operations' export default class TrezorBridgeKeyring extends EventEmitter { constructor () { @@ -81,6 +83,27 @@ export default class TrezorBridgeKeyring extends EventEmitter { return { success: true, payload: data.payload.payload } } + signPersonalMessage = async (path: string, message: string): Promise => { + if (!this.isUnlocked() && !(await this.unlock())) { + return { success: false, error: getLocale('braveWalletUnlockError') } + } + const data = await this.sendTrezorCommand({ + command: TrezorCommand.SignMessage, + // @ts-expect-error + id: crypto.randomUUID(), + payload: this.prepareSignMessagePayload(path, message), + origin: window.origin + }) + if (!data) { + return { success: false, error: getLocale('braveWalletProcessMessageError') } + } + if (!data.payload.success) { + const unsuccess = data.payload + return { success: false, error: unsuccess.payload.error, code: unsuccess.payload.code } + } + return { success: true, payload: data.payload.payload.signature } + } + isUnlocked = () => { return this.unlocked_ } @@ -160,6 +183,10 @@ export default class TrezorBridgeKeyring extends EventEmitter { } } + private readonly prepareSignMessagePayload = (path: string, message: string): SignMessageCommandPayload => { + return { path: path, message: message } + } + private readonly publicKeyToAddress = (key: string) => { const buffer = Buffer.from(key, 'hex') const address = publicToAddress(buffer, true).toString('hex') diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index 721bf96e2f2e..12b56eac709d 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -33,12 +33,6 @@ export interface AssetOptionType { logo: string } -export interface SignatureVRS { - v: number, - r: string, - s: string -} - export interface UserAssetOptionType { asset: AssetOptionType assetBalance: number diff --git a/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts b/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts index deab8496d33b..3fce659b9f0a 100644 --- a/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts +++ b/components/brave_wallet_ui/panel/async/wallet_panel_async_handler.ts @@ -33,7 +33,8 @@ import { } from '../../common/async/lib' import { signTrezorTransaction, - signLedgerTransaction + signLedgerTransaction, + signMessageWithHardwareKeyring } from '../../common/async/hardware' import { fetchSwapQuoteFactory } from '../../common/async/handlers' import { Store } from '../../common/async/types' @@ -247,32 +248,35 @@ handler.on(PanelActions.signMessageProcessed.getType(), async (store: Store, pay handler.on(PanelActions.signMessageHardware.getType(), async (store, messageData: SignMessageData) => { const apiProxy = await getAPIProxy() - const braveWalletService = apiProxy.braveWalletService const hardwareAccount = await findHardwareAccountInfo(messageData.address) - if (hardwareAccount && hardwareAccount.hardware) { - let deviceKeyring = await apiProxy.getKeyringsByType(hardwareAccount.hardware.vendor) - deviceKeyring.signPersonalMessage(hardwareAccount.hardware.path, hardwareAccount.address, messageData.message). - then(async (signature: string) => { - store.dispatch(PanelActions.signMessageHardwareProcessed({ success: true, id: messageData.id, signature: signature, error: '' })) - }).catch(async (error: any) => { - store.dispatch(PanelActions.signMessageHardwareProcessed({ success: false, id: messageData.id, signature: '', error: error.message })) - }) + if (!hardwareAccount || !hardwareAccount.hardware) { + const braveWalletService = apiProxy.braveWalletService + braveWalletService.notifySignMessageHardwareRequestProcessed(false, messageData.id, + '', getLocale('braveWalletHardwareAccountNotFound')) + const signMessageRequest = await getPendingSignMessageRequest() + if (signMessageRequest) { + store.dispatch(PanelActions.signMessage(signMessageRequest)) + return + } + apiProxy.closeUI() return } - await braveWalletService.notifySignMessageHardwareRequestProcessed(false, messageData.id, - '', getLocale('braveWalletHardwareAccountNotFound')) - const signMessageRequest = await getPendingSignMessageRequest() - if (signMessageRequest) { - store.dispatch(PanelActions.signMessage(signMessageRequest)) + const info = hardwareAccount.hardware + apiProxy.setCloseOnDeactivate(false) + const signature = await signMessageWithHardwareKeyring(apiProxy, info.vendor, info.path, messageData.address, messageData.message) + apiProxy.setCloseOnDeactivate(true) + if (!signature || !signature.success) { + store.dispatch(PanelActions.signMessageHardwareProcessed({ success: false, id: messageData.id, error: signature.error })) return } + store.dispatch(PanelActions.signMessageHardwareProcessed({ success: true, id: messageData.id, signature: signature.payload })) apiProxy.closeUI() }) handler.on(PanelActions.signMessageHardwareProcessed.getType(), async (store, payload: SignMessageHardwareProcessedPayload) => { const apiProxy = await getAPIProxy() const braveWalletService = apiProxy.braveWalletService - await braveWalletService.notifySignMessageHardwareRequestProcessed(payload.success, payload.id, payload.signature, payload.error) + await braveWalletService.notifySignMessageHardwareRequestProcessed(payload.success, payload.id, payload.signature || '', payload.error || '') const signMessageRequest = await getPendingSignMessageRequest() if (signMessageRequest) { store.dispatch(PanelActions.signMessage(signMessageRequest)) diff --git a/components/brave_wallet_ui/panel/constants/action_types.ts b/components/brave_wallet_ui/panel/constants/action_types.ts index cce9655b09b0..330673070044 100644 --- a/components/brave_wallet_ui/panel/constants/action_types.ts +++ b/components/brave_wallet_ui/panel/constants/action_types.ts @@ -39,8 +39,8 @@ export type SignMessageProcessedPayload = { export type SignMessageHardwareProcessedPayload = { success: boolean, id: number, - signature: string, - error: string + signature?: string, + error?: string } export type SwitchEthereumChainProcessedPayload = { diff --git a/components/brave_wallet_ui/panel/wallet_panel_api_proxy.js b/components/brave_wallet_ui/panel/wallet_panel_api_proxy.js index 5639e7278ebb..2ffd22fbe963 100644 --- a/components/brave_wallet_ui/panel/wallet_panel_api_proxy.js +++ b/components/brave_wallet_ui/panel/wallet_panel_api_proxy.js @@ -20,6 +20,7 @@ class WalletPanelApiProxy { connectToSite(accounts, origin, tab_id) {} cancelConnectToSite(origin, tab_id) {} + setCloseOnDeactivate(close) {} } /** @implements {WalletPanelApiProxy} */ @@ -64,6 +65,10 @@ export default class WalletPanelApiProxyImpl extends WalletApiProxy { cancelConnectToSite(origin, tab_id) { this.panelHandler.cancelConnectToSite(origin, tab_id); } + /** @override */ + setCloseOnDeactivate(close) { + this.panelHandler.setCloseOnDeactivate(close); + } } // TODO(petemill): Use module-scoped variable and custom `getInstance` static diff --git a/components/brave_wallet_ui/trezor/trezor.ts b/components/brave_wallet_ui/trezor/trezor.ts index 88f08aa9a75e..12b5d86eb5dc 100644 --- a/components/brave_wallet_ui/trezor/trezor.ts +++ b/components/brave_wallet_ui/trezor/trezor.ts @@ -13,7 +13,10 @@ import { GetAccountsResponsePayload, TrezorGetPublicKeyResponse, SignTransactionCommand, - SignTransactionResponsePayload + SignTransactionResponsePayload, + SignMessageCommand, + SignMessageResponsePayload, + SignMessageResponse } from '../common/trezor/trezor-messages' import { addTrezorCommandHandler } from '../common/trezor/trezor-command-handler' @@ -58,3 +61,11 @@ addTrezorCommandHandler(TrezorCommand.SignTransaction, (command: SignTransaction }) }) }) + +addTrezorCommandHandler(TrezorCommand.SignMessage, (command: SignMessageCommand, source: Window): Promise => { + return new Promise(async (resolve) => { + TrezorConnect.ethereumSignMessage(command.payload).then((result: SignMessageResponse) => { + resolve({ id: command.id, command: command.command, payload: result, origin: command.origin }) + }) + }) +}) diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index 9e2cbc65d50d..c10a784aadb7 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -339,4 +339,6 @@ Unable to find hardware transaction, please try again Received error from hardware device, probably you need to confirm the transaction on your device Error processing the transaction, please try again + Error signing message, please try again + Error signing message, unknown keyring requested, please try again