Skip to content

Commit

Permalink
Merge pull request #2 from penumbra-zone/lp-current-state-asset-rende…
Browse files Browse the repository at this point in the history
…rings

LP Current Status Functionality
  • Loading branch information
philipjames44 authored Mar 2, 2024
2 parents f2786db + 94aa2ee commit 6682e47
Show file tree
Hide file tree
Showing 19 changed files with 550 additions and 40 deletions.
20 changes: 19 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
"@radix-ui/react-icons": "^1.3.0",
"@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19",
"bignumber.js": "^9.1.2",
"framer-motion": "^11.0.5",
"grpc_tools_node_protoc_ts": "^5.3.3",
"grpc-tools": "^1.12.4",
"next": "^14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"zod": "^3.22.4"
},
"engines": {
"node": ">=18"
Expand Down
Binary file added public/assets/icons/penumbra.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/assets/icons/swap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const Layout = ({ children, pageTitle }: LayoutProps) => {
<title className={styles.title}>{pageTitle}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<header className={styles.header}>
<header className={styles.header} >
<HStack>
<Image
src="/favicon.ico"
Expand Down
141 changes: 136 additions & 5 deletions src/components/liquidityPositions/status.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
// components/LPStatus.js

import React from "react";
import { Box, VStack, Text, Divider, Badge, HStack } from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import {
Box,
VStack,
Text,
Divider,
Badge,
HStack,
Image,
Avatar,
} from "@chakra-ui/react";
import {
Position,
PositionState,
} from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb";
import { fromBaseUnit } from "../../utils/math/hiLo";
import { uint8ArrayToBase64 } from "../../utils/math/base64";
import { tokenConfigMapOnInner, Token } from "../../constants/tokenConstants";
import { fetchToken } from "../../utils/token/tokenFetch";
import BigNumber from "bignumber.js";

interface LPStatusProps {
nftId: string;
position: Position;
}

const LPStatus = ({ nftId, position }: LPStatusProps) => {
// Process position to human readable pieces
// First process position to human readable pieces

// Get status
const status = position.state;
Expand Down Expand Up @@ -43,16 +57,84 @@ const LPStatus = ({ nftId, position }: LPStatusProps) => {
// Get fee tier
const feeTier = position!.phi!.component!.fee;

const asset1 = position!.phi!.pair!.asset1;
const asset2 = position!.phi!.pair!.asset2;

// States for tokens
const [asset1Token, setAsset1Token] = useState<Token | undefined>();
const [asset2Token, setAsset2Token] = useState<Token | undefined>();
const [assetError, setAssetError] = useState<string | undefined>();

useEffect(() => {
// Function to fetch tokens asynchronously
const fetchTokens = async () => {
try {
const asset1 = position!.phi!.pair!.asset1;
const asset2 = position!.phi!.pair!.asset2;

if (asset1 && asset1.inner) {
const fetchedAsset1Token = await fetchToken(asset1.inner);
if (!fetchedAsset1Token) {
setAssetError("Asset 1 token not found");
throw new Error("Asset 1 token not found");
}
setAsset1Token(fetchedAsset1Token);
}

if (asset2 && asset2.inner) {
const fetchedAsset2Token = await fetchToken(asset2.inner);
if (!fetchedAsset2Token) {
setAssetError("Asset 2 token not found");
throw new Error("Asset 2 token not found");
}
setAsset2Token(fetchedAsset2Token);
}
} catch (error) {
console.error(error);
}
};

fetchTokens();
}, [position]);

if (!asset1Token || !asset2Token) {
return <div>{`LP exists, but ${assetError}.`}</div>;
}

const reserves1 = fromBaseUnit(
position!.reserves!.r1?.lo,
position!.reserves!.r1?.hi,
asset1Token.decimals
);

const reserves2 = fromBaseUnit(
position!.reserves!.r2?.lo,
position!.reserves!.r2?.hi,
asset2Token.decimals
);

const p: BigNumber = fromBaseUnit(
position!.phi!.component!.p!.lo,
position!.phi!.component!.p!.hi,
asset1Token.decimals
);
const q: BigNumber = fromBaseUnit(
position!.phi!.component!.q!.lo,
position!.phi!.component!.q!.hi,
asset2Token.decimals
);

return (
<Box
outline={".15em solid black"}
outline={".15em solid var(--complimentary-background)"}
borderRadius={".5em"}
padding={15}
width="fit-content"
backgroundImage={"var(--background-gradient)"}
>
<VStack width={"100%"}>
<Text>{nftId}</Text>
<HStack width={"100%"} justifyContent={"center"}>
<HStack width={"100%"} justifyContent={"center"} paddingTop={"1em"}>
<HStack>
<Badge colorScheme="blue">Status:</Badge>
<Text>{statusText}</Text>
Expand All @@ -63,6 +145,55 @@ const LPStatus = ({ nftId, position }: LPStatusProps) => {
<Text>{feeTier + "bps"}</Text>
</HStack>
</HStack>
<VStack width={"100%"} padding="1em" alignItems={"left"}>
{/* Asset 1 */}
<HStack>
<Avatar
name={asset1Token.symbol}
src={asset1Token.imagePath}
size="sm"
borderRadius="50%"
/>
<Text>
{`Sell ${Number.parseFloat(reserves1.toFixed(18))} ${
asset1Token.symbol
} @ ${Number.parseFloat(p.div(q).toFixed(18))} ${
asset2Token.symbol
} / ${asset1Token.symbol} `}
</Text>
</HStack>

{/* Swap Icon*/}
<HStack width={"100%"} justifyContent={"left"} paddingLeft=".15em">
<Image
src="/assets/icons/swap.svg"
alt="swap"
width="7"
paddingTop=".2em"
paddingBottom=".2em"
sx={{
transform: "rotate(90deg)",
}}
/>
</HStack>

{/* Asset 2 */}
<HStack>
<Avatar
name={asset2Token.symbol}
src={asset2Token.imagePath}
size="sm"
borderRadius="50%"
/>
<Text>
{`Sell ${Number.parseFloat(reserves2.toFixed(18))} ${
asset2Token.symbol
} @ ${Number.parseFloat(q.div(p).toFixed(18))} ${
asset1Token.symbol
} / ${asset2Token.symbol} `}
</Text>
</HStack>
</VStack>
</VStack>
</Box>
);
Expand Down
9 changes: 9 additions & 0 deletions src/constants/configConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface GrpcConfig {
grpcEndpoint: string;
}

const defaultPenumbraGrpcEndpoint = "https://grpc.testnet.penumbra.zone";

export const testnetConstants: GrpcConfig = {
grpcEndpoint: process.env.PENUMBRA_GRPC_ENDPOINT ? process.env.PENUMBRA_GRPC_ENDPOINT : defaultPenumbraGrpcEndpoint,
};
24 changes: 24 additions & 0 deletions src/constants/tokenConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface Token {
decimals: number;
symbol: string;
inner: string;
imagePath?: string;
}

export const tokenConfigMapOnInner: { [key: string]: Token } = {
"KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA=": {
decimals: 6,
symbol: "penumbra",
inner: "KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA=",
imagePath: "/assets/icons/penumbra.png",
},
};

// Recreate from tokenConfigMapOnInner to not have to redefine the same data
export const tokenConfigMapOnSymbol: { [key: string]: Token } =
Object.fromEntries(
Object.entries(tokenConfigMapOnInner).map(([key, value]) => [
value.symbol.toLowerCase(), // Lowercase to help index if we have discrepancies
value,
])
);
19 changes: 16 additions & 3 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import type { AppProps } from "next/app";
import "@/global.css";
import { ChakraProvider } from "@chakra-ui/react";
import { ChakraProvider, extendTheme } from "@chakra-ui/react";

function MyApp({ Component, pageProps }: AppProps) {

// May not necessarily be the best way to apply global styles
const theme = extendTheme({
styles: {
global: {
// Apply some styles globally across all elements
body: {
bg: "var(--charcoal)",
color: "var(--light-grey)",
fontFamily: "sans-serif",
fontWeight: "400",
},
},
},
});
return (
<ChakraProvider>
<ChakraProvider theme={theme}>
<Component {...pageProps} />
</ChakraProvider>
);
Expand Down
14 changes: 10 additions & 4 deletions src/pages/lp/[lp_nft_id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import styles from "../../../styles/Home.module.css";
import { LiquidityPositionQuerier } from "../../utils/protos/services/dex/liquidity-positions";
import { testnetConstants } from "../../utils/protos/constants";
import { testnetConstants } from "../../constants/configConstants";
import {
PositionId,
Position,
Expand Down Expand Up @@ -61,18 +61,24 @@ export default function LP() {
<>
<VStack width={"100%"} paddingTop={"4em"}>
<VStack>
<Text fontWeight={"bold"} width={"100%"} alignContent={"left"} fontSize={"1.5em"}>
<Text
fontWeight={"bold"}
width={"100%"}
alignContent={"left"}
fontSize={"1.5em"}
>
Position Status
</Text>
<LPStatus nftId={lp_nft_id} position={liquidityPosition} />
</VStack>
</VStack>
{/*
<br />
<br />
<br />
<h1>NFT ID: {lp_nft_id}</h1>
{/* todo */}
<p>{JSON.stringify(liquidityPosition)}</p> {/* todo */}
<p>{JSON.stringify(liquidityPosition)}</p>
*/}
</>
) : (
<p>No liquidity position found.</p>
Expand Down
29 changes: 29 additions & 0 deletions src/utils/math/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { validateSchema } from "./validation";
import { z } from "zod";

export const Base64StringSchema = z.string().refine(
(str) => {
// Regular expression that matches base64 strings
const base64Regex =
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
return base64Regex.test(str);
},
{
message: "Invalid base64 string",
}
);

export type Base64Str = z.infer<typeof Base64StringSchema>;

export const InnerBase64Schema = z.object({ inner: Base64StringSchema });

export const base64ToUint8Array = (base64: string): Uint8Array => {
const validated = validateSchema(Base64StringSchema, base64);
const binString = atob(validated);
return Uint8Array.from(binString, (byte) => byte.codePointAt(0)!);
};

export const uint8ArrayToBase64 = (byteArray: Uint8Array): string => {
const binString = String.fromCodePoint(...byteArray);
return btoa(binString);
};
Loading

0 comments on commit 6682e47

Please sign in to comment.