From eadb2850270b212f74f01ca178d87ae8f308ee7b Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sat, 22 Feb 2025 22:27:00 +0100 Subject: [PATCH 1/7] chore(desktop): generate secret keys instead storing in localStorage --- src/store/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/store/index.js b/src/store/index.js index 0c233f764..a07ebda1c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -50,6 +50,7 @@ export default new Vuex.Store({ list: list.map(({ address, source }) => { switch (source.type) { case 'hd-wallet': + case 'hd-wallet-desktop': return { address, source: pick(source, ['type', 'idx']), @@ -88,7 +89,7 @@ export default new Vuex.Store({ registerServiceWorker, reverseIframe, ...(ENV_MOBILE_DEVICE ? [] : [syncLedgerAccounts]), - ...(RUNNING_IN_FRAME ? [unlockWalletIfNotEncrypted] : []), + ...(RUNNING_IN_FRAME || !ENV_MOBILE_DEVICE ? [unlockWalletIfNotEncrypted] : []), ]), ], From 09582c25962b7209b45330059b4bca7bcd520f08 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sat, 22 Feb 2025 18:38:45 +0100 Subject: [PATCH 2/7] chore: use new sdk in aepp-wallet connection --- src/lib/sdkWallet.js | 260 ++++++++++++++++++++++++++++ src/pages/aens/NameDetails.vue | 14 +- src/pages/mobile/AppBrowser.vue | 15 +- src/store/index.js | 2 - src/store/modules/accounts/index.js | 5 + src/store/plugins/initSdk.js | 169 ------------------ src/store/plugins/reverseIframe.js | 30 ++-- 7 files changed, 294 insertions(+), 201 deletions(-) create mode 100644 src/lib/sdkWallet.js delete mode 100644 src/store/plugins/initSdk.js diff --git a/src/lib/sdkWallet.js b/src/lib/sdkWallet.js new file mode 100644 index 000000000..c3afcdc1f --- /dev/null +++ b/src/lib/sdkWallet.js @@ -0,0 +1,260 @@ +import { + MemoryAccount, + AeSdkWallet, + Node, + BrowserWindowMessageConnection, + WALLET_TYPE, + RpcRejectedByUserError, + RpcNoNetworkById, + RpcNotAuthorizeError, + encode, + Encoding, + SUBSCRIPTION_TYPES, +} from '@aeternity/aepp-sdk-next'; + +function isRejectedByUserError(error) { + return ['Rejected by user', 'Cancelled by user'].includes(error.message); +} + +class AccountStore extends MemoryAccount { + #store; + + constructor(address, store) { + super(encode(Buffer.alloc(32), Encoding.AccountSecretKey)); + this.address = address; + this.#store = store; + } + + async #switchAccount() { + const initialAccountIdx = this.#store.state.accounts.activeIdx; + const requiredAccountIdx = this.#store.state.accounts.list.findIndex( + ({ address }) => address === this.address, + ); + await this.#store.dispatch('accounts/setActiveIdx', requiredAccountIdx); + return () => this.#store.dispatch('accounts/setActiveIdx', initialAccountIdx); + } + + async sign(data, { signal } = {}) { + const restore = await this.#switchAccount(); + try { + return await this.#store.dispatch('accounts/sign', { data, signal }); + } catch (error) { + if (isRejectedByUserError(error)) throw new RpcRejectedByUserError(); + throw error; + } finally { + await restore(); + } + } + + async signTransaction(transaction, { signal } = {}) { + const restore = await this.#switchAccount(); + try { + return await this.#store.dispatch('accounts/signTransaction', { transaction, signal }); + } catch (error) { + if (isRejectedByUserError(error)) throw new RpcRejectedByUserError(); + throw error; + } finally { + await restore(); + } + } +} + +function setupNodeWatch(store, sdk) { + return store.watch( + (_state, { node }) => node, + (node) => { + sdk.pool = new Map([['node', node]]); + sdk.selectNode('node'); + }, + { immediate: true }, + ); +} + +function setupAccountsWatch(store, sdk, host, aeppId) { + const getAccessibleAddresses = () => + store.getters.getApp(host)?.permissions.accessToAccounts ?? []; + const getCurrentAddress = () => store.getters['accounts/active'].address; + + const unwatchAppAddresses = store.watch( + () => getAccessibleAddresses(), + (addresses) => { + sdk.accounts = Object.fromEntries( + addresses.map((address) => [address, new AccountStore(address, store)]), + ); + if (addresses.length) { + const address = getCurrentAddress(); + sdk.selectAccount(sdk.accounts[address] ? address : addresses[0]); + } else sdk._pushAccountsToApps(); + }, + { immediate: true }, + ); + + let accountAccessPromise; + function ensureCurrentAccountAccess() { + async function ensureCurrentAccountAccessPure() { + if (getAccessibleAddresses().includes(getCurrentAddress())) return; + + const controller = new AbortController(); + const unsubscribe = store.watch( + () => [getCurrentAddress(), getAccessibleAddresses()], + ([address, allowed]) => allowed.includes(address) && controller.abort(), + ); + try { + await store.dispatch('modals/open', { + name: 'confirmAccountAccess', + signal: controller.signal, + appHost: host, + }); + } catch (error) { + if (error.message === 'Modal aborted') return; + throw error; + } finally { + unsubscribe(); + } + + const accountAddress = getCurrentAddress(); + if (!getAccessibleAddresses().includes(accountAddress)) { + store.commit('toggleAccessToAccount', { appHost: host, accountAddress }); + } + } + + accountAccessPromise ??= ensureCurrentAccountAccessPure().finally(() => { + accountAccessPromise = null; + }); + return accountAccessPromise; + } + + const unwatchCurrentAddress = store.watch( + () => getCurrentAddress(), + async (address) => { + if (getAccessibleAddresses().includes(address)) { + sdk.selectAccount(address); + return; + } + const client = sdk._getClient(aeppId); + if (client.addressSubscription.size === 0) return; + ensureCurrentAccountAccess(); + }, + { immediate: true }, + ); + + return [ + ensureCurrentAccountAccess, + () => [unwatchAppAddresses, unwatchCurrentAddress].forEach((unwatch) => unwatch()), + ]; +} + +function setupConnection(target, sdk) { + const connection = new BrowserWindowMessageConnection({ target }); + const aeppId = sdk.addRpcClient(connection); + sdk.shareWalletInfo(aeppId); + const intervalId = setInterval(() => sdk.shareWalletInfo(aeppId), 3000); + return [ + aeppId, + () => { + clearInterval(intervalId); + if (sdk._clients.has(aeppId)) sdk.removeRpcClient(aeppId); + }, + ]; +} + +export default (store, target, host) => { + let aeppInfo; + let authAeppId; + let ensureCurrentAccountAccess; + let unbindConnection; + let unbindAccounts; + + function ensureAuthorized(aeppId, origin) { + const originHost = new URL(origin).host; + host ??= originHost; + if (originHost === host && aeppId === authAeppId) return; + throw new RpcNotAuthorizeError(); + } + + function confirmAction(action) { + const res = confirm(`Aepp "${aeppInfo.name}" at ${origin} is requesting ${action}`); + if (res === false) throw new RpcRejectedByUserError(); + } + + const sdk = new AeSdkWallet({ + id: window.origin, + type: WALLET_TYPE.window, + name: 'Base Aepp', + onConnection: (aeppId, params, origin) => { + ensureAuthorized(aeppId, origin); + aeppInfo = params; + [ensureCurrentAccountAccess, unbindAccounts] = setupAccountsWatch(store, sdk, host, aeppId); + }, + onSubscription: (aeppId, params, origin) => { + ensureAuthorized(aeppId, origin); + if (params.type === SUBSCRIPTION_TYPES.subscribe) void ensureCurrentAccountAccess(); + }, + onAskAccounts: async (aeppId, _params, origin) => { + ensureAuthorized(aeppId, origin); + try { + await ensureCurrentAccountAccess(); + } catch (error) { + if (isRejectedByUserError(error)) throw new RpcRejectedByUserError(); + throw error; + } + }, + onAskToSelectNetwork: async (aeppId, parameters, origin) => { + ensureAuthorized(aeppId, origin); + + function switchToNetwork({ name, url, networkId }) { + const details = [url, networkId].filter(Boolean).join(', '); + confirmAction(`a network switch to ${name} (${details})`); + store.commit('setSdkUrl', url); + } + + if (parameters.networkId) { + const network = ( + await Promise.allSettled( + store.getters.networks.map(async (network) => ({ + ...network, + networkId: await new Node(network.url, { retryCount: 0 }).getNetworkId(), + })), + ) + ) + .filter(({ status }) => status === 'fulfilled') + .map(({ value }) => value) + .find(({ networkId }) => networkId === parameters.networkId); + if (network == null) throw new RpcNoNetworkById(parameters.networkId); + switchToNetwork(network); + return; + } + + const network = store.getters.networks.find( + ({ url }) => new URL(url).toString() === new URL(parameters.nodeUrl).toString(), + ); + if (network) { + switchToNetwork(network); + return; + } + + const networkId = await new Node(parameters.nodeUrl, { retryCount: 0 }).getNetworkId(); + confirmAction(`a network switch to ${parameters.nodeUrl} (${networkId})`); + store.commit('addNetwork', { + name: `By ${host}`, + url: parameters.nodeUrl, + }); + store.commit('setSdkUrl', parameters.nodeUrl); + }, + onDisconnect: (aeppId, params) => { + unbindConnection(); + unbindAccounts(); + [authAeppId, unbindConnection] = setupConnection(target, sdk); + }, + }); + + const unbindNodeWatch = setupNodeWatch(store, sdk); + + [authAeppId, unbindConnection] = setupConnection(target, sdk); + + return () => { + unbindConnection(); + unbindAccounts?.(); + unbindNodeWatch(); + }; +}; diff --git a/src/pages/aens/NameDetails.vue b/src/pages/aens/NameDetails.vue index e8098fdf7..c8160437e 100644 --- a/src/pages/aens/NameDetails.vue +++ b/src/pages/aens/NameDetails.vue @@ -99,12 +99,14 @@ export default { const requiredAccountIdx = this.$store.state.accounts.list.findIndex( ({ address }) => address === this.details.owner, ); - if (initialAccountIdx !== requiredAccountIdx) { - this.$store.commit('accounts/setActiveIdx', requiredAccountIdx); - } - await this.$store.dispatch('names/updatePointer', { name: this.name, address: this.address }); - if (initialAccountIdx !== requiredAccountIdx) { - this.$store.commit('accounts/setActiveIdx', initialAccountIdx); + await this.$store.dispatch('accounts/setActiveIdx', requiredAccountIdx); + try { + await this.$store.dispatch('names/updatePointer', { + name: this.name, + address: this.address, + }); + } finally { + await this.$store.dispatch('accounts/setActiveIdx', initialAccountIdx); } }, async goToTransactionDetails() { diff --git a/src/pages/mobile/AppBrowser.vue b/src/pages/mobile/AppBrowser.vue index d7de2e1fb..73bb83522 100644 --- a/src/pages/mobile/AppBrowser.vue +++ b/src/pages/mobile/AppBrowser.vue @@ -41,7 +41,7 @@