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

refactor - Faucet: clean up faucet API & pow utils #627

Merged
merged 2 commits into from
Feb 13, 2024
Merged
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
13 changes: 9 additions & 4 deletions apps/faucet/src/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { FaucetForm } from "App/Faucet";
import { chains } from "@namada/chains";
import { useUntil } from "@namada/hooks";
import { Account, AccountType } from "@namada/types";
import { requestSettings } from "utils";
import { API } from "utils";
import dotsBackground from "../../public/bg-dots.svg";
import { CallToActionCard } from "./CallToActionCard";
import { CardsContainer } from "./Card.components";
Expand All @@ -42,6 +42,7 @@ const {

const apiUrl = isProxied ? `http://localhost:${proxyPort}/proxy` : faucetApiUrl;
const url = `${apiUrl}${faucetApiEndpoint}`;
const api = new API(url);
const limit = parseInt(faucetLimit);
const runFullNodeUrl = "https://docs.namada.net/operators/ledger";
const becomeBuilderUrl = "https://docs.namada.net/integrating-with-namada";
Expand All @@ -57,6 +58,7 @@ type AppContext = Settings & {
limit: number;
url: string;
settingsError?: string;
api: API;
};

const START_TIME_UTC = 1702918800;
Expand All @@ -81,6 +83,7 @@ export const AppContext = createContext<AppContext>({
...defaults,
limit,
url,
api,
});

enum ExtensionAttachStatus {
Expand Down Expand Up @@ -138,8 +141,9 @@ export const App: React.FC = () => {
// Fetch settings from faucet API
(async () => {
try {
const { difficulty, tokens_alias_to_address: tokens } =
await requestSettings(url).catch((e) => {
const { difficulty, tokens_alias_to_address: tokens } = await api
.settings()
.catch((e) => {
const message = e.errors?.message;
setSettingsError(
`Error requesting settings: ${message?.join(" ")}`
Expand Down Expand Up @@ -190,6 +194,7 @@ export const App: React.FC = () => {
settingsError,
limit,
url,
api,
...settings,
}}
>
Expand Down Expand Up @@ -225,9 +230,9 @@ export const App: React.FC = () => {
)}
{isExtensionConnected && (
<FaucetForm
isTestnetLive={isTestnetLive}
accounts={accounts}
integration={integration}
isTestnetLive={isTestnetLive}
/>
)}
{extensionAttachStatus === ExtensionAttachStatus.Installed &&
Expand Down
32 changes: 9 additions & 23 deletions apps/faucet/src/App/Faucet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ import { Namada } from "@namada/integrations";
import { Account } from "@namada/types";
import { bech32mValidation, shortenAddress } from "@namada/utils";

import {
TransferResponse,
computePowSolution,
requestChallenge,
requestTransfer,
} from "../utils";
import { TransferResponse, computePowSolution } from "../utils";
import { AppContext } from "./App";
import { InfoContainer } from "./App.components";
import {
Expand Down Expand Up @@ -48,7 +43,7 @@ export const FaucetForm: React.FC<Props> = ({
integration,
isTestnetLive,
}) => {
const { difficulty, settingsError, limit, tokens, url } =
const { api, difficulty, settingsError, limit, tokens } =
useContext(AppContext);

const accountLookup = accounts.reduce(
Expand All @@ -58,7 +53,6 @@ export const FaucetForm: React.FC<Props> = ({
},
{} as Record<string, Account>
);

const [account, setAccount] = useState<Account>(accounts[0]);
const [tokenAddress, setTokenAddress] = useState<string>();
const [amount, setAmount] = useState<number | undefined>(undefined);
Expand Down Expand Up @@ -126,22 +120,14 @@ export const FaucetForm: React.FC<Props> = ({
throw new Error("Account does not have a public key!");
}

const { challenge, tag } =
(await requestChallenge(url, account.publicKey).catch(
({ message, code }) => {
throw new Error(`${code} - ${message}`);
}
)) || {};
if (!tag || !challenge) {
throw new Error("Request challenge did not return a valid response");
}
const { challenge, tag } = await api
.challenge(account.publicKey)
.catch(({ message, code }) => {
throw new Error(`Unable to request challenge: ${code} - ${message}`);
});

const solution = computePowSolution(challenge, difficulty || 0);

if (!solution) {
throw new Error("A solution was not computed!");
}

const signer = integration.signer();
if (!signer) {
throw new Error("signer not defined");
Expand All @@ -165,10 +151,10 @@ export const FaucetForm: React.FC<Props> = ({
},
};

const response = await requestTransfer(url, submitData).catch((e) => {
const response = await api.submitTransfer(submitData).catch((e) => {
console.info(e);
const { code, message } = e;
throw new Error(`Unable to request transfer: ${code} ${message}`);
throw new Error(`Unable to submit transfer: ${code} ${message}`);
});

if (response.sent) {
Expand Down
89 changes: 89 additions & 0 deletions apps/faucet/src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
ChallengeResponse,
Data,
ErrorResponse,
SettingsResponse,
TransferResponse,
} from "./types";

enum Endpoint {
Settings = "/setting",
Challenge = "/challenge",
Transfer = "",
}

export class API {
constructor(protected readonly url: string) {}

/**
* Wrapper for fetch requests to handle ReadableStream response when errors are received from API
*
* @param {string} endpoint
* @param {RequestInit} options
*
* @returns Object
*/
async request<T = unknown>(
endpoint: string,
options: RequestInit = { method: "GET" }
): Promise<T> {
return await fetch(new URL(`${this.url}${endpoint}`), {
...options,
})
.then((response) => {
if (response.ok) {
return response.json();
}
const reader = response?.body?.getReader();
const errors = reader
?.read()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if it matters, but I think this can still return a resolved Promise<undefined> even though the methods that use this return Promise<ChallengeResponse> etc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, I was trying to figure out why TypeScript doesn't catch that. I'm not 100% sure, but if you add a return type to the then callback, it will catch it:

return (await fetch(new URL(`${this.url}${endpoint}`), {
  ...options,                                           
})                                                      
  .then((response): Promise<T> => {
TS2322: Type 'Promise<T> | undefined' is not assignable to type 'Promise<T>'.
Type 'undefined' is not assignable to type 'Promise<T>'.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! I'll take a look. I think in the case it can return undefined, we should throw an exception so it can be caught in the app. I wanted to remove any case of undefined as it's very tedious

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added a type for that error response, with { message, code }, so a correctly parsed error should always be this type, and in the case of undefined (if it can't get the reader and parse the error response) it will instead throw an error

.then(
(data): Promise<ErrorResponse> =>
Promise.reject(JSON.parse(new TextDecoder().decode(data.value)))
);
if (!errors) {
throw new Error("Unable to parse error response");
}
return errors;
})
.catch((e) => {
console.error(e);
return Promise.reject(e);
});
}

/**
* Request faucet settings
*
* @returns Object
*/
async settings(): Promise<SettingsResponse> {
return this.request(Endpoint.Settings);
}

/**
* Request challenge from endpoint url
*
* @param {string} publicKey
* @returns Object
*/
async challenge(publicKey: string): Promise<ChallengeResponse> {
return this.request(`${Endpoint.Challenge}/${publicKey}`);
}

/**
* Submit a transfer request
*
* @param {Data} data
* @returns {Object}
*/
async submitTransfer(data: Data): Promise<TransferResponse> {
return this.request(Endpoint.Transfer, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
}
}
Loading