diff --git a/package.json b/package.json index 6b2f2d9cc..ffb0365d5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@apollo/client": "^3.7.2", "@babel/core": "7.19.3", "@babel/runtime": "^7.19.0", + "@bitcoinerlab/secp256k1": "^1.0.5", "@dicebear/avatars": "4.2.5", "@dicebear/avatars-bottts-sprites": "4.2.5", "@dicebear/avatars-jdenticon-sprites": "4.2.5", @@ -66,6 +67,7 @@ "@types/debounce": "^1.2.1", "@types/debounce-promise": "3.1.4", "@types/elliptic": "6.4.14", + "@types/events": "^3.0.3", "@types/fontfaceobserver": "0.0.6", "@types/jest": "27.0.3", "@types/ledgerhq__hw-transport": "4.21.3", @@ -86,6 +88,7 @@ "@types/resolve": "^1.20.2", "@types/scryptsy": "^2.0.0", "@types/typedarray-to-buffer": "^4.0.0", + "@types/uuid": "^9.0.7", "@types/w3c-web-hid": "^1.0.3", "@types/webextension-polyfill": "^0.9.1", "@types/webpack": "^5.28.0", @@ -99,7 +102,9 @@ "axios": "0.26.1", "babel-loader": "8.2.5", "bignumber.js": "9.1.0", + "bip32": "^4.0.0", "bip39": "3.0.4", + "bitcoinjs-lib": "^6.1.5", "bs58check": "2.1.2", "buffer": "5.6.0", "clean-webpack-plugin": "4.0.0", @@ -125,6 +130,8 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4", "eslint-webpack-plugin": "^3.2.0", + "ethers": "^6.8.1", + "events": "^3.3.0", "fast-glob": "^3.2.12", "file-loader": "6.2.0", "firebase": "^10.0.0", @@ -191,6 +198,7 @@ "use-force-update": "1.0.7", "use-onclickoutside": "0.4.1", "util": "0.11.1", + "uuid": "^9.0.1", "wasm-themis": "0.14.6", "webextension-polyfill": "^0.10.0", "webpack": "^5.74.0", diff --git a/src/app/ConfirmPage.tsx b/src/app/ConfirmPage.tsx index 3c7b11389..4e343c8c7 100644 --- a/src/app/ConfirmPage.tsx +++ b/src/app/ConfirmPage.tsx @@ -1,5 +1,7 @@ import React, { FC, Fragment, memo, Suspense, useCallback, useMemo, useState } from 'react'; +import { isDefined } from '@rnw-community/shared'; + import { Alert, FormSubmitButton, FormSecondaryButton } from 'app/atoms'; import AccountTypeBadge from 'app/atoms/AccountTypeBadge'; import ConfirmLedgerOverlay from 'app/atoms/ConfirmLedgerOverlay'; @@ -73,9 +75,41 @@ const PayloadContent: React.FC = ({ }) => { const allAccounts = useRelevantAccounts(false); const AccountOptionContent = useMemo(() => AccountOptionContentHOC(payload.networkRpc), [payload.networkRpc]); + const EvmAccountOptionContent = useMemo(() => EvmAccountOptionContentHOC(), []); const chainId = useCustomChainId(payload.networkRpc, true)!; const mainnet = chainId === TempleChainId.Mainnet; + if (payload.type === 'switch_evm_network') { + return null; + } + + if (payload.type === 'connect_evm') { + return ( +
+

+ + + + + + + +

+ + + activeItemId={accountPkhToConnect} + getItemId={getEvmPkh} + items={allAccounts.filter(acc => acc.type === TempleAccountType.HD && isDefined(acc.evmPublicKeyHash))} + maxHeight="8rem" + onSelect={setAccountPkhToConnect} + OptionIcon={AccountIcon} + OptionContent={EvmAccountOptionContent} + autoFocus + /> +
+ ); + } + return payload.type === 'connect' ? (

@@ -113,14 +147,13 @@ const PayloadContent: React.FC = ({ export default ConfirmPage; const getPkh = (account: TempleAccount) => account.publicKeyHash; +const getEvmPkh = (account: TempleAccount) => account.evmPublicKeyHash ?? ''; const ConfirmDAppForm: FC = () => { const { getDAppPayload, confirmDAppPermission, confirmDAppOperation, confirmDAppSign } = useTempleClient(); const allAccounts = useRelevantAccounts(false); const account = useAccount(); - const [accountPkhToConnect, setAccountPkhToConnect] = useState(account.publicKeyHash); - const loc = useLocation(); const id = useMemo(() => { const usp = new URLSearchParams(loc.search); @@ -140,9 +173,23 @@ const ConfirmDAppForm: FC = () => { const payload = data!; const payloadError = data!.error; + const [accountPkhToConnect, setAccountPkhToConnect] = useState( + payload.type === 'connect_evm' ? account.evmPublicKeyHash! : account.publicKeyHash + ); + const connectedAccount = useMemo( () => - allAccounts.find(a => a.publicKeyHash === (payload.type === 'connect' ? accountPkhToConnect : payload.sourcePkh)), + allAccounts.find(a => { + if (payload.type === 'connect') { + return a.publicKeyHash === accountPkhToConnect; + } + + if (payload.type === 'connect_evm' || payload.type === 'switch_evm_network') { + return a.evmPublicKeyHash === accountPkhToConnect; + } + + return a.publicKeyHash === payload.sourcePkh; + }), [payload, allAccounts, accountPkhToConnect] ); @@ -152,10 +199,16 @@ const ConfirmDAppForm: FC = () => { case 'connect': return confirmDAppPermission(id, confimed, accountPkhToConnect); + case 'connect_evm': + case 'switch_evm_network': + return confirmDAppPermission(id, confimed, accountPkhToConnect, true); + + case 'confirm_evm_operations': case 'confirm_operations': return confirmDAppOperation(id, confimed, modifiedTotalFee, modifiedStorageLimit); case 'sign': + case 'sign_evm': return confirmDAppSign(id, confimed); } }, @@ -224,6 +277,7 @@ const ConfirmDAppForm: FC = () => { const content = useMemo(() => { switch (payload.type) { case 'connect': + case 'connect_evm': return { title: t('confirmAction', t('connection').toLowerCase()), declineActionTitle: t('cancel'), @@ -248,6 +302,7 @@ const ConfirmDAppForm: FC = () => { }; case 'confirm_operations': + case 'confirm_evm_operations': return { title: t('confirmAction', t('operations').toLowerCase()), declineActionTitle: t('reject'), @@ -276,7 +331,25 @@ const ConfirmDAppForm: FC = () => { ) }; + case 'switch_evm_network': + return { + title: 'Confirm network switch', + declineActionTitle: t('reject'), + confirmActionTitle: 'Switch', + want: ( +
+
+ + + {payload.appMeta.name} + +
+
+ ) + }; + case 'sign': + case 'sign_evm': return { title: t('confirmAction', t('signAction').toLowerCase()), declineActionTitle: t('reject'), @@ -364,7 +437,7 @@ const ConfirmDAppForm: FC = () => { /> ) : ( <> - {payload.type !== 'connect' && connectedAccount && ( + {payload.type !== 'connect' && payload.type !== 'connect_evm' && connectedAccount && ( ); }); + +const EvmAccountOptionContentHOC = () => + memo>(({ item: acc }) => { + return ( + <> +
+ {acc.name} + +
+ +
+
+ +
+
+ + ); + }); diff --git a/src/app/PageRouter.tsx b/src/app/PageRouter.tsx index 41aaa485f..431d31c2f 100644 --- a/src/app/PageRouter.tsx +++ b/src/app/PageRouter.tsx @@ -30,6 +30,7 @@ import { Notifications, NotificationsItem } from 'lib/notifications'; import { useTempleClient } from 'lib/temple/front'; import * as Woozie from 'lib/woozie'; +import { TokenPage } from './pages/TokenPage/TokenPage'; import { WithDataLoading } from './WithDataLoading'; interface RouteContext { @@ -75,6 +76,7 @@ const ROUTE_MAP = Woozie.createMap([ ['/loading', (_p, ctx) => (ctx.ready ? : )], ['/', (_p, ctx) => (ctx.ready ? : )], ['/explore/:assetSlug?', onlyReady(({ assetSlug }) => )], + ['/nonTezosTokenPage/:tokenAddress?', onlyReady(({ tokenAddress }) => )], ['/create-wallet', onlyNotReady(() => )], ['/create-account', onlyReady(() => )], ['/import-account/:tabSlug?', onlyReady(({ tabSlug }) => )], diff --git a/src/app/layouts/PageLayout/ConfirmationOverlay.tsx b/src/app/layouts/PageLayout/ConfirmationOverlay.tsx index 52ba8ee0c..cae6b8513 100644 --- a/src/app/layouts/PageLayout/ConfirmationOverlay.tsx +++ b/src/app/layouts/PageLayout/ConfirmationOverlay.tsx @@ -8,8 +8,12 @@ import InternalConfirmation from 'app/templates/InternalConfirmation'; import { useTempleClient } from 'lib/temple/front'; import Portal from 'lib/ui/Portal'; +import { InternalBtcConfirmation } from '../../templates/InternalBtcConfirmation'; +import { InternalEvmConfirmation } from '../../templates/InternalEvmConfirmation'; + const ConfirmationOverlay: FC = () => { - const { confirmation, resetConfirmation, confirmInternal } = useTempleClient(); + const { confirmation, resetConfirmation, confirmInternal, confirmBtcInternal, confirmEvmInternal } = + useTempleClient(); const displayed = Boolean(confirmation); useLayoutEffect(() => { @@ -36,6 +40,26 @@ const ConfirmationOverlay: FC = () => { [confirmation, confirmInternal, resetConfirmation] ); + const handleBtcConfirm = useCallback( + async (confirmed: boolean) => { + if (confirmation) { + await confirmBtcInternal(confirmation.id, confirmed); + } + resetConfirmation(); + }, + [confirmation, confirmBtcInternal, resetConfirmation] + ); + + const handleEvmConfirm = useCallback( + async (confirmed: boolean) => { + if (confirmation) { + await confirmEvmInternal(confirmation.id, confirmed); + } + resetConfirmation(); + }, + [confirmation, confirmEvmInternal, resetConfirmation] + ); + return ( <> {displayed && } @@ -52,13 +76,29 @@ const ConfirmationOverlay: FC = () => { unmountOnExit >
- {confirmation && ( - + )} + {confirmation && confirmation.payload.type === 'evm_operations' && ( + )} + {confirmation && + confirmation.payload.type !== 'btc_operations' && + confirmation.payload.type !== 'evm_operations' && ( + + )}
diff --git a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx index ad5d8d90a..03c65913a 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/Tokens.tsx @@ -30,6 +30,10 @@ import { useLocalStorage } from 'lib/ui/local-storage'; import Popper, { PopperRenderProps } from 'lib/ui/Popper'; import { Link, navigate } from 'lib/woozie'; +import { useBtcWalletAddresses } from '../../../../../lib/temple/front/ready'; +import { Image } from '../../../../../lib/ui/Image'; +import Spinner from '../../../../atoms/Spinner/Spinner'; +import { getBitcoinWithBalance, getEvmTokensWithBalances, NonTezosToken } from '../../../TokenPage/TokenPage'; import { HomeSelectors } from '../../Home.selectors'; import { AssetsSelectors } from '../Assets.selectors'; import { AcceptAdsBanner } from './AcceptAdsBanner'; @@ -44,9 +48,28 @@ export const TokensTab: FC = () => { const chainId = useChainId(true)!; const balances = useBalancesWithDecimals(); - const { publicKeyHash } = useAccount(); + const { publicKeyHash, evmPublicKeyHash } = useAccount(); const { isSyncing } = useSyncTokens(); const { popup } = useAppEnv(); + const btcWalletAddresses = useBtcWalletAddresses(); + + const [nonTezosTokens, setNonTezosTokens] = useState([]); + + const getAllNonTezosTokens = async () => { + if (!evmPublicKeyHash) { + setNonTezosTokens([]); + return; + } + + const bitcoin = await getBitcoinWithBalance(btcWalletAddresses); + const evmTokens = await getEvmTokensWithBalances(evmPublicKeyHash); + + setNonTezosTokens([bitcoin, ...evmTokens]); + }; + + useEffect(() => { + getAllNonTezosTokens(); + }, [evmPublicKeyHash]); const { data: tokens = [] } = useDisplayedFungibleTokens(chainId, publicKeyHash); @@ -183,6 +206,39 @@ export const TokensTab: FC = () => { {isEnabledAdsBanner && } + {nonTezosTokens.length !== 0 ? ( + nonTezosTokens.map((token: NonTezosToken) => ( + +
+ {token.name} +
+
+ {token.symbol} + {' (' + token.chainName + ')'} +
+
{token.name}
+
+
+
+ {(Number(token.balance) / 10 ** token.decimals).toFixed(6) + ' ' + token.symbol} +
+ + )) + ) : ( +
+
+ +
+
+ )} + {filteredAssets.length === 0 ? (

diff --git a/src/app/pages/TokenPage/ReceiveTab.tsx b/src/app/pages/TokenPage/ReceiveTab.tsx new file mode 100644 index 000000000..7ca5589b5 --- /dev/null +++ b/src/app/pages/TokenPage/ReceiveTab.tsx @@ -0,0 +1,99 @@ +import React, { FC, useEffect, useState } from 'react'; + +import classNames from 'clsx'; + +import { ReactComponent as CopyIcon } from 'app/icons/copy.svg'; + +import { setTestID } from '../../../lib/analytics'; +import { T, t } from '../../../lib/i18n'; +import { useTempleClient } from '../../../lib/temple/front'; +import { useBtcWalletAddresses } from '../../../lib/temple/front/ready'; +import useCopyToClipboard from '../../../lib/ui/useCopyToClipboard'; +import { FormField, FormSubmitButton } from '../../atoms'; +import Spinner from '../../atoms/Spinner/Spinner'; +import { ReceiveSelectors } from '../Receive/Receive.selectors'; + +interface Props { + isBitcoin: boolean; + address?: string; +} + +export const ReceiveTab: FC = ({ isBitcoin, address }) => { + const { fieldRef, copy, copied } = useCopyToClipboard(); + const { createNewBtcAddress } = useTempleClient(); + const btcWalletAddresses = useBtcWalletAddresses(); + + const [btcAddress, setBtcAddress] = useState(); + const [generating, setGenerating] = useState(false); + + const handleNewBtcAddressGeneration = async () => { + setGenerating(true); + await createNewBtcAddress(); + setGenerating(false); + + setBtcAddress(btcWalletAddresses[btcWalletAddresses.length - 1]); + }; + + useEffect(() => { + setBtcAddress(btcWalletAddresses[btcWalletAddresses.length - 1]); + }, [btcWalletAddresses]); + + if (!address) { + return ( +

+
+ +
+
+ ); + } + + return ( +
+ {isBitcoin && ( +
+ + Generate New Address + +
+ )} + + + +
+ ); +}; diff --git a/src/app/pages/TokenPage/SendTab.tsx b/src/app/pages/TokenPage/SendTab.tsx new file mode 100644 index 000000000..5912ab6b1 --- /dev/null +++ b/src/app/pages/TokenPage/SendTab.tsx @@ -0,0 +1,159 @@ +import React, { FC, useCallback, useMemo, useState } from 'react'; + +import { nanoid } from 'nanoid'; +import { Controller, useForm } from 'react-hook-form'; + +import { T, t } from '../../../lib/i18n'; +import { assertResponse, request, useTempleClient } from '../../../lib/temple/front'; +import { TempleMessageType } from '../../../lib/temple/types'; +import { FormSubmitButton, HashChip, NoSpaceField } from '../../atoms'; +import AssetField from '../../atoms/AssetField'; +import Spinner from '../../atoms/Spinner/Spinner'; +import { SendFormSelectors } from '../../templates/SendForm/selectors'; +import { NonTezosToken } from './TokenPage'; + +interface FormData { + to: string; + amount: string; +} + +interface Props { + isBitcoin: boolean; + accountPkh?: string; + token?: NonTezosToken; +} + +export const SendTab: FC = ({ isBitcoin, accountPkh, token }) => { + const [isProcessing, setProcessing] = useState(false); + const [hash, setHash] = useState(); + const { confirmationIdRef } = useTempleClient(); + + const { watch, handleSubmit, control, setValue, formState } = useForm({ + mode: 'onChange' + }); + + const toValue = watch('to'); + + const cleanToField = useCallback(() => { + setValue('to', ''); + }, [setValue]); + + const rpcUrl = useMemo(() => { + switch (token?.chainName) { + case 'Ethereum Sepolia': + return 'https://ethereum-sepolia.publicnode.com'; + case 'Ethereum Goerli': + return 'https://ethereum-goerli.publicnode.com'; + case 'Polygon Mumbai': + return 'https://polygon-mumbai-bor.publicnode.com'; + case 'BSC Testnet': + return 'https://bsc-testnet.publicnode.com'; + case 'Avalanche Testnet': + return 'https://avalanche-fuji-c-chain.publicnode.com'; + case 'Fantom Testnet': + return 'https://fantom-testnet.publicnode.com'; + default: + return token?.chainName ?? ''; + } + }, [token?.chainName]); + + const onSubmit = async ({ to, amount }: FormData) => { + if (isBitcoin) { + console.log('isBitcoin'); + setProcessing(true); + + const id = nanoid(); + confirmationIdRef.current = id; + + const res = await request({ + type: TempleMessageType.BtcOperationsRequest, + id, + toAddress: to, + amount + }); + assertResponse(res.type === TempleMessageType.BtcOperationsResponse); + + setHash(res.txId); + setProcessing(false); + + return; + } + + if (!accountPkh || !token) return; + + setProcessing(true); + + const id = nanoid(); + confirmationIdRef.current = id; + + const res = await request({ + type: TempleMessageType.EvmOperationsRequest, + id, + sourcePkh: accountPkh, + networkRpc: rpcUrl, + toAddress: to, + token, + amount + }); + assertResponse(res.type === TempleMessageType.EvmOperationsResponse); + + setHash(res.txHash); + setProcessing(false); + }; + + if (!token) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+ } + control={control} + onChange={([v]) => v} + textarea + rows={2} + placeholder={`e.g. ${isBitcoin ? 'tb1qufwmq' : '0xe03CE86'}...`} + cleanable={Boolean(toValue)} + onClean={cleanToField} + id="send-to" + label={t('recipient')} + labelDescription={`${isBitcoin ? 'Bitcoin' : 'EVM'} address to send funds to`} + style={{ + resize: 'none' + }} + containerClassName="mb-4" + testID={SendFormSelectors.recipientInput} + /> + } + control={control} + onChange={([v]) => v} + id="send-amount" + assetSymbol={token.symbol} + assetDecimals={token.decimals} + label={t('amount')} + placeholder={t('amountPlaceholder')} + containerClassName="mb-4" + /> + + + + + + {hash && ( + + Tx hash: + + )} + + ); +}; diff --git a/src/app/pages/TokenPage/TokenPage.tsx b/src/app/pages/TokenPage/TokenPage.tsx new file mode 100644 index 000000000..7d0bd3584 --- /dev/null +++ b/src/app/pages/TokenPage/TokenPage.tsx @@ -0,0 +1,160 @@ +import React, { FC, useEffect, useMemo, useState } from 'react'; + +import { isDefined } from '@rnw-community/shared'; +import axios from 'axios'; +import clsx from 'clsx'; + +import { TID } from '../../../lib/i18n'; +import { useAccount } from '../../../lib/temple/front'; +import { useBtcWalletAddresses } from '../../../lib/temple/front/ready'; +import { Image } from '../../../lib/ui/Image'; +import Spinner from '../../atoms/Spinner/Spinner'; +import { useTabSlug } from '../../atoms/useTabSlug'; +import { useAppEnv } from '../../env'; +import PageLayout from '../../layouts/PageLayout'; +import { TabsBar } from '../../templates/TabBar'; +import EditableTitle from '../Home/OtherComponents/EditableTitle'; +import { ReceiveTab } from './ReceiveTab'; +import { SendTab } from './SendTab'; + +export interface NonTezosToken { + token_address: string; + symbol: string; + name: string; + logo: string; + thumbnail: string; + decimals: number; + balance: string; + chainName: string; + nativeToken: boolean; + possible_spam: boolean; +} + +interface TabData { + name: string; + titleI18nKey: TID; + Component: FC; +} + +interface Props { + tokenAddress: string | null; +} +export const TokenPage: FC = ({ tokenAddress }) => { + const { fullPage } = useAppEnv(); + const tabSlug = useTabSlug(); + const { evmPublicKeyHash } = useAccount(); + const isBitcoin = tokenAddress === 'btc'; + const btcWalletAddresses = useBtcWalletAddresses(); + console.log(btcWalletAddresses); + + const [nonTezosTokens, setNonTezosTokens] = useState([]); + + const getAllNonTezosTokens = async () => { + if (isBitcoin) { + const bitcoin = await getBitcoinWithBalance(btcWalletAddresses); + + setNonTezosTokens([bitcoin]); + return; + } + + if (!evmPublicKeyHash) { + setNonTezosTokens([]); + return; + } + + const evmTokens = await getEvmTokensWithBalances(evmPublicKeyHash); + + setNonTezosTokens(evmTokens); + }; + + useEffect(() => { + getAllNonTezosTokens(); + }, []); + + const currentToken = useMemo(() => { + return nonTezosTokens.find(token => tokenAddress === token.token_address); + }, [nonTezosTokens, tokenAddress]); + + const tabs: TabData[] = useMemo( + () => [ + { + name: 'Receive', + titleI18nKey: 'receive', + Component: () => + }, + { + name: 'Send', + titleI18nKey: 'send', + Component: () => + } + ], + [currentToken, evmPublicKeyHash, isBitcoin] + ); + + const { name, Component } = useMemo(() => { + const tab = tabSlug ? tabs.find(currentTab => currentTab.name === tabSlug) : null; + return tab ?? tabs[0]; + }, [tabSlug, tabs]); + + return ( + + {isBitcoin ? 'Bitcoin' : isDefined(currentToken) ? currentToken.name : 'Token page'} + + } + > + {fullPage && ( +
+ +
+
+ )} +
+ {isDefined(currentToken) ? ( +
+
+ {currentToken.name} +
+
+ {currentToken.symbol} + {' (' + currentToken.chainName + ')'} +
+
{currentToken.name}
+
+
+
+ {(Number(currentToken.balance) / 10 ** currentToken.decimals).toFixed(6) + ' ' + currentToken.symbol} +
+
+ ) : ( +
+
+ +
+
+ )} +
+ +
+ + +
+
+ ); +}; + +export const getBitcoinWithBalance = async (addresses: string[]) => { + const addressesConcat = addresses.slice(-7).join(';'); + + console.log(addressesConcat, 'cococ'); + const response = await axios.get(`http://localhost:3000/api/bitcoin?addresses=${addressesConcat}`); + + return response.data; +}; + +export const getEvmTokensWithBalances = async (pkh: string) => { + const response = await axios.get(`http://localhost:3000/api/evm-tokens?address=${pkh}`); + + return response.data; +}; diff --git a/src/app/templates/InternalBtcConfirmation.tsx b/src/app/templates/InternalBtcConfirmation.tsx new file mode 100644 index 000000000..e0cccc3ed --- /dev/null +++ b/src/app/templates/InternalBtcConfirmation.tsx @@ -0,0 +1,106 @@ +import React, { FC, useCallback } from 'react'; + +import classNames from 'clsx'; + +import { FormSubmitButton, FormSecondaryButton } from 'app/atoms'; +import Logo from 'app/atoms/Logo'; +import SubTitle from 'app/atoms/SubTitle'; +import { useAppEnv } from 'app/env'; +import { T, t } from 'lib/i18n'; +import { TempleBtcOpsConfirmationPayload } from 'lib/temple/types'; + +import { useSafeState } from '../../lib/ui/hooks'; +import { delay } from '../../lib/utils'; + +interface Props { + payload: TempleBtcOpsConfirmationPayload; + onConfirm: (confirmed: boolean) => Promise; + error?: any; +} + +export const InternalBtcConfirmation: FC = ({ onConfirm }) => { + const { popup } = useAppEnv(); + + const [confirming, setConfirming] = useSafeState(false); + const [declining, setDeclining] = useSafeState(false); + + const confirm = useCallback( + async (confirmed: boolean) => { + try { + await onConfirm(confirmed); + } catch (err: any) { + // Human delay. + await delay(); + } + }, + [onConfirm] + ); + + const handleDeclineClick = useCallback(async () => { + if (confirming || declining) return; + + setDeclining(true); + await confirm(false); + setDeclining(false); + }, [confirming, declining, setDeclining, confirm]); + + const handleConfirmClick = useCallback(async () => { + if (confirming || declining) return; + + setConfirming(true); + await confirm(true); + setConfirming(false); + }, [confirming, declining, setConfirming, confirm]); + + return ( +
+
+
+ +
+
+ +
+
+ + + +
+ +
+ +
+
+ + + +
+ +
+ + + +
+
+
+
+ ); +}; diff --git a/src/app/templates/InternalConfirmation.tsx b/src/app/templates/InternalConfirmation.tsx index 55330a0b6..a177b65dd 100644 --- a/src/app/templates/InternalConfirmation.tsx +++ b/src/app/templates/InternalConfirmation.tsx @@ -26,14 +26,19 @@ import { T, t } from 'lib/i18n'; import { useRetryableSWR } from 'lib/swr'; import { useCustomChainId, useNetwork, useRelevantAccounts, tryParseExpenses, useBalance } from 'lib/temple/front'; import { tzToMutez } from 'lib/temple/helpers'; -import { TempleAccountType, TempleChainId, TempleConfirmationPayload } from 'lib/temple/types'; +import { + TempleAccountType, + TempleChainId, + TempleOpsConfirmationPayload, + TempleSignConfirmationPayload +} from 'lib/temple/types'; import { useSafeState } from 'lib/ui/hooks'; import { isTruthy, delay } from 'lib/utils'; import { InternalConfirmationSelectors } from './InternalConfirmation.selectors'; type InternalConfiramtionProps = { - payload: TempleConfirmationPayload; + payload: TempleSignConfirmationPayload | TempleOpsConfirmationPayload; onConfirm: (confirmed: boolean, modifiedTotalFee?: number, modifiedStorageLimit?: number) => Promise; error?: any; }; diff --git a/src/app/templates/InternalEvmConfirmation.tsx b/src/app/templates/InternalEvmConfirmation.tsx new file mode 100644 index 000000000..73253a87f --- /dev/null +++ b/src/app/templates/InternalEvmConfirmation.tsx @@ -0,0 +1,109 @@ +import React, { FC, useCallback } from 'react'; + +import classNames from 'clsx'; + +import { FormSubmitButton, FormSecondaryButton } from 'app/atoms'; +import Logo from 'app/atoms/Logo'; +import SubTitle from 'app/atoms/SubTitle'; +import { useAppEnv } from 'app/env'; +import { T, t } from 'lib/i18n'; +import { TempleEvmOpsConfirmationPayload } from 'lib/temple/types'; + +import { useSafeState } from '../../lib/ui/hooks'; +import { delay } from '../../lib/utils'; +import NetworkBanner from './NetworkBanner'; + +interface Props { + payload: TempleEvmOpsConfirmationPayload; + onConfirm: (confirmed: boolean) => Promise; + error?: any; +} + +export const InternalEvmConfirmation: FC = ({ payload, onConfirm }) => { + const { popup } = useAppEnv(); + + const [confirming, setConfirming] = useSafeState(false); + const [declining, setDeclining] = useSafeState(false); + + const confirm = useCallback( + async (confirmed: boolean) => { + try { + await onConfirm(confirmed); + } catch (err: any) { + // Human delay. + await delay(); + } + }, + [onConfirm] + ); + + const handleDeclineClick = useCallback(async () => { + if (confirming || declining) return; + + setDeclining(true); + await confirm(false); + setDeclining(false); + }, [confirming, declining, setDeclining, confirm]); + + const handleConfirmClick = useCallback(async () => { + if (confirming || declining) return; + + setConfirming(true); + await confirm(true); + setConfirming(false); + }, [confirming, declining, setConfirming, confirm]); + + return ( +
+
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+
+ + + +
+ +
+ + + +
+
+
+
+ ); +}; diff --git a/src/app/templates/OperationView.tsx b/src/app/templates/OperationView.tsx index b0c90eed4..48928ef36 100644 --- a/src/app/templates/OperationView.tsx +++ b/src/app/templates/OperationView.tsx @@ -12,10 +12,19 @@ import ViewsSwitcher from 'app/templates/ViewsSwitcher/ViewsSwitcher'; import { TEZ_TOKEN_SLUG, toTokenSlug } from 'lib/assets'; import { T, t } from 'lib/i18n'; import { tryParseExpenses } from 'lib/temple/front'; -import { TempleDAppOperationsPayload, TempleDAppSignPayload } from 'lib/temple/types'; +import { + TempleDAppOperationsPayload, + TempleDAppSignPayload, + TempleEvmDAppOperationsPayload, + TempleEvmDAppSignPayload +} from 'lib/temple/types'; type OperationViewProps = { - payload: TempleDAppOperationsPayload | TempleDAppSignPayload; + payload: + | TempleEvmDAppSignPayload + | TempleEvmDAppOperationsPayload + | TempleDAppOperationsPayload + | TempleDAppSignPayload; networkRpc?: string; mainnet?: boolean; error?: any; @@ -142,6 +151,25 @@ const OperationView: FC = ({ ); } + if (payload.type === 'sign_evm') { + return ( +
+

+ + + +

+ + 1 ? { height: '11rem' } : undefined} + label={null} + /> +
+ ); + } + if (payload.type === 'confirm_operations') { return (
@@ -186,6 +214,25 @@ const OperationView: FC = ({ ); } + if (payload.type === 'confirm_evm_operations') { + return ( +
+

+ + + +

+ + 1 ? { height: '11rem' } : undefined} + label={null} + /> +
+ ); + } + return null; }; diff --git a/src/contentScript.ts b/src/contentScript.ts index 65b7e8ac4..07da2c8e6 100644 --- a/src/contentScript.ts +++ b/src/contentScript.ts @@ -70,6 +70,30 @@ const SENDER = { iconUrl: 'https://templewallet.com/logo.png' }; +window.addEventListener('passToBackground', evt => { + // @ts-ignore + console.log('passToBackground_content_script', evt.detail.args); + getIntercom() + .request({ + type: TempleMessageType.PageRequest, + // @ts-ignore + origin: evt.detail.origin, + // @ts-ignore + payload: evt.detail.args, + // @ts-ignore + sourcePkh: evt.detail.sourcePkh, + // @ts-ignore + chainId: evt.detail.chainId, + evm: true + }) + .then((res: TempleResponse) => { + if (res?.type === TempleMessageType.PageResponse && res.payload) { + window.dispatchEvent(new CustomEvent('responseFromBackground', { detail: res.payload })); + } + }) + .catch(err => console.error(err)); +}); + window.addEventListener( 'message', evt => { diff --git a/src/inpage.ts b/src/inpage.ts new file mode 100644 index 000000000..c8510423e --- /dev/null +++ b/src/inpage.ts @@ -0,0 +1,42 @@ +import type { Eip1193Provider } from 'ethers'; +import { v4 as uuid } from 'uuid'; + +import { TempleWeb3Provider } from './newChains/temple-web3-provider'; + +interface EIP6963ProviderInfo { + uuid: string; + name: string; + icon: string; + rdns?: string; +} + +console.log('TEMPLE_INPAGE_SCRIPT_INJECTED'); + +const provider = new TempleWeb3Provider(); + +setGlobalProvider(provider); + +const info: EIP6963ProviderInfo = { + uuid: uuid(), + name: 'Temple Wallet', + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzlfMTc4KSI+CjxwYXRoIGQ9Ik01NS4zOTA0IDI0LjYyNUg2Ny42ODczTDYxLjUzMSAwSDE4LjQ2ODVMMjQuNjA5MSAyNC42MjVIMzYuOTIxNkw0My4wNzc5IDQ5LjIzNDRIMzUuMzkwNEwzOC40Njg1IDYxLjU0NjlINDYuMTU2TDQ5LjIzNDEgNzMuODU5NEg2Ny42ODczTDU1LjM5MDQgMjQuNjI1WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzlfMTc4KSIvPgo8cGF0aCBkPSJNNjEuNTMxMyAzMC43ODFINTYuOTIxOUw1NS4zOTA2IDI0LjYyNDhINjcuNjg3NUw2MS41MzEzIDMwLjc4MVoiIGZpbGw9InVybCgjcGFpbnQxX2xpbmVhcl85XzE3OCkiLz4KPHBhdGggZD0iTTU1LjM5MDYgMTIuMzEyM0gyNy42ODc1TDI2LjkyMTkgOS4yMzQxM0g1Ny43MDMxTDU1LjM5MDYgMTIuMzEyM1oiIGZpbGw9InVybCgjcGFpbnQyX2xpbmVhcl85XzE3OCkiLz4KPHBhdGggZD0iTTE4LjQ2ODggMzAuNzgxMkwxMi4zMTI1IDYuMTU2MjVMMTguNDY4OCAwTDI0LjYwOTQgMjQuNjI1TDE4LjQ2ODggMzAuNzgxMloiIGZpbGw9InVybCgjcGFpbnQzX2xpbmVhcl85XzE3OCkiLz4KPHBhdGggZD0iTTQ2LjE1NjIgNjEuNTQ2NkgzOC40Njg3TDMyLjMxMjUgNjcuNzAyOUg0MEw0Ni4xNTYyIDYxLjU0NjZaIiBmaWxsPSJ1cmwoI3BhaW50NF9saW5lYXJfOV8xNzgpIi8+CjxwYXRoIGQ9Ik0zMC43NjU0IDMwLjc4MUgxOC40Njg1TDI0LjYwOTEgMjQuNjI0OEgzNi45MjE2TDMwLjc2NTQgMzAuNzgxWiIgZmlsbD0idXJsKCNwYWludDVfbGluZWFyXzlfMTc4KSIvPgo8cGF0aCBkPSJNNDMuMDc4NCA0OS4yMzQxSDM1LjM5MDlMMzAuNzY1OSAzMC43ODFMMzYuOTIyMSAyNC42MjQ4TDQzLjA3ODQgNDkuMjM0MVoiIGZpbGw9InVybCgjcGFpbnQ2X2xpbmVhcl85XzE3OCkiLz4KPHBhdGggZD0iTTM4LjQ2ODggNjEuNTQ3MUwzNS4zOTA2IDQ5LjIzNDZMMjkuMjM0NCA1NS4zOTA5TDMyLjMxMjUgNjcuNzAzNEwzOC40Njg4IDYxLjU0NzFaIiBmaWxsPSJ1cmwoI3BhaW50N19saW5lYXJfOV8xNzgpIi8+CjxwYXRoIGQ9Ik00OS4yMzQ0IDczLjg1OTFMNDYuMTU2MyA2MS41NDY2TDQwIDY3LjcwMjlMNDMuMDc4MSA3OS45OTk4TDQ5LjIzNDQgNzMuODU5MVoiIGZpbGw9InVybCgjcGFpbnQ4X2xpbmVhcl85XzE3OCkiLz4KPHBhdGggZD0iTTYxLjUzMTIgNzMuODU5NEw0Ny42ODc1IDE4LjQ2ODhMNDUuMjM0NCAyMC45MjE5TDU4LjQ2ODcgNzMuODU5NEw1NiA3Ni4zMTI1SDU5LjA3ODFMNjEuNTMxMiA3My44NTk0WiIgZmlsbD0idXJsKCNwYWludDlfbGluZWFyXzlfMTc4KSIvPgo8cGF0aCBkPSJNNjAgMTguNDY4NUg1Ni45MjE5TDU1LjM5MDYgMTIuMzEyM0w1Ny43MDMxIDkuMjM0MTNMNjAgMTguNDY4NVoiIGZpbGw9InVybCgjcGFpbnQxMF9saW5lYXJfOV8xNzgpIi8+CjxwYXRoIGQ9Ik02MS41MzE1IDczLjg1OTlMNTkuMDc4NCA3Ni4zMTNINTYuMDAwM0w1OC40NjkgNzMuODU5OUg0OS4yMzQ2TDQzLjA3ODQgODAuMDAwNUg2MS41MzE1TDY3LjY4NzggNzMuODU5OUg2MS41MzE1WiIgZmlsbD0idXJsKCNwYWludDExX2xpbmVhcl85XzE3OCkiLz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzlfMTc4IiB4MT0iMTguNDYxMyIgeTE9IjM2LjkyODYiIHgyPSI2Ny42OTIxIiB5Mj0iMzYuOTI4NiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMDAxODgwNzkiIHN0b3AtY29sb3I9IiNGQ0MzM0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRkZFRTUwIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl85XzE3OCIgeDE9IjY3LjY5MjMiIHkxPSIyNy42OTc2IiB4Mj0iNTUuMzg0NiIgeTI9IjI3LjY5NzYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0ZCOTgyOCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGOTZDMTMiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDJfbGluZWFyXzlfMTc4IiB4MT0iNTcuNjk2IiB5MT0iMTAuNzc0NSIgeDI9IjI2LjkyMzEiIHkyPSIxMC43NzQ1IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNGODQyMDAiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRjk2QzEzIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQzX2xpbmVhcl85XzE3OCIgeDE9IjE0Ljg1MDIiIHkxPSIwLjkwNTk1MyIgeDI9IjIyLjA3MjkiIHkyPSIyOS44NzQ0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNGOTZDMTMiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRkI5ODI4Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQ0X2xpbmVhcl85XzE3OCIgeDE9IjMyLjMwNzYiIHkxPSI2NC42MjA3IiB4Mj0iNDYuMTUzOSIgeTI9IjY0LjYyMDciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0Y4NDIwMCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGOTZDMTMiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDVfbGluZWFyXzlfMTc4IiB4MT0iMTguNDYxMyIgeTE9IjI3LjY5NzYiIHgyPSIzNi45MjI4IiB5Mj0iMjcuNjk3NiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjg0MjAwIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0Y5NkMxMyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50Nl9saW5lYXJfOV8xNzgiIHgxPSIzOS40NTAzIiB5MT0iNTAuMTQwNCIgeDI9IjMzLjMxMiIgeTI9IjI1LjUyMTEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0Y5NkMxMyIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGQjk4MjgiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDdfbGluZWFyXzlfMTc4IiB4MT0iMzUuOTIyOSIgeTE9IjY2Ljc5NjciIHgyPSIzMS43Njk0IiB5Mj0iNTAuMTM3OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRkI5ODI4Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0Y5NkMxMyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50OF9saW5lYXJfOV8xNzgiIHgxPSI0Ni42OTIxIiB5MT0iNzkuMTAzOSIgeDI9IjQyLjUzODciIHkyPSI2Mi40NDUxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNGOTZDMTMiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRkI5ODI4Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQ5X2xpbmVhcl85XzE3OCIgeDE9IjU5LjA3NTkiIHkxPSI3Ni4zMTIzIiB4Mj0iNDQuODMxMyIgeTI9IjE5LjE4MDQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0Y5NkMxMyIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGODQyMDAiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDEwX2xpbmVhcl85XzE3OCIgeDE9IjU4Ljc4MzYiIHkxPSIxOS43NjY4IiB4Mj0iNTYuNDgxOSIgeTI9IjEwLjUzNTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0Y4NDIwMCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGOTZDMTMiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDExX2xpbmVhcl85XzE3OCIgeDE9IjQ1LjY1ODQiIHkxPSI3Ni45MjkxIiB4Mj0iNjQuMDQ1IiB5Mj0iNzYuOTI5MSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjg0MjAwIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0Y5NkMxMyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzlfMTc4Ij4KPHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=', + rdns: 'com.templewallet' +}; + +window.addEventListener('eip6963:requestProvider', announceProvider); + +announceProvider(); + +function setGlobalProvider(providerInstance: Eip1193Provider) { + (window as Record).ethereum = providerInstance; + console.log((window as Record).ethereum); + window.dispatchEvent(new Event('ethereum#initialized')); +} + +function announceProvider() { + window.dispatchEvent( + new CustomEvent('eip6963:announceProvider', { + detail: Object.freeze({ info, provider }) + }) + ); +} diff --git a/src/lib/temple/back/actions.ts b/src/lib/temple/back/actions.ts index d363e1631..6a20f6a47 100644 --- a/src/lib/temple/back/actions.ts +++ b/src/lib/temple/back/actions.ts @@ -22,6 +22,8 @@ import { } from 'lib/temple/types'; import { createQueue, delay } from 'lib/utils'; +import { NonTezosToken } from '../../../app/pages/TokenPage/TokenPage'; +import { EvmRequestArguments } from '../../../newChains/temple-web3-provider'; import { getCurrentPermission, requestPermission, @@ -34,6 +36,7 @@ import { import { intercom } from './defaults'; import type { DryRunResult } from './dryrun'; import { buildFinalOpParmas, dryRunOpParams } from './dryrun'; +import { connectEvm, requestEvmOperation, requestEvmSign, switchChain } from './evmDapp'; import { toFront, store, @@ -43,7 +46,8 @@ import { accountsUpdated, settingsUpdated, withInited, - withUnlocked + withUnlocked, + btcWalletAddressesUpdated } from './store'; import { Vault } from './vault'; @@ -112,8 +116,10 @@ export function unlock(password: string) { enqueueUnlock(async () => { const vault = await Vault.setup(password, BACKGROUND_IS_WORKER); const accounts = await vault.fetchAccounts(); + const btcWalletAddresses = await vault.fetchBtcWalletAddresses(); + console.log(btcWalletAddresses, 'btcWalletAddresses'); const settings = await vault.fetchSettings(); - unlocked({ vault, accounts, settings }); + unlocked({ vault, accounts, btcWalletAddresses, settings }); }) ); } @@ -123,8 +129,10 @@ export async function unlockFromSession() { const vault = await Vault.recoverFromSession(); if (vault == null) return; const accounts = await vault.fetchAccounts(); + const btcWalletAddresses = await vault.fetchBtcWalletAddresses(); + console.log(btcWalletAddresses, 'btcWalletAddresses'); const settings = await vault.fetchSettings(); - unlocked({ vault, accounts, settings }); + unlocked({ vault, accounts, btcWalletAddresses, settings }); }); } @@ -142,6 +150,13 @@ export function createHDAccount(name?: string) { }); } +export function createNewBtcAddress() { + return withUnlocked(async ({ vault }) => { + const updatedAddresses = await vault.createNewBtcAddress(); + btcWalletAddressesUpdated(updatedAddresses); + }); +} + export function revealMnemonic(password: string) { return withUnlocked(() => Vault.revealMnemonic(password)); } @@ -260,6 +275,153 @@ export function sendOperations( }); } +export const sendBtcOperations = async ( + port: Runtime.Port, + id: string, + toAddress: string, + amount: string +): Promise<{ txId: string }> => { + return withUnlocked(async () => { + return new Promise((resolve, reject) => promisableBtcUnlock(resolve, reject, port, id, toAddress, amount)); + }); +}; + +const promisableBtcUnlock = async ( + resolve: (arg: { txId: string }) => void, + reject: (err: Error) => void, + port: Runtime.Port, + id: string, + toPkh: string, + amount: string +) => { + intercom.notify(port, { + type: TempleMessageType.ConfirmationRequested, + id, + payload: { + type: 'btc_operations' + } + }); + + let closing = false; + + const decline = () => { + reject(new Error('Declined')); + }; + const declineAndClose = () => { + decline(); + closing = close(closing, port, id, stopTimeout, stopRequestListening, stopDisconnectListening); + }; + + const stopRequestListening = intercom.onRequest(async (req: TempleRequest, reqPort) => { + if (reqPort === port && req?.type === TempleMessageType.BtcConfirmationRequest && req?.id === id) { + if (req.confirmed) { + try { + console.log('trysend'); + const txId = await withUnlocked(({ vault }) => vault.sendBtcOperations(toPkh, amount)); + + resolve({ txId }); + } catch (err: any) { + reject(err); + } + } else { + decline(); + } + + closing = close(closing, port, id, stopTimeout, stopRequestListening, stopDisconnectListening); + + return { + type: TempleMessageType.BtcConfirmationResponse + }; + } + return undefined; + }); + + const stopDisconnectListening = intercom.onDisconnect(port, declineAndClose); + + // Decline after timeout + const t = setTimeout(declineAndClose, AUTODECLINE_AFTER); + const stopTimeout = () => clearTimeout(t); +}; + +export const sendEvmOperations = async ( + port: Runtime.Port, + id: string, + token: NonTezosToken, + sourceAddress: string, + toAddress: string, + amount: string, + rpcUrl: string +): Promise<{ txHash: string }> => { + return withUnlocked(async () => { + return new Promise((resolve, reject) => + promisableEvmUnlock(resolve, reject, port, id, sourceAddress, toAddress, rpcUrl, token, amount) + ); + }); +}; + +const promisableEvmUnlock = async ( + resolve: (arg: { txHash: string }) => void, + reject: (err: Error) => void, + port: Runtime.Port, + id: string, + sourcePkh: string, + toPkh: string, + networkRpc: string, + token: NonTezosToken, + amount: string +) => { + intercom.notify(port, { + type: TempleMessageType.ConfirmationRequested, + id, + payload: { + type: 'evm_operations', + sourcePkh, + networkRpc + } + }); + + let closing = false; + + const decline = () => { + reject(new Error('Declined')); + }; + const declineAndClose = () => { + decline(); + closing = close(closing, port, id, stopTimeout, stopRequestListening, stopDisconnectListening); + }; + + const stopRequestListening = intercom.onRequest(async (req: TempleRequest, reqPort) => { + if (reqPort === port && req?.type === TempleMessageType.EvmConfirmationRequest && req?.id === id) { + if (req.confirmed) { + try { + const transaction = await withUnlocked(({ vault }) => + vault.sendEvmOperations(sourcePkh, networkRpc, token, amount, toPkh) + ); + + resolve({ txHash: transaction.hash }); + } catch (err: any) { + reject(err); + } + } else { + decline(); + } + + closing = close(closing, port, id, stopTimeout, stopRequestListening, stopDisconnectListening); + + return { + type: TempleMessageType.EvmConfirmationResponse + }; + } + return undefined; + }); + + const stopDisconnectListening = intercom.onDisconnect(port, declineAndClose); + + // Decline after timeout + const t = setTimeout(declineAndClose, AUTODECLINE_AFTER); + const stopTimeout = () => clearTimeout(t); +}; + const promisableUnlock = async ( resolve: (arg: { opHash: string }) => void, reject: (err: Error) => void, @@ -414,6 +576,31 @@ export async function processDApp(origin: string, req: TempleDAppRequest): Promi } } +export async function processEvmDApp( + origin: string, + payload: EvmRequestArguments, + chainId?: string, + sourcePkh?: string +): Promise { + console.log(origin, 'origin'); + const { method, params } = payload; + + switch (method) { + case 'eth_requestAccounts': + return withInited(() => enqueueDApp(() => connectEvm(origin, chainId))); + case 'wallet_switchEthereumChain': + return withInited(() => enqueueDApp(() => switchChain(params))); + case 'eth_sendTransaction': + //@ts-ignore + return withInited(() => enqueueDApp(() => requestEvmOperation(origin, sourcePkh, chainId, params))); + case 'eth_signTypedData_v4': + //@ts-ignore + return withInited(() => enqueueDApp(() => requestEvmSign(origin, sourcePkh, chainId, params))); + default: + return 'No handler for this type of request'; + } +} + export async function getBeaconMessage(origin: string, msg: string, encrypted = false) { let recipientPubKey: string | null = null; let payload = null; diff --git a/src/lib/temple/back/dapp.ts b/src/lib/temple/back/dapp.ts index 3ef1ac466..25cff92dc 100644 --- a/src/lib/temple/back/dapp.ts +++ b/src/lib/temple/back/dapp.ts @@ -40,7 +40,7 @@ import { withUnlocked } from './store'; const CONFIRM_WINDOW_WIDTH = 380; const CONFIRM_WINDOW_HEIGHT = 632; -const AUTODECLINE_AFTER = 120_000; +export const AUTODECLINE_AFTER = 120_000; const STORAGE_KEY = 'dapp_sessions'; const HEX_PATTERN = /^[0-9a-fA-F]+$/; const TEZ_MSG_SIGN_PATTERN = /^0501[a-f0-9]{8}54657a6f73205369676e6564204d6573736167653a20[a-f0-9]*$/; @@ -508,7 +508,7 @@ function removeLastSlash(str: string) { return str.endsWith('/') ? str.slice(0, -1) : str; } -async function createConfirmationWindow(confirmationId: string) { +export async function createConfirmationWindow(confirmationId: string) { const isWin = (await browser.runtime.getPlatformInfo()).os === 'win'; const height = isWin ? CONFIRM_WINDOW_HEIGHT + 17 : CONFIRM_WINDOW_HEIGHT; diff --git a/src/lib/temple/back/evmDapp.ts b/src/lib/temple/back/evmDapp.ts new file mode 100644 index 000000000..e69c205b7 --- /dev/null +++ b/src/lib/temple/back/evmDapp.ts @@ -0,0 +1,277 @@ +import { nanoid } from 'nanoid'; +import browser, { Runtime } from 'webextension-polyfill'; + +import { TempleDAppPayload, TempleMessageType, TempleRequest } from '../types'; +import { AUTODECLINE_AFTER, createConfirmationWindow } from './dapp'; +import { intercom } from './defaults'; +import { withUnlocked } from './store'; + +type RequestConfirmParams = { + id: string; + payload: TempleDAppPayload; + onDecline: () => void; + handleIntercomRequest: (req: TempleRequest, decline: () => void) => Promise; +}; + +export const requestConfirm = async ({ id, payload, onDecline, handleIntercomRequest }: RequestConfirmParams) => { + let closing = false; + const close = async () => { + if (closing) return; + closing = true; + + try { + stopTimeout(); + stopRequestListening(); + stopWinRemovedListening(); + + await closeWindow(); + } catch (_err) {} + }; + + const declineAndClose = () => { + onDecline(); + close(); + }; + + let knownPort: Runtime.Port | undefined; + const stopRequestListening = intercom.onRequest(async (req: TempleRequest, port) => { + if (req?.type === TempleMessageType.DAppGetPayloadRequest && req.id === id) { + knownPort = port; + + return { + type: TempleMessageType.DAppGetPayloadResponse, + payload + }; + } else { + if (knownPort !== port) return; + + const result = await handleIntercomRequest(req, onDecline); + if (result) { + close(); + return result; + } + } + }); + + const confirmWin = await createConfirmationWindow(id); + + const closeWindow = async () => { + if (confirmWin.id) { + const win = await browser.windows.get(confirmWin.id); + if (win.id) { + await browser.windows.remove(win.id); + } + } + }; + + const handleWinRemoved = (winId: number) => { + if (winId === confirmWin?.id) { + declineAndClose(); + } + }; + browser.windows.onRemoved.addListener(handleWinRemoved); + const stopWinRemovedListening = () => browser.windows.onRemoved.removeListener(handleWinRemoved); + + // Decline after timeout + const t = setTimeout(declineAndClose, AUTODECLINE_AFTER); + const stopTimeout = () => clearTimeout(t); +}; + +export const connectEvm = async (origin: string, chainId?: string) => { + if (!chainId) return new Error('chainId is not defined'); + + return new Promise(async (resolve, reject) => { + const id = nanoid(); + + await requestConfirm({ + id, + payload: { + type: 'connect_evm', + origin, + networkRpc: getRpcUrlByChainId(chainId), + appMeta: { name: origin.split('.')[1] } + }, + onDecline: () => { + const err = new Error('Connection declined'); + //@ts-ignore + err.code = 4001; + + reject(err); + }, + handleIntercomRequest: async (confirmReq, decline) => { + if (confirmReq?.type === TempleMessageType.DAppPermConfirmationRequest && confirmReq?.id === id) { + const { confirmed, accountPublicKeyHash } = confirmReq; + if (confirmed && accountPublicKeyHash) { + resolve([accountPublicKeyHash]); + } else { + decline(); + } + + return { + type: TempleMessageType.DAppPermConfirmationResponse + }; + } + return undefined; + } + }); + }); +}; + +export const switchChain = async (params: unknown[] | Record | undefined) => { + if (!params) return new Error('Request params is not defined'); + //@ts-ignore + const chainIdHex = params[0].chainId; + const rpcUrl = getRpcUrlByChainId(chainIdHex); + + return new Promise(async (resolve, reject) => { + const id = nanoid(); + + await requestConfirm({ + id, + payload: { + type: 'switch_evm_network', + origin, + networkRpc: rpcUrl, + appMeta: { name: origin.split('.')[1] } + }, + onDecline: () => { + const err = new Error('Network switch declined'); + //@ts-ignore + err.code = 4001; + + reject(err); + }, + handleIntercomRequest: async (confirmReq, decline) => { + if (confirmReq?.type === TempleMessageType.DAppPermConfirmationRequest && confirmReq?.id === id) { + const { confirmed } = confirmReq; + if (confirmed) { + resolve({ chainId: chainIdHex, rpcUrl }); + } else { + decline(); + } + + return { + type: TempleMessageType.DAppPermConfirmationResponse + }; + } + return undefined; + } + }); + }); +}; + +export async function requestEvmOperation( + origin: string, + sourcePkh: string | undefined, + chainId: string | undefined, + opParams: any[] +) { + if (!sourcePkh) return new Error('sourcePkh is not defined'); + if (!chainId) return new Error('chainId is not defined'); + + const rpcUrl = getRpcUrlByChainId(chainId); + + return new Promise(async (resolve, reject) => { + const id = nanoid(); + + await requestConfirm({ + id, + payload: { + type: 'confirm_evm_operations', + origin, + sourcePkh, + networkRpc: rpcUrl, + appMeta: { name: origin.split('.')[1] }, + opParams + }, + onDecline: () => { + const err = new Error('Operation declined'); + //@ts-ignore + err.code = 4001; + reject(err); + }, + handleIntercomRequest: async (confirmReq, decline) => { + if (confirmReq?.type === TempleMessageType.DAppOpsConfirmationRequest && confirmReq?.id === id) { + if (confirmReq.confirmed) { + try { + const op = await withUnlocked(({ vault }) => vault.sendEvmDAppOperations(sourcePkh, rpcUrl, opParams)); + + resolve(op.hash); + } catch (err) { + console.error(err); + } + } else { + decline(); + } + + return { + type: TempleMessageType.DAppOpsConfirmationResponse + }; + } + return undefined; + } + }); + }); +} + +export async function requestEvmSign( + origin: string, + sourcePkh: string | undefined, + chainId: string | undefined, + opParams: any[] +) { + if (!sourcePkh) return new Error('sourcePkh is not defined'); + if (!chainId) return new Error('chainId is not defined'); + + const rpcUrl = getRpcUrlByChainId(chainId); + + return new Promise(async (resolve, reject) => { + const id = nanoid(); + + await requestConfirm({ + id, + payload: { + type: 'sign_evm', + origin, + sourcePkh, + networkRpc: rpcUrl, + appMeta: { name: origin.split('.')[1] }, + opParams + }, + onDecline: () => { + const err = new Error('Sign declined'); + //@ts-ignore + err.code = 4001; + reject(err); + }, + handleIntercomRequest: async (confirmReq, decline) => { + if (confirmReq?.type === TempleMessageType.DAppSignConfirmationRequest && confirmReq?.id === id) { + if (confirmReq.confirmed) { + const result = await withUnlocked(({ vault }) => vault.signEvm(sourcePkh, rpcUrl, opParams)); + resolve(result); + } else { + decline(); + } + + return { + type: TempleMessageType.DAppSignConfirmationResponse + }; + } + return undefined; + } + }); + }); +} + +const chainIdHexRpcUrlRecord: Record = { + '0xaa36a7': 'https://ethereum-sepolia.publicnode.com', + '0x5': 'https://ethereum-goerli.publicnode.com', + '0x13881': 'https://polygon-mumbai-bor.publicnode.com', + '0x61': 'https://bsc-testnet.publicnode.com', + '0xa869': 'https://avalanche-fuji-c-chain.publicnode.com', + '0xfa2': 'https://fantom-testnet.publicnode.com' +}; + +function getRpcUrlByChainId(chainIdHex: string) { + return chainIdHexRpcUrlRecord[chainIdHex] ?? chainIdHex; +} diff --git a/src/lib/temple/back/main.ts b/src/lib/temple/back/main.ts index 34e52aab1..4e362fd13 100644 --- a/src/lib/temple/back/main.ts +++ b/src/lib/temple/back/main.ts @@ -65,6 +65,10 @@ const processRequest = async (req: TempleRequest, port: Runtime.Port): Promise { expect(status).toBe(TempleStatus.Locked); }); it('Unlocked event', () => { - unlocked({ vault: {} as Vault, accounts: [], settings: {} }); + unlocked({ vault: {} as Vault, accounts: [], btcWalletAddresses: [], settings: {} }); const { status } = store.getState(); expect(status).toBe(TempleStatus.Ready); }); diff --git a/src/lib/temple/back/store.ts b/src/lib/temple/back/store.ts index bc31c7fdc..f14505e48 100644 --- a/src/lib/temple/back/store.ts +++ b/src/lib/temple/back/store.ts @@ -14,10 +14,11 @@ interface UnlockedStoreState extends StoreState { vault: Vault; } -export function toFront({ status, accounts, networks, settings }: StoreState): TempleState { +export function toFront({ status, accounts, btcWalletAddresses, networks, settings }: StoreState): TempleState { return { status, accounts, + btcWalletAddresses, networks, settings }; @@ -34,10 +35,12 @@ export const locked = createEvent('Locked'); export const unlocked = createEvent<{ vault: Vault; accounts: TempleAccount[]; + btcWalletAddresses: string[]; settings: TempleSettings; }>('Unlocked'); export const accountsUpdated = createEvent('Accounts updated'); +export const btcWalletAddressesUpdated = createEvent('btcWalletAddresses updated'); export const settingsUpdated = createEvent('Settings updated'); @@ -50,6 +53,7 @@ export const store = createStore({ vault: null, status: TempleStatus.Idle, accounts: [], + btcWalletAddresses: [], networks: [], settings: null }) @@ -69,20 +73,26 @@ export const store = createStore({ vault: null, status: TempleStatus.Locked, accounts: [], + btcWalletAddresses: [], networks: NETWORKS, settings: null })) - .on(unlocked, (state, { vault, accounts, settings }) => ({ + .on(unlocked, (state, { vault, accounts, btcWalletAddresses, settings }) => ({ ...state, vault, status: TempleStatus.Ready, accounts, + btcWalletAddresses, settings })) .on(accountsUpdated, (state, accounts) => ({ ...state, accounts })) + .on(btcWalletAddressesUpdated, (state, btcWalletAddresses) => ({ + ...state, + btcWalletAddresses + })) .on(settingsUpdated, (state, settings) => ({ ...state, settings diff --git a/src/lib/temple/back/vault/index.ts b/src/lib/temple/back/vault/index.ts index 87fa5ba85..b82204d15 100644 --- a/src/lib/temple/back/vault/index.ts +++ b/src/lib/temple/back/vault/index.ts @@ -1,9 +1,11 @@ +import { isDefined } from '@rnw-community/shared'; import { HttpResponseError } from '@taquito/http-utils'; import { DerivationType } from '@taquito/ledger-signer'; import { localForger } from '@taquito/local-forging'; import { CompositeForger, RpcForger, Signer, TezosOperationError, TezosToolkit } from '@taquito/taquito'; import * as TaquitoUtils from '@taquito/utils'; import * as Bip39 from 'bip39'; +import { ethers } from 'ethers'; import * as WasmThemis from 'wasm-themis'; import { formatOpParamsBeforeSend, loadFastRpcClient, michelEncoder } from 'lib/temple/helpers'; @@ -11,20 +13,23 @@ import * as Passworder from 'lib/temple/passworder'; import { clearAsyncStorages } from 'lib/temple/reset'; import { TempleAccount, TempleAccountType, TempleSettings } from 'lib/temple/types'; +import { NonTezosToken } from '../../../../app/pages/TokenPage/TokenPage'; +import { getBitcoinXPubFromMnemonic, getNextBitcoinHDWallet, sendBitcoin } from '../../../../newChains/bitcoin'; +import ERC20ABI from '../../../../newChains/erc20abi.json'; import { createLedgerSigner } from '../ledger'; import { PublicError } from '../PublicError'; import { transformHttpResponseError } from './helpers'; import { MIGRATIONS } from './migrations'; import { - seedToHDPrivateKey, - seedToPrivateKey, - deriveSeed, - generateCheck, - fetchNewAccountName, concatAccount, createMemorySigner, + deriveSeed, + fetchNewAccountName, + generateCheck, getMainDerivationPath, getPublicKeyAndHash, + seedToHDPrivateKey, + seedToPrivateKey, withError } from './misc'; import { @@ -40,14 +45,15 @@ import { } from './safe-storage'; import * as SessionStore from './session-store'; import { + accountsStrgKey, + accPrivKeyStrgKey, + accPubKeyStrgKey, + BtcPkhPrivKeyRecord, checkStrgKey, + legacyMigrationLevelStrgKey, migrationLevelStrgKey, mnemonicStrgKey, - accPrivKeyStrgKey, - accPubKeyStrgKey, - accountsStrgKey, - settingsStrgKey, - legacyMigrationLevelStrgKey + settingsStrgKey } from './storage-keys'; const TEMPLE_SYNC_PREFIX = 'templesync'; @@ -284,8 +290,72 @@ export class Vault { ); } - fetchAccounts() { - return fetchAndDecryptOne(accountsStrgKey, this.passKey); + async fetchAccounts() { + const accounts = await fetchAndDecryptOne(accountsStrgKey, this.passKey); + const mnemonic = await fetchAndDecryptOne(mnemonicStrgKey, this.passKey); + + for (let i = 0; i < accounts.length; i++) { + const currentAccount = accounts[i]; + + if (currentAccount.type === TempleAccountType.HD && !currentAccount.evmPublicKeyHash) { + const derivationPath = isDefined(currentAccount.derivationPath) + ? currentAccount.derivationPath + : `m/44'/1729'/${currentAccount.hdIndex}'/0'`; + + const evmWallet = ethers.Wallet.fromPhrase(mnemonic).derivePath(derivationPath); + + if (evmWallet) { + currentAccount.derivationPath = derivationPath; + currentAccount.evmPublicKeyHash = evmWallet.address; + + await encryptAndSaveMany([[accPrivKeyStrgKey(evmWallet.address), evmWallet.privateKey]], this.passKey); + } + } + } + + return accounts; + } + + async fetchBtcWalletAddresses() { + const mnemonic = await fetchAndDecryptOne(mnemonicStrgKey, this.passKey); + + try { + //throw new Error('1'); + const btcWalletAddressesWithPrivateKeysRecord = await fetchAndDecryptOne( + BtcPkhPrivKeyRecord, + this.passKey + ); + + return Object.keys(btcWalletAddressesWithPrivateKeysRecord); + } catch { + const hdMaster = getBitcoinXPubFromMnemonic(mnemonic); + const { address, privateKey } = getNextBitcoinHDWallet(hdMaster, 0); + + await encryptAndSaveMany([[BtcPkhPrivKeyRecord, { [address]: privateKey }]], this.passKey); + + return [address]; + } + } + + async createNewBtcAddress() { + const mnemonic = await fetchAndDecryptOne(mnemonicStrgKey, this.passKey); + const btcWalletAddressesWithPrivateKeys = await fetchAndDecryptOne>( + BtcPkhPrivKeyRecord, + this.passKey + ); + + const walletAddresses = Object.keys(btcWalletAddressesWithPrivateKeys); + + const nextAccIndex = walletAddresses.length; + + const hdMaster = getBitcoinXPubFromMnemonic(mnemonic); + const { address, privateKey } = getNextBitcoinHDWallet(hdMaster, nextAccIndex); + + btcWalletAddressesWithPrivateKeys[address] = privateKey; + + await encryptAndSaveMany([[BtcPkhPrivKeyRecord, btcWalletAddressesWithPrivateKeys]], this.passKey); + + return [...walletAddresses, address]; } async fetchSettings() { @@ -539,6 +609,72 @@ export class Vault { }); } + async sendBtcOperations(toAddress: string, amount: string): Promise { + console.log('sending'); + const btcWalletAddressesWithPrivateKeysRecord = await fetchAndDecryptOne>( + BtcPkhPrivKeyRecord, + this.passKey + ); + + console.log(btcWalletAddressesWithPrivateKeysRecord, 'record'); + + const txId = await sendBitcoin( + toAddress, + amount, + btcWalletAddressesWithPrivateKeysRecord, + this.createNewBtcAddress.bind(this) + ); + + return txId; + } + + async sendEvmOperations( + accPublicKeyHash: string, + rpc: string, + token: NonTezosToken, + amount: string, + toAddress: string + ): Promise { + return this.withEvmSigner(accPublicKeyHash, rpc, async signer => { + const parsedAmount = ethers.parseUnits(amount, token.decimals); + + if (token.nativeToken) { + const tx = { + to: toAddress, + value: parsedAmount + }; + + return signer.sendTransaction(tx); + } else { + const contract = new ethers.Contract(token.token_address, ERC20ABI, signer); + + return contract.transfer(toAddress, parsedAmount); + } + }); + } + + async sendEvmDAppOperations( + accPublicKeyHash: string, + rpc: string, + opParams: any[] + ): Promise { + return this.withEvmSigner(accPublicKeyHash, rpc, async signer => { + console.log('trying to send dApp tx'); + return signer.sendTransaction(opParams[0]); + }); + } + + async signEvm(accPublicKeyHash: string, rpc: string, opParams: any[]): Promise { + return this.withEvmSigner(accPublicKeyHash, rpc, async signer => { + console.log('trying to sign dApp params'); + const { types, domain, message } = JSON.parse(opParams[1]); + //ethers will add it yourself + delete types.EIP712Domain; + + return signer.signTypedData(domain, types, message); + }); + } + private async withSigner(accPublicKeyHash: string, factory: (signer: Signer) => Promise) { const { signer, cleanup } = await this.getSigner(accPublicKeyHash); try { @@ -548,6 +684,15 @@ export class Vault { } } + private async withEvmSigner( + accPublicKeyHash: string, + rpcUrl: string, + factory: (signer: ethers.Wallet) => Promise + ) { + const { signer } = await this.getEvmSigner(accPublicKeyHash, rpcUrl); + return await factory(signer); + } + private async getSigner(accPublicKeyHash: string): Promise<{ signer: Signer; cleanup: () => void }> { const allAccounts = await this.fetchAccounts(); const acc = allAccounts.find(a => a.publicKeyHash === accPublicKeyHash); @@ -569,4 +714,18 @@ export class Vault { return { signer, cleanup: () => {} }; } } + + private async getEvmSigner(accPublicKeyHash: string, rpcUrl: string): Promise<{ signer: ethers.Wallet }> { + const allAccounts = await this.fetchAccounts(); + const acc = allAccounts.find(a => a.evmPublicKeyHash === accPublicKeyHash); + if (!acc) { + throw new PublicError('Account not found'); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const privateKey = await fetchAndDecryptOne(accPrivKeyStrgKey(accPublicKeyHash), this.passKey); + const signer = new ethers.Wallet(privateKey, provider); + + return { signer }; + } } diff --git a/src/lib/temple/back/vault/storage-keys.ts b/src/lib/temple/back/vault/storage-keys.ts index 999843c26..85483557f 100644 --- a/src/lib/temple/back/vault/storage-keys.ts +++ b/src/lib/temple/back/vault/storage-keys.ts @@ -5,6 +5,7 @@ enum StorageEntity { MigrationLevel = 'migration', Mnemonic = 'mnemonic', AccPrivKey = 'accprivkey', + BtcPkhPrivKeyRecord = 'btcpkhprivkeyrecord', AccPubKey = 'accpubkey', Accounts = 'accounts', Settings = 'settings', @@ -17,6 +18,7 @@ export const mnemonicStrgKey = createStorageKey(StorageEntity.Mnemonic); export const accPrivKeyStrgKey = createDynamicStorageKey(StorageEntity.AccPrivKey); export const accPubKeyStrgKey = createDynamicStorageKey(StorageEntity.AccPubKey); export const accountsStrgKey = createStorageKey(StorageEntity.Accounts); +export const BtcPkhPrivKeyRecord = createStorageKey(StorageEntity.BtcPkhPrivKeyRecord); export const settingsStrgKey = createStorageKey(StorageEntity.Settings); export const legacyMigrationLevelStrgKey = createStorageKey(StorageEntity.LegacyMigrationLevel); diff --git a/src/lib/temple/front/client.ts b/src/lib/temple/front/client.ts index e21c7f8e9..58b2d79e2 100644 --- a/src/lib/temple/front/client.ts +++ b/src/lib/temple/front/client.ts @@ -90,7 +90,7 @@ export const [TempleClientProvider, useTempleClient] = constate(() => { * Aliases */ - const { status, networks: defaultNetworks, accounts, settings } = state; + const { status, networks: defaultNetworks, accounts, btcWalletAddresses, settings } = state; const idle = status === TempleStatus.Idle; const locked = status === TempleStatus.Locked; const ready = status === TempleStatus.Ready; @@ -137,6 +137,13 @@ export const [TempleClientProvider, useTempleClient] = constate(() => { assertResponse(res.type === TempleMessageType.CreateAccountResponse); }, []); + const createNewBtcAddress = useCallback(async () => { + const res = await request({ + type: TempleMessageType.CreateNewBtcAddressRequest + }); + assertResponse(res.type === TempleMessageType.CreateNewBtcAddressResponse); + }, []); + const revealPrivateKey = useCallback(async (accountPublicKeyHash: string, password: string) => { const res = await request({ type: TempleMessageType.RevealPrivateKeyRequest, @@ -266,6 +273,24 @@ export const [TempleClientProvider, useTempleClient] = constate(() => { [] ); + const confirmBtcInternal = useCallback(async (id: string, confirmed: boolean) => { + const res = await request({ + type: TempleMessageType.BtcConfirmationRequest, + id, + confirmed + }); + assertResponse(res.type === TempleMessageType.BtcConfirmationResponse); + }, []); + + const confirmEvmInternal = useCallback(async (id: string, confirmed: boolean) => { + const res = await request({ + type: TempleMessageType.EvmConfirmationRequest, + id, + confirmed + }); + assertResponse(res.type === TempleMessageType.EvmConfirmationResponse); + }, []); + const getDAppPayload = useCallback(async (id: string) => { const res = await request({ type: TempleMessageType.DAppGetPayloadRequest, @@ -275,13 +300,13 @@ export const [TempleClientProvider, useTempleClient] = constate(() => { return res.payload; }, []); - const confirmDAppPermission = useCallback(async (id: string, confirmed: boolean, pkh: string) => { + const confirmDAppPermission = useCallback(async (id: string, confirmed: boolean, pkh: string, evm?: boolean) => { const res = await request({ type: TempleMessageType.DAppPermConfirmationRequest, id, confirmed, accountPublicKeyHash: pkh, - accountPublicKey: confirmed ? await getPublicKey(pkh) : '' + accountPublicKey: confirmed && !evm ? await getPublicKey(pkh) : '' }); assertResponse(res.type === TempleMessageType.DAppPermConfirmationResponse); }, []); @@ -353,6 +378,7 @@ export const [TempleClientProvider, useTempleClient] = constate(() => { customNetworks, networks, accounts, + btcWalletAddresses, settings, idle, locked, @@ -367,6 +393,7 @@ export const [TempleClientProvider, useTempleClient] = constate(() => { unlock, lock, createAccount, + createNewBtcAddress, revealPrivateKey, revealMnemonic, generateSyncPayload, @@ -380,6 +407,9 @@ export const [TempleClientProvider, useTempleClient] = constate(() => { createLedgerAccount, updateSettings, confirmInternal, + confirmBtcInternal, + confirmEvmInternal, + confirmationIdRef, getDAppPayload, confirmDAppPermission, confirmDAppOperation, diff --git a/src/lib/temple/front/ready.ts b/src/lib/temple/front/ready.ts index 875af9c89..c21885383 100644 --- a/src/lib/temple/front/ready.ts +++ b/src/lib/temple/front/ready.ts @@ -32,6 +32,7 @@ export const [ useSetNetworkId, useNetwork, useAllAccounts, + useBtcWalletAddresses, useSetAccountPkh, useAccount, useAccountPkh, @@ -43,6 +44,7 @@ export const [ v => v.setNetworkId, v => v.network, v => v.allAccounts, + v => v.btcWalletAddresses, v => v.setAccountPkh, v => v.account, v => v.accountPkh, @@ -57,6 +59,7 @@ function useReadyTemple() { const { networks: allNetworks, accounts: allAccounts, + btcWalletAddresses, settings, createTaquitoSigner, createTaquitoWallet @@ -146,6 +149,7 @@ function useReadyTemple() { setNetworkId, allAccounts, + btcWalletAddresses, account, accountPkh, setAccountPkh, diff --git a/src/lib/temple/types.ts b/src/lib/temple/types.ts index f0631e2a8..bcf1326f0 100644 --- a/src/lib/temple/types.ts +++ b/src/lib/temple/types.ts @@ -4,6 +4,7 @@ import { TempleDAppMetadata, TempleDAppNetwork } from '@temple-wallet/dapp/dist/ import type { TID } from 'lib/i18n/types'; +import { NonTezosToken } from '../../app/pages/TokenPage/TokenPage'; import { TempleSendPageEventRequest, TempleSendPageEventResponse, @@ -32,6 +33,7 @@ export interface TempleDAppSession { export interface TempleState { status: TempleStatus; accounts: TempleAccount[]; + btcWalletAddresses: string[]; networks: TempleNetwork[]; settings: TempleSettings | null; } @@ -94,6 +96,7 @@ interface TempleAccountBase { type: TempleAccountType; name: string; publicKeyHash: string; + evmPublicKeyHash?: string; hdIndex?: number; derivationPath?: string; derivationType?: DerivationType; @@ -161,13 +164,13 @@ interface TempleConfirmationPayloadBase { sourcePkh: string; } -interface TempleSignConfirmationPayload extends TempleConfirmationPayloadBase { +export interface TempleSignConfirmationPayload extends TempleConfirmationPayloadBase { type: 'sign'; bytes: string; watermark?: string; } -interface TempleOpsConfirmationPayload extends TempleConfirmationPayloadBase { +export interface TempleOpsConfirmationPayload extends TempleConfirmationPayloadBase { type: 'operations'; networkRpc: string; opParams: any[]; @@ -176,7 +179,20 @@ interface TempleOpsConfirmationPayload extends TempleConfirmationPayloadBase { estimates?: Estimate[]; } -export type TempleConfirmationPayload = TempleSignConfirmationPayload | TempleOpsConfirmationPayload; +export interface TempleEvmOpsConfirmationPayload extends TempleConfirmationPayloadBase { + type: 'evm_operations'; + networkRpc: string; +} + +export interface TempleBtcOpsConfirmationPayload extends Omit { + type: 'btc_operations'; +} + +export type TempleConfirmationPayload = + | TempleSignConfirmationPayload + | TempleOpsConfirmationPayload + | TempleEvmOpsConfirmationPayload + | TempleBtcOpsConfirmationPayload; /** * DApp confirmation payloads @@ -198,6 +214,26 @@ interface TempleDAppConnectPayload extends TempleDAppPayloadBase { type: 'connect'; } +interface TempleEvmDAppConnectPayload extends TempleDAppPayloadBase { + type: 'connect_evm'; +} + +interface TempleEvmDAppSwitchNetworkPayload extends TempleDAppPayloadBase { + type: 'switch_evm_network'; +} + +export interface TempleEvmDAppOperationsPayload extends TempleDAppPayloadBase { + type: 'confirm_evm_operations'; + sourcePkh: string; + opParams: any[]; +} + +export interface TempleEvmDAppSignPayload extends TempleDAppPayloadBase { + type: 'sign_evm'; + sourcePkh: string; + opParams: any[]; +} + export interface TempleDAppOperationsPayload extends TempleDAppPayloadBase { type: 'confirm_operations'; sourcePkh: string; @@ -215,7 +251,14 @@ export interface TempleDAppSignPayload extends TempleDAppPayloadBase { preview: any; } -export type TempleDAppPayload = TempleDAppConnectPayload | TempleDAppOperationsPayload | TempleDAppSignPayload; +export type TempleDAppPayload = + | TempleEvmDAppSignPayload + | TempleEvmDAppSwitchNetworkPayload + | TempleEvmDAppOperationsPayload + | TempleEvmDAppConnectPayload + | TempleDAppConnectPayload + | TempleDAppOperationsPayload + | TempleDAppSignPayload; /** * Messages @@ -239,7 +282,9 @@ export enum TempleMessageType { LockRequest = 'TEMPLE_LOCK_REQUEST', LockResponse = 'TEMPLE_LOCK_RESPONSE', CreateAccountRequest = 'TEMPLE_CREATE_ACCOUNT_REQUEST', + CreateNewBtcAddressRequest = 'TEMPLE_CREATE_NEW_BTC_ADDRESS_REQUEST', CreateAccountResponse = 'TEMPLE_CREATE_ACCOUNT_RESPONSE', + CreateNewBtcAddressResponse = 'TEMPLE_CREATE_NEW_BTC_ADDRESS_RESPONSE', RevealPublicKeyRequest = 'TEMPLE_REVEAL_PUBLIC_KEY_REQUEST', RevealPublicKeyResponse = 'TEMPLE_REVEAL_PUBLIC_KEY_RESPONSE', RevealPrivateKeyRequest = 'TEMPLE_REVEAL_PRIVATE_KEY_REQUEST', @@ -268,10 +313,18 @@ export enum TempleMessageType { UpdateSettingsResponse = 'TEMPLE_UPDATE_SETTINGS_RESPONSE', OperationsRequest = 'TEMPLE_OPERATIONS_REQUEST', OperationsResponse = 'TEMPLE_OPERATIONS_RESPONSE', + EvmOperationsRequest = 'TEMPLE_EVM_OPERATIONS_REQUEST', + BtcOperationsRequest = 'TEMPLE_BTC_OPERATIONS_REQUEST', + EvmOperationsResponse = 'TEMPLE_EVM_OPERATIONS_RESPONSE', + BtcOperationsResponse = 'TEMPLE_BTC_OPERATIONS_RESPONSE', SignRequest = 'TEMPLE_SIGN_REQUEST', SignResponse = 'TEMPLE_SIGN_RESPONSE', ConfirmationRequest = 'TEMPLE_CONFIRMATION_REQUEST', + EvmConfirmationRequest = 'TEMPLE_EVM_CONFIRMATION_REQUEST', + BtcConfirmationRequest = 'TEMPLE_BTC_CONFIRMATION_REQUEST', ConfirmationResponse = 'TEMPLE_CONFIRMATION_RESPONSE', + EvmConfirmationResponse = 'TEMPLE_EVM_CONFIRMATION_RESPONSE', + BtcConfirmationResponse = 'TEMPLE_BTC_CONFIRMATION_RESPONSE', PageRequest = 'TEMPLE_PAGE_REQUEST', PageResponse = 'TEMPLE_PAGE_RESPONSE', DAppGetPayloadRequest = 'TEMPLE_DAPP_GET_PAYLOAD_REQUEST', @@ -305,6 +358,7 @@ export type TempleRequest = | TempleUnlockRequest | TempleLockRequest | TempleCreateAccountRequest + | TempleCreateNewBtcAddressRequest | TempleRevealPublicKeyRequest | TempleRevealPrivateKeyRequest | TempleRevealMnemonicRequest @@ -328,7 +382,11 @@ export type TempleRequest = | TempleUpdateSettingsRequest | TempleGetAllDAppSessionsRequest | TempleRemoveDAppSessionRequest + | TempleEvmOperationsRequest + | TempleBtcOperationsRequest | TempleSendTrackEventRequest + | TempleEvmConfirmationRequest + | TempleBtcConfirmationRequest | TempleSendPageEventRequest; export type TempleResponse = @@ -338,6 +396,7 @@ export type TempleResponse = | TempleUnlockResponse | TempleLockResponse | TempleCreateAccountResponse + | TempleCreateNewBtcAddressResponse | TempleRevealPublicKeyResponse | TempleRevealPrivateKeyResponse | TempleRevealMnemonicResponse @@ -361,7 +420,11 @@ export type TempleResponse = | TempleUpdateSettingsResponse | TempleGetAllDAppSessionsResponse | TempleRemoveDAppSessionResponse + | TempleEvmOperationsResponse + | TempleBtcOperationsResponse | TempleSendTrackEventResponse + | TempleEvmConfirmationResponse + | TempleBtcConfirmationResponse | TempleSendPageEventResponse; export interface TempleMessageBase { @@ -437,10 +500,18 @@ interface TempleCreateAccountRequest extends TempleMessageBase { name?: string; } +interface TempleCreateNewBtcAddressRequest extends TempleMessageBase { + type: TempleMessageType.CreateNewBtcAddressRequest; +} + interface TempleCreateAccountResponse extends TempleMessageBase { type: TempleMessageType.CreateAccountResponse; } +interface TempleCreateNewBtcAddressResponse extends TempleMessageBase { + type: TempleMessageType.CreateNewBtcAddressResponse; +} + interface TempleRevealPublicKeyRequest extends TempleMessageBase { type: TempleMessageType.RevealPublicKeyRequest; accountPublicKeyHash: string; @@ -583,11 +654,38 @@ interface TempleOperationsRequest extends TempleMessageBase { opParams: any[]; } +interface TempleEvmOperationsRequest extends TempleMessageBase { + type: TempleMessageType.EvmOperationsRequest; + id: string; + sourcePkh: string; + networkRpc: string; + token: NonTezosToken; + toAddress: string; + amount: string; +} + +interface TempleBtcOperationsRequest extends TempleMessageBase { + type: TempleMessageType.BtcOperationsRequest; + id: string; + toAddress: string; + amount: string; +} + interface TempleOperationsResponse extends TempleMessageBase { type: TempleMessageType.OperationsResponse; opHash: string; } +interface TempleEvmOperationsResponse extends TempleMessageBase { + type: TempleMessageType.EvmOperationsResponse; + txHash: string; +} + +interface TempleBtcOperationsResponse extends TempleMessageBase { + type: TempleMessageType.BtcOperationsResponse; + txId: string; +} + interface TempleSignRequest extends TempleMessageBase { type: TempleMessageType.SignRequest; id: string; @@ -609,14 +707,37 @@ interface TempleConfirmationRequest extends TempleMessageBase { modifiedStorageLimit?: number; } +interface TempleEvmConfirmationRequest extends TempleMessageBase { + type: TempleMessageType.EvmConfirmationRequest; + id: string; + confirmed: boolean; +} + +interface TempleBtcConfirmationRequest extends TempleMessageBase { + type: TempleMessageType.BtcConfirmationRequest; + id: string; + confirmed: boolean; +} + interface TempleConfirmationResponse extends TempleMessageBase { type: TempleMessageType.ConfirmationResponse; } +interface TempleEvmConfirmationResponse extends TempleMessageBase { + type: TempleMessageType.EvmConfirmationResponse; +} + +interface TempleBtcConfirmationResponse extends TempleMessageBase { + type: TempleMessageType.BtcConfirmationResponse; +} + interface TemplePageRequest extends TempleMessageBase { type: TempleMessageType.PageRequest; origin: string; payload: any; + evm?: boolean; + chainId?: string; + sourcePkh?: string; beacon?: boolean; encrypted?: boolean; } diff --git a/src/newChains/bitcoin.ts b/src/newChains/bitcoin.ts new file mode 100644 index 000000000..27198b1e8 --- /dev/null +++ b/src/newChains/bitcoin.ts @@ -0,0 +1,158 @@ +import ecc from '@bitcoinerlab/secp256k1'; +import BIP32Factory, { BIP32Interface } from 'bip32'; +import * as Bip39 from 'bip39'; +import * as Bitcoin from 'bitcoinjs-lib'; + +import { btcWalletAddressesUpdated } from '../lib/temple/back/store'; + +const testnet = Bitcoin.networks.testnet; + +// SegWit +const getBitcoinAddress = (node: BIP32Interface, network: Bitcoin.networks.Network) => + Bitcoin.payments.p2wpkh({ pubkey: node.publicKey, network }).address; + +export const getBitcoinXPubFromMnemonic = (mnemonic: string) => { + const seed = Bip39.mnemonicToSeedSync(mnemonic); + const bip32 = BIP32Factory(ecc); + + return bip32.fromSeed(seed, testnet); +}; + +export const getNextBitcoinHDWallet = (hdMaster: BIP32Interface, index: number) => { + const keyPair = hdMaster.derivePath(`m/84'/0'/0'/0/${index}`); + + return { address: getBitcoinAddress(keyPair, testnet)!, privateKey: keyPair.toBase58() }; +}; + +interface UnspentOutput { + txid: string; + vout: number; + status: { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; + }; + value: number; +} + +interface UtxoResponse { + address: string; + utxos: UnspentOutput[]; +} + +const OUTPUTS_COUNT = 2; + +export const sendBitcoin = async ( + receiverAddress: string, + amount: string, + addressKeyPairsRecord: Record, + createNewBtcAddress: () => Promise +): Promise => { + const userAddressesConcat = Object.keys(addressKeyPairsRecord).slice(-7).join(';'); + + const satoshiToSend = Number((Number(amount) * 100000000).toFixed()); + console.log(satoshiToSend, 'amount'); + + const response = await fetch(`http://localhost:3000/api/bitcoin-utxos?addresses=${userAddressesConcat}`); + const utxoResponse: UtxoResponse[] = await response.json(); + console.log(utxoResponse, 'utxoResponse'); + + const allUtxos = utxoResponse.flatMap(({ utxos }) => utxos); + + const totalAmountAvailable = allUtxos.reduce((acc, utxo) => (acc += utxo.value), 0); + console.log(totalAmountAvailable, 'totalAmount'); + + const psbt = new Bitcoin.Psbt({ network: testnet }); + const bip32 = BIP32Factory(ecc); + + console.log(psbt, 'psbt1'); + + // Add inputs + utxoResponse.forEach(({ address, utxos }) => { + utxos.forEach(utxo => { + const signer = bip32.fromBase58(addressKeyPairsRecord[address], testnet); + const payment = Bitcoin.payments.p2wpkh({ pubkey: signer.publicKey, network: testnet }); + + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + script: payment.output!, + value: utxo.value + } + }); + }); + }); + + const transactionSize = psbt.inputCount * 146 + OUTPUTS_COUNT * 34 + 10 - psbt.inputCount; + + const fee = transactionSize; // for testnet + console.log(fee, 'fee'); + + const change = totalAmountAvailable - satoshiToSend - fee; + + console.log(change, 'change'); + + // Check if we have enough funds to cover the transaction + if (change < 0) { + throw new Error('Balance is too low for this transaction'); + } + + console.log(psbt.inputCount, 'inputsCount'); + + // set the receiving address and the amount to send + psbt.addOutput({ address: receiverAddress, value: satoshiToSend }); + + // set the change address and the amount to send + if (change > 0) { + const allAddresses = await createNewBtcAddress(); + btcWalletAddressesUpdated(allAddresses); + const changeAddress = allAddresses[allAddresses.length - 1]; + console.log(changeAddress, 'changeAddress'); + psbt.addOutput({ address: changeAddress, value: change }); + } + + let currentInputIndex = 0; + + // Sign the inputs + utxoResponse.forEach(({ address, utxos }) => { + utxos.forEach((_, index) => { + console.log('sign', index); + const signer = bip32.fromBase58(addressKeyPairsRecord[address], testnet); + psbt.signInput(currentInputIndex, signer); + currentInputIndex++; + }); + }); + + console.log('signed normally'); + + console.log(psbt, 'psbt2'); + + psbt.finalizeAllInputs(); + + // serialized transaction + const txHex = psbt.extractTransaction().toHex(); + console.log(txHex, 'txHex'); + + const txId = await broadcastTransaction(txHex); + console.log(txId, 'txId'); + + return txId; +}; + +const broadcastTransaction = async (txHex: string) => { + try { + const response = await fetch(`http://localhost:3000/api/bitcoin-broadcast-tx?txHex=${txHex}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + const data = await response.json(); + + return data.tx.hash; + } catch (error) { + console.error('Error broadcasting transaction:', error); + } +}; diff --git a/src/newChains/erc20abi.json b/src/newChains/erc20abi.json new file mode 100644 index 000000000..405d6b364 --- /dev/null +++ b/src/newChains/erc20abi.json @@ -0,0 +1,222 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] diff --git a/src/newChains/temple-web3-provider.ts b/src/newChains/temple-web3-provider.ts new file mode 100644 index 000000000..73ac047a5 --- /dev/null +++ b/src/newChains/temple-web3-provider.ts @@ -0,0 +1,121 @@ +import { ethers } from 'ethers'; +import { EventEmitter } from 'events'; + +const SEPOLIA_RPC_URL = 'https://ethereum-sepolia.publicnode.com'; +const SEPOLIA_CHAIN_ID = '0xaa36a7'; + +export interface EvmRequestArguments { + /** The RPC method to request. */ + method: string; + + /** The params of the RPC method, if any. */ + params?: unknown[] | Record; +} + +export class TempleWeb3Provider extends EventEmitter { + private _BaseProvider: ethers.JsonRpcProvider; + private _isConnected: boolean; + private _accounts: string[]; + private _chainId: string; + + private _handleConnect(args: EvmRequestArguments) { + window.dispatchEvent( + new CustomEvent('passToBackground', { + detail: { args, origin: window.origin, chainId: this._chainId } + }) + ); + + return new Promise(resolve => { + const listener = (evt: Event) => { + window.removeEventListener('responseFromBackground', listener); + if (!this._isConnected) { + this._isConnected = true; + this.emit('connect', { chainId: this._chainId }); + } + //@ts-ignore + this._accounts = evt.detail; + //@ts-ignore + console.log('inpage got response from bg', evt.detail); + //@ts-ignore + resolve(evt.detail); + }; + window.addEventListener('responseFromBackground', listener); + }); + } + + private _handleSign(args: EvmRequestArguments) { + window.dispatchEvent( + new CustomEvent('passToBackground', { + detail: { args, origin: window.origin, chainId: this._chainId, sourcePkh: this._accounts[0] } + }) + ); + + return new Promise(resolve => { + const listener = (evt: Event) => { + window.removeEventListener('responseFromBackground', listener); + //@ts-ignore + console.log('inpage got response from bg', evt.detail); + //@ts-ignore + resolve(evt.detail); + }; + window.addEventListener('responseFromBackground', listener); + }); + } + + private _handleChainChange(args: EvmRequestArguments) { + window.dispatchEvent( + new CustomEvent('passToBackground', { + detail: { args, origin: window.origin } + }) + ); + + return new Promise(resolve => { + const listener = (evt: Event) => { + window.removeEventListener('responseFromBackground', listener); + //@ts-ignore + console.log('inpage got response from bg', evt.detail); + //@ts-ignore + this._chainId = evt.detail.chainId; + //@ts-ignore + this._BaseProvider = new ethers.JsonRpcProvider(evt.detail.rpcUrl); + resolve(null); + }; + window.addEventListener('responseFromBackground', listener); + }); + } + + constructor() { + super(); + this._BaseProvider = new ethers.JsonRpcProvider(SEPOLIA_RPC_URL); + this._isConnected = false; + this._accounts = []; + this._chainId = SEPOLIA_CHAIN_ID; + + this.request = this.request.bind(this); + } + + async enable() { + return this._handleConnect({ method: 'eth_requestAccounts' }); + } + + async request(args: EvmRequestArguments) { + switch (args.method) { + case 'eth_accounts': + return this._accounts; + case 'eth_requestAccounts': + if (this._accounts.length === 0) { + return this._handleConnect(args); + } else { + return this._accounts; + } + case 'wallet_switchEthereumChain': + return this._handleChainChange(args); + case 'eth_sendTransaction': + case 'eth_signTypedData_v4': + return this._handleSign(args); + default: + // @ts-ignore + return this._BaseProvider.send(args.method, args.params); + } + } +} diff --git a/webpack.config.ts b/webpack.config.ts index fadd7b31c..e2e6d990c 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -10,7 +10,6 @@ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import * as Path from 'path'; -import ExtensionReloaderBadlyTyped, { ExtensionReloader as ExtensionReloaderType } from 'webpack-ext-reloader'; import ExtensionReloaderMV3BadlyTyped, { ExtensionReloader as ExtensionReloaderMV3Type } from 'webpack-ext-reloader-mv3'; @@ -32,7 +31,6 @@ import { buildManifest } from './webpack/manifest'; import { PATHS } from './webpack/paths'; import { isTruthy } from './webpack/utils'; -const ExtensionReloader = ExtensionReloaderBadlyTyped as ExtensionReloaderType; const ExtensionReloaderMV3 = ExtensionReloaderMV3BadlyTyped as ExtensionReloaderMV3Type; const PAGES_NAMES = ['popup', 'fullpage', 'confirm', 'options']; @@ -42,7 +40,7 @@ const HTML_TEMPLATES = PAGES_NAMES.map(name => { return { name, filename, path }; }); -const CONTENT_SCRIPTS = ['contentScript']; +const CONTENT_SCRIPTS = ['contentScript', 'inpage']; if (BACKGROUND_IS_WORKER) CONTENT_SCRIPTS.push('keepBackgroundWorkerAlive'); const mainConfig = (() => { @@ -159,7 +157,8 @@ const scriptsConfig = (() => { const config = buildBaseConfig(); config.entry = { - contentScript: Path.join(PATHS.SOURCE, 'contentScript.ts') + contentScript: Path.join(PATHS.SOURCE, 'contentScript.ts'), + inpage: Path.join(PATHS.SOURCE, 'inpage.ts') }; if (BACKGROUND_IS_WORKER) @@ -182,18 +181,7 @@ const scriptsConfig = (() => { cleanOnceBeforeBuildPatterns: ['scripts/**'], cleanStaleWebpackAssets: false, verbose: false - }), - - /* Page reloading in development mode */ - DEVELOPMENT_ENV && - new ExtensionReloader({ - port: RELOADER_PORTS.SCRIPTS, - reloadPage: true, - entries: { - background: '', - contentScript: CONTENT_SCRIPTS - } - }) + }) ].filter(isTruthy) ); diff --git a/webpack/manifest.ts b/webpack/manifest.ts index 20ce4849b..d382b348e 100644 --- a/webpack/manifest.ts +++ b/webpack/manifest.ts @@ -88,7 +88,7 @@ const buildManifestV2 = (vendor: string): Manifest.WebExtensionManifest => { const AUTHOR_URL = 'https://madfish.solutions'; -const PERMISSIONS = ['storage', 'unlimitedStorage', 'clipboardWrite', 'activeTab']; +const PERMISSIONS = ['storage', 'unlimitedStorage', 'clipboardWrite', 'activeTab', 'scripting']; const HOST_PERMISSIONS: string[] = ['http://localhost:8732/']; @@ -152,6 +152,14 @@ const buildManifestCommons = (vendor: string): Omit=12.12.47", "@types/node@>=13.7.0": version "20.4.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" @@ -3446,6 +3491,11 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== +"@types/uuid@^9.0.7": + version "9.0.7" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8" + integrity sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g== + "@types/w3c-web-hid@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@types/w3c-web-hid/-/w3c-web-hid-1.0.3.tgz#e08587a7d737f8654ea6bc0a88689ce5d3ce2d19" @@ -4122,6 +4172,11 @@ address@^1.1.2: resolved "https://registry.yarnpkg.com/address/-/address-1.2.1.tgz#25bb61095b7522d65b357baa11bc05492d4c8acd" integrity sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA== +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + ag-channel@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ag-channel/-/ag-channel-5.0.0.tgz#c2c00dfbe372ae43e0466ec89e29aca1bbb2fb3e" @@ -4697,6 +4752,11 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -4712,6 +4772,21 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== +bip174@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.1.tgz#ef3e968cf76de234a546962bcf572cc150982f9f" + integrity sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ== + +bip32@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-4.0.0.tgz#7fac3c05072188d2d355a4d6596b37188f06aa2f" + integrity sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ== + dependencies: + "@noble/hashes" "^1.2.0" + "@scure/base" "^1.1.1" + typeforce "^1.11.5" + wif "^2.0.6" + bip39@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.2.tgz#2baf42ff3071fc9ddd5103de92e8f80d9257ee32" @@ -4732,6 +4807,18 @@ bip39@3.0.4: pbkdf2 "^3.0.9" randombytes "^2.0.1" +bitcoinjs-lib@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz#3b03509ae7ddd80a440f10fc38c4a97f0a028d8c" + integrity sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ== + dependencies: + "@noble/hashes" "^1.2.0" + bech32 "^2.0.0" + bip174 "^2.1.1" + bs58check "^3.0.1" + typeforce "^1.11.3" + varuint-bitcoin "^1.1.2" + bl@^1.2.1: version "1.2.3" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" @@ -4905,7 +4992,7 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" -bs58check@2.1.2, bs58check@^2.1.2: +bs58check@2.1.2, bs58check@<3.0.0, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== @@ -6526,6 +6613,19 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +ethers@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.8.1.tgz#ee2a1a39b5f62a13678f90ccd879175391d0a2b4" + integrity sha512-iEKm6zox5h1lDn6scuRWdIdFJUCGg3+/aQWu0F4K0GVyEZiktFkqrJbRjTn1FlYEPz7RKA707D6g5Kdk6j7Ljg== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.5.0" + eventemitter3@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" @@ -12142,6 +12242,11 @@ typedarray-to-buffer@^4.0.0: resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-4.0.0.tgz#cdd2933c61dd3f5f02eda5d012d441f95bfeb50a" integrity sha512-6dOYeZfS3O9RtRD1caom0sMxgK59b27+IwoNy8RDPsmslSGOyU+mpTamlaIW7aNKi90ZQZ9DFaZL3YRoiSCULQ== +typeforce@^1.11.3, typeforce@^1.11.5: + version "1.18.0" + resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" + integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== + typescript@4.5.5: version "4.5.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" @@ -12336,6 +12441,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -12363,6 +12473,13 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +varuint-bitcoin@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz#e76c138249d06138b480d4c5b40ef53693e24e92" + integrity sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw== + dependencies: + safe-buffer "^5.1.1" + vinyl-buffer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/vinyl-buffer/-/vinyl-buffer-1.0.1.tgz#96c1a3479b8c5392542c612029013b5b27f88bbf" @@ -12645,6 +12762,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wif@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + integrity sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ== + dependencies: + bs58check "<3.0.0" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -12693,7 +12817,7 @@ write@^1.0.3: dependencies: mkdirp "^0.5.1" -ws@7.4.6, ws@^7.4.6, ws@^7.5.1, ws@^7.5.2, ws@^8.4.2, ws@^8.6.0, ws@^8.9.0: +ws@7.4.6, ws@8.5.0, ws@^7.4.6, ws@^7.5.1, ws@^7.5.2, ws@^8.4.2, ws@^8.6.0, ws@^8.9.0: version "7.4.6" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==