Skip to content

Commit

Permalink
fix: organize utils
Browse files Browse the repository at this point in the history
  • Loading branch information
jurevans committed Feb 13, 2024
1 parent d007863 commit 9b81184
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 173 deletions.
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
25 changes: 9 additions & 16 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,12 +120,11 @@ 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}`);
}
)) || {};
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);

Expand All @@ -158,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()
.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",
},
});
}
}
155 changes: 2 additions & 153 deletions apps/faucet/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,154 +1,3 @@
import { fromHex, toHex } from "@cosmjs/encoding";
import { sha256 } from "node-forge";
import {
ChallengeResponse,
Data,
SettingsResponse,
TransferResponse,
} from "./types";
/**
* Wrapper for fetch requests to handle ReadableStream response when errors are received from API
*/
export async function request<T = unknown>(
url: string,
options: RequestInit = { method: "GET" }
): Promise<T> {
return (await fetch(new URL(url), {
...options,
})
.then((response) => {
if (response.ok) {
return response.json();
}
const reader = response?.body?.getReader();
return reader
?.read()
.then((data) =>
Promise.reject(JSON.parse(new TextDecoder().decode(data.value)))
);
})
.catch((e) => {
console.error(e);
return Promise.reject(e);
})) as T;
}

/**
* Request faucet settings
*/
export const requestSettings = async (
url: string
): Promise<SettingsResponse> => {
return request(`${url}/setting`);
};

/**
* Request challenge from endpoint url
*
* @param {string} url
* @returns Object
*/
export const requestChallenge = async (
url: string,
publicKey: string
): Promise<ChallengeResponse> => {
return request(`${url}/challenge/${publicKey}`);
};

/**
* Submit a transfer request
*
* @param {string} url
* @param {Data} data
* @returns {Object}
*/
export const requestTransfer = async (
url: string,
data: Data
): Promise<TransferResponse> => {
return request(url, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
};

/**
* Validate solution
*
* @param {Uint8Array} solution
* @param {number} difficulty
* @returns {boolean}
*/
export const isValidPow = (
solution: Uint8Array,
difficulty: number
): boolean => {
for (let i = 0; i < difficulty; i++) {
if (solution[i] !== 0) {
return false;
}
}
return true;
};

/**
* Provided an integer, convert to bytes and pad
*
* @param {number} int
* @returns {Uint8Array}
*/
export const getSolutionBytes = (int: number): Uint8Array => {
const buffer = new ArrayBuffer(64);
const view = new DataView(buffer, 60, 4);
view.setInt32(0, int, false);

// Return solution byte array
return new Uint8Array(buffer);
};

/**
* Compute proof of work solution
*
* @param {string} challenge
* @param {number} difficulty
* @returns {Uint8Array}
*/
export const computePowSolution = (
challenge: string,
difficulty: number
): string => {
let i = 0;
let solution: string = "";

while (i >= 0) {
const solutionBytes = getSolutionBytes(i);

const solutionByteString = String.fromCharCode.apply(null, [
...solutionBytes,
]);
const challengeByteString = String.fromCharCode.apply(null, [
...fromHex(challenge),
]);

const hasher = sha256.create();
hasher.update(challengeByteString);
hasher.update(solutionByteString);

const digestHex = hasher.digest().toHex();
const hash = fromHex(digestHex);
const isValid = isValidPow(hash, difficulty);

if (isValid) {
solution = toHex(solutionBytes);
break;
}

i += 1;
}
return solution;
};

export * from "./api";
export * from "./pow";
export * from "./types";
Loading

0 comments on commit 9b81184

Please sign in to comment.