Skip to content

Commit

Permalink
feat/1295 - Update Namada Keychain integration (#1298)
Browse files Browse the repository at this point in the history
* feat: begin porting Namada Keychain integration

* fix: tests and begin hooking up chainId

* feat: remove isShielded, update as needed

* feat: make specific hook for namada keychain

* feat: hooking up new hook for namada keychain

* feat: remove dependency on integrations package

* feat: provide chainId, allow approval to enable signing on chainId

* fix: fix tests

* fix: clean-up
  • Loading branch information
jurevans authored Nov 30, 2024
1 parent d96fbbc commit b09a16e
Show file tree
Hide file tree
Showing 36 changed files with 383 additions and 221 deletions.
16 changes: 14 additions & 2 deletions apps/extension/src/Approvals/ApproveConnection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ export const ApproveConnection: React.FC = () => {
const requester = useRequester();
const params = useQuery();
const interfaceOrigin = params.get("interfaceOrigin");
const chainId = params.get("chainId");

const handleResponse = async (allowConnection: boolean): Promise<void> => {
if (interfaceOrigin) {
await requester.sendMessage(
Ports.Background,
new ConnectInterfaceResponseMsg(interfaceOrigin, allowConnection)
new ConnectInterfaceResponseMsg(
interfaceOrigin,
allowConnection,
chainId || undefined
)
);
await closeCurrentTab();
}
Expand All @@ -26,7 +31,14 @@ export const ApproveConnection: React.FC = () => {
<PageHeader title="Approve Request" />
<Stack full className="justify-between" gap={12}>
<Alert type="warning">
Approve connection for <strong>{interfaceOrigin}</strong>?
Approve connection for <strong>{interfaceOrigin}</strong>
{chainId && (
<>
{" "}
and enable signing for <strong>{chainId}</strong>
</>
)}
?
</Alert>
<Stack gap={2}>
<ActionButton onClick={() => handleResponse(true)}>
Expand Down
5 changes: 3 additions & 2 deletions apps/extension/src/background/approvals/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,16 @@ describe("approvals handler", () => {
handler(env, approveTxMsg);
expect(service.approveSignTx).toBeCalled();

const chainId = "chain-id";
const rejectTxMsg = new RejectSignTxMsg("msgId");
handler(env, rejectTxMsg);
expect(service.rejectSignTx).toBeCalled();

const isConnectionApprovedMsg = new IsConnectionApprovedMsg();
const isConnectionApprovedMsg = new IsConnectionApprovedMsg(chainId);
handler(env, isConnectionApprovedMsg);
expect(service.isConnectionApproved).toBeCalled();

const approveConnectInterfaceMsg = new ApproveConnectInterfaceMsg();
const approveConnectInterfaceMsg = new ApproveConnectInterfaceMsg(chainId);
handler(env, approveConnectInterfaceMsg);
expect(service.approveConnection).toBeCalled();

Expand Down
17 changes: 9 additions & 8 deletions apps/extension/src/background/approvals/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,16 @@ export const getHandler: (service: ApprovalsService) => Handler = (service) => {
const handleIsConnectionApprovedMsg: (
service: ApprovalsService
) => InternalHandler<IsConnectionApprovedMsg> = (service) => {
return async (_, { origin }) => {
return await service.isConnectionApproved(origin);
return async (_, { origin, chainId }) => {
return await service.isConnectionApproved(origin, chainId);
};
};

const handleApproveConnectInterfaceMsg: (
service: ApprovalsService
) => InternalHandler<ApproveConnectInterfaceMsg> = (service) => {
return async (_, { origin }) => {
return await service.approveConnection(origin);
return async (_, { origin, chainId }) => {
return await service.approveConnection(origin, chainId);
};
};

Expand All @@ -135,21 +135,22 @@ const handleConnectInterfaceResponseMsg: (
) => InternalHandler<ConnectInterfaceResponseMsg> = (service) => {
return async (
{ senderTabId: popupTabId },
{ interfaceOrigin, allowConnection }
{ interfaceOrigin, allowConnection, chainId }
) => {
return await service.approveConnectionResponse(
popupTabId,
interfaceOrigin,
allowConnection
allowConnection,
chainId
);
};
};

const handleApproveDisconnectInterfaceMsg: (
service: ApprovalsService
) => InternalHandler<ApproveDisconnectInterfaceMsg> = (service) => {
return async (_, { origin }) => {
return await service.approveDisconnection(origin);
return async (_, { origin, chainId }) => {
return await service.approveDisconnection(origin, chainId);
};
};

Expand Down
3 changes: 2 additions & 1 deletion apps/extension/src/background/approvals/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ export class ConnectInterfaceResponseMsg extends Message<void> {

constructor(
public readonly interfaceOrigin: string,
public readonly allowConnection: boolean
public readonly allowConnection: boolean,
public readonly chainId?: string
) {
super();
}
Expand Down
6 changes: 4 additions & 2 deletions apps/extension/src/background/approvals/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ describe("approvals service", () => {
{ interfaceOrigin }
);
expect(service.isConnectionApproved).toHaveBeenCalledWith(
interfaceOrigin
interfaceOrigin,
undefined
);
await expect(promise).resolves.toBeDefined();
});
Expand Down Expand Up @@ -371,7 +372,8 @@ describe("approvals service", () => {
{ interfaceOrigin }
);
expect(service.isConnectionApproved).toHaveBeenCalledWith(
interfaceOrigin
interfaceOrigin,
undefined
);
await expect(promise).resolves.toBeDefined();
});
Expand Down
67 changes: 57 additions & 10 deletions apps/extension/src/background/approvals/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,20 +185,48 @@ export class ApprovalsService {
resolvers.reject(new Error("Sign Tx rejected"));
}

async isConnectionApproved(interfaceOrigin: string): Promise<boolean> {
async isConnectionApproved(
interfaceOrigin: string,
chainId?: string
): Promise<boolean> {
const approvedOrigins =
(await this.localStorage.getApprovedOrigins()) || [];

if (chainId) {
const currentChainId = await this.chainService.getChain();
if (chainId !== currentChainId) {
return false;
}
}

return approvedOrigins.includes(interfaceOrigin);
}

async approveConnection(interfaceOrigin: string): Promise<void> {
const alreadyApproved = await this.isConnectionApproved(interfaceOrigin);
async approveConnection(
interfaceOrigin: string,
chainId?: string
): Promise<void> {
const alreadyApproved = await this.isConnectionApproved(
interfaceOrigin,
chainId
);

const approveConnectionPopupProps: {
interfaceOrigin: string;
chainId?: string;
} = {
interfaceOrigin,
};

if (chainId) {
approveConnectionPopupProps["chainId"] = chainId;
}

if (!alreadyApproved) {
return this.launchApprovalPopup(TopLevelRoute.ApproveConnection, {
interfaceOrigin,
});
return this.launchApprovalPopup(
TopLevelRoute.ApproveConnection,
approveConnectionPopupProps
);
}

// A resolved promise is implicitly returned here if the origin had
Expand All @@ -208,13 +236,26 @@ export class ApprovalsService {
async approveConnectionResponse(
popupTabId: number,
interfaceOrigin: string,
allowConnection: boolean
allowConnection: boolean,
chainId?: string
): Promise<void> {
const resolvers = this.getResolver(popupTabId);

if (allowConnection) {
try {
await this.localStorage.addApprovedOrigin(interfaceOrigin);
if (
!(await this.localStorage.getApprovedOrigins())?.includes(
interfaceOrigin
)
) {
// Add approved origin if it hasn't been added
await this.localStorage.addApprovedOrigin(interfaceOrigin);
}

if (chainId) {
// Set approved signing chainId
await this.chainService.updateChain(chainId);
}
} catch (e) {
resolvers.reject(e);
}
Expand All @@ -224,8 +265,14 @@ export class ApprovalsService {
}
}

async approveDisconnection(interfaceOrigin: string): Promise<void> {
const isConnected = await this.isConnectionApproved(interfaceOrigin);
async approveDisconnection(
interfaceOrigin: string,
chainId?: string
): Promise<void> {
const isConnected = await this.isConnectionApproved(
interfaceOrigin,
chainId
);

if (isConnected) {
return this.launchApprovalPopup(TopLevelRoute.ApproveDisconnection, {
Expand Down
34 changes: 18 additions & 16 deletions apps/extension/src/provider/InjectedNamada.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
DerivedAccount,
Account,
Namada as INamada,
Signer as ISigner,
SignArbitraryProps,
Expand All @@ -11,30 +11,32 @@ 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<void> {
return await InjectedProxy.requestMethod<string, void>("connect");
public async connect(chainId?: string): Promise<void> {
return await InjectedProxy.requestMethod<string, void>("connect", chainId);
}

public async disconnect(): Promise<void> {
return await InjectedProxy.requestMethod<string, void>("disconnect");
public async disconnect(chainId?: string): Promise<void> {
return await InjectedProxy.requestMethod<string, void>(
"disconnect",
chainId
);
}

public async isConnected(): Promise<boolean> {
return await InjectedProxy.requestMethod<string, boolean>("isConnected");
public async isConnected(chainId?: string): Promise<boolean> {
return await InjectedProxy.requestMethod<string, boolean>(
"isConnected",
chainId
);
}

public async accounts(): Promise<DerivedAccount[]> {
return await InjectedProxy.requestMethod<string, DerivedAccount[]>(
"accounts"
);
public async accounts(): Promise<Account[]> {
return await InjectedProxy.requestMethod<string, Account[]>("accounts");
}

public async defaultAccount(): Promise<DerivedAccount> {
return await InjectedProxy.requestMethod<string, DerivedAccount>(
"defaultAccount"
);
public async defaultAccount(): Promise<Account> {
return await InjectedProxy.requestMethod<string, Account>("defaultAccount");
}

public async updateDefaultAccount(address: string): Promise<void> {
Expand Down
5 changes: 4 additions & 1 deletion apps/extension/src/provider/Namada.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { VaultService } from "background/vault";
import * as utils from "extension/utils";
import { VaultStorage } from "storage";
import { KVStoreMock, init } from "test/init";
import { toPublicAccount } from "utils";
import { ACTIVE_ACCOUNT, keyStore, password } from "./data.mock";

// Needed for now as utils import webextension-polyfill directly
Expand Down Expand Up @@ -60,6 +61,8 @@ describe("Namada", () => {
await utilityStore.set(PARENT_ACCOUNT_ID_KEY, ACTIVE_ACCOUNT);
const storedKeyStore = keyStore.map((store) => store.public);
const storedAccounts = await namada.accounts();
expect(storedAccounts).toEqual(storedKeyStore);
expect(storedAccounts).toEqual(
storedKeyStore.map((derivedAccount) => toPublicAccount(derivedAccount))
);
});
});
41 changes: 23 additions & 18 deletions apps/extension/src/provider/Namada.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
DerivedAccount,
Account,
Namada as INamada,
SignArbitraryProps,
SignArbitraryResponse,
Expand All @@ -8,7 +8,7 @@ import {
} from "@namada/types";
import { MessageRequester, Ports } from "router";

import { toEncodedTx } from "utils";
import { toEncodedTx, toPublicAccount } from "utils";
import {
ApproveConnectInterfaceMsg,
ApproveDisconnectInterfaceMsg,
Expand All @@ -28,43 +28,48 @@ export class Namada implements INamada {
protected readonly requester?: MessageRequester
) {}

public async connect(): Promise<void> {
public async connect(chainId?: string): Promise<void> {
return await this.requester?.sendMessage(
Ports.Background,
new ApproveConnectInterfaceMsg()
new ApproveConnectInterfaceMsg(chainId)
);
}

public async disconnect(): Promise<void> {
public async disconnect(chainId?: string): Promise<void> {
return await this.requester?.sendMessage(
Ports.Background,
new ApproveDisconnectInterfaceMsg(location.origin)
new ApproveDisconnectInterfaceMsg(location.origin, chainId)
);
}

public async isConnected(): Promise<boolean> {
public async isConnected(chainId?: string): Promise<boolean> {
if (!this.requester) {
throw new Error("no requester");
}

return await this.requester.sendMessage(
Ports.Background,
new IsConnectionApprovedMsg()
new IsConnectionApprovedMsg(chainId)
);
}

public async accounts(): Promise<DerivedAccount[] | undefined> {
return await this.requester?.sendMessage(
Ports.Background,
new QueryAccountsMsg()
);
public async accounts(): Promise<Account[] | undefined> {
return (
await this.requester?.sendMessage(
Ports.Background,
new QueryAccountsMsg()
)
)?.map(toPublicAccount);
}

public async defaultAccount(): Promise<DerivedAccount | undefined> {
return await this.requester?.sendMessage(
Ports.Background,
new QueryDefaultAccountMsg()
);
public async defaultAccount(): Promise<Account | undefined> {
return await this.requester
?.sendMessage(Ports.Background, new QueryDefaultAccountMsg())
.then((defaultAccount) => {
if (defaultAccount) {
return toPublicAccount(defaultAccount);
}
});
}

public async updateDefaultAccount(address: string): Promise<void> {
Expand Down
Loading

1 comment on commit b09a16e

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.