Skip to content

Commit

Permalink
feat: add ETH balance in USD (#30) (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
yum0e authored Nov 12, 2023
1 parent ccf68ed commit fbbb309
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 35 deletions.
2 changes: 2 additions & 0 deletions front/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SessionList from "@/components/SessionList";
import WCInput from "@/components/WCInput";
import PassKey from "@/components/PassKey";
import { SendTransaction } from "@/components/SendTransaction";
import Balance from "@/components/Balance";

export default async function Home() {
return (
Expand All @@ -14,6 +15,7 @@ export default async function Home() {
<SessionList />
<PassKey />
<SendTransaction />
<Balance />
</Box>
</Flex>
);
Expand Down
59 changes: 59 additions & 0 deletions front/src/app/api/price/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { stringify } from "viem";
import fs from "fs";

export async function GET(req: Request) {
const searchParams = new URL(req.url).searchParams;
const ids = searchParams.get("ids");
const currencies = searchParams.get("currencies");

const { isCached, priceCached } = getFromCache(ids, currencies);
if (isCached) {
return Response.json(JSON.parse(priceCached));
}

const price = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=${currencies}`,
);

const priceJson = await price.json();

saveToCache(ids, currencies, priceJson);

return Response.json(JSON.parse(stringify(priceJson)));
}

function saveToCache(
ids: string | null,
currencies: string | null,
priceJson: Response | null,
): void {
const key = `${ids}-${currencies}`;
// create cache folder if not exist
if (!fs.existsSync("./cache")) {
fs.mkdirSync("./cache");
}
// save to local files
fs.writeFileSync(`./cache/${key}.json`, JSON.stringify({ ...priceJson, timestamp: Date.now() }));
}

function getFromCache(
ids: string | null,
currencies: string | null,
): { isCached: boolean; priceCached: string } {
const key = `${ids}-${currencies}`;
// retrieve from local files
try {
const priceCached = fs.readFileSync(`./cache/${key}.json`, "utf8");
const priceCachedJson = JSON.parse(priceCached);

const timestamp = priceCachedJson.timestamp;
const now = Date.now();
// cache for 1 minute
if (now - timestamp > 1 * 60 * 1000) {
return { isCached: false, priceCached: "" };
}
return { isCached: true, priceCached };
} catch (e) {
return { isCached: false, priceCached: "" };
}
}
16 changes: 6 additions & 10 deletions front/src/app/api/users/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import { PUBLIC_CLIENT } from "@/constants/client";
import { FACTORY_ABI, FACTORY_ADDRESS } from "@/constants/factory";
import { Hex, createPublicClient, http, stringify, toHex } from "viem";
import { baseGoerli } from "viem/chains";
import { Hex, stringify, toHex } from "viem";

export async function GET(_req: Request, { params }: { params: { id: Hex } }) {
const { id } = params;
console.log(id);
if (!id) {
return Response.json(JSON.parse(stringify({ error: "id is required" })));
}

const publicClient = createPublicClient({
chain: baseGoerli,
transport: http(),
});

const user = await publicClient.readContract({
const user = await PUBLIC_CLIENT.readContract({
address: FACTORY_ADDRESS,
abi: FACTORY_ABI,
functionName: "getUser",
args: [BigInt(id)],
});

return Response.json(JSON.parse(stringify({ ...user, id: toHex(user.id) })));
const balance = await PUBLIC_CLIENT.getBalance({ address: user.account });

return Response.json(JSON.parse(stringify({ ...user, id: toHex(user.id), balance })));
}
21 changes: 12 additions & 9 deletions front/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ThemeProvider from "@/providers/ThemeProvider";
import { WalletConnectProvider } from "@/libs/wallet-connect";
import { SmartWalletProvider } from "@/libs/smart-wallet/SmartWalletProvider";
import { ModalProvider } from "@/providers/ModalProvider";
import { BalanceProvider } from "@/providers/BalanceProvider";

export const metadata = {
title: "HocusPocus XYZ",
Expand All @@ -14,15 +15,17 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
<html lang="en" suppressHydrationWarning>
<body>
<SmartWalletProvider>
<WalletConnectProvider>
<ThemeProvider attribute="class">
<Theme>
<ModalProvider>{children}</ModalProvider>
</Theme>
</ThemeProvider>
</WalletConnectProvider>
</SmartWalletProvider>
<BalanceProvider>
<SmartWalletProvider>
<WalletConnectProvider>
<ThemeProvider attribute="class">
<Theme>
<ModalProvider>{children}</ModalProvider>
</Theme>
</ThemeProvider>
</WalletConnectProvider>
</SmartWalletProvider>
</BalanceProvider>
</body>
</html>
);
Expand Down
28 changes: 28 additions & 0 deletions front/src/components/Balance/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { useBalance } from "@/providers/BalanceProvider";
import { Flex, Text } from "@radix-ui/themes";
import { CSSProperties, useEffect } from "react";

const css: CSSProperties = {
margin: "1rem",
};

export default function Balance() {
const KEY_ID = "0x9e925f1ff5b39500f805ff205534b589c72603c740b3de6975511818095eec36";

const { balance, getBalance } = useBalance();

useEffect(() => {
getBalance(KEY_ID);
console.log("balance", balance);
}, [balance, getBalance]);

return (
<Flex style={css} direction="column">
<Text highContrast={true} color="green" weight="bold" size="8">
$ {balance.toFixed(2)}
</Text>
</Flex>
);
}
7 changes: 5 additions & 2 deletions front/src/components/SendTransaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import { Chain, EstimateFeesPerGasReturnType, Hex, toHex } from "viem";
import { smartWallet } from "@/libs/smart-wallet";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Link } from "@radix-ui/themes";
import LoadingSpinner from "@/components/LoadingSpinner";
import { UserOpBuilder, emptyHex } from "@/libs/smart-wallet/service/userOps";
import { useBalance } from "@/providers/BalanceProvider";

smartWallet.init();
const builder = new UserOpBuilder(smartWallet.client.chain as Chain);
Expand All @@ -14,6 +15,8 @@ export function SendTransaction() {
const [txReceipt, setTxReceipt] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);

const KEY_ID = "0x9e925f1ff5b39500f805ff205534b589c72603c740b3de6975511818095eec36";

return (
<>
<form
Expand All @@ -39,7 +42,7 @@ export function SendTransaction() {
maxPriorityFeePerGas: maxPriorityFeePerGas as bigint,
// TODO: replace this with the keyId provided by the auth context
// this is the keyId for bigq
keyId: "0x9e925f1ff5b39500f805ff205534b589c72603c740b3de6975511818095eec36",
keyId: KEY_ID,
});

const hash = await smartWallet.sendUserOperation({ userOp });
Expand Down
7 changes: 7 additions & 0 deletions front/src/constants/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createPublicClient, http } from "viem";
import { baseGoerli } from "viem/chains";

export const PUBLIC_CLIENT = createPublicClient({
chain: baseGoerli,
transport: http(),
});
7 changes: 4 additions & 3 deletions front/src/libs/factory/getUser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Address, Hex } from "viem";

export async function getUser(
id: Hex,
): Promise<{ id: Hex; pubKey: { x: Hex; y: Hex }; account: Address }> {
export type User = { id: Hex; pubKey: { x: Hex; y: Hex }; account: Address; balance: bigint };

export async function getUser(id: Hex): Promise<User> {
const response = await fetch(`/api/users/${id}`, {
method: "GET",
});
Expand All @@ -15,5 +15,6 @@ export async function getUser(
y: user.publicKey[1],
},
account: user.account,
balance: user.balance,
};
}
14 changes: 3 additions & 11 deletions front/src/libs/smart-wallet/SmartWalletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,12 @@ const SmartWalletContext = React.createContext<UseSmartWallet | null>(null);
export const useWalletConnect = (): UseSmartWallet => {
const context = useContext(SmartWalletContext);
if (!context) {
throw new Error(
"useSmartWalletHook must be used within a SmartWalletProvider"
);
throw new Error("useSmartWalletHook must be used within a SmartWalletProvider");
}
return context;
};

export function SmartWalletProvider({
children,
}: {
children: React.ReactNode;
}) {
export function SmartWalletProvider({ children }: { children: React.ReactNode }) {
const smartWalletValue = useSmartWalletHook();
const wagmiConfig = createConfig({
autoConnect: true,
Expand All @@ -31,9 +25,7 @@ export function SmartWalletProvider({

return (
<WagmiConfig config={wagmiConfig}>
<SmartWalletContext.Provider value={smartWalletValue}>
{children}
</SmartWalletContext.Provider>
<SmartWalletContext.Provider value={smartWalletValue}>{children}</SmartWalletContext.Provider>
</WagmiConfig>
);
}
40 changes: 40 additions & 0 deletions front/src/providers/BalanceProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import { getUser } from "@/libs/factory/getUser";
import { createContext, useContext, useState } from "react";
import { Hex } from "viem";

function useBalanceHook() {
// balance in usd
const [balance, setBalance] = useState<number>(0);

async function getBalance(keyId: Hex) {
const user = await getUser(keyId);
const ethBalance = Number(user.balance) / 1e18;
const priceData = await fetch("/api/price?ids=ethereum&currencies=usd");
const price: number = (await priceData.json()).ethereum.usd;
setBalance(ethBalance * price);
}

return {
balance,
getBalance,
};
}

type UseBalanceHook = ReturnType<typeof useBalanceHook>;
const BalanceContext = createContext<UseBalanceHook | null>(null);

export const useBalance = (): UseBalanceHook => {
const context = useContext(BalanceContext);
if (!context) {
throw new Error("useBalanceHook must be used within a BalanceProvider");
}
return context;
};

export function BalanceProvider({ children }: { children: React.ReactNode }) {
const hook = useBalanceHook();

return <BalanceContext.Provider value={hook}>{children}</BalanceContext.Provider>;
}

0 comments on commit fbbb309

Please sign in to comment.