diff --git a/backend/src/api/auth/strategies/jwt.strategy.ts b/backend/src/api/auth/strategies/jwt.strategy.ts
index c34a7b4..aad8962 100644
--- a/backend/src/api/auth/strategies/jwt.strategy.ts
+++ b/backend/src/api/auth/strategies/jwt.strategy.ts
@@ -17,7 +17,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
async validate(payload: any) {
return {
userId: payload.userId,
- isAdmin: payload.isAdmin
+ username: payload.username,
+ isAdmin: payload.isAdmin,
};
}
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 70fc7c3..1a8558d 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -17,6 +17,7 @@ import { ForbiddenPage } from "./pages/errors/ForbiddenPage";
import { ClientPages } from "./pages/client/ClientPages";
import { NavigationBar } from "./components/NavigationBar";
import { DashboardLayout } from "./components/Layout";
+import { Web3ContextProvider } from "./context/Web3Context";
function App() {
return (
@@ -32,21 +33,26 @@ function App() {
}}
>
-
-
-
- }>
- {AdminPages}
- {ClientPages}
- {AccountPages}
+
+
+
+
+ }>
+ {AdminPages}
+ {ClientPages}
+ {AccountPages}
-
- } />
+
+ } />
+
+ }
+ />
- } />
-
-
-
+
+
+
diff --git a/frontend/src/components/NavigationBar.tsx b/frontend/src/components/NavigationBar.tsx
index 297ffa3..b050d51 100644
--- a/frontend/src/components/NavigationBar.tsx
+++ b/frontend/src/components/NavigationBar.tsx
@@ -72,21 +72,31 @@ export const NavigationBar: React.FC = ({ ...props }) => {
)}
{!authContext.isAdmin && (
- navigate("/client/issue")}
- >
- Place order
-
+
+ navigate("/client/placeOrder")}
+ >
+ Place order
+
+ navigate("/client/holdings")}
+ >
+ Holdings
+
+
)}
{
fontSize: 14,
}}
>
- {authContext.userId}
+ {authContext.username}
diff --git a/frontend/src/constants/Assets.ts b/frontend/src/constants/Assets.ts
index c5e6f08..a4ba6fb 100644
--- a/frontend/src/constants/Assets.ts
+++ b/frontend/src/constants/Assets.ts
@@ -11,4 +11,6 @@ export const ASSETS = {
name: 'Notas do Tesouro Nacional ',
address: process.env.REACT_APP_TOKEN_NTN_ADDRESS
}
-}
\ No newline at end of file
+}
+
+export const SUPPORTED_ASSETS = Object.keys(ASSETS);
\ No newline at end of file
diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx
index de2ccfc..dc8717f 100644
--- a/frontend/src/context/AuthContext.tsx
+++ b/frontend/src/context/AuthContext.tsx
@@ -12,6 +12,7 @@ interface AuthContextPayloadType {
isAuthenticated: boolean;
userId: string;
+ username: string;
isAdmin: boolean;
}
@@ -48,7 +49,11 @@ const isTokenValid = () => {
return isFuture(new Date((decodedToken.exp || 0) * 1000));
};
-const decodeToken = (): { userId?: string; isAdmin?: boolean } & JWTPayload => {
+const decodeToken = (): {
+ userId?: string;
+ username?: string;
+ isAdmin?: boolean;
+} & JWTPayload => {
const token = localStorage.getItem(LocalStorage.AuthToken);
if (!token) {
@@ -64,6 +69,7 @@ const defaultContextData: AuthContextPayloadType = {
isAuthenticated: isTokenValid(),
isAdmin: decodeToken().isAdmin || false,
userId: decodeToken().userId || "",
+ username: decodeToken().username || "",
};
const AuthContext = React.createContext(
@@ -93,12 +99,14 @@ export const AuthContextProvider: React.FC> = ({
...prevState,
isAuthenticated: true,
isAdmin: decoded.isAdmin!,
+ username: decoded.username!,
userId: decoded.userId!,
}));
return {
isAdmin: decoded.isAdmin!,
userId: decoded.userId!,
+ username: decoded.username!,
isAuthenticated: true,
};
};
diff --git a/frontend/src/context/Web3Context.tsx b/frontend/src/context/Web3Context.tsx
new file mode 100644
index 0000000..06dae7e
--- /dev/null
+++ b/frontend/src/context/Web3Context.tsx
@@ -0,0 +1,137 @@
+import React from "react";
+import { ethers } from "ethers";
+import { useSnackbar } from "notistack";
+
+// ---- User state types ---- //
+
+interface Web3ContextValues {
+ connected: boolean;
+ provider?: ethers.providers.Web3Provider;
+ account?: string;
+ chainId?: number;
+ signer?: ethers.Signer;
+}
+
+interface Web3ContextFns {
+ connectWallet: () => Promise;
+}
+
+const Web3Context = React.createContext(
+ {} as any
+);
+
+export const Web3ContextProvider: React.FC> = ({
+ children,
+}) => {
+ const snackbar = useSnackbar();
+
+ const defaultWeb3ContextData = {
+ connected: false,
+ };
+
+ const [web3ContextState, setWeb3ContextState] =
+ React.useState(defaultWeb3ContextData);
+ const connectWallet = async () => {
+ if (web3ContextState.provider) {
+ try {
+ await web3ContextState.provider.send("eth_requestAccounts", []);
+ const signer = web3ContextState.provider.getSigner();
+ const account = await signer.getAddress();
+ const network = await web3ContextState.provider.getNetwork();
+
+ snackbar.enqueueSnackbar("Successfully connected account.", {
+ variant: "success",
+ });
+
+ setWeb3ContextState({
+ ...web3ContextState,
+ signer: signer,
+ account: account,
+ chainId: network.chainId,
+ connected: true,
+ });
+ } catch (e) {
+ snackbar.enqueueSnackbar("Failed to connect account.", {
+ variant: "error",
+ });
+ }
+ }
+ };
+ // endregion
+
+ // region Effects
+
+ React.useEffect(() => {
+ if ((window as any).ethereum) {
+ const provider = new ethers.providers.Web3Provider(
+ (window as any).ethereum,
+ "any"
+ );
+
+ const setAccountChanged = (accounts: any[]) => {
+ snackbar.enqueueSnackbar("Account change detected.", {
+ variant: "info",
+ });
+ if (accounts.length > 0) {
+ setWeb3ContextState((prev) => ({
+ ...prev,
+ account: accounts[0],
+ }));
+ } else {
+ setWeb3ContextState((prev) => ({
+ ...prev,
+ connected: false,
+ account: undefined,
+ }));
+ }
+ };
+ (window as any).ethereum.on("accountsChanged", setAccountChanged);
+
+ const setChainChanged = (chainId: string) => {
+ snackbar.enqueueSnackbar("Network change detected.", {
+ variant: "info",
+ });
+
+ setWeb3ContextState((prev) => ({
+ ...prev,
+ chainId: parseInt(chainId),
+ }));
+ };
+ (window as any).ethereum.on("chainChanged", setChainChanged);
+
+ setWeb3ContextState({
+ ...web3ContextState,
+ provider: provider,
+ });
+
+ return () => {
+ (window as any).ethereum?.removeListener(
+ "accountsChanged",
+ setAccountChanged
+ );
+ (window as any).ethereum?.removeListener(
+ "chainChanged",
+ setChainChanged
+ );
+ };
+ }
+
+ return;
+ }, []);
+
+ // Update the context on graphql data changes
+
+ // endregion
+
+ return (
+
+ );
+};
+
+export const useWeb3Context = () => React.useContext(Web3Context);
diff --git a/frontend/src/pages/admin/pages/OrdersPage.tsx b/frontend/src/pages/admin/pages/OrdersPage.tsx
index 1e2a145..2d3d0e1 100644
--- a/frontend/src/pages/admin/pages/OrdersPage.tsx
+++ b/frontend/src/pages/admin/pages/OrdersPage.tsx
@@ -8,16 +8,7 @@ import { AuthenticatedOnly } from "../../../components/AuthenticatedOnly";
import { OrdersDialog } from "./components/OrdersDialog";
import { SettleDialog } from "./components/SettleDialog";
import { OrdersExchange__factory } from "../../../typechain";
-
-const supportedTokens = [
- {
- name: "Letras Financeiras do Tesouro",
- address: "0xD73464667d5F2e15dd0A3C58C3610c39c1b1c2d4"
- }, {
- name: "Letras do Tesouro Nacional",
- address: "0x0d9D5372b5F889bCEcb930b1540f7D1595075177"
- }
-];
+import { ASSETS, SUPPORTED_ASSETS } from "../../../constants/Assets";
export const OrdersPage: React.FC = () => {
const [tokenDetails, setTokenDetails] = React.useState<{
@@ -36,13 +27,13 @@ export const OrdersPage: React.FC = () => {
amount: BigNumber;
recipient: string;
isBuyOrder: boolean;
- }[]
+ }[];
}>;
}>();
const [{ signer, address }, setSigner] = React.useState<{
- address?: string,
- signer?: ethers.Signer
+ address?: string;
+ signer?: ethers.Signer;
}>({});
const contract = React.useMemo(() => {
@@ -63,7 +54,7 @@ export const OrdersPage: React.FC = () => {
setSigner({
signer,
- address: await signer.getAddress()
+ address: await signer.getAddress(),
});
};
@@ -75,34 +66,37 @@ export const OrdersPage: React.FC = () => {
// Get the token details
const details = await contract.availableTokens(tokenAddress);
- const epochList: Record> & {
- orders: Awaited>
- }> = {};
+ const epochList: Record<
+ number,
+ Awaited> & {
+ orders: Awaited>;
+ }
+ > = {};
await Promise.all(
- Array.from(Array(details.currentEpoch.toNumber() + 1))
- .map(async (_, index) => {
- epochList[index] = {
- ...(await contract.epochDetails(tokenAddress, index)) as any,
- orders: (await contract.epochOrders(tokenAddress, index))
- };
- })
- );
-
- setTokenDetails({
+ Array.from(Array(details.currentEpoch.toNumber() + 1)).map(
+ async (_, index) => {
+ epochList[index] = {
+ ...((await contract.epochDetails(tokenAddress, index)) as any),
+ orders: await contract.epochOrders(tokenAddress, index),
+ };
+ }
+ )
+ );
+
+ setTokenDetails({
tokenDetails: {
tokenAddress,
- currentEpoch: details.currentEpoch
+ currentEpoch: details.currentEpoch,
},
epochList: Object.entries(epochList)
.map(([key, value]) => ({ id: Number(key), ...value }))
- .sort((a, b) => b.id - a.id) as any
+ .sort((a, b) => b.id - a.id) as any,
});
};
console.log(tokenDetails);
-
return (
{
mt: "64px",
display: "flex",
flexFlow: "column",
- alignItems: "center"
+ alignItems: "center",
}}
>
{!signer && (
-
+
)}
{signer && (
@@ -126,119 +116,121 @@ export const OrdersPage: React.FC = () => {
setTokenDetails(null as any)}
>
- Select an asset
-
-
-
- Connected to {address}
+ {tokenDetails ? "Go back to asset selection" : "Select an asset"}
- {!tokenDetails && supportedTokens.map((supportedToken) => (
-
-
- {supportedToken.name}
-
-
- (
+
- {supportedToken.address}
-
-
- ))}
+
+ {ASSETS[supportedToken as "LFT" | "LFN"].name}
+
+
+
+ {ASSETS[supportedToken as "LFT" | "LFN"].address}
+
+
+ ))}
{tokenDetails && (
value ? "Yes" : "No"
- }, {
- minWidth: 200,
- field: "valueBought",
- headerName: "Value Bought",
- renderCell: ({ value }) => (
- `~${ethers.utils.formatUnits(value, 6)} BRL`
- )
- }, {
- minWidth: 200,
- field: "amountSold",
- headerName: "Amount Sold",
- renderCell: ({ value }) => (
- `~${ethers.utils.formatUnits(value, 18)} units`
- )
- }, {
- minWidth: 200,
- field: "orders",
- headerName: "Orders in settlement",
- renderCell: ({ value }) => (
- value.length
- )
- }, {
- flex: 1,
- field: "id",
- headerName: "Actions",
- renderCell: ({ value, row }) => {
- return (
-
-
-
- {!row.settled && (
-
- )}
-
- );
- }
- }]}
+ columns={[
+ {
+ field: "settled",
+ headerName: "Is Settled",
+ renderCell: ({ value }) => (value ? "Yes" : "No"),
+ },
+ {
+ minWidth: 200,
+ field: "valueBought",
+ headerName: "Value Bought",
+ renderCell: ({ value }) =>
+ `~${ethers.utils.formatUnits(value, 6)} BRL`,
+ },
+ {
+ minWidth: 200,
+ field: "amountSold",
+ headerName: "Amount Sold",
+ renderCell: ({ value }) =>
+ `~${ethers.utils.formatUnits(value, 18)} units`,
+ },
+ {
+ minWidth: 200,
+ field: "orders",
+ headerName: "Orders in settlement",
+ renderCell: ({ value }) => value.length,
+ },
+ {
+ flex: 1,
+ field: "id",
+ headerName: "Actions",
+ renderCell: ({ value, row }) => {
+ return (
+
+
+
+ {!row.settled && (
+
+ )}
+
+ );
+ },
+ },
+ ]}
/>
)}
)}
-
-
- )
- ;
+ );
};
diff --git a/frontend/src/pages/client/ClientPages.tsx b/frontend/src/pages/client/ClientPages.tsx
index 5f3d10f..d769de3 100644
--- a/frontend/src/pages/client/ClientPages.tsx
+++ b/frontend/src/pages/client/ClientPages.tsx
@@ -1,8 +1,10 @@
import { Route } from "react-router-dom";
import { IssuePage } from "./pages/IssuePage";
+import { HoldingsPage } from "./pages/HoldingsPage";
export const ClientPages = (
- } />
+ } />
+ } />
);
diff --git a/frontend/src/pages/client/pages/HoldingsPage.tsx b/frontend/src/pages/client/pages/HoldingsPage.tsx
new file mode 100644
index 0000000..c545e6e
--- /dev/null
+++ b/frontend/src/pages/client/pages/HoldingsPage.tsx
@@ -0,0 +1,112 @@
+import React from "react";
+import { Box, Button, Typography } from "@mui/material";
+import { AuthenticatedOnly } from "../../../components/AuthenticatedOnly";
+import { ASSETS, SUPPORTED_ASSETS } from "../../../constants/Assets";
+import { BigNumber, ethers } from "ethers";
+import { ERC20Mock__factory } from "../../../typechain";
+import { useWeb3Context } from "../../../context/Web3Context";
+
+export const HoldingsPage: React.FC = () => {
+ const web3Context = useWeb3Context();
+
+ const [{ signer }, setSigner] = React.useState<{
+ address?: string;
+ signer?: ethers.Signer;
+ }>({});
+
+ const [balances, setBalances] = React.useState<{ [key: string]: string }>();
+
+ const fetchBalances = async () => {
+ const address = await signer!.getAddress();
+
+ let x = {};
+ for await (const asset of SUPPORTED_ASSETS) {
+ const contract = new ERC20Mock__factory()
+ .connect(signer!)
+ .attach(ASSETS[asset as "LFT" | "LFN"].address! as string);
+
+ const balance = await contract.balanceOf(address);
+
+ console.log(balance.toString());
+
+ x = {
+ ...x,
+ [asset]: balance.div(BigNumber.from(10).pow(18)).toString(),
+ };
+ }
+
+ setBalances(x);
+ };
+
+ React.useEffect(() => {
+ if (web3Context) {
+ setSigner({ signer: web3Context.signer, address: web3Context.account });
+ }
+ }, [web3Context]);
+
+ React.useEffect(() => {
+ if (signer) {
+ fetchBalances();
+ }
+ }, [signer]);
+
+ return (
+
+
+ {!signer && (
+
+ )}
+ {SUPPORTED_ASSETS.map((supportedToken) => (
+
+
+ {ASSETS[supportedToken as "LFT" | "LFN"].name}
+
+
+
+ {ASSETS[supportedToken as "LFT" | "LFN"].address}
+
+ {signer && balances && (
+
+ Balance: {balances[supportedToken]} {supportedToken}
+
+ )}
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/src/pages/client/pages/components/InteractionsWidget.tsx b/frontend/src/pages/client/pages/components/InteractionsWidget.tsx
index 1a62eeb..5b635f3 100644
--- a/frontend/src/pages/client/pages/components/InteractionsWidget.tsx
+++ b/frontend/src/pages/client/pages/components/InteractionsWidget.tsx
@@ -23,6 +23,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSnackbar } from "notistack";
import { LoadingButton } from "@mui/lab";
+import { useWeb3Context } from "../../../../context/Web3Context";
const Schema = z.object({
amount: z.number().nonnegative().int(),
@@ -32,6 +33,7 @@ type SchemaType = z.infer;
export const InteractionsWidget: React.FC = () => {
const snackbar = useSnackbar();
+ const web3Context = useWeb3Context();
const [value, setValue] = React.useState(0);
const [asset, setAsset] = React.useState<"LFT" | "LFN" | "NTN-F">("LFT");
@@ -110,19 +112,11 @@ export const InteractionsWidget: React.FC = () => {
}
};
- const connectWallet = async () => {
- const provider = new ethers.providers.Web3Provider(
- (window as any).ethereum
- );
-
- await provider.send("eth_requestAccounts", []);
- const signer = provider.getSigner();
-
- setSigner({
- signer,
- address: await signer.getAddress(),
- });
- };
+ React.useEffect(() => {
+ if (web3Context) {
+ setSigner({ signer: web3Context.signer, address: web3Context.account });
+ }
+ }, [web3Context]);
return (
@@ -165,7 +159,7 @@ export const InteractionsWidget: React.FC = () => {