Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: feat/1078 - Extension - Enable chain approval #1091

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion apps/extension/src/Approvals/ApproveConnection.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
import { ActionButton, Alert, GapPatterns, Stack } from "@namada/components";
import { Chain } from "@namada/types";
import { PageHeader } from "App/Common";
import { ConnectInterfaceResponseMsg } from "background/approvals";
import { useQuery } from "hooks";
import { useRequester } from "hooks/useRequester";
import { GetChainMsg } from "provider";
import { useEffect, useState } from "react";
import { Ports } from "router";
import { closeCurrentTab } from "utils";

export const ApproveConnection: React.FC = () => {
const requester = useRequester();
const params = useQuery();
const interfaceOrigin = params.get("interfaceOrigin");
const chainId = params.get("chainId") || undefined;
const [chain, setChain] = useState<Chain>();

const fetchChain = async (): Promise<Chain> => {
const chainResponse = await requester.sendMessage(
Ports.Background,
new GetChainMsg()
);
return chainResponse;
};

useEffect(() => {
if (chainId) {
fetchChain()
.then((chain) => {
setChain(chain);
})
.catch((e) => console.error(e));
}
}, [chainId]);

const handleResponse = async (allowConnection: boolean): Promise<void> => {
if (interfaceOrigin) {
await requester.sendMessage(
Ports.Background,
new ConnectInterfaceResponseMsg(interfaceOrigin, allowConnection)
new ConnectInterfaceResponseMsg(
interfaceOrigin,
allowConnection,
chainId
)
);
await closeCurrentTab();
}
Expand All @@ -28,6 +55,9 @@ export const ApproveConnection: React.FC = () => {
<Alert type="warning">
Approve connection for <strong>{interfaceOrigin}</strong>?
</Alert>
{chainId && chain && chain.chainId !== chainId && (
<Alert type="warning">Enable signing for {chainId}?</Alert>
)}
<Stack gap={2}>
<ActionButton onClick={() => handleResponse(true)}>
Approve
Expand Down
9 changes: 5 additions & 4 deletions apps/extension/src/background/approvals/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ const handleIsConnectionApprovedMsg: (
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,12 +135,13 @@ const handleConnectInterfaceResponseMsg: (
) => InternalHandler<ConnectInterfaceResponseMsg> = (service) => {
return async (
{ senderTabId: popupTabId },
{ interfaceOrigin, allowConnection }
{ interfaceOrigin, allowConnection, chainId }
) => {
return await service.approveConnectionResponse(
popupTabId,
interfaceOrigin,
allowConnection
allowConnection,
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
39 changes: 31 additions & 8 deletions apps/extension/src/background/approvals/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,38 @@ 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()) || [];

return approvedOrigins.includes(interfaceOrigin);
const { chainId: currentChainId } = await this.chainService.getChain();
const isChainIdMatched = chainId ? chainId === currentChainId : true;

return approvedOrigins.includes(interfaceOrigin) && isChainIdMatched;
}

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 params: Record<string, string> = {
interfaceOrigin,
};

if (chainId) {
params.chainId = chainId;
}

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

// A resolved promise is implicitly returned here if the origin had
Expand All @@ -207,13 +225,18 @@ 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 (chainId) {
await this.chainService.updateChain(chainId);
}
} catch (e) {
resolvers.reject(e);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/extension/src/provider/InjectedNamada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { Signer } from "./Signer";
export class InjectedNamada implements INamada {
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> {
Expand Down
4 changes: 2 additions & 2 deletions apps/extension/src/provider/Namada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ 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)
);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/extension/src/provider/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class ApproveConnectInterfaceMsg extends Message<void> {
return MessageType.ApproveConnectInterface;
}

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

Expand Down
11 changes: 9 additions & 2 deletions apps/namadillo/src/App/Common/ConnectExtensionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { ActionButton } from "@namada/components";
import { useUntilIntegrationAttached } from "@namada/integrations";
import { chainParametersAtom } from "atoms/chain";
import { namadaExtensionConnectedAtom } from "atoms/settings";
import { useExtensionConnect } from "hooks/useExtensionConnect";
import { useAtomValue } from "jotai";

export const ConnectExtensionButton = (): JSX.Element => {
const extensionAttachStatus = useUntilIntegrationAttached();
const isExtensionConnected = useAtomValue(namadaExtensionConnectedAtom);
const { connect } = useExtensionConnect();
const { data: chain } = useAtomValue(chainParametersAtom);
const chainId = chain?.chainId || "";
const { connect } = useExtensionConnect("namada", chainId);

return (
<>
{extensionAttachStatus === "attached" && !isExtensionConnected && (
<ActionButton backgroundColor="yellow" size="sm" onClick={connect}>
<ActionButton
backgroundColor="yellow"
size="sm"
onClick={() => connect(chainId)}
>
Connect Keychain
</ActionButton>
)}
Expand Down
12 changes: 7 additions & 5 deletions apps/namadillo/src/hooks/useExtensionConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,32 @@ import { useEffect } from "react";
type UseConnectOutput = {
connectionStatus: ConnectStatus;
isConnected: boolean;
connect: () => Promise<void>;
connect: (chainId: string) => Promise<void>;
};

export const useExtensionConnect = (
chainKey: ChainKey = "namada"
chainKey: ChainKey = "namada",
chainId?: string
): UseConnectOutput => {
const [connectionStatus, setConnectionStatus] = useAtom(
namadaExtensionConnectionStatus
);

const [_integration, isConnectingToExtension, withConnection] =
useIntegrationConnection(chainKey);
useIntegrationConnection(chainKey, chainId);

useEffect(() => {
if (isConnectingToExtension) {
setConnectionStatus("connecting");
}
}, [isConnectingToExtension]);

const handleConnectExtension = async (): Promise<void> => {
const handleConnectExtension = async (chainId: string): Promise<void> => {
if (connectionStatus === "connected") return;
withConnection(
() => setConnectionStatus("connected"),
() => setConnectionStatus("error")
() => setConnectionStatus("error"),
chainId
);
};

Expand Down
4 changes: 2 additions & 2 deletions packages/integrations/src/Namada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export default class Namada implements Integration<Account, Signer> {
return !!this._namada;
}

public async connect(): Promise<void> {
await this._namada?.connect();
public async connect(chainId?: string): Promise<void> {
await this._namada?.connect(chainId);
}

public async disconnect(): Promise<void> {
Expand Down
20 changes: 11 additions & 9 deletions packages/integrations/src/hooks/useIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { ChainKey, ExtensionKey } from "@namada/types";

type ExtensionConnection<T, U> = (
onSuccess: () => T,
onFail?: () => U
onFail?: () => U,
chainId?: string
) => Promise<void>;

type IntegrationFromExtensionKey<K extends ExtensionKey> =
Expand Down Expand Up @@ -60,21 +61,22 @@ export const useIntegration = <K extends ChainKey>(
* Tuple of integration, connection status and connection function.
*/
export const useIntegrationConnection = <TSuccess, TFail, K extends ChainKey>(
chainKey: K
chainKey: K,
chainId?: string
): [
IntegrationFromChainKey<K>,
boolean,
ExtensionConnection<TSuccess, TFail>,
] => {
IntegrationFromChainKey<K>,
boolean,
ExtensionConnection<TSuccess, TFail>,
] => {
const integration = useIntegration(chainKey);
const [isConnectingToExtension, setIsConnectingToExtension] = useState(false);

const connect: ExtensionConnection<TSuccess, TFail> = useCallback(
async (onSuccess, onFail) => {
async (onSuccess, onFail, chainId) => {
setIsConnectingToExtension(true);
try {
if (integration.detect()) {
await integration.connect();
await integration.connect(chainId);
await onSuccess();
}
} catch {
Expand All @@ -84,7 +86,7 @@ export const useIntegrationConnection = <TSuccess, TFail, K extends ChainKey>(
}
setIsConnectingToExtension(false);
},
[chainKey]
[chainKey, chainId]
);

return [integration, isConnectingToExtension, connect];
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/namada.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type BalancesProps = {

export interface Namada {
accounts(chainId?: string): Promise<DerivedAccount[] | undefined>;
connect(): Promise<void>;
connect(chainId?: string): Promise<void>;
disconnect(): Promise<void>;
isConnected(): Promise<boolean | undefined>;
defaultAccount(chainId?: string): Promise<DerivedAccount | undefined>;
Expand Down
Loading