diff --git a/apps/extension/src/background/approvals/handler.ts b/apps/extension/src/background/approvals/handler.ts index dcb8af7694..4c5604ba50 100644 --- a/apps/extension/src/background/approvals/handler.ts +++ b/apps/extension/src/background/approvals/handler.ts @@ -2,6 +2,7 @@ import { ApproveConnectInterfaceMsg, ApproveSignArbitraryMsg, ApproveTxMsg, + IsConnectionApprovedMsg, } from "provider"; import { Env, Handler, InternalHandler, Message } from "router"; import { @@ -26,6 +27,11 @@ export const getHandler: (service: ApprovalsService) => Handler = (service) => { env, msg as SubmitApprovedTxMsg ); + case IsConnectionApprovedMsg: + return handleIsConnectionApprovedMsg(service)( + env, + msg as IsConnectionApprovedMsg + ); case ApproveConnectInterfaceMsg: return handleApproveConnectInterfaceMsg(service)( env, @@ -87,6 +93,14 @@ const handleSubmitApprovedTxMsg: ( }; }; +const handleIsConnectionApprovedMsg: ( + service: ApprovalsService +) => InternalHandler = (service) => { + return async (_, { origin }) => { + return await service.isConnectionApproved(origin); + }; +}; + const handleApproveConnectInterfaceMsg: ( service: ApprovalsService ) => InternalHandler = (service) => { diff --git a/apps/extension/src/background/approvals/init.ts b/apps/extension/src/background/approvals/init.ts index 2513c54d5e..9746575b6c 100644 --- a/apps/extension/src/background/approvals/init.ts +++ b/apps/extension/src/background/approvals/init.ts @@ -2,6 +2,7 @@ import { ApproveConnectInterfaceMsg, ApproveSignArbitraryMsg, ApproveTxMsg, + IsConnectionApprovedMsg, } from "provider"; import { Router } from "router"; import { @@ -24,6 +25,7 @@ export function init(router: Router, service: ApprovalsService): void { router.registerMessage(ApproveSignArbitraryMsg); router.registerMessage(RejectSignatureMsg); router.registerMessage(SubmitApprovedSignatureMsg); + router.registerMessage(IsConnectionApprovedMsg); router.registerMessage(ApproveConnectInterfaceMsg); router.registerMessage(ConnectInterfaceResponseMsg); router.registerMessage(RevokeConnectionMsg); diff --git a/apps/extension/src/background/approvals/service.test.ts b/apps/extension/src/background/approvals/service.test.ts index 35b5c5f519..e7342143fe 100644 --- a/apps/extension/src/background/approvals/service.test.ts +++ b/apps/extension/src/background/approvals/service.test.ts @@ -16,6 +16,7 @@ import { KeyRingService, TabStore } from "background/keyring"; import { LedgerService } from "background/ledger"; import { VaultService } from "background/vault"; import BigNumber from "bignumber.js"; +import { ExtensionBroadcaster } from "extension"; import createMockInstance from "jest-create-mock-instance"; import { KVStoreMock } from "test/init"; import { ApprovalsService } from "./service"; @@ -55,6 +56,8 @@ describe.only("approvals service", () => { const vaultService: jest.Mocked = createMockInstance( VaultService as any ); + const broadcaster: jest.Mocked = + createMockInstance(ExtensionBroadcaster); service = new ApprovalsService( txStore, @@ -63,7 +66,8 @@ describe.only("approvals service", () => { approvedOriginsStore, keyRingService, ledgerService, - vaultService + vaultService, + broadcaster ); }); diff --git a/apps/extension/src/background/approvals/service.ts b/apps/extension/src/background/approvals/service.ts index 03eff62b7e..576c4263eb 100644 --- a/apps/extension/src/background/approvals/service.ts +++ b/apps/extension/src/background/approvals/service.ts @@ -22,8 +22,8 @@ import { import { assertNever, paramsToUrl } from "@namada/utils"; import { KeyRingService, TabStore } from "background/keyring"; import { LedgerService } from "background/ledger"; - import { VaultService } from "background/vault"; +import { ExtensionBroadcaster } from "extension"; import { ApprovedOriginsStore, TxStore } from "./types"; import { APPROVED_ORIGINS_KEY, @@ -52,7 +52,8 @@ export class ApprovalsService { protected readonly approvedOriginsStore: KVStore, protected readonly keyRingService: KeyRingService, protected readonly ledgerService: LedgerService, - protected readonly vaultService: VaultService + protected readonly vaultService: VaultService, + protected readonly broadcaster: ExtensionBroadcaster ) {} async approveSignature( @@ -344,6 +345,13 @@ export class ApprovalsService { return await this._clearPendingTx(msgId); } + async isConnectionApproved(interfaceOrigin: string): Promise { + const approvedOrigins = + (await this.approvedOriginsStore.get(APPROVED_ORIGINS_KEY)) || []; + + return approvedOrigins.includes(interfaceOrigin); + } + async approveConnection( interfaceTabId: number, interfaceOrigin: string @@ -357,10 +365,9 @@ export class ApprovalsService { interfaceOrigin, }); - const approvedOrigins = - (await this.approvedOriginsStore.get(APPROVED_ORIGINS_KEY)) || []; + const alreadyApproved = await this.isConnectionApproved(interfaceOrigin); - if (!approvedOrigins.includes(interfaceOrigin)) { + if (!alreadyApproved) { const approvalWindow = await this._launchApprovalWindow(url); const popupTabId = approvalWindow.tabs?.[0]?.id; @@ -376,6 +383,9 @@ export class ApprovalsService { this.resolverMap[popupTabId] = { resolve, reject }; }); } + + // A resolved promise is implicitly returned here if the origin had + // previously been approved. } async approveConnectionResponse( @@ -403,7 +413,8 @@ export class ApprovalsService { } async revokeConnection(originToRevoke: string): Promise { - return removeApprovedOrigin(this.approvedOriginsStore, originToRevoke); + await removeApprovedOrigin(this.approvedOriginsStore, originToRevoke); + await this.broadcaster.revokeConnection(); } private async _clearPendingTx(msgId: string): Promise { diff --git a/apps/extension/src/background/index.ts b/apps/extension/src/background/index.ts index e3fc72ff38..5dc9304128 100644 --- a/apps/extension/src/background/index.ts +++ b/apps/extension/src/background/index.ts @@ -113,7 +113,8 @@ const init = new Promise(async (resolve) => { approvedOriginsStore, keyRingService, ledgerService, - vaultService + vaultService, + broadcaster ); // Initialize messages and handlers diff --git a/apps/extension/src/content/events.ts b/apps/extension/src/content/events.ts index abd1e12cb0..12e783cc58 100644 --- a/apps/extension/src/content/events.ts +++ b/apps/extension/src/content/events.ts @@ -201,6 +201,26 @@ export class VaultLockedEventMsg extends Message { } } +export class ConnectionRevokedEventMsg extends Message { + public static type(): Events { + return Events.ConnectionRevoked; + } + + constructor() { + super(); + } + + validate(): void {} + + route(): string { + return Routes.InteractionForeground; + } + + type(): string { + return ConnectionRevokedEventMsg.type(); + } +} + export function initEvents(router: Router): void { router.registerMessage(AccountChangedEventMsg); router.registerMessage(NetworkChangedEventMsg); @@ -210,6 +230,7 @@ export function initEvents(router: Router): void { router.registerMessage(TxStartedEvent); router.registerMessage(TxCompletedEvent); router.registerMessage(VaultLockedEventMsg); + router.registerMessage(ConnectionRevokedEventMsg); router.addHandler(Routes.InteractionForeground, (_, msg) => { const clonedMsg = @@ -248,6 +269,9 @@ export function initEvents(router: Router): void { case VaultLockedEventMsg: window.dispatchEvent(new CustomEvent(Events.ExtensionLocked)); break; + case ConnectionRevokedEventMsg: + window.dispatchEvent(new CustomEvent(Events.ConnectionRevoked)); + break; default: throw new Error("Unknown msg type"); } diff --git a/apps/extension/src/extension/ExtensionBroadcaster.ts b/apps/extension/src/extension/ExtensionBroadcaster.ts index 9751919679..7bbb496ec5 100644 --- a/apps/extension/src/extension/ExtensionBroadcaster.ts +++ b/apps/extension/src/extension/ExtensionBroadcaster.ts @@ -3,6 +3,7 @@ import { KVStore } from "@namada/storage"; import { TabStore, syncTabs } from "background/keyring"; import { AccountChangedEventMsg, + ConnectionRevokedEventMsg, NetworkChangedEventMsg, ProposalsUpdatedEventMsg, TxCompletedEvent, @@ -59,6 +60,10 @@ export class ExtensionBroadcaster { await this.sendMsgToTabs(new VaultLockedEventMsg()); } + async revokeConnection(): Promise { + await this.sendMsgToTabs(new ConnectionRevokedEventMsg()); + } + /** * Query all existing tabs, and send provided message to each */ diff --git a/apps/extension/src/provider/InjectedNamada.ts b/apps/extension/src/provider/InjectedNamada.ts index 7bd3d6a6a8..bbcbae86f8 100644 --- a/apps/extension/src/provider/InjectedNamada.ts +++ b/apps/extension/src/provider/InjectedNamada.ts @@ -13,12 +13,16 @@ import { InjectedProxy } from "./InjectedProxy"; import { Signer } from "./Signer"; export class InjectedNamada implements INamada { - constructor(private readonly _version: string) { } + constructor(private readonly _version: string) {} public async connect(): Promise { return await InjectedProxy.requestMethod("connect"); } + public async isConnected(): Promise { + return await InjectedProxy.requestMethod("isConnected"); + } + public async accounts(): Promise { return await InjectedProxy.requestMethod( "accounts" diff --git a/apps/extension/src/provider/Namada.ts b/apps/extension/src/provider/Namada.ts index 273b44a5af..d00cb4e487 100644 --- a/apps/extension/src/provider/Namada.ts +++ b/apps/extension/src/provider/Namada.ts @@ -18,6 +18,7 @@ import { FetchAndStoreMaspParamsMsg, GetChainMsg, HasMaspParamsMsg, + IsConnectionApprovedMsg, QueryAccountsMsg, QueryBalancesMsg, QueryDefaultAccountMsg, @@ -28,7 +29,7 @@ export class Namada implements INamada { constructor( private readonly _version: string, protected readonly requester?: MessageRequester - ) { } + ) {} public async connect(): Promise { return await this.requester?.sendMessage( @@ -37,6 +38,17 @@ export class Namada implements INamada { ); } + public async isConnected(): Promise { + if (!this.requester) { + throw new Error("no requester"); + } + + return await this.requester.sendMessage( + Ports.Background, + new IsConnectionApprovedMsg() + ); + } + public async accounts( // TODO: This argument should be removed in the future! _chainId?: string diff --git a/apps/extension/src/provider/Proxy.ts b/apps/extension/src/provider/Proxy.ts index 2761e181a6..b970bcb411 100644 --- a/apps/extension/src/provider/Proxy.ts +++ b/apps/extension/src/provider/Proxy.ts @@ -1,5 +1,8 @@ import { KVStore } from "@namada/storage"; -import { ApprovedOriginsStore, APPROVED_ORIGINS_KEY } from "background/approvals"; +import { + APPROVED_ORIGINS_KEY, + ApprovedOriginsStore, +} from "background/approvals"; import { Namada } from "./Namada"; import { ProxyRequest, ProxyRequestResponse, ProxyRequestTypes } from "./types"; @@ -7,7 +10,7 @@ import { ProxyRequest, ProxyRequestResponse, ProxyRequestTypes } from "./types"; export class Proxy { static start( namada: Namada, - approvedOriginsStore: KVStore, + approvedOriginsStore: KVStore ): void { Proxy.addMessageListener(async (e) => { const message = e.data; @@ -18,8 +21,9 @@ export class Proxy { const { method, args } = message; - if (method !== "connect") { - const approvedOrigins = await approvedOriginsStore.get(APPROVED_ORIGINS_KEY) || []; + if (method !== "connect" && method !== "isConnected") { + const approvedOrigins = + (await approvedOriginsStore.get(APPROVED_ORIGINS_KEY)) || []; if (!approvedOrigins.includes(e.origin)) { return; } diff --git a/apps/extension/src/provider/messages.ts b/apps/extension/src/provider/messages.ts index 90f16b7421..c89ef1398d 100644 --- a/apps/extension/src/provider/messages.ts +++ b/apps/extension/src/provider/messages.ts @@ -21,6 +21,7 @@ enum Route { } enum MessageType { + IsConnectionApproved = "is-connection-approved", ApproveConnectInterface = "approve-connect-interface", QueryAccounts = "query-accounts", QueryDefaultAccount = "query-default-account", @@ -42,6 +43,27 @@ enum MessageType { /** * Messages routed from providers to Chains service */ +export class IsConnectionApprovedMsg extends Message { + public static type(): MessageType { + return MessageType.IsConnectionApproved; + } + + constructor() { + super(); + } + + validate(): void { + return; + } + + route(): string { + return Route.Approvals; + } + + type(): string { + return IsConnectionApprovedMsg.type(); + } +} export class ApproveConnectInterfaceMsg extends Message { public static type(): MessageType { @@ -74,7 +96,7 @@ export class GetChainMsg extends Message { super(); } - validate(): void { } + validate(): void {} route(): string { return Route.Chains; diff --git a/apps/extension/src/test/init.ts b/apps/extension/src/test/init.ts index aeb3ac7f03..62325d1472 100644 --- a/apps/extension/src/test/init.ts +++ b/apps/extension/src/test/init.ts @@ -43,7 +43,7 @@ const cryptoMemory = require("@namada/crypto").__wasm.memory; export class KVStoreMock implements KVStore { private storage: { [key: string]: T | null } = {}; - constructor(readonly _prefix: string) { } + constructor(readonly _prefix: string) {} get(key: string): Promise { return new Promise((resolve) => { @@ -141,7 +141,8 @@ export const init = async (): Promise<{ approvedOriginsStore, keyRingService, ledgerService, - vaultService + vaultService, + broadcaster ); const init = new Promise(async (resolve) => { diff --git a/packages/integrations/src/Namada.ts b/packages/integrations/src/Namada.ts index c8adf5446e..e6516524b2 100644 --- a/packages/integrations/src/Namada.ts +++ b/packages/integrations/src/Namada.ts @@ -14,7 +14,7 @@ import { BridgeProps, Integration } from "./types/Integration"; export default class Namada implements Integration { private _namada: WindowWithNamada["namada"] | undefined; - constructor(public readonly chain: Chain) { } + constructor(public readonly chain: Chain) {} public get instance(): INamada | undefined { return this._namada; @@ -35,6 +35,10 @@ export default class Namada implements Integration { await this._namada?.connect(chainId); } + public async isConnected(): Promise { + return await this._namada?.isConnected(); + } + public async getChain(): Promise { return await this._namada?.getChain(); } diff --git a/packages/types/src/events.ts b/packages/types/src/events.ts index b4dc006069..fe97f2283a 100644 --- a/packages/types/src/events.ts +++ b/packages/types/src/events.ts @@ -10,6 +10,7 @@ export enum Events { UpdatedStaking = "namada-updated-staking", ProposalsUpdated = "namada-proposals-updated", ExtensionLocked = "namada-extension-locked", + ConnectionRevoked = "namada-connection-revoked", } // Keplr extension events diff --git a/packages/types/src/namada.ts b/packages/types/src/namada.ts index e08f505f70..0a164d525c 100644 --- a/packages/types/src/namada.ts +++ b/packages/types/src/namada.ts @@ -33,6 +33,7 @@ export interface Namada { props: BalancesProps ): Promise<{ token: string; amount: string }[] | undefined>; connect(chainId?: string): Promise; + isConnected(): Promise; defaultAccount(chainId?: string): Promise; sign(props: SignArbitraryProps): Promise; verify(props: VerifyArbitraryProps): Promise;