Skip to content

Commit

Permalink
feat/261 - Extension events improvements (anoma#262)
Browse files Browse the repository at this point in the history
* begin updating to identify parent account by ID

* clean up

* Update keyring to store "parentId" for derived accounts

* Add store to persist selected parent account, return accounts by
parentId

* Implement message for account switching from extension

* Set up parent account querying and messaging

* Initialize parent account from storage for persistence

* Clean up provider tests

* Add events service/hooks, begin setting up for extension account changed

* Simplify handler, prepare to broadcast chainId affected by changed
accounts

* Restructure extensionEvents service to account for additional extensions

* Begin implementing event handlers in extension for accounts

* Add background content service for events

* continue hooking up content service, clean up

* senderTabId shouldn't be required for now, hook up to Settings page

* Clean up

* Move initial call to dispatch account changed message to service

* Simplified event broadcast and provider in interface

* Update to track connected interfaces by tab ID

* Clean up

* Add single store instantiated at start up for tracking tab IDs

* clean up

* clean up, only append to store if chain ID matches

* Fix to invoke "connect()" when querying accounts, needed to track tab ID

* Move tab storage to local storage, track timestamp to later expire

* Add logic to update tab timestamp when interface reconnects

* Simplify logic
  • Loading branch information
jurevans authored May 8, 2023
1 parent c76f657 commit 04819dd
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 7 deletions.
5 changes: 5 additions & 0 deletions apps/extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const { REACT_APP_NAMADA_URL = DEFAULT_URL } = process.env;
get: browser.storage.local.get,
set: browser.storage.local.set,
});
const connectedTabsStore = new ExtensionKVStore(KVPrefix.LocalStorage, {
get: browser.storage.local.get,
set: browser.storage.local.set,
});

const requester = new ExtensionRequester(messenger, extensionStore);
const router = new ExtensionRouter(
Expand All @@ -59,6 +63,7 @@ const { REACT_APP_NAMADA_URL = DEFAULT_URL } = process.env;
store,
sdkStore,
activeAccountStore,
connectedTabsStore,
defaultChainId,
sdk,
cryptoMemory,
Expand Down
14 changes: 14 additions & 0 deletions apps/extension/src/background/keyring/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
QueryParentAccountsMsg,
} from "./messages";
import {
ConnectInterfaceMsg,
SubmitTransferMsg,
EncodeInitAccountMsg,
QueryAccountsMsg,
Expand All @@ -26,6 +27,11 @@ export const getHandler: (service: KeyRingService) => Handler = (service) => {
switch (msg.constructor) {
case CheckIsLockedMsg:
return handleCheckIsLockedMsg(service)(env, msg as CheckIsLockedMsg);
case ConnectInterfaceMsg:
return handleConnectInterfaceMsg(service)(
env,
msg as ConnectInterfaceMsg
);
case LockKeyRingMsg:
return handleLockKeyRingMsg(service)(env, msg as LockKeyRingMsg);
case UnlockKeyRingMsg:
Expand Down Expand Up @@ -80,6 +86,14 @@ export const getHandler: (service: KeyRingService) => Handler = (service) => {
};
};

const handleConnectInterfaceMsg: (
service: KeyRingService
) => InternalHandler<ConnectInterfaceMsg> = (service) => {
return async ({ senderTabId }, { chainId }) => {
return await service.connect(senderTabId, chainId);
};
};

const handleCheckIsLockedMsg: (
service: KeyRingService
) => InternalHandler<CheckIsLockedMsg> = (service) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/background/keyring/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
QueryParentAccountsMsg,
} from "./messages";
import {
ConnectInterfaceMsg,
SubmitTransferMsg,
EncodeInitAccountMsg,
QueryAccountsMsg,
Expand All @@ -26,6 +27,7 @@ import { KeyRingService } from "./service";

export function init(router: Router, service: KeyRingService): void {
router.registerMessage(CheckIsLockedMsg);
router.registerMessage(ConnectInterfaceMsg);
router.registerMessage(DeriveAccountMsg);
router.registerMessage(QueryAccountsMsg);
router.registerMessage(LockKeyRingMsg);
Expand Down
38 changes: 33 additions & 5 deletions apps/extension/src/background/keyring/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AccountType, Bip44Path, DerivedAccount } from "@anoma/types";
import { Sdk } from "@anoma/shared";

import { KeyRing } from "./keyring";
import { KeyRingStatus, KeyStore } from "./types";
import { KeyRingStatus, KeyStore, TabStore } from "./types";
import { ExtensionRequester } from "extension";
import { Ports } from "router";
import { AccountChangedEventMsg } from "content/events";
Expand All @@ -17,6 +17,7 @@ export class KeyRingService {
protected readonly kvStore: KVStore<KeyStore[]>,
protected readonly sdkStore: KVStore<string>,
protected readonly accountAccountStore: KVStore<string>,
protected readonly connectedTabsStore: KVStore<TabStore[]>,
protected readonly chainId: string,
protected readonly sdk: Sdk,
protected readonly cryptoMemory: WebAssembly.Memory,
Expand Down Expand Up @@ -49,6 +50,28 @@ export class KeyRingService {
return this._keyRing.isLocked();
}

// Track connected tabs by ID
async connect(senderTabId: number, chainId: string): Promise<void> {
// Validate chainId, if valid, append tab unless it already exists
if (chainId === this.chainId) {
const tabs = (await this.connectedTabsStore.get(chainId)) || [];
const tabIndex = tabs.findIndex((tab) => tab.tabId === senderTabId);

if (tabIndex > -1) {
// If tab exists, update timestamp
tabs[tabIndex].timestamp = Date.now();
} else {
// Add tab to storage
tabs.push({
tabId: senderTabId,
timestamp: Date.now(),
});
}
return await this.connectedTabsStore.set(chainId, tabs);
}
throw new Error("Connect: Invalid chainId");
}

async checkPassword(password: string): Promise<boolean> {
return await this._keyRing.checkPassword(password);
}
Expand Down Expand Up @@ -131,12 +154,17 @@ export class KeyRingService {
}

async setActiveAccountId(accountId: string): Promise<void> {
const tabs = await this.connectedTabsStore.get(this.chainId);
await this._keyRing.setActiveAccountId(accountId);

try {
return await this.requester.sendMessageToCurrentTab(
Ports.WebBrowser,
new AccountChangedEventMsg(this.chainId)
);
tabs?.forEach(({ tabId }: TabStore) => {
this.requester.sendMessageToTab(
tabId,
Ports.WebBrowser,
new AccountChangedEventMsg(this.chainId)
);
});
} catch (e) {
console.warn(e);
}
Expand Down
5 changes: 5 additions & 0 deletions apps/extension/src/background/keyring/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@ export enum KeyRingStatus {
Locked,
Unlocked,
}

export type TabStore = {
tabId: number;
timestamp: number;
};
3 changes: 3 additions & 0 deletions apps/extension/src/extension/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { KVStore } from "@anoma/storage";
import { Env, MessageSender } from "router/types";

const ROUTER_ID_KEY = "anomaExtensionRouterId";
const NO_TAB_ID = -2;

export const getAnomaRouterId = async (
store: KVStore<number>
Expand All @@ -19,9 +20,11 @@ export const getAnomaRouterId = async (
export class ContentScriptEnv {
static readonly produceEnv = (sender: MessageSender): Env => {
const isInternalMsg = sender.id === browser.runtime.id;
const senderTabId = sender.tab?.id || NO_TAB_ID;

return {
isInternalMsg,
senderTabId,
requestInteraction: () => {
throw new Error(
"ContentScriptEnv doesn't support `requestInteraction`"
Expand Down
7 changes: 5 additions & 2 deletions apps/extension/src/provider/Anoma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Anoma as IAnoma, Chain, DerivedAccount } from "@anoma/types";
import { Ports, MessageRequester } from "router";

import {
ConnectInterfaceMsg,
GetChainMsg,
GetChainsMsg,
SuggestChainMsg,
Expand All @@ -20,8 +21,10 @@ export class Anoma implements IAnoma {
) {}

public async connect(chainId: string): Promise<void> {
// TODO: Implement this
console.info("connect", chainId);
return await this.requester?.sendMessage(
Ports.Background,
new ConnectInterfaceMsg(chainId)
);
}

public async chain(chainId: string): Promise<Chain | undefined> {
Expand Down
26 changes: 26 additions & 0 deletions apps/extension/src/provider/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum Route {
}

enum MessageType {
ConnectInterface = "connect-interface",
QueryAccounts = "query-accounts",
SignTx = "sign-tx",
SubmitTransfer = "submit-transfer",
Expand All @@ -29,6 +30,31 @@ enum MessageType {
/**
* Messages routed from providers to Chains service
*/

export class ConnectInterfaceMsg extends Message<void> {
public static type(): MessageType {
return MessageType.ConnectInterface;
}

constructor(public readonly chainId: string) {
super();
}

validate(): void {
if (!this.chainId) {
throw new Error("chain ID not set");
}
}

route(): string {
return Route.KeyRing;
}

type(): string {
return ConnectInterfaceMsg.type();
}
}

export class SuggestChainMsg extends Message<void> {
public static type(): MessageType {
return MessageType.SuggestChain;
Expand Down
1 change: 1 addition & 0 deletions apps/extension/src/router/types/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum KVPrefix {
LocalStorage = "Anoma::LocalStorage",
SDK = "Anoma::SDK",
ActiveAccount = "Anoma::ActiveAccount",
ConnectedTabs = "Anoma::ConnectedTabs",
}

export enum KVKeys {
Expand Down
1 change: 1 addition & 0 deletions apps/extension/src/router/types/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type MessageSender = Pick<Runtime.MessageSender, "id" | "url" | "tab">;

export interface Env {
readonly isInternalMsg: boolean;
readonly senderTabId: number;
readonly requestInteraction: () => void;
}

Expand Down
6 changes: 6 additions & 0 deletions apps/extension/src/test/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
KeyRingService,
init as initKeyRing,
KeyStore,
TabStore,
} from "../background/keyring";
import { Anoma } from "provider";
import { Chain } from "@anoma/types";
Expand Down Expand Up @@ -51,11 +52,15 @@ export const init = (): {
const sdkStore = new KVStoreMock<string>(KVPrefix.SDK);
const extStore = new KVStoreMock<number>(KVPrefix.IndexedDB);
const activeAccountStore = new KVStoreMock<string>(KVPrefix.ActiveAccount);
const connectedTabsStore = new KVStoreMock<TabStore[]>(
KVPrefix.ConnectedTabs
);
const requester = new ExtensionRequester(messenger, extStore);

const router = new ExtensionRouter(
() => ({
isInternalMsg: true,
senderTabId: -2,
requestInteraction: () => {
throw new Error("Test env doesn't support `requestInteraction`");
},
Expand All @@ -74,6 +79,7 @@ export const init = (): {
iDBStore as KVStore<KeyStore[]>,
sdkStore,
activeAccountStore,
connectedTabsStore,
"namada-75a7e12.69483d59a9fb174",
sdk,
cryptoMemory,
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/src/Anoma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default class Anoma
}

public async accounts(): Promise<readonly Account[] | undefined> {
await this.connect();
const signer = this._anoma?.getSigner(this.chain.chainId);
return await signer?.accounts();
}
Expand Down

0 comments on commit 04819dd

Please sign in to comment.