diff --git a/.well-known/walletconnect.txt b/.well-known/walletconnect.txt index f7386465d..6cd27d9de 100644 --- a/.well-known/walletconnect.txt +++ b/.well-known/walletconnect.txt @@ -1 +1 @@ -b1cd76a2-edfb-437e-9989-2e611c9f9d32=0b3866d7e128b47bd13f304a2e342497615e75db9bb597f35043c80a40264011 \ No newline at end of file +9d0cc95e-718d-4cf7-8000-5f88022fa568=0b3866d7e128b47bd13f304a2e342497615e75db9bb597f35043c80a40264011 \ No newline at end of file diff --git a/src/background/bgPopupHandler.ts b/src/background/bgPopupHandler.ts index 0b6b678ea..4b67a9f3c 100644 --- a/src/background/bgPopupHandler.ts +++ b/src/background/bgPopupHandler.ts @@ -29,7 +29,7 @@ export const getAeppUrl = (v: any) => new URL(v.connection.port.sender.url); export const openPopup = async ( popupType: PopupType, aepp: string | object, - params: Partial = {}, + popupProps: Partial = {}, ) => { const id = uuid(); const { href, protocol, host } = (typeof aepp === 'object') ? getAeppUrl(aepp) : new URL(aepp); @@ -65,16 +65,13 @@ export const openPopup = async ( popups[id] = { id, props: { + ...popupProps, app: { url: href, name, protocol, host, }, - message: params.message, - tx: params.tx, - txBase64: params.txBase64, - data: params.data, }, }; return popups[id]; diff --git a/src/background/index.ts b/src/background/index.ts index 931f2971d..019ba6bac 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,4 +1,4 @@ -import { PopupActionType } from '@/types'; +import { IPopupMessageData } from '@/types'; import { openPopup, removePopup, getPopup } from './bgPopupHandler'; import { updateDynamicRules } from './redirectRule'; @@ -25,32 +25,28 @@ import { updateDynamicRules } from './redirectRule'; }); })(); -export type PopupMessageData = { - target?: 'background' | 'offscreen'; - method?: 'openPopup' | 'removePopup' | 'getPopup'; - type?: PopupActionType; - uuid?: string; - params?: any; - payload?: any; -}; - /** * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage#sending_an_asynchronous_response_using_sendresponse */ -function handleMessage(msg: PopupMessageData, _: any, sendResponse: Function) { +function handleMessage(msg: IPopupMessageData, _: any, sendResponse: Function) { if (msg.target === 'background') { - const { popupType, aepp, params } = msg.params; + const { + aepp, + id, + popupProps, + popupType, + } = msg.params!; switch (msg.method) { case 'openPopup': - openPopup(popupType, aepp, params).then((popupConfig) => { + openPopup(popupType!, aepp!, popupProps).then((popupConfig) => { sendResponse(popupConfig); }); return true; case 'removePopup': - sendResponse(removePopup(msg.params.id)); + sendResponse(removePopup(id!)); return false; case 'getPopup': - sendResponse(getPopup(msg.params.id)); + sendResponse(getPopup(id!)); return false; default: break; @@ -58,7 +54,7 @@ function handleMessage(msg: PopupMessageData, _: any, sendResponse: Function) { } // forward messages to the offscreen page - browser.runtime.sendMessage({ + browser.runtime.sendMessage({ ...msg, target: 'offscreen', }); diff --git a/src/composables/walletConnect.ts b/src/composables/walletConnect.ts index 58e5f5513..c7f33af50 100644 --- a/src/composables/walletConnect.ts +++ b/src/composables/walletConnect.ts @@ -46,6 +46,9 @@ let web3wallet: Awaited> | null; const wcSession = useStorageRef( null, STORAGE_KEYS.walletConnectSession, + { + backgroundSync: true, + }, ); const wcState = reactive({ @@ -58,7 +61,7 @@ const wcState = reactive({ /** * TODO add description */ -export function useWalletConnect() { +export function useWalletConnect({ offscreen } = { offscreen: false }) { const { activeAccount, accountsGroupedByProtocol, getLastActiveProtocolAccount } = useAccounts(); const { activeNetwork, networks } = useNetworks(); const { openDefaultModal } = useModals(); @@ -72,9 +75,9 @@ export function useWalletConnect() { [key in SupportedRequestMethod]: (p: any) => Promise }> = { eth_sendTransaction: async (params: SendTransactionParams) => { + const { url, name } = wcSession.value?.peer.metadata! || {}; const senderId = toChecksumAddress(params.from); const recipientId = toChecksumAddress(params.to); - const { url, name } = wcSession.value?.peer.metadata! || {}; const isCoinTransfer = !!params.value; // `value` is present only when sending ETH const tag = (params.data) ? Tag.ContractCallTx : Tag.SpendTx; const modalProps: IModalProps = { @@ -313,23 +316,32 @@ export function useWalletConnect() { if (!composableInitialized) { composableInitialized = true; - // Restore open WC session after refreshing the tab or extension. // As the session is not important immediately after opening the app we are delaying it // until other more important features are ready. setTimeout(async () => { - if (wcSession.value) { - web3wallet = await initWeb3wallet(); - const sessions = web3wallet.getActiveSessions(); - const restoredTopic = Object.values(sessions)?.[0]?.topic; - - // If restored session is different than the currently open we need to close session. - if (wcSession.value?.topic !== restoredTopic) { - disconnect(); - } else if (restoredTopic) { - monitorActiveSessionEvents(); - monitorActiveAccountAndNetwork(); + // Try to restore open WC session: + // - after refreshing the tab or extension (only once), + // - in the extension offscreen when the new session state is detected (constant monitoring). + watch(wcSession, async (session, oldSession) => { + if (session) { + if (!web3wallet) { + web3wallet = await initWeb3wallet(); + } + + const sessions = web3wallet.getActiveSessions(); + const activeTopic = Object.values(sessions)?.[0]?.topic; + + if (!oldSession && activeTopic && activeTopic === session.topic) { + monitorActiveSessionEvents(); + + if (!offscreen) { + monitorActiveAccountAndNetwork(); + } + } else { + disconnect(); + } } - } + }, { deep: true, immediate: true, once: !offscreen }); }, 1000); } diff --git a/src/constants/common.ts b/src/constants/common.ts index 5fb60c6f6..84bc7d02d 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -361,6 +361,15 @@ export const POPUP_ACTIONS = { reject: 'reject', } as const; +export const POPUP_METHODS = { + openPopup: 'openPopup', + removePopup: 'removePopup', + getPopup: 'getPopup', + reload: 'reload', + paste: 'paste', + checkHasAccount: 'checkHasAccount', // TODO check if still used +} as const; + export const AIRGAP_SIGNED_TRANSACTION_MESSAGE_TYPE = 'airgap-signed-transaction'; export const PERMISSION_DEFAULTS: IPermission = { diff --git a/src/manifest.json b/src/manifest.json index 4906c6a67..12e4d64a1 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -3,7 +3,7 @@ "description": "Superhero Wallet is a multi-blockchain wallet to manage crypto assets and navigate the web3 and DeFi space. Powered by æternity.", "manifest_version": 3, "content_security_policy": { - "extension_pages": "default-src 'self'; script-src 'self'; connect-src *; font-src * data:; img-src * data:; style-src-elem * 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " + "extension_pages": "default-src 'self'; script-src 'self'; connect-src *; font-src * data:; frame-src *; img-src * data:; style-src-elem * 'unsafe-inline'; style-src 'self' 'unsafe-inline';" }, "permissions": [ "storage", diff --git a/src/offscreen/offscreen.ts b/src/offscreen/offscreen.ts index 988369507..77616f34a 100644 --- a/src/offscreen/offscreen.ts +++ b/src/offscreen/offscreen.ts @@ -1,6 +1,8 @@ import '@/lib/initPolyfills'; import '@/protocols/registerAdapters'; -import { IS_FIREFOX, UNFINISHED_FEATURES } from '@/constants/environment'; +import { IPopupMessageData } from '@/types'; +import { IS_FIREFOX, POPUP_METHODS, UNFINISHED_FEATURES } from '@/constants'; +import { useWalletConnect } from '@/composables'; import * as wallet from './wallet'; import { useAccounts } from '../composables/accounts'; import { updateDynamicRules } from '../background/redirectRule'; @@ -11,21 +13,19 @@ if (IS_FIREFOX) { browser.runtime.onInstalled.addListener(updateDynamicRules); } -browser.runtime.onMessage.addListener(async (msg: any) => { - const { method } = msg; - - if (method === 'reload') { +browser.runtime.onMessage.addListener(async ({ method }: IPopupMessageData) => { + if (method === POPUP_METHODS.reload) { wallet.disconnect(); window.location.reload(); return null; } - if (method === 'checkHasAccount') { + if (method === POPUP_METHODS.checkHasAccount) { const { isLoggedIn } = useAccounts(); return isLoggedIn.value; } - if (UNFINISHED_FEATURES && method === 'paste') { + if (UNFINISHED_FEATURES && method === POPUP_METHODS.paste) { let result = ''; const textarea = document.createElement('textarea'); document.body.appendChild(textarea); @@ -41,3 +41,6 @@ browser.runtime.onMessage.addListener(async (msg: any) => { }); wallet.init(); + +// Initialize the WalletConnect state monitoring to allow opening the confirmation popup windows. +useWalletConnect({ offscreen: true }); diff --git a/src/offscreen/popupHandler.ts b/src/offscreen/popupHandler.ts index 266c52da5..079cba811 100644 --- a/src/offscreen/popupHandler.ts +++ b/src/offscreen/popupHandler.ts @@ -4,6 +4,7 @@ import type { IPopupProps, PopupType, } from '@/types'; +import { POPUP_METHODS } from '@/constants'; import { executeOrSendMessageToBackground } from './utils'; interface IPopupConfig { @@ -16,30 +17,31 @@ const popups: Dictionary = {}; export const openPopup = async ( popupType: PopupType, aepp: string | object, - params: Partial = {}, + popupProps: Partial = {}, ) => executeOrSendMessageToBackground( - 'openPopup', + POPUP_METHODS.openPopup, { + popupProps, popupType, aepp, - params, }, -).then((popupConfig) => new Promise((resolve, reject) => { - const popupWithActions = { - ...popupConfig, - actions: { - resolve, - reject, - }, - }; - const { id } = popupWithActions; - popups[id] = popupWithActions; - return popupWithActions; -})); +) + .then((popupConfig) => new Promise((resolve, reject) => { + const popupWithActions = { + ...popupConfig, + actions: { + resolve, + reject, + }, + }; + const { id } = popupWithActions; + popups[id] = popupWithActions; + return popupWithActions; + })); export const removePopup = async (id: string) => { delete popups[id]; - executeOrSendMessageToBackground('removePopup', { id }); + executeOrSendMessageToBackground(POPUP_METHODS.removePopup, { id }); }; export const getPopup = (id: string) => popups[id]; diff --git a/src/offscreen/utils.ts b/src/offscreen/utils.ts index 4663771f4..bf18888ab 100644 --- a/src/offscreen/utils.ts +++ b/src/offscreen/utils.ts @@ -1,7 +1,7 @@ -import { CONNECTION_TYPES, IS_FIREFOX } from '@/constants'; import type { Runtime } from 'webextension-polyfill'; +import type { IPopupMessageData, PopupMethod } from '@/types'; +import { CONNECTION_TYPES, IS_FIREFOX, POPUP_METHODS } from '@/constants'; import { openPopup, removePopup, getPopup } from '@/background/bgPopupHandler'; -import { PopupMessageData } from '@/background'; import { getCleanModalOptions } from '@/utils'; export const detectConnectionType = (port: Runtime.Port) => { @@ -22,21 +22,24 @@ export const detectConnectionType = (port: Runtime.Port) => { * because we "are" on the background page * instead call the function directly from bgPopupHandler.ts */ -export async function executeOrSendMessageToBackground(method: PopupMessageData['method'], params: PopupMessageData['params']) { +export async function executeOrSendMessageToBackground( + method: PopupMethod, + params: Required['params'], +) { if (IS_FIREFOX) { switch (method) { - case 'openPopup': - return openPopup(params.popupType, params.aepp, params.params); - case 'removePopup': - return removePopup(params.id); - case 'getPopup': - return getPopup(params.id); + case POPUP_METHODS.openPopup: + return openPopup(params.popupType!, params.aepp!, params.popupProps); + case POPUP_METHODS.removePopup: + return removePopup(params.id!); + case POPUP_METHODS.getPopup: + return getPopup(params.id!); default: return null; } } const cleanParams = getCleanModalOptions(params); - return browser.runtime.sendMessage({ + return browser.runtime.sendMessage({ target: 'background', method, params: cleanParams, diff --git a/src/offscreen/wallet.ts b/src/offscreen/wallet.ts index 187490b0e..50c69b4c4 100644 --- a/src/offscreen/wallet.ts +++ b/src/offscreen/wallet.ts @@ -2,8 +2,8 @@ import { watch } from 'vue'; import { isEqual } from 'lodash-es'; import type { Runtime } from 'webextension-polyfill'; import { BrowserRuntimeConnection } from '@aeternity/aepp-sdk'; +import type { IPopupMessageData } from '@/types'; import { CONNECTION_TYPES, POPUP_ACTIONS } from '@/constants'; -import { PopupMessageData } from '@/background'; import { useAccounts, useAeSdk, useNetworks } from '@/composables'; import { removePopup, getPopup } from './popupHandler'; import { detectConnectionType } from './utils'; @@ -37,7 +37,7 @@ export async function init() { switch (detectConnectionType(port as Runtime.Port)) { case CONNECTION_TYPES.POPUP: { const id = new URL(port?.sender?.url!).searchParams.get('id'); - port.onMessage.addListener(async (msg: PopupMessageData) => { + port.onMessage.addListener(async (msg: IPopupMessageData) => { const popup = getPopup(id!); if (msg.type === POPUP_ACTIONS.getProps) { diff --git a/src/popup/components/Modals/ConfirmTransactionSign.vue b/src/popup/components/Modals/ConfirmTransactionSign.vue index 91fdc74b8..e6369b28f 100644 --- a/src/popup/components/Modals/ConfirmTransactionSign.vue +++ b/src/popup/components/Modals/ConfirmTransactionSign.vue @@ -284,7 +284,7 @@ export default defineComponent({ const activeAccount = getLastActiveProtocolAccount(protocol); const transaction = ref({ protocol, - tx: popupProps.value?.tx, + tx: popupProps.value?.tx || {}, } as ITransaction); const { diff --git a/src/popup/components/NameItem.vue b/src/popup/components/NameItem.vue index 9fc5ed00b..5d055e857 100644 --- a/src/popup/components/NameItem.vue +++ b/src/popup/components/NameItem.vue @@ -155,12 +155,13 @@ import { watch, } from 'vue'; import { useI18n } from 'vue-i18n'; -import { IName } from '@/types'; +import { IName, IPopupMessageData } from '@/types'; import { Clipboard } from '@capacitor/clipboard'; import { IS_EXTENSION, IS_MOBILE_APP, MODAL_CONFIRM, + POPUP_METHODS, UNFINISHED_FEATURES, } from '@/constants'; import Logger from '@/lib/logger'; @@ -252,7 +253,9 @@ export default defineComponent({ text = value; } } else if (IS_EXTENSION) { - text = await browser!.runtime.sendMessage({ method: 'paste' }); + text = await browser!.runtime.sendMessage({ + method: POPUP_METHODS.paste, + }); } else { try { text = await navigator.clipboard.readText(); diff --git a/src/types/index.ts b/src/types/index.ts index 1da0c5361..53d801ac8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,15 +12,16 @@ import { } from '@aeternity/aepp-sdk'; import type { Animation } from '@ionic/vue'; import { + ACCOUNT_TYPES, ALLOWED_ICON_STATUSES, + ASSET_TYPES, INPUT_MESSAGE_STATUSES, NOTIFICATION_TYPES, POPUP_ACTIONS, + POPUP_METHODS, POPUP_TYPES, - ASSET_TYPES, STORAGE_KEYS, TRANSFER_SEND_STEPS, - ACCOUNT_TYPES, } from '@/constants'; import type { CoinGeckoMarketResponse } from '@/lib/CoinGecko'; import type { RejectedByUserError } from '@/lib/errors'; @@ -670,6 +671,7 @@ export interface IMiddlewareStatus { } export type PopupActionType = ObjectValues; +export type PopupMethod = ObjectValues; export interface IPopupActions { resolve: ResolveCallback; @@ -706,6 +708,20 @@ export interface IModalProps extends Partial { [key: string]: any; // Props defined on the component's level } +export interface IPopupMessageData { + target?: 'background' | 'offscreen'; + method?: PopupMethod; + type?: PopupActionType; + uuid?: string; + params?: { + aepp?: string | object; + popupType?: PopupType; + popupProps?: Partial; + id?: string; + }; + payload?: any; +} + export interface IResponseChallenge { challenge: string; payload: string; diff --git a/src/utils/getPopupProps.ts b/src/utils/getPopupProps.ts index 30bcf5f3f..55c9a0a5b 100644 --- a/src/utils/getPopupProps.ts +++ b/src/utils/getPopupProps.ts @@ -4,6 +4,7 @@ import type { Dictionary, IPopupActions, IPopupData, + IPopupMessageData, IPopupProps, TxType, } from '@/types'; @@ -15,7 +16,6 @@ import { POPUP_TYPE, RUNNING_IN_TESTS, } from '@/constants'; -import { PopupMessageData } from '@/background'; export function buildTx(txType: TxType) { const params = { @@ -30,7 +30,7 @@ const postMessage = (() => { const pendingRequests: Dictionary = {}; let background: browser.runtime.Port; - return async ({ type, payload }: PopupMessageData): Promise => { + return async ({ type, payload }: IPopupMessageData): Promise => { if (!IS_EXTENSION || !browser) { throw new Error('Supported only in browser extension'); } @@ -54,7 +54,7 @@ const postMessage = (() => { }; })(); -const postMessageTest = async ({ type }: PopupMessageData): Promise => { +const postMessageTest = async ({ type }: IPopupMessageData): Promise => { switch (type) { case POPUP_ACTIONS.getProps: { const { txType } = await browser.storage.local.get('txType');