Skip to content

Commit

Permalink
feat/550 - Fix chain ID issues in Extension (#551)
Browse files Browse the repository at this point in the history
* feat: pass chain ID to integration connect()

* feat: initialize SDK inside of keyring, hook up chains

* feat: added update chain form, getChain integration, sdk service

* feat: sanitize inputs, add events for network change

* feat: hook up new chain in interface

* fix: bump extension version 0.1.0 -> 0.2.0
  • Loading branch information
jurevans authored Jan 8, 2024
1 parent 8f8e541 commit 2bf3a75
Show file tree
Hide file tree
Showing 40 changed files with 491 additions and 325 deletions.
2 changes: 1 addition & 1 deletion apps/extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@namada/extension",
"version": "0.1.0",
"version": "0.2.0",
"description": "Namada Browser Extension",
"repository": "https://github.com/anoma/namada-interface/",
"author": "Heliax Dev <[email protected]>",
Expand Down
4 changes: 2 additions & 2 deletions apps/extension/src/App/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import {
ViewMnemonic,
} from "./Accounts";
import { ParentAccounts } from "./Accounts/ParentAccounts";
import { ConnectedSites } from "./ConnectedSites";
import { ChangePassword } from "./Settings/ChangePassword";
import { ChangePassword, ConnectedSites, Network } from "./Settings";
import { Setup } from "./Setup";
import routes from "./routes";
import { LoadingStatus } from "./types";
Expand Down Expand Up @@ -134,6 +133,7 @@ export const AppContent = (): JSX.Element => {
/>
}
/>
<Route path={routes.network()} element={<Network />} />
{/* Routes that depend on a parent account existing in storage */}
{accounts.length > 0 && (
<>
Expand Down
6 changes: 6 additions & 0 deletions apps/extension/src/App/Common/AppHeaderNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ export const AppHeaderNavigation = ({
>
Connected Sites
</li>
<li
className={listItemClassList}
onClick={() => goTo(routes.network)}
>
Network
</li>
<li className={listItemClassList} onClick={onLock}>
Lock Wallet
</li>
Expand Down
File renamed without changes.
131 changes: 131 additions & 0 deletions apps/extension/src/App/Settings/Network.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { sanitize } from "dompurify";

import {
ActionButton,
Alert,
GapPatterns,
Heading,
Input,
Stack,
} from "@namada/components";
import { isUrlValid } from "@namada/utils";
import { UpdateChainMsg } from "background/chains";
import { useRequester } from "hooks/useRequester";
import { GetChainMsg } from "provider";
import React, { useCallback, useEffect, useState } from "react";
import { Ports } from "router";

enum Status {
Unsubmitted,
Pending,
Complete,
Failed,
}

export const Network = (): JSX.Element => {
const requester = useRequester();
const [chainId, setChainId] = useState("");
const [url, setUrl] = useState("");
const [status, setStatus] = useState<Status>(Status.Unsubmitted);
const [errorMessage, setErrorMessage] = useState("");

// TODO: Validate URL and sanitize inputs!
const shouldDisableSubmit = status === Status.Pending || !chainId || !url;

useEffect(() => {
(async () => {
try {
const chain = await requester.sendMessage(
Ports.Background,
new GetChainMsg()
);
if (!chain) {
throw new Error("Chain not found!");
}
const { chainId, rpc } = chain;
setUrl(rpc);
setChainId(chainId);
} catch (e) {
setErrorMessage(`${e}`);
}
})();
}, []);

const handleSubmit = useCallback(
async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
setStatus(Status.Pending);
setErrorMessage("");
const sanitizedChainId = sanitize(chainId);
const sanitizedUrl = sanitize(url);

// Validate sanitized chain ID
if (!sanitizedChainId) {
setErrorMessage("Invalid chain ID!");
setStatus(Status.Failed);
return;
}

// Validate sanitized URL
const isValidUrl = isUrlValid(sanitizedUrl);
if (!isValidUrl) {
setErrorMessage("Invalid URL!");
setStatus(Status.Failed);
return;
}

try {
await requester.sendMessage(
Ports.Background,
new UpdateChainMsg(sanitizedChainId, sanitizedUrl)
);
setStatus(Status.Complete);
} catch (err) {
setStatus(Status.Failed);
setErrorMessage(`${err}`);
}
},
[chainId, url]
);

return (
<Stack
as="form"
gap={GapPatterns.TitleContent}
onSubmit={handleSubmit}
full
>
<Heading className="uppercase text-2xl text-center text-white">
Network
</Heading>
<Stack full gap={GapPatterns.FormFields}>
<Input
label="Chain ID"
variant="Text"
type="text"
value={chainId}
onChange={(e) => setChainId(e.target.value)}
error={chainId.length === 0 ? "URL is required!" : ""}
/>
<Input
label="URL"
variant="Text"
value={url}
onChange={(e) => setUrl(e.target.value)}
error={url.length === 0 ? "URL is required!" : ""}
/>
{errorMessage && <Alert type="error">{errorMessage}</Alert>}
{status === Status.Complete && (
<Alert type="info">Successfully updated network!</Alert>
)}
</Stack>
<ActionButton
size="lg"
disabled={shouldDisableSubmit}
onSubmit={handleSubmit}
>
Submit
</ActionButton>
</Stack>
);
};
3 changes: 3 additions & 0 deletions apps/extension/src/App/Settings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./ChangePassword";
export * from "./ConnectedSites";
export * from "./Network";
2 changes: 1 addition & 1 deletion apps/extension/src/App/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default {
setup: (): string => `/setup`,
changePassword: (): string => `/change-password`,
connectedSites: (): string => `/connected-sites`,

network: (): string => `/network`,
viewAccountList: () => `/accounts/view`,
viewAccountMnemonic: (accountId: string = ":accountId") =>
`/accounts/mnemonic/${accountId}`,
Expand Down
51 changes: 11 additions & 40 deletions apps/extension/src/background/chains/handler.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,33 @@
import { Chain } from "@namada/types";
import { Handler, Env, Message, InternalHandler } from "router";
import { GetChainMsg } from "provider/messages";
import { Env, Handler, InternalHandler, Message } from "router";
import { UpdateChainMsg } from "./messages";
import { ChainsService } from "./service";
import { RemoveChainMsg } from "./messages";
import { SuggestChainMsg, GetChainsMsg, GetChainMsg } from "provider/messages";

type Writeable<T> = { -readonly [P in keyof T]: T[P] };

export const getHandler: (service: ChainsService) => Handler = (service) => {
return (env: Env, msg: Message<unknown>) => {
switch (msg.constructor) {
case GetChainMsg:
return handleGetChainMsg(service)(env, msg as GetChainMsg);
case GetChainsMsg:
return handleGetChainsMsg(service)(env, msg as GetChainsMsg);
case SuggestChainMsg:
return handleSuggestChainMsg(service)(env, msg as SuggestChainMsg);
case RemoveChainMsg:
return handleRemoveChainMsg(service)(env, msg as RemoveChainMsg);
case UpdateChainMsg:
return handleUpdateChainMsg(service)(env, msg as UpdateChainMsg);
default:
throw new Error("Unknown msg type");
}
};
};

const handleGetChainsMsg: (
service: ChainsService
) => InternalHandler<GetChainsMsg> = (service) => {
return async () => {
return await service.getChains();
};
};

const handleGetChainMsg: (
service: ChainsService
) => InternalHandler<GetChainMsg> = (service) => {
return async (_, msg) => {
return await service.getChain(msg.chainId);
};
};

const handleSuggestChainMsg: (
service: ChainsService
) => InternalHandler<SuggestChainMsg> = (service) => {
return async (env, msg) => {
if (await service.hasChain(msg.chain.chainId)) {
return;
}

const chain = msg.chain as Writeable<Chain>;
await service.suggestChain(env, chain, msg.origin);
return async () => {
return await service.getChain();
};
};

const handleRemoveChainMsg: (
const handleUpdateChainMsg: (
service: ChainsService
) => InternalHandler<RemoveChainMsg> = (service) => {
return async (_, msg) => {
await service.removeChain(msg.chainId);
return await service.getChains();
) => InternalHandler<UpdateChainMsg> = (service) => {
return async (_, { chainId, url }) => {
return await service.updateChain(chainId, url);
};
};
4 changes: 2 additions & 2 deletions apps/extension/src/background/chains/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from "./messages";
export * from "./handler";
export * from "./service";
export * from "./init";
export * from "./messages";
export * from "./service";
8 changes: 3 additions & 5 deletions apps/extension/src/background/chains/init.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { GetChainMsg } from "provider/messages";
import { Router } from "router";
import { ROUTE } from "./constants";
import { RemoveChainMsg } from "./messages";
import { SuggestChainMsg, GetChainsMsg, GetChainMsg } from "provider/messages";
import { getHandler } from "./handler";
import { UpdateChainMsg } from "./messages";
import { ChainsService } from "./service";

export function init(router: Router, service: ChainsService): void {
router.registerMessage(GetChainMsg);
router.registerMessage(GetChainsMsg);
router.registerMessage(SuggestChainMsg);
router.registerMessage(RemoveChainMsg);
router.registerMessage(UpdateChainMsg);

router.addHandler(ROUTE, getHandler(service));
}
18 changes: 11 additions & 7 deletions apps/extension/src/background/chains/messages.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import { Chain } from "@namada/types";
import { Message } from "router";
import { ROUTE } from "./constants";

enum MessageType {
RemoveChain = "remove-chain",
Connect = "connect",
UpdateChain = "update-chain",
}

export class RemoveChainMsg extends Message<Chain[]> {
export class UpdateChainMsg extends Message<void> {
public static type(): MessageType {
return MessageType.RemoveChain;
return MessageType.UpdateChain;
}

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

validate(): void {
if (!this.chainId) {
throw new Error("Chain ID not provided!");
}
if (!this.url) {
throw new Error("URL not provided!");
}
}

route(): string {
return ROUTE;
}

type(): string {
return RemoveChainMsg.type();
return UpdateChainMsg.type();
}
}
Loading

1 comment on commit 2bf3a75

@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.