Skip to content

Commit

Permalink
feat: update permissions for extension
Browse files Browse the repository at this point in the history
  • Loading branch information
jurevans committed Oct 24, 2024
1 parent f4ff5c0 commit 296cc50
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 8 deletions.
9 changes: 6 additions & 3 deletions apps/extension/src/background/approvals/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { chains } from "@namada/chains";
import { WrapperTxMsgValue } from "@namada/types";
import { ChainsService } from "background/chains";
import { KeyRingService } from "background/keyring";
import { PermissionsService } from "background/permissions";
import { SdkService } from "background/sdk";
import { VaultService } from "background/vault";
import BigNumber from "bignumber.js";
Expand Down Expand Up @@ -49,6 +50,7 @@ describe("approvals service", () => {
let sdkService: jest.Mocked<SdkService>;
let keyRingService: jest.Mocked<KeyRingService>;
let chainService: jest.Mocked<ChainsService>;
let permissionsService: jest.Mocked<PermissionsService>;
let dataStore: KVStoreMock<string>;
let txStore: KVStoreMock<PendingTx>;
let localStorage: LocalStorage;
Expand Down Expand Up @@ -78,6 +80,7 @@ describe("approvals service", () => {
keyRingService,
vaultService,
chainService,
permissionsService,
broadcaster
);
});
Expand Down Expand Up @@ -530,7 +533,7 @@ describe("approvals service", () => {
describe("getResolver", () => {
it("should get the related tab id resolver from resolverMap", async () => {
const popupTabId = 1;
const resolver = { resolve: () => { }, reject: () => { } };
const resolver = { resolve: () => {}, reject: () => {} };
service["resolverMap"] = {
[popupTabId]: resolver,
};
Expand All @@ -541,7 +544,7 @@ describe("approvals service", () => {
it("should throw an error if there is no resolver for the tab id", async () => {
const popupTabId = 1;
service["resolverMap"] = {
[popupTabId]: { resolve: () => { }, reject: () => { } },
[popupTabId]: { resolve: () => {}, reject: () => {} },
};

expect(() => service["getResolver"](999)).toThrow();
Expand All @@ -552,7 +555,7 @@ describe("approvals service", () => {
it("should remove related tab id resolver from resolverMap", async () => {
const popupTabId = 1;
service["resolverMap"] = {
[popupTabId]: { resolve: () => { }, reject: () => { } },
[popupTabId]: { resolve: () => {}, reject: () => {} },
};
service["removeResolver"](popupTabId);

Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/background/approvals/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ResponseSign } from "@zondax/ledger-namada";
import { TopLevelRoute } from "Approvals/types";
import { ChainsService } from "background/chains";
import { KeyRingService } from "background/keyring";
import { PermissionsService } from "background/permissions";
import { SdkService } from "background/sdk";
import { VaultService } from "background/vault";
import { ExtensionBroadcaster } from "extension";
Expand All @@ -36,6 +37,7 @@ export class ApprovalsService {
protected readonly keyRingService: KeyRingService,
protected readonly vaultService: VaultService,
protected readonly chainService: ChainsService,
protected readonly permissionsService: PermissionsService,
protected readonly broadcaster: ExtensionBroadcaster
) {
browser.tabs.onRemoved.addListener((tabId) => {
Expand Down
4 changes: 4 additions & 0 deletions apps/extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { LocalStorage, VaultStorage } from "storage";
import { ApprovalsService, init as initApprovals } from "./approvals";
import { ChainsService, init as initChains } from "./chains";
import { KeyRingService, UtilityStore, init as initKeyRing } from "./keyring";
import { PermissionsService } from "./permissions";
import { SdkService } from "./sdk/service";
import { VaultService, init as initVault } from "./vault";

Expand Down Expand Up @@ -69,10 +70,12 @@ const init = new Promise<void>(async (resolve) => {
localStorage,
broadcaster
);
const permissionsService = new PermissionsService(localStorage);
const keyRingService = new KeyRingService(
vaultService,
sdkService,
chainsService,
permissionsService,
utilityStore,
localStorage,
vaultStorage,
Expand All @@ -87,6 +90,7 @@ const init = new Promise<void>(async (resolve) => {
keyRingService,
vaultService,
chainsService,
permissionsService,
broadcaster
);

Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/background/keyring/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { Result, truncateInMiddle } from "@namada/utils";

import { ChainsService } from "background/chains";
import { PermissionsService } from "background/permissions";
import { SdkService } from "background/sdk/service";
import { VaultService } from "background/vault";
import { ExtensionBroadcaster, ExtensionRequester } from "extension";
Expand All @@ -32,6 +33,7 @@ export class KeyRingService {
protected readonly vaultService: VaultService,
protected readonly sdkService: SdkService,
protected readonly chainsService: ChainsService,
protected readonly permissionsService: PermissionsService,
protected readonly utilityStore: KVStore<UtilityStore>,
protected readonly localStorage: LocalStorage,
protected readonly vaultStorage: VaultStorage,
Expand Down
1 change: 1 addition & 0 deletions apps/extension/src/background/permissions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./service";
59 changes: 59 additions & 0 deletions apps/extension/src/background/permissions/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { AllowedPermissions, LocalStorage, PermissionKind } from "storage";

export class PermissionsService {
constructor(protected readonly localStorage: LocalStorage) {}

async enablePermissions(
domain: string,
chainId: string,
allowed: AllowedPermissions
): Promise<void> {
const existingPermissions = await this.localStorage.getPermissions();
const newPermissions = [...new Set<PermissionKind>(allowed)];

if (existingPermissions) {
existingPermissions[domain] = existingPermissions[domain] || {};
existingPermissions[domain][chainId] = newPermissions;
return await this.localStorage.setPermissions(existingPermissions);
}

return await this.localStorage.setPermissions({
[domain]: {
[chainId]: newPermissions,
},
});
}

async revokeChainPermissions(domain: string, chainId: string): Promise<void> {
const updatedPermissions = await this.localStorage.getPermissions();
if (
!updatedPermissions ||
!updatedPermissions[domain] ||
!updatedPermissions[domain][chainId]
) {
return;
}
delete updatedPermissions[domain][chainId];
await this.localStorage.setPermissions(updatedPermissions);
}

async permissionsByDomain(
domain: string
): Promise<Record<string, AllowedPermissions> | undefined> {
const permissions = await this.localStorage.getPermissions();

if (permissions && permissions[domain]) {
return permissions[domain];
}
}

async permissionsByChain(
domain: string,
chainId: string
): Promise<AllowedPermissions | undefined> {
const permissions = await this.localStorage.getPermissions();
if (permissions && permissions[domain] && permissions[domain][chainId]) {
return permissions[domain][chainId];
}
}
}
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 @@ -23,6 +23,7 @@ export enum KVPrefix {
RevealedPK = "Namada::RevealedPK",
SessionStorage = "Namada::SessionStorage",
WasmHashesStorage = "Namada::WasmHashesStorage",
Permissions = "Namada::Permissions",
}

export enum KVKeys {
Expand Down
64 changes: 63 additions & 1 deletion apps/extension/src/storage/LocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,42 @@ const Chain = t.intersection([
]);
type ChainType = t.TypeOf<typeof Chain>;

// TODO: Remove the folllowing once NamadaKeychainPermissions is working!
const NamadaExtensionApprovedOrigins = t.array(t.string);
type NamadaExtensionApprovedOriginsType = t.TypeOf<
typeof NamadaExtensionApprovedOrigins
>;

export type PermissionKind = "accounts" | "signing" | "proofGenKeys";

export const KeychainPermissions: Record<
PermissionKind,
{ description: string }
> = {
accounts: {
description: "Allow approved clients to read account public data",
},
signing: { description: "Allow approved clients to sign transactions" },
proofGenKeys: {
description:
"Allow approved clients to request proof generation keys for shielded accounts",
},
};
export type AllowedPermissions = (keyof typeof KeychainPermissions)[];

// Define keychain permissions schema
const PermissionDomain = t.string;
const PermissionChainId = t.string;
const NamadaKeychainPermissions = t.record(
PermissionDomain,
t.record(PermissionChainId, t.array(t.keyof(KeychainPermissions)))
);

// Export keychain permissions type
export type NamadaKeychainPermissionsType = t.TypeOf<
typeof NamadaKeychainPermissions
>;

const NamadaExtensionRouterId = t.number;
type NamadaExtensionRouterIdType = t.TypeOf<typeof NamadaExtensionRouterId>;

Expand All @@ -69,18 +100,21 @@ type LocalStorageTypes =
type LocalStorageSchemas =
| typeof Chain
| typeof NamadaExtensionApprovedOrigins
| typeof NamadaExtensionRouterId;
| typeof NamadaExtensionRouterId
| typeof NamadaKeychainPermissions;

export type LocalStorageKeys =
| "chains"
| "namadaExtensionApprovedOrigins"
| "namadaExtensionRouterId"
| "namadaKeychainPermissions"
| "tabs";

const schemasMap = new Map<LocalStorageSchemas, LocalStorageKeys>([
[Chain, "chains"],
[NamadaExtensionApprovedOrigins, "namadaExtensionApprovedOrigins"],
[NamadaExtensionRouterId, "namadaExtensionRouterId"],
[NamadaKeychainPermissions, "namadaKeychainPermissions"],
]);

export class LocalStorage extends ExtStorage {
Expand Down Expand Up @@ -155,6 +189,34 @@ export class LocalStorage extends ExtStorage {
await this.setRaw(this.getKey(NamadaExtensionApprovedOrigins), origins);
}

async getPermissions(): Promise<NamadaKeychainPermissionsType | undefined> {
const data = await this.getRaw(this.getKey(NamadaKeychainPermissions));
const Schema = t.union([NamadaKeychainPermissions, t.undefined]);
const decodedData = Schema.decode(data);

if (E.isLeft(decodedData)) {
throw new Error("");
}
return decodedData.right;
}

async setPermissions(
permissions: NamadaKeychainPermissionsType
): Promise<void> {
// Validate permissions against schema
const Schema = t.union([NamadaKeychainPermissions, t.undefined]);
const decodedData = Schema.decode(permissions);

if (E.isLeft(decodedData)) {
throw new Error("Invalid permissions data!");
}

await this.setRaw(
this.getKey(NamadaKeychainPermissions),
decodedData.right
);
}

private getKey<S extends LocalStorageSchemas>(schema: S): LocalStorageKeys {
const key = schemasMap.get(schema);
if (!key) {
Expand Down
4 changes: 4 additions & 0 deletions apps/extension/src/test/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from "../background/approvals";

import { ChainsService } from "background/chains";
import { PermissionsService } from "background/permissions";
import { SdkService } from "background/sdk";
import { Namada } from "provider";
import { LocalStorage, VaultStorage } from "storage";
Expand Down Expand Up @@ -82,6 +83,7 @@ export const init = async (): Promise<{
);

const sdkService = await SdkService.init(localStorage);
const permissionsService = new PermissionsService(localStorage);

const vaultService = new VaultService(vaultStorage, sessionStore, sdkService);
await vaultService.initialize();
Expand All @@ -96,6 +98,7 @@ export const init = async (): Promise<{
vaultService,
sdkService,
chainsService,
permissionsService,
utilityStore,
localStorage,
vaultStorage,
Expand All @@ -111,6 +114,7 @@ export const init = async (): Promise<{
keyRingService,
vaultService,
chainsService,
permissionsService,
broadcaster
);

Expand Down
9 changes: 8 additions & 1 deletion apps/namadillo/src/App/Common/ConnectExtensionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ActionButton } from "@namada/components";
import { chainParametersAtom } from "atoms/chain";
import { namadaExtensionAttachStatus } from "atoms/settings";
import { useExtensionConnect } from "hooks/useExtensionConnect";
import { useAtomValue } from "jotai";

export const ConnectExtensionButton = (): JSX.Element => {
const extensionAttachStatus = useAtomValue(namadaExtensionAttachStatus);
const { data: chain } = useAtomValue(chainParametersAtom);
const chainId = chain?.chainId;
const { connect, isConnected } = useExtensionConnect();

// TODO create an action button when the extension is connected
Expand All @@ -13,7 +16,11 @@ export const ConnectExtensionButton = (): JSX.Element => {
return (
<>
{extensionAttachStatus === "attached" && !isConnected && (
<ActionButton backgroundColor="yellow" size="sm" onClick={connect}>
<ActionButton
backgroundColor="yellow"
size="sm"
onClick={() => (chainId ? connect(chainId) : undefined)}
>
Connect Keychain
</ActionButton>
)}
Expand Down
15 changes: 12 additions & 3 deletions packages/integrations/src/Namada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@ export default class Namada implements Integration<Account, Signer> {
return !!this._namada;
}

public async connect(chainId: string): Promise<void> {
public async connect(chainId?: string): Promise<void> {
if (!chainId) {
throw new Error("The Namada Keychain integration requires a chainId!");
}
await this._namada?.connect(chainId);
}

public async disconnect(chainId: string): Promise<void> {
public async disconnect(chainId?: string): Promise<void> {
if (!chainId) {
throw new Error("The Namada Keychain integration requires a chainId!");
}
await this._namada?.disconnect(chainId);
}

public async isConnected(chainId: string): Promise<boolean | undefined> {
public async isConnected(chainId?: string): Promise<boolean | undefined> {
if (!chainId) {
throw new Error("The Namada Keychain integration requires a chainId!");
}
return await this._namada?.isConnected(chainId);
}

Expand Down

0 comments on commit 296cc50

Please sign in to comment.