Skip to content

Commit

Permalink
feat: Faucet - Make extension connection optional (#1089)
Browse files Browse the repository at this point in the history
* feat: make extension optional

* fix: remove unneeded message
  • Loading branch information
jurevans authored Sep 13, 2024
1 parent 2c0bc92 commit 18791db
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 98 deletions.
81 changes: 4 additions & 77 deletions apps/faucet/src/App/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { createContext, useCallback, useEffect, useState } from "react";
import React, { createContext, useEffect, useState } from "react";
import { GoGear } from "react-icons/go";
import { ThemeProvider } from "styled-components";

import { ActionButton, Alert, Modal } from "@namada/components";
import { Namada } from "@namada/integrations";
import { Alert, Modal } from "@namada/components";
import { ColorMode, getTheme } from "@namada/utils";

import {
Expand All @@ -20,9 +19,6 @@ import {
} from "App/App.components";
import { FaucetForm } from "App/Faucet";

import { chains } from "@namada/chains";
import { useUntil } from "@namada/hooks";
import { Account } from "@namada/types";
import { API, toNam } from "utils";
import dotsBackground from "../../public/bg-dots.svg";
import {
Expand Down Expand Up @@ -82,21 +78,9 @@ const START_TIME_TEXT = new Date(START_TIME_UTC * 1000).toLocaleString(

export const AppContext = createContext<AppContext | null>(null);

enum ExtensionAttachStatus {
PendingDetection,
NotInstalled,
Installed,
}

export const App: React.FC = () => {
const initialColorMode = "dark";
const chain = chains.namada;
const integration = new Namada(chain);
const [extensionAttachStatus, setExtensionAttachStatus] = useState(
ExtensionAttachStatus.PendingDetection
);
const [isExtensionConnected, setIsExtensionConnected] = useState(false);
const [accounts, setAccounts] = useState<Account[]>([]);

const [colorMode, _] = useState<ColorMode>(initialColorMode);
const [isTestnetLive, setIsTestnetLive] = useState(true);
const [settings, setSettings] = useState<Settings>({
Expand Down Expand Up @@ -129,20 +113,6 @@ export const App: React.FC = () => {
});
};

useUntil(
{
predFn: async () => Promise.resolve(integration.detect()),
onSuccess: () => {
setExtensionAttachStatus(ExtensionAttachStatus.Installed);
},
onFail: () => {
setExtensionAttachStatus(ExtensionAttachStatus.NotInstalled);
},
},
{ tries: 5, ms: 300 },
[integration]
);

useEffect(() => {
// Sync url to localStorage
localStorage.setItem("baseUrl", url);
Expand All @@ -168,27 +138,6 @@ export const App: React.FC = () => {
.catch((e) => setSettingsError(`Failed to load settings! ${e}`));
}, [url]);

const handleConnectExtensionClick = useCallback(async (): Promise<void> => {
if (integration) {
try {
const isIntegrationDetected = integration.detect();

if (!isIntegrationDetected) {
throw new Error("Extension not installed!");
}

await integration.connect();
const accounts = await integration.accounts();
if (accounts) {
setAccounts(accounts.filter((account) => !account.isShielded));
}
setIsExtensionConnected(true);
} catch (e) {
console.error(e);
}
}
}, [integration]);

return (
<AppContext.Provider
value={{
Expand Down Expand Up @@ -227,29 +176,7 @@ export const App: React.FC = () => {
</InfoContainer>
)}

{extensionAttachStatus ===
ExtensionAttachStatus.PendingDetection && (
<InfoContainer>
<Alert type="info">Detecting extension...</Alert>
</InfoContainer>
)}
{extensionAttachStatus === ExtensionAttachStatus.NotInstalled && (
<InfoContainer>
<Alert type="error">You must download the extension!</Alert>
</InfoContainer>
)}

{isExtensionConnected && (
<FaucetForm accounts={accounts} isTestnetLive={isTestnetLive} />
)}
{extensionAttachStatus === ExtensionAttachStatus.Installed &&
!isExtensionConnected && (
<InfoContainer>
<ActionButton onClick={handleConnectExtensionClick}>
Connect to Namada Extension
</ActionButton>
</InfoContainer>
)}
<FaucetForm isTestnetLive={isTestnetLive} />
</FaucetContainer>
{isModalOpen && (
<Modal onClose={() => setIsModalOpen(false)}>
Expand Down
127 changes: 106 additions & 21 deletions apps/faucet/src/App/Faucet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import {
Alert,
AmountInput,
Input,
Option,
Select,
} from "@namada/components";
import { Account } from "@namada/types";
import { bech32mValidation, shortenAddress } from "@namada/utils";

import { chains } from "@namada/chains";
import { useUntil } from "@namada/hooks";
import { Namada } from "@namada/integrations";
import { Data, PowChallenge, TransferResponse } from "../utils";
import { AppContext } from "./App";
import {
Expand All @@ -33,18 +37,28 @@ enum Status {
}

type Props = {
accounts: Account[];
isTestnetLive: boolean;
};

const bech32mPrefix = "tnam";

export const FaucetForm: React.FC<Props> = ({ accounts, isTestnetLive }) => {
enum ExtensionAttachStatus {
PendingDetection,
NotInstalled,
Installed,
}

export const FaucetForm: React.FC<Props> = ({ isTestnetLive }) => {
const {
api,
settings: { difficulty, tokens, withdrawLimit },
} = useContext(AppContext)!;
const [extensionAttachStatus, setExtensionAttachStatus] = useState(
ExtensionAttachStatus.PendingDetection
);
const [isExtensionConnected, setIsExtensionConnected] = useState(false);

const [accounts, setAccounts] = useState<Account[]>([]);
const accountLookup = accounts.reduce(
(acc, account) => {
acc[account.address] = account;
Expand All @@ -53,24 +67,37 @@ export const FaucetForm: React.FC<Props> = ({ accounts, isTestnetLive }) => {
{} as Record<string, Account>
);

const [account, setAccount] = useState<Account>(accounts[0]);
const chain = chains.namada;
const integration = new Namada(chain);
const [account, setAccount] = useState<Account>();
const [accountsSelectData, setAccountsSelectData] = useState<
Option<string>[]
>([]);
const [target, setTarget] = useState<string>();
const [tokenAddress, setTokenAddress] = useState<string>();
const [amount, setAmount] = useState<number | undefined>(undefined);
const [error, setError] = useState<string>();
const [status, setStatus] = useState(Status.Completed);
const [statusText, setStatusText] = useState<string>();
const [responseDetails, setResponseDetails] = useState<TransferResponse>();

const accountsSelectData = accounts.map(({ alias, address }) => ({
label: `${alias} - ${shortenAddress(address)}`,
value: address,
}));

const powSolver: Worker = useMemo(
() => new Worker(new URL("../workers/powWorker.ts", import.meta.url)),
[]
);

useEffect(() => {
if (accounts) {
setAccountsSelectData(
accounts.map(({ alias, address }) => ({
label: `${alias} - ${shortenAddress(address)}`,
value: address,
}))
);
setAccount(accounts[0]);
}
}, [accounts]);

useEffect(() => {
if (tokens?.NAM) {
setTokenAddress(tokens.NAM);
Expand All @@ -81,7 +108,7 @@ export const FaucetForm: React.FC<Props> = ({ accounts, isTestnetLive }) => {
Boolean(tokenAddress) &&
Boolean(amount) &&
(amount || 0) <= withdrawLimit &&
Boolean(account) &&
Boolean(target) &&
status !== Status.PendingPowSolution &&
status !== Status.PendingTransfer &&
typeof difficulty !== "undefined" &&
Expand Down Expand Up @@ -127,7 +154,7 @@ export const FaucetForm: React.FC<Props> = ({ accounts, isTestnetLive }) => {
async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (
!account ||
!target ||
!amount ||
!tokenAddress ||
typeof difficulty === "undefined"
Expand All @@ -145,9 +172,9 @@ export const FaucetForm: React.FC<Props> = ({ accounts, isTestnetLive }) => {
return;
}

if (!account) {
if (!target) {
setStatus(Status.Error);
setError("No account found!");
setError("No target specified!");
return;
}

Expand Down Expand Up @@ -175,7 +202,7 @@ export const FaucetForm: React.FC<Props> = ({ accounts, isTestnetLive }) => {
tag,
challenge,
transfer: {
target: account.address,
target,
token: sanitizedToken,
amount: amount * 1_000_000,
},
Expand All @@ -190,29 +217,87 @@ export const FaucetForm: React.FC<Props> = ({ accounts, isTestnetLive }) => {
[account, tokenAddress, amount]
);

useUntil(
{
predFn: async () => Promise.resolve(integration.detect()),
onSuccess: () => {
setExtensionAttachStatus(ExtensionAttachStatus.Installed);
},
onFail: () => {
setExtensionAttachStatus(ExtensionAttachStatus.NotInstalled);
},
},
{ tries: 5, ms: 300 },
[integration]
);

const handleConnectExtensionClick = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
e.preventDefault();
if (integration) {
try {
const isIntegrationDetected = integration.detect();

if (!isIntegrationDetected) {
throw new Error("Extension not installed!");
}

await integration.connect();
const accounts = await integration.accounts();
if (accounts) {
setAccounts(accounts.filter((account) => !account.isShielded));
}
setIsExtensionConnected(true);
} catch (e) {
console.error(e);
}
}
},
[integration]
);

useEffect(() => {
if (account) {
setTarget(account.address);
}
}, [account]);

return (
<FaucetFormContainer>
<InputContainer>
{accounts.length > 0 ?
{account && accounts.length && (
<Select
data={accountsSelectData}
value={account.address}
label="Account"
label="Target"
onChange={(e) => setAccount(accountLookup[e.target.value])}
/>
: <div>
You have no signing accounts! Import or create an account in the
extension, then reload this page.
</div>
}
)}
</InputContainer>

<InputContainer>
<Input
label="Target Address"
value={target}
onChange={(e) => setTarget(e.target.value)}
autoFocus={true}
/>
</InputContainer>

{extensionAttachStatus === ExtensionAttachStatus.Installed &&
!isExtensionConnected && (
<InputContainer>
<ActionButton onClick={handleConnectExtensionClick}>
Load Accounts from Extension
</ActionButton>
</InputContainer>
)}

<InputContainer>
<Input
label="Token Address (defaults to NAM)"
value={tokenAddress}
onChange={(e) => setTokenAddress(e.target.value)}
autoFocus={true}
/>
</InputContainer>

Expand Down

1 comment on commit 18791db

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