From 440302a6bd4f97269999763b75d3e1bc78dc0712 Mon Sep 17 00:00:00 2001 From: Aristides Staffieri Date: Fri, 28 Jul 2023 11:01:06 -0600 Subject: [PATCH] splits out sign blob and sign tx bodies --- @shared/api/external.ts | 15 +- .../freighterApiMessageListener.ts | 19 +- .../messageListener/popupMessageListener.ts | 18 +- .../src/helpers/__tests__/stellar.test.ts | 4 +- extension/src/helpers/stellar.ts | 2 +- extension/src/helpers/urls.ts | 16 +- extension/src/popup/ducks/access.ts | 13 +- extension/src/popup/metrics/views.ts | 4 +- .../views/SignTransaction/SignBlob/index.tsx | 228 ++++++ .../views/SignTransaction/SignTx/index.tsx | 446 ++++++++++++ .../src/popup/views/SignTransaction/index.tsx | 647 ++---------------- .../popup/views/SignTransaction/styles.scss | 2 +- 12 files changed, 765 insertions(+), 649 deletions(-) create mode 100644 extension/src/popup/views/SignTransaction/SignBlob/index.tsx create mode 100644 extension/src/popup/views/SignTransaction/SignTx/index.tsx diff --git a/@shared/api/external.ts b/@shared/api/external.ts index ea7aa86443..d2a2b71f57 100644 --- a/@shared/api/external.ts +++ b/@shared/api/external.ts @@ -71,15 +71,14 @@ export const submitTransaction = async ( export const submitBlob = async ( blob: string, - opts: - | { - network: string - accountToSign: string - networkPassphrase: string - }, + opts: { + network: string; + accountToSign: string; + networkPassphrase: string; + }, ): Promise => { let response = { signedTransaction: "", error: "" }; - const {network, networkPassphrase, accountToSign} = opts + const { network, networkPassphrase, accountToSign } = opts; try { response = await sendMessageToContentScript({ transactionXDR: blob, @@ -97,7 +96,7 @@ export const submitBlob = async ( throw error; } return signedTransaction; -} +}; export const requestNetwork = async (): Promise => { let response = { network: "", error: "" }; diff --git a/extension/src/background/messageListener/freighterApiMessageListener.ts b/extension/src/background/messageListener/freighterApiMessageListener.ts index b4df022592..dfaddbfc85 100644 --- a/extension/src/background/messageListener/freighterApiMessageListener.ts +++ b/extension/src/background/messageListener/freighterApiMessageListener.ts @@ -34,7 +34,11 @@ import { } from "background/helpers/dataStorage"; import { publicKeySelector } from "background/ducks/session"; -import { blobQueue, responseQueue, transactionQueue } from "./popupMessageListener"; +import { + blobQueue, + responseQueue, + transactionQueue, +} from "./popupMessageListener"; const localStore = dataStorageAccess(browserLocalStorage); @@ -189,7 +193,7 @@ export const freighterApiMessageListener = ( transactionQueue.push(transaction); const encodedBlob = encodeObject(transactionInfo); - + const popup = browser.windows.create({ url: chrome.runtime.getURL( `/index.html#/sign-transaction?${encodedBlob}`, @@ -224,10 +228,7 @@ export const freighterApiMessageListener = ( }; const submitBlob = async () => { - const { - transactionXdr, - accountToSign, - } = request; + const { transactionXdr, accountToSign } = request; const { tab, url: tabUrl = "" } = sender; const domain = getUrlHostname(tabUrl); @@ -243,11 +244,11 @@ export const freighterApiMessageListener = ( tab, blob: transactionXdr, url: tabUrl, - accountToSign - } + accountToSign, + }; blobQueue.push(blob); - const encodedBlob = encodeObject(blob) + const encodedBlob = encodeObject(blob); const popup = browser.windows.create({ url: chrome.runtime.getURL( `/index.html#/sign-transaction?${encodedBlob}`, diff --git a/extension/src/background/messageListener/popupMessageListener.ts b/extension/src/background/messageListener/popupMessageListener.ts index b262e7f8d2..8404f38e16 100644 --- a/extension/src/background/messageListener/popupMessageListener.ts +++ b/extension/src/background/messageListener/popupMessageListener.ts @@ -95,12 +95,12 @@ export const transactionQueue: Array<{ toXDR: () => void; }> = []; export const blobQueue: Array<{ - isDomainListedAllowed: boolean, - domain: string, - tab: browser.Tabs.Tab | undefined, - blob: string, - url: string, - accountToSign: string + isDomainListedAllowed: boolean; + domain: string; + tab: browser.Tabs.Tab | undefined; + blob: string; + url: string; + accountToSign: string; }> = []; interface KeyPair { @@ -916,7 +916,9 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { const sourceKeys = SDK.Keypair.fromSecret(privateKey); const blob = blobQueue.pop(); - const response = blob ? await sourceKeys.sign(Buffer.from(blob.blob, 'base64')) : null + const response = blob + ? await sourceKeys.sign(Buffer.from(blob.blob, "base64")) + : null; const blobResponse = responseQueue.pop(); @@ -927,7 +929,7 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => { } return { error: "Session timed out" }; - } + }; const rejectTransaction = () => { transactionQueue.pop(); diff --git a/extension/src/helpers/__tests__/stellar.test.ts b/extension/src/helpers/__tests__/stellar.test.ts index b9398d5bc7..24afd294e4 100644 --- a/extension/src/helpers/__tests__/stellar.test.ts +++ b/extension/src/helpers/__tests__/stellar.test.ts @@ -25,7 +25,7 @@ describe("getTransactionInfo", () => { flaggedKeys: { test: { tags: [""] } }, tab: {}, }); - const info = getTransactionInfo("foo") + const info = getTransactionInfo("foo"); if (!("blob" in info)) { expect(info.isHttpsDomain).toBe(true); } @@ -40,7 +40,7 @@ describe("getTransactionInfo", () => { flaggedKeys: { test: { tags: [""] } }, tab: {}, }); - const info = getTransactionInfo("foo") + const info = getTransactionInfo("foo"); if (!("blob" in info)) { expect(info.isHttpsDomain).toBe(false); } diff --git a/extension/src/helpers/stellar.ts b/extension/src/helpers/stellar.ts index 45673b41b4..c714daab6c 100644 --- a/extension/src/helpers/stellar.ts +++ b/extension/src/helpers/stellar.ts @@ -37,7 +37,7 @@ export const getTransactionInfo = (search: string) => { const searchParams = parsedSearchParam(search); if ("blob" in searchParams) { - return searchParams + return searchParams; } const { diff --git a/extension/src/helpers/urls.ts b/extension/src/helpers/urls.ts index e39555e2ff..d3296dd3ea 100644 --- a/extension/src/helpers/urls.ts +++ b/extension/src/helpers/urls.ts @@ -3,12 +3,12 @@ import browser from "webextension-polyfill"; import { TransactionInfo } from "../types/transactions"; export interface BlobToSign { - isDomainListedAllowed: boolean, - domain: string, - tab: browser.Tabs.Tab | undefined, - blob: string, - url: string, - accountToSign: string + isDomainListedAllowed: boolean; + domain: string; + tab: browser.Tabs.Tab | undefined; + blob: string; + url: string; + accountToSign: string; } export const encodeObject = (obj: {}) => @@ -22,7 +22,9 @@ export const newTabHref = (path = "", queryParams = "") => export const removeQueryParam = (url = "") => url.replace(/\?(.*)/, ""); -export const parsedSearchParam = (param: string): TransactionInfo | BlobToSign => { +export const parsedSearchParam = ( + param: string, +): TransactionInfo | BlobToSign => { const decodedSearchParam = decodeString(param.replace("?", "")); return decodedSearchParam ? JSON.parse(decodedSearchParam) : {}; }; diff --git a/extension/src/popup/ducks/access.ts b/extension/src/popup/ducks/access.ts index 72d696ddeb..740492b586 100644 --- a/extension/src/popup/ducks/access.ts +++ b/extension/src/popup/ducks/access.ts @@ -4,7 +4,7 @@ import { rejectAccess as internalRejectAccess, grantAccess as internalGrantAccess, signTransaction as internalSignTransaction, - signBlob as internalSignBlob + signBlob as internalSignBlob, } from "@shared/api/internal"; export const grantAccess = createAsyncThunk("grantAccess", internalGrantAccess); @@ -19,10 +19,7 @@ export const signTransaction = createAsyncThunk( internalSignTransaction, ); -export const signBlob = createAsyncThunk( - "signBlob", - internalSignBlob, -); +export const signBlob = createAsyncThunk("signBlob", internalSignBlob); // Basically an alias for metrics purposes export const rejectTransaction = createAsyncThunk( @@ -31,8 +28,4 @@ export const rejectTransaction = createAsyncThunk( ); // Basically an alias for metrics purposes -export const rejectBlob = createAsyncThunk( - "rejectBlob", - internalRejectAccess, -); - +export const rejectBlob = createAsyncThunk("rejectBlob", internalRejectAccess); diff --git a/extension/src/popup/metrics/views.ts b/extension/src/popup/metrics/views.ts index 808488e854..4b9a22fafa 100644 --- a/extension/src/popup/metrics/views.ts +++ b/extension/src/popup/metrics/views.ts @@ -87,14 +87,14 @@ registerHandler(navigate, (_, a) => { const info = getTransactionInfo(search); if (!("blob" in info)) { - const { operations, operationTypes } = info + const { operations, operationTypes } = info; const METRIC_OPTIONS = { domain: getUrlDomain(url), subdomain: getUrlHostname(url), number_of_operations: operations.length, operationTypes, }; - + emitMetric(eventName, METRIC_OPTIONS); } } else { diff --git a/extension/src/popup/views/SignTransaction/SignBlob/index.tsx b/extension/src/popup/views/SignTransaction/SignBlob/index.tsx new file mode 100644 index 0000000000..c64e0a509a --- /dev/null +++ b/extension/src/popup/views/SignTransaction/SignBlob/index.tsx @@ -0,0 +1,228 @@ +import React from "react"; +import { Card, Icon } from "@stellar/design-system"; +import { useTranslation, Trans } from "react-i18next"; + +import { truncatedPublicKey } from "helpers/stellar"; +import { Button } from "popup/basics/buttons/Button"; +import { InfoBlock } from "popup/basics/InfoBlock"; +import { signBlob } from "popup/ducks/access"; +import { confirmPassword } from "popup/ducks/accountServices"; + +import { + ButtonsContainer, + ModalHeader, + ModalWrapper, +} from "popup/basics/Modal"; + +import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; +import { AccountList, OptionTag } from "popup/components/account/AccountList"; +import { PunycodedDomain } from "popup/components/PunycodedDomain"; +import { + WarningMessageVariant, + WarningMessage, + FirstTimeWarningMessage, +} from "popup/components/WarningMessages"; +import { LedgerSign } from "popup/components/hardwareConnect/LedgerSign"; +import { SlideupModal } from "popup/components/SlideupModal"; + +import { VerifyAccount } from "popup/views/VerifyAccount"; +import { AppDispatch } from "popup/App"; +import { + ShowOverlayStatus, + startHwSign, +} from "popup/ducks/transactionSubmission"; + +import { Account } from "@shared/api/types"; +import { BlobToSign } from "helpers/urls"; + +import "../styles.scss"; +import { useDispatch } from "react-redux"; + +interface SignBlobBodyProps { + accountNotFound: boolean; + allAccounts: Account[]; + blob: BlobToSign; + currentAccount: Account; + handleApprove: (signAndClose: () => Promise) => () => Promise; + hwStatus: ShowOverlayStatus; + isConfirming: boolean; + isDropdownOpen: boolean; + isExperimentalModeEnabled: boolean; + isHardwareWallet: boolean; + isPasswordRequired: boolean; + publicKey: string; + rejectAndClose: () => void; + setIsDropdownOpen: (isRequired: boolean) => void; + setIsPasswordRequired: (isRequired: boolean) => void; + setStartedHwSign: (hasStarted: boolean) => void; +} + +export const SignBlobBody = ({ + publicKey, + allAccounts, + isDropdownOpen, + handleApprove, + isConfirming, + accountNotFound, + rejectAndClose, + currentAccount, + setIsDropdownOpen, + setIsPasswordRequired, + isPasswordRequired, + blob, + isExperimentalModeEnabled, + hwStatus, + isHardwareWallet, + setStartedHwSign, +}: SignBlobBodyProps) => { + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + const { accountToSign, domain, isDomainListedAllowed } = blob; + + const signAndClose = async () => { + if (isHardwareWallet) { + await dispatch( + startHwSign({ transactionXDR: blob.blob, shouldSubmit: false }), + ); + setStartedHwSign(true); + } else { + await dispatch(signBlob()); + window.close(); + } + }; + + const _handleApprove = handleApprove(signAndClose); + + const verifyPasswordThenSign = async (password: string) => { + const confirmPasswordResp = await dispatch(confirmPassword(password)); + + if (confirmPassword.fulfilled.match(confirmPasswordResp)) { + await signAndClose(); + } + }; + + if (!domain.startsWith("https") && !isExperimentalModeEnabled) { + return ( + + window.close()} + isActive + variant={WarningMessageVariant.warning} + header={t("WEBSITE CONNECTION IS NOT SECURE")} + > +

+ + The website {{ domain }} does not use an SSL + certificate. For additional safety Freighter only works with + websites that provide an SSL certificate. + +

+
+
+ ); + } + + return isPasswordRequired ? ( + setIsPasswordRequired(false)} + customSubmit={verifyPasswordThenSign} + /> + ) : ( + <> + {hwStatus === ShowOverlayStatus.IN_PROGRESS && } +
+ + + {t("Confirm Data")} + + {isExperimentalModeEnabled ? ( + +

+ {t( + "You are interacting with data that may be using untested and changing schemas. Proceed at your own risk.", + )} +

+
+ ) : null} + {!isDomainListedAllowed ? : null} +
+ + +
+ {t("is requesting approval to sign a blob of data")} +
+
+
+ {t("Approve using")}: +
+
setIsDropdownOpen(true)} + > + + + +
+ +
+
+
+
+ {accountNotFound && accountToSign ? ( +
+ + {t("The application is requesting a specific account")} ( + {truncatedPublicKey(accountToSign)}),{" "} + {t( + "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this transaction.", + )} + +
+ ) : null} +
+
+ + + + + +
+ +
+
+
+ + ); +}; diff --git a/extension/src/popup/views/SignTransaction/SignTx/index.tsx b/extension/src/popup/views/SignTransaction/SignTx/index.tsx new file mode 100644 index 0000000000..524936833d --- /dev/null +++ b/extension/src/popup/views/SignTransaction/SignTx/index.tsx @@ -0,0 +1,446 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import { Card, Icon } from "@stellar/design-system"; +import StellarSdk, { FederationServer, MuxedAccount } from "stellar-sdk"; +import * as SorobanSdk from "soroban-client"; +import { useTranslation, Trans } from "react-i18next"; + +import { TRANSACTION_WARNING } from "constants/transaction"; + +import { emitMetric } from "helpers/metrics"; +import { + isFederationAddress, + isMuxedAccount, + truncatedPublicKey, +} from "helpers/stellar"; +import { decodeMemo } from "popup/helpers/parseTransaction"; +import { Button } from "popup/basics/buttons/Button"; +import { InfoBlock } from "popup/basics/InfoBlock"; +import { TransactionHeading } from "popup/basics/TransactionHeading"; +import { signTransaction } from "popup/ducks/access"; + +import { + ButtonsContainer, + ModalHeader, + ModalWrapper, +} from "popup/basics/Modal"; + +import { METRIC_NAMES } from "popup/constants/metricsNames"; + +import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; +import { AccountList, OptionTag } from "popup/components/account/AccountList"; +import { PunycodedDomain } from "popup/components/PunycodedDomain"; +import { + WarningMessageVariant, + WarningMessage, + FirstTimeWarningMessage, + FlaggedWarningMessage, +} from "popup/components/WarningMessages"; +import { Transaction } from "popup/components/signTransaction/Transaction"; +import { LedgerSign } from "popup/components/hardwareConnect/LedgerSign"; +import { SlideupModal } from "popup/components/SlideupModal"; + +import { VerifyAccount } from "popup/views/VerifyAccount"; +import { + ShowOverlayStatus, + startHwSign, +} from "popup/ducks/transactionSubmission"; + +import { Account } from "@shared/api/types"; +import { FlaggedKeys } from "types/transactions"; +import { AppDispatch } from "popup/App"; + +import "../styles.scss"; +import { TransactionInfo } from "popup/components/signTransaction/TransactionInfo"; +import { + confirmPassword, + makeAccountActive, +} from "popup/ducks/accountServices"; +import { useDispatch } from "react-redux"; + +interface SignTxBodyProps { + accountNotFound: boolean; + allAccounts: Account[]; + currentAccount: Account; + handleApprove: (signAndClose: () => Promise) => () => Promise; + hwStatus: ShowOverlayStatus; + isConfirming: boolean; + isDropdownOpen: boolean; + isExperimentalModeEnabled: boolean; + isHardwareWallet: boolean; + isPasswordRequired: boolean; + networkName: string; + networkPassphrase: string; + publicKey: string; + rejectAndClose: () => void; + setAccountNotFound: (isNotFound: boolean) => void; + setCurrentAccount: (account: Account) => void; + setIsDropdownOpen: (isRequired: boolean) => void; + setIsPasswordRequired: (isRequired: boolean) => void; + setStartedHwSign: (hasStarted: boolean) => void; + startedHwSign: boolean; + tx: { + accountToSign: string | undefined; + transactionXdr: string; + domain: string; + domainTitle: any; + isHttpsDomain: boolean; + operations: any; + operationTypes: any; + isDomainListedAllowed: boolean; + flaggedKeys: FlaggedKeys; + }; +} + +export const SignTxBody = ({ + setCurrentAccount, + setAccountNotFound, + startedHwSign, + setStartedHwSign, + publicKey, + allAccounts, + isDropdownOpen, + handleApprove, + isConfirming, + accountNotFound, + rejectAndClose, + currentAccount, + setIsDropdownOpen, + setIsPasswordRequired, + isPasswordRequired, + tx, + isExperimentalModeEnabled, + networkName, + networkPassphrase, + hwStatus, + isHardwareWallet, +}: SignTxBodyProps) => { + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + + const { + accountToSign: _accountToSign, + transactionXdr, + domain, + isDomainListedAllowed, + isHttpsDomain, + flaggedKeys, + } = tx; + + /* + Reconstruct the tx from xdr as passing a tx through extension contexts + loses custom prototypes associated with some values. This is fine for most cases + where we just need a high level overview of the tx, like just a list of operations. + But in this case, we will need the hostFn prototype associated with Soroban tx operations. + */ + + const SDK = isExperimentalModeEnabled ? SorobanSdk : StellarSdk; + const transaction = SDK.TransactionBuilder.fromXDR( + transactionXdr, + networkPassphrase, + ); + + const { + _fee, + _innerTransaction, + _memo, + _networkPassphrase, + _sequence, + } = transaction; + + const isFeeBump = !!_innerTransaction; + const memo = decodeMemo(_memo); + let accountToSign = _accountToSign; + + useEffect(() => { + if (startedHwSign && hwStatus === ShowOverlayStatus.IDLE) { + window.close(); + } + }, [startedHwSign, hwStatus]); + + const signAndClose = async () => { + if (isHardwareWallet) { + await dispatch( + startHwSign({ transactionXDR: transactionXdr, shouldSubmit: false }), + ); + setStartedHwSign(true); + } else { + await dispatch(signTransaction()); + window.close(); + } + }; + + const _handleApprove = handleApprove(signAndClose); + + const verifyPasswordThenSign = async (password: string) => { + const confirmPasswordResp = await dispatch(confirmPassword(password)); + + if (confirmPassword.fulfilled.match(confirmPasswordResp)) { + await signAndClose(); + } + }; + + const flaggedKeyValues = Object.values(flaggedKeys); + const isUnsafe = flaggedKeyValues.some(({ tags }) => + tags.includes(TRANSACTION_WARNING.unsafe), + ); + const isMalicious = flaggedKeyValues.some(({ tags }) => + tags.includes(TRANSACTION_WARNING.malicious), + ); + const isMemoRequired = flaggedKeyValues.some( + ({ tags }) => tags.includes(TRANSACTION_WARNING.memoRequired) && !memo, + ); + + // the public key the user had selected before starting this flow + const defaultPublicKey = useRef(publicKey); + const allAccountsMap = useRef({} as { [key: string]: Account }); + + const resolveFederatedAddress = useCallback(async (inputDest) => { + let resolvedPublicKey; + try { + const fedResp = await FederationServer.resolve(inputDest); + resolvedPublicKey = fedResp.account_id; + } catch (e) { + console.error(e); + } + + return resolvedPublicKey; + }, []); + + const decodeAccountToSign = async () => { + if (_accountToSign) { + if (isMuxedAccount(_accountToSign)) { + const mAccount = MuxedAccount.fromAddress(_accountToSign, "0"); + accountToSign = mAccount.baseAccount().accountId(); + } + if (isFederationAddress(_accountToSign)) { + accountToSign = (await resolveFederatedAddress( + accountToSign, + )) as string; + } + } + }; + decodeAccountToSign(); + + useEffect(() => { + if (isMemoRequired) { + emitMetric(METRIC_NAMES.signTransactionMemoRequired); + } + if (isUnsafe) { + emitMetric(METRIC_NAMES.signTransactionUnsafe); + } + if (isMalicious) { + emitMetric(METRIC_NAMES.signTransactionMalicious); + } + }, [isMemoRequired, isMalicious, isUnsafe]); + + useEffect(() => { + // handle auto selecting the right account based on `accountToSign` + let autoSelectedAccountDetails; + + allAccounts.forEach((account) => { + if (accountToSign) { + // does the user have the `accountToSign` somewhere in the accounts list? + if (account.publicKey === accountToSign) { + // if the `accountToSign` is found, but it isn't active, make it active + if (defaultPublicKey.current !== account.publicKey) { + dispatch(makeAccountActive(account.publicKey)); + } + + // save the details of the `accountToSign` + autoSelectedAccountDetails = account; + } + } + + // create an object so we don't need to keep iterating over allAccounts when we switch accounts + allAccountsMap.current[account.publicKey] = account; + }); + + if (!autoSelectedAccountDetails) { + setAccountNotFound(true); + } + }, [accountToSign, allAccounts, dispatch, setAccountNotFound]); + + useEffect(() => { + // handle any changes to the current acct - whether by auto select or manual select + setCurrentAccount(allAccountsMap.current[publicKey] || ({} as Account)); + }, [allAccounts, publicKey, setCurrentAccount]); + + const isSubmitDisabled = isMemoRequired || isMalicious; + + if (_networkPassphrase !== networkPassphrase) { + return ( + + window.close()} + isActive + header={`${t("Freighter is set to")} ${networkName}`} + > +

+ {t("The transaction you’re trying to sign is on")}{" "} + {_networkPassphrase}. +

+

{t("Signing this transaction is not possible at the moment.")}

+
+
+ ); + } + + if (!isHttpsDomain && !isExperimentalModeEnabled) { + return ( + + window.close()} + isActive + variant={WarningMessageVariant.warning} + header={t("WEBSITE CONNECTION IS NOT SECURE")} + > +

+ + The website {{ domain }} does not use an SSL + certificate. For additional safety Freighter only works with + websites that provide an SSL certificate. + +

+
+
+ ); + } + return isPasswordRequired ? ( + setIsPasswordRequired(false)} + customSubmit={verifyPasswordThenSign} + /> + ) : ( + <> + {hwStatus === ShowOverlayStatus.IN_PROGRESS && } +
+ + + {t("Confirm Transaction")} + + {isExperimentalModeEnabled ? ( + +

+ {t( + "You are interacting with a transaction that may be using untested and changing schemas. Proceed at your own risk.", + )} +

+
+ ) : null} + {flaggedKeyValues.length ? ( + + ) : null} + {!isDomainListedAllowed && !isSubmitDisabled ? ( + + ) : null} +
+ + +
+ {t("is requesting approval to a")}{" "} + {isFeeBump ? "fee bump " : ""} + {t("transaction")}: +
+
+
+ {t("Approve using")}: +
+
setIsDropdownOpen(true)} + > + + + +
+ +
+
+
+
+ {accountNotFound && accountToSign ? ( +
+ + {t("The application is requesting a specific account")} ( + {truncatedPublicKey(accountToSign)}),{" "} + {t( + "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this transaction.", + )} + +
+ ) : null} +
+ {isFeeBump ? ( +
+ +
+ ) : ( + + )} + {t("Transaction Info")} + +
+ + + + + +
+ +
+
+
+ + ); +}; diff --git a/extension/src/popup/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index d507166f88..1f4c5d4636 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -1,28 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; -import { Card, Icon } from "@stellar/design-system"; -import StellarSdk, { FederationServer, MuxedAccount } from "stellar-sdk"; -import * as SorobanSdk from "soroban-client"; -import { useTranslation, Trans } from "react-i18next"; - -import { TRANSACTION_WARNING } from "constants/transaction"; - -import { emitMetric } from "helpers/metrics"; -import { - getTransactionInfo, - isFederationAddress, - isMuxedAccount, - truncatedPublicKey, -} from "helpers/stellar"; -import { decodeMemo } from "popup/helpers/parseTransaction"; -import { Button } from "popup/basics/buttons/Button"; -import { InfoBlock } from "popup/basics/InfoBlock"; -import { TransactionHeading } from "popup/basics/TransactionHeading"; -import { rejectTransaction, rejectBlob, signBlob, signTransaction } from "popup/ducks/access"; +import { getTransactionInfo } from "helpers/stellar"; +import { rejectTransaction, rejectBlob } from "popup/ducks/access"; import { allAccountsSelector, - confirmPassword, hasPrivateKeySelector, makeAccountActive, publicKeySelector, @@ -33,48 +15,23 @@ import { settingsExperimentalModeSelector, } from "popup/ducks/settings"; -import { - ButtonsContainer, - ModalHeader, - ModalWrapper, -} from "popup/basics/Modal"; - -import { METRIC_NAMES } from "popup/constants/metricsNames"; - -import { AccountListIdenticon } from "popup/components/identicons/AccountListIdenticon"; -import { AccountList, OptionTag } from "popup/components/account/AccountList"; -import { PunycodedDomain } from "popup/components/PunycodedDomain"; -import { - WarningMessageVariant, - WarningMessage, - FirstTimeWarningMessage, - FlaggedWarningMessage, -} from "popup/components/WarningMessages"; -import { Transaction } from "popup/components/signTransaction/Transaction"; -import { LedgerSign } from "popup/components/hardwareConnect/LedgerSign"; -import { SlideupModal } from "popup/components/SlideupModal"; - -import { VerifyAccount } from "popup/views/VerifyAccount"; import { ShowOverlayStatus, - startHwSign, transactionSubmissionSelector, } from "popup/ducks/transactionSubmission"; import { Account } from "@shared/api/types"; -import { FlaggedKeys } from "types/transactions"; import { AppDispatch } from "popup/App"; import "./styles.scss"; -import { TransactionInfo } from "popup/components/signTransaction/TransactionInfo"; -import { BlobToSign } from "helpers/urls"; +import { SignBlobBody } from "./SignBlob"; +import { SignTxBody } from "./SignTx"; export const SignTransaction = () => { const location = useLocation(); const blobOrTx = getTransactionInfo(location.search); - const isBlob = "blob" in blobOrTx + const isBlob = "blob" in blobOrTx; - const { t } = useTranslation(); const dispatch: AppDispatch = useDispatch(); const { networkName, networkPassphrase } = useSelector( settingsNetworkDetailsSelector, @@ -103,10 +60,10 @@ export const SignTransaction = () => { // the public key the user had selected before starting this flow const defaultPublicKey = useRef(publicKey); const allAccountsMap = useRef({} as { [key: string]: Account }); - const accountToSign = blobOrTx.accountToSign // both types have this key + const accountToSign = blobOrTx.accountToSign; // both types have this key const rejectAndClose = () => { - const _reject = isBlob ? rejectTransaction : rejectBlob + const _reject = isBlob ? rejectTransaction : rejectBlob; dispatch(_reject()); window.close(); }; @@ -163,562 +120,50 @@ export const SignTransaction = () => { if ("blob" in blobOrTx) { return ( - - ) + + ); } return ( - ) -}; - -interface SignBlobBodyProps { - publicKey: string - currentAccount: Account - allAccounts: Account[] - isPasswordRequired: boolean - isDropdownOpen: boolean - isHardwareWallet: boolean - isConfirming: boolean - accountNotFound: boolean - hwStatus: ShowOverlayStatus - isExperimentalModeEnabled: boolean - blob: BlobToSign - t: any - dispatch: AppDispatch - setStartedHwSign: (hasStarted: boolean) => void - setIsPasswordRequired: (isRequired: boolean) => void - setIsDropdownOpen: (isRequired: boolean) => void - rejectAndClose: () => void - handleApprove: (signAndClose: () => Promise) => () => Promise -} - -const SignBlobBody = ({ publicKey, allAccounts, isDropdownOpen, handleApprove, isConfirming, accountNotFound, rejectAndClose, currentAccount, setIsDropdownOpen, setIsPasswordRequired, isPasswordRequired, blob, isExperimentalModeEnabled, t, dispatch, hwStatus, isHardwareWallet, setStartedHwSign }: SignBlobBodyProps) => { - const { accountToSign, domain, isDomainListedAllowed } = blob - - const signAndClose = async () => { - if (isHardwareWallet) { - await dispatch( - startHwSign({ transactionXDR: blob.blob, shouldSubmit: false }), - ); - setStartedHwSign(true); - } else { - - await dispatch(signBlob()); - window.close(); - } - }; - - const _handleApprove = handleApprove(signAndClose) - - const verifyPasswordThenSign = async (password: string) => { - const confirmPasswordResp = await dispatch(confirmPassword(password)); - - if (confirmPassword.fulfilled.match(confirmPasswordResp)) { - await signAndClose(); - } - }; - - if (!domain.startsWith("https") && !isExperimentalModeEnabled) { - return ( - - window.close()} - isActive - variant={WarningMessageVariant.warning} - header={t("WEBSITE CONNECTION IS NOT SECURE")} - > -

- - The website {{ domain }} does not use an SSL - certificate. For additional safety Freighter only works with - websites that provide an SSL certificate. - -

-
-
- ); - } - - return isPasswordRequired ? ( - setIsPasswordRequired(false)} - customSubmit={verifyPasswordThenSign} - /> - ) : ( - <> - {hwStatus === ShowOverlayStatus.IN_PROGRESS && } -
- - - {t("Confirm Data")} - - {isExperimentalModeEnabled ? ( - -

- {t( - "You are interacting with data that may be using untested and changing schemas. Proceed at your own risk.", - )} -

-
- ) : null} - {!isDomainListedAllowed ? ( - - ) : null} -
- - -
- {t("is requesting approval to sign a blob of data")} -
-
-
- {t("Approve using")}: -
-
setIsDropdownOpen(true)} - > - - - -
- -
-
-
-
- {accountNotFound && accountToSign ? ( -
- - {t("The application is requesting a specific account")} ( - {truncatedPublicKey(accountToSign)}),{" "} - {t( - "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this transaction.", - )} - -
- ) : null} -
-
- - - - - -
- -
-
-
- - ); -} - -interface SignTxBodyProps { - publicKey: string - currentAccount: Account - allAccounts: Account[] - isPasswordRequired: boolean - isDropdownOpen: boolean - isConfirming: boolean - accountNotFound: boolean - isHardwareWallet: boolean - startedHwSign: boolean - hwStatus: ShowOverlayStatus - isExperimentalModeEnabled: boolean - networkName: string - networkPassphrase: string - dispatch: AppDispatch - setStartedHwSign: (hasStarted: boolean) => void - setIsPasswordRequired: (isRequired: boolean) => void - setIsDropdownOpen: (isRequired: boolean) => void - rejectAndClose: () => void - handleApprove: (signAndClose: () => Promise) => () => Promise - setAccountNotFound: (isNotFound: boolean) => void - setCurrentAccount: (account: Account) => void - t: any - tx: { - accountToSign: string | undefined - transactionXdr: string; - domain: string; - domainTitle: any; - isHttpsDomain: boolean; - operations: any; - operationTypes: any; - isDomainListedAllowed: boolean; - flaggedKeys: FlaggedKeys; - } -} - -const SignTxBody = ({ setCurrentAccount, setAccountNotFound, startedHwSign, setStartedHwSign, publicKey, allAccounts, isDropdownOpen, handleApprove, isConfirming, accountNotFound, rejectAndClose, currentAccount, setIsDropdownOpen, setIsPasswordRequired, isPasswordRequired, tx, isExperimentalModeEnabled, t, dispatch, networkName, networkPassphrase, hwStatus, isHardwareWallet }: SignTxBodyProps) => { - - const { - accountToSign: _accountToSign, - transactionXdr, - domain, - isDomainListedAllowed, - isHttpsDomain, - flaggedKeys, - } = tx - - /* - Reconstruct the tx from xdr as passing a tx through extension contexts - loses custom prototypes associated with some values. This is fine for most cases - where we just need a high level overview of the tx, like just a list of operations. - But in this case, we will need the hostFn prototype associated with Soroban tx operations. - */ - - const SDK = isExperimentalModeEnabled ? SorobanSdk : StellarSdk; - const transaction = SDK.TransactionBuilder.fromXDR( - transactionXdr, - networkPassphrase, - ); - - const { - _fee, - _innerTransaction, - _memo, - _networkPassphrase, - _sequence, - } = transaction; - - const isFeeBump = !!_innerTransaction; - const memo = decodeMemo(_memo); - let accountToSign = _accountToSign; - - useEffect(() => { - if (startedHwSign && hwStatus === ShowOverlayStatus.IDLE) { - window.close(); - } - }, [startedHwSign, hwStatus]); - - const signAndClose = async () => { - if (isHardwareWallet) { - await dispatch( - startHwSign({ transactionXDR: transactionXdr, shouldSubmit: false }), - ); - setStartedHwSign(true); - } else { - - await dispatch(signTransaction()); - window.close(); - } - }; - - const _handleApprove = handleApprove(signAndClose) - - const verifyPasswordThenSign = async (password: string) => { - const confirmPasswordResp = await dispatch(confirmPassword(password)); - - if (confirmPassword.fulfilled.match(confirmPasswordResp)) { - await signAndClose(); - } - }; - - const flaggedKeyValues = Object.values(flaggedKeys); - const isUnsafe = flaggedKeyValues.some(({ tags }) => - tags.includes(TRANSACTION_WARNING.unsafe), - ); - const isMalicious = flaggedKeyValues.some(({ tags }) => - tags.includes(TRANSACTION_WARNING.malicious), - ); - const isMemoRequired = flaggedKeyValues.some( - ({ tags }) => tags.includes(TRANSACTION_WARNING.memoRequired) && !memo, - ); - - // the public key the user had selected before starting this flow - const defaultPublicKey = useRef(publicKey); - const allAccountsMap = useRef({} as { [key: string]: Account }); - - const resolveFederatedAddress = useCallback(async (inputDest) => { - let resolvedPublicKey; - try { - const fedResp = await FederationServer.resolve(inputDest); - resolvedPublicKey = fedResp.account_id; - } catch (e) { - console.error(e); - } - - return resolvedPublicKey; - }, []); - - const decodeAccountToSign = async () => { - if (_accountToSign) { - if (isMuxedAccount(_accountToSign)) { - const mAccount = MuxedAccount.fromAddress(_accountToSign, "0"); - accountToSign = mAccount.baseAccount().accountId(); - } - if (isFederationAddress(_accountToSign)) { - accountToSign = await resolveFederatedAddress(accountToSign) as string; - } - } - }; - decodeAccountToSign(); - - useEffect(() => { - if (isMemoRequired) { - emitMetric(METRIC_NAMES.signTransactionMemoRequired); - } - if (isUnsafe) { - emitMetric(METRIC_NAMES.signTransactionUnsafe); - } - if (isMalicious) { - emitMetric(METRIC_NAMES.signTransactionMalicious); - } - }, [isMemoRequired, isMalicious, isUnsafe]); - - useEffect(() => { - // handle auto selecting the right account based on `accountToSign` - let autoSelectedAccountDetails; - - allAccounts.forEach((account) => { - if (accountToSign) { - // does the user have the `accountToSign` somewhere in the accounts list? - if (account.publicKey === accountToSign) { - // if the `accountToSign` is found, but it isn't active, make it active - if (defaultPublicKey.current !== account.publicKey) { - dispatch(makeAccountActive(account.publicKey)); - } - - // save the details of the `accountToSign` - autoSelectedAccountDetails = account; - } - } - - // create an object so we don't need to keep iterating over allAccounts when we switch accounts - allAccountsMap.current[account.publicKey] = account; - }); - - if (!autoSelectedAccountDetails) { - setAccountNotFound(true); - } - }, [accountToSign, allAccounts, dispatch, setAccountNotFound]); - - useEffect(() => { - // handle any changes to the current acct - whether by auto select or manual select - setCurrentAccount(allAccountsMap.current[publicKey] || ({} as Account)); - }, [allAccounts, publicKey, setCurrentAccount]); - - const isSubmitDisabled = isMemoRequired || isMalicious; - - if (_networkPassphrase !== networkPassphrase) { - return ( - - window.close()} - isActive - header={`${t("Freighter is set to")} ${networkName}`} - > -

- {t("The transaction you’re trying to sign is on")}{" "} - {_networkPassphrase}. -

-

{t("Signing this transaction is not possible at the moment.")}

-
-
- ); - } - - if (!isHttpsDomain && !isExperimentalModeEnabled) { - return ( - - window.close()} - isActive - variant={WarningMessageVariant.warning} - header={t("WEBSITE CONNECTION IS NOT SECURE")} - > -

- - The website {{ domain }} does not use an SSL - certificate. For additional safety Freighter only works with - websites that provide an SSL certificate. - -

-
-
- ); - } - return isPasswordRequired ? ( - setIsPasswordRequired(false)} - customSubmit={verifyPasswordThenSign} + publicKey={publicKey} + rejectAndClose={rejectAndClose} + setAccountNotFound={setAccountNotFound} + setCurrentAccount={setCurrentAccount} + setIsDropdownOpen={setIsDropdownOpen} + setIsPasswordRequired={setIsPasswordRequired} + setStartedHwSign={setStartedHwSign} + startedHwSign={startedHwSign} + tx={blobOrTx} /> - ) : ( - <> - {hwStatus === ShowOverlayStatus.IN_PROGRESS && } -
- - - {t("Confirm Transaction")} - - {isExperimentalModeEnabled ? ( - -

- {t( - "You are interacting with a transaction that may be using untested and changing schemas. Proceed at your own risk.", - )} -

-
- ) : null} - {flaggedKeyValues.length ? ( - - ) : null} - {!isDomainListedAllowed && !isSubmitDisabled ? ( - - ) : null} -
- - -
- {t("is requesting approval to a")}{" "} - {isFeeBump ? "fee bump " : ""} - {t("transaction")}: -
-
-
- {t("Approve using")}: -
-
setIsDropdownOpen(true)} - > - - - -
- -
-
-
-
- {accountNotFound && accountToSign ? ( -
- - {t("The application is requesting a specific account")} ( - {truncatedPublicKey(accountToSign)}),{" "} - {t( - "which is not available on Freighter. If you own this account, you can import it into Freighter to complete this transaction.", - )} - -
- ) : null} -
- {isFeeBump ? ( -
- -
- ) : ( - - )} - {t("Transaction Info")} - -
- - - - - -
- -
-
-
- ); -} +}; diff --git a/extension/src/popup/views/SignTransaction/styles.scss b/extension/src/popup/views/SignTransaction/styles.scss index e2ee8c7797..bc3ff3dd37 100644 --- a/extension/src/popup/views/SignTransaction/styles.scss +++ b/extension/src/popup/views/SignTransaction/styles.scss @@ -1,4 +1,4 @@ -.SignTransaction { +.SignTransaction, .SignBlob { height: var(--popup--height); overflow: hidden; position: relative;