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

Dynamic client side envs 2 #80

Merged
merged 15 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PENUMBRA_GRPC_ENDPOINT=
PENUMBRA_INDEXER_ENDPOINT=
PENUMBRA_INDEXER_CA_CERT=
PENUMBRA_CHAIN_ID=
PENUMBRA_CUILOA_URL=
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ or plug in credentials for an already running database via environment variables
# add these to e.g. `.envrc`:
export PENUMBRA_GRPC_ENDPOINT="https://testnet.plinfra.net"
export PENUMBRA_INDEXER_ENDPOINT="postgresql://<PGUSER>:<PGPASS>@<PGHOST>:<PGPORT>/<PGDATABASE>?sslmode=require""
export NEXT_PUBLIC_CHAIN_ID="penumbra-testnet-phobos-2"
export PENUMBRA_CHAIN_ID="penumbra-testnet-phobos-2"
# optional: if you see "self-signed certificate in certificate chain" errors,
# you'll likely need to export a `ca-cert.pem` file for the DB TLS.
# export PENUMBRA_INDEXER_CA_CERT="$(cat ca-cert.pem)"
Expand All @@ -49,8 +49,8 @@ you'll want to set are:
* `PENUMBRA_GRPC_ENDPOINT`: the URL to a remote node's `pd` gRPC service
* `PENUMBRA_INDEXER_ENDPOINT`: the URL to a Postgre database containing ABCI events
* `PENUMBRA_INDEXER_CA_CERT`: optional; if set, the database connection will use the provided certificate authority when validating TLS
* `NEXT_PUBLIC_CHAIN_ID`: the chain id for the network being indexed, controls asset-registry lookups
* `NEXT_PUBLIC_CUILOA_URL`: the URL for a block-explorer application, for generating URLs for more block/transaction info
* `PENUMBRA_CHAIN_ID`: the chain id for the network being indexed, controls asset-registry lookups
* `PENUMBRA_CUILOA_URL`: the URL for a block-explorer application, for generating URLs for more block/transaction info

## Name

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@rehooks/component-size": "^1.0.3",
"@styled-icons/octicons": "^10.47.0",
"@tanstack/react-query": "^5.59.0",
"@tsconfig/strictest": "^2.0.5",
"@tsconfig/vite-react": "^3.0.2",
"@vercel/analytics": "^1.3.1",
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

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

12 changes: 12 additions & 0 deletions src/fetchers/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { ClientEnv } from '@/utils/env/types';

export const useEnv = () => {
return useQuery({
queryKey: ['clientEnv'],
queryFn: async (): Promise<ClientEnv> => {
return fetch('/api/env').then(resp => resp.json() as unknown as ClientEnv);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: In async functions, I find sticking to async await to be more readable than promise chaining:

    queryFn: async (): Promise<ClientEnv> => {
      const res = await fetch('/api/env');
      return (await res.json()) as ClientEnv;
    },

},
staleTime: Infinity,
});
};
21 changes: 21 additions & 0 deletions src/fetchers/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ChainRegistryClient, Registry } from '@penumbra-labs/registry';
import { useQuery } from '@tanstack/react-query';
import { useEnv } from './env';

export const chainRegistryClient = new ChainRegistryClient();

export const useRegistry = () => {
const { data: env } = useEnv();
const chainId = env?.PENUMBRA_CHAIN_ID;

return useQuery({
queryKey: ['penumbraRegistry', chainId],
queryFn: async (): Promise<Registry | null> => {
if (!chainId) {
return null;
Copy link
Contributor

@grod220 grod220 Oct 7, 2024

Choose a reason for hiding this comment

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

issue: in general, let's try to avoid null when we can. This can go into bike shedding territory, but I'm in the camp that it is a mistake in the language.

There is another issue though, the returned loading and error states from useRegistry are not quite true. We should combine them together:

export const useRegistry = () => {
  const { data: env, isLoading: isEnvLoading, error: envError } = useEnv();

  const {
    data: registry,
    isLoading: isRegistryLoading,
    error: registryError,
  } = useQuery({
    queryKey: ['penumbraRegistry', env],
    queryFn: async () => {
      const chainId = env?.PENUMBRA_CHAIN_ID;
      if (!chainId) {
        throw new Error('chain id not available to query registry');
      }
      return chainRegistryClient.remote.get(chainId);
    },
    staleTime: Infinity,
    enabled: Boolean(env),
  });

  return {
    registry,
    isLoading: isEnvLoading || isRegistryLoading,
    error: envError || registryError,
  };
};

}
return chainRegistryClient.remote.get(chainId);
},
staleTime: Infinity,
});
};
48 changes: 48 additions & 0 deletions src/fetchers/tokenAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useRegistry } from "./registry";
import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { decimalsFromDenomUnits, imagePathFromAssetImages } from '@/old/utils/token/tokenFetch'
import { uint8ArrayToBase64, base64ToUint8Array } from '@/old/utils/math/base64';
import { Token } from '@/old/utils/types/token';

export const useTokenAssets = (): Token[] => {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: with useRegistry() returning data/loading/error, this hook should also return the same so that can properly be handled by consumers if desired.

const { data: registry } = useRegistry();
if (!registry) {
return [];
}

const assets = registry.getAllAssets();

return assets
.filter(asset => asset.penumbraAssetId && !asset.display.startsWith('delegation_'))
.map(asset => {
const displayParts = asset.display.split('/');
return {
decimals: decimalsFromDenomUnits(asset.denomUnits),
display: displayParts[displayParts.length - 1] ?? '',
symbol: asset.symbol,
inner: asset.penumbraAssetId?.inner && uint8ArrayToBase64(asset.penumbraAssetId.inner),
imagePath: imagePathFromAssetImages(asset.images),
};
}) as Token[];
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: it is tempting to use our own types, but we should do our best to use types from BufBuild that are more universally relied upon in the penumbra ecosystem. For instance, we shouldn't have components that accept a Token, but probably ones that accept a ValueView type

};

export const useTokenAsset = (tokenId: Uint8Array | string): null | Token => {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: we should return some data field + loading + error states

const { data: registry } = useRegistry();

if (!registry) {
return null;
}

const assetId: AssetId = new AssetId();
assetId.inner = typeof tokenId !== 'string' ? tokenId : base64ToUint8Array(tokenId);

const tokenMetadata = registry.getMetadata(assetId);
const displayParts = tokenMetadata.display.split('/');
return {
decimals: decimalsFromDenomUnits(tokenMetadata.denomUnits),
display: displayParts[displayParts.length - 1] ?? '',
symbol: tokenMetadata.symbol,
inner: typeof tokenId !== 'string' ? uint8ArrayToBase64(tokenId) : tokenId,
imagePath: imagePathFromAssetImages(tokenMetadata.images),
};
}
5 changes: 3 additions & 2 deletions src/old/components/copiedTx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React, { FC, useState } from "react";
import { CopyIcon } from "@radix-ui/react-icons";
import { HStack } from "@chakra-ui/react";
import { Constants } from "@/old/utils/configConstants";
import { useEnv } from "@/fetchers/env";

interface CopyTxToClipboardProps {
txHash: string;
Expand All @@ -14,6 +14,7 @@ const CopyTxToClipboard: FC<CopyTxToClipboardProps> = ({
txHash,
clipboardPopupText,
}) => {
const env = useEnv();
const [isCopied, setIsCopied] = useState(false);

const handleCopy = () => {
Expand All @@ -26,7 +27,7 @@ const CopyTxToClipboard: FC<CopyTxToClipboardProps> = ({
return (
<HStack align={"center"} spacing={".5em"}>
<a
href={`${Constants.cuiloaUrl}/transaction/${txHash}`}
href={`${env?.PENUMBRA_CUILOA_URL}/transaction/${txHash}`}
target="_blank"
rel="noreferrer"
style={{
Expand Down
86 changes: 20 additions & 66 deletions src/old/components/liquidityPositions/currentStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,14 @@ import { fetchTokenAsset } from "@/old/utils/token/tokenFetch";
import BigNumber from "bignumber.js";
import { CopyIcon } from "@radix-ui/react-icons";
import { Token } from "@/old/utils/types/token";
import { useTokenAsset } from "@/fetchers/tokenAssets";

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

const CurrentLPStatus = ({ nftId, position }: CurrentLPStatusProps) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isCopied, setIsCopied] = useState<boolean>(false);

const handleCopy = () => {
navigator.clipboard.writeText(nftId).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1500); // Hide popup after 1.5 seconds
});
};

// First process position to human readable pieces

function getStatusText(position): string {
// Get status
const status = (position.state!).state.toString();

Expand All @@ -57,64 +46,29 @@ const CurrentLPStatus = ({ nftId, position }: CurrentLPStatusProps) => {
statusText = "Unknown";
}

// Get fee tier
const feeTier = Number(position.phi!.component!.fee);

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

// States for tokens
const [asset1Token, setAsset1Token] = useState<Token>({
symbol: "UNKNOWN",
display: "UNKNOWN",
decimals: 0,
inner: "UNKNOWN",
imagePath: "UNKNOWN",
});
const [asset2Token, setAsset2Token] = useState<Token>({
symbol: "UNKNOWN",
display: "UNKNOWN",
decimals: 0,
inner: "UNKNOWN",
imagePath: "UNKNOWN",
});
const [assetError, setAssetError] = useState<string | undefined>();
return statusText;
}

useEffect(() => {
// Function to fetch tokens asynchronously
const fetchTokens = async () => {
try {
const asset1 = position.phi!.pair!.asset1;
const asset2 = position.phi!.pair!.asset2;
const CurrentLPStatus = ({ nftId, position }: CurrentLPStatusProps) => {
const [isCopied, setIsCopied] = useState<boolean>(false);

if (asset1?.inner) {
const fetchedAsset1Token = fetchTokenAsset(asset1.inner);
if (!fetchedAsset1Token) {
setAssetError("Asset 1 token not found");
throw new Error("Asset 1 token not found");
}
setAsset1Token(fetchedAsset1Token);
}
const handleCopy = () => {
navigator.clipboard.writeText(nftId).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1500); // Hide popup after 1.5 seconds
});
};

if (asset2?.inner) {
const fetchedAsset2Token = fetchTokenAsset(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);
}
};
// First process position to human readable pieces
const statusText = getStatusText(position);

fetchTokens();
}, [position]);
// Get fee tier
const feeTier = Number(position.phi!.component!.fee);

if (!isLoading && (!asset1Token || !asset2Token)) {
return <div>{`LP exists, but ${assetError}.`}</div>;
}
const { asset1, asset2 } = position.phi!.pair!;
const asset1Token = useTokenAsset(asset1.inner);
const asset2Token = useTokenAsset(asset2.inner);
const [assetError, setAssetError] = useState<string | undefined>();

const reserves1 = fromBaseUnit(
BigInt(position.reserves!.r1?.lo || 0),
Expand Down
81 changes: 8 additions & 73 deletions src/old/components/lpAssetView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
LiquidityPositionEvent,
PositionExecutionEvent,
} from "@/old/utils/indexer/types/lps";
import { fetchTokenAsset } from "@/old/utils/token/tokenFetch";
import { useTokenAsset } from "@/fetchers/tokenAssets";
import { fromBaseUnit } from "@/old/utils/math/hiLo";
import { base64ToUint8Array } from "@/old/utils/math/base64";
import { Token } from "@/old/utils/types/token";
Expand All @@ -18,82 +18,17 @@ interface LPAssetViewProps {
}

const LPAssetView: FC<LPAssetViewProps> = ({ sectionTitle, lp_event }) => {
// States for tokens
const [asset1Token, setAsset1Token] = useState<Token>({
symbol: "UNKNOWN",
display: "UNKNOWN",
decimals: 0,
inner: "UNKNOWN",
imagePath: "UNKNOWN",
});
const [asset2Token, setAsset2Token] = useState<Token>({
symbol: "UNKNOWN",
display: "UNKNOWN",
decimals: 0,
inner: "UNKNOWN",
imagePath: "UNKNOWN",
});
const [assetError, setAssetError] = useState<string | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [reserves1, setReserves1] = useState<number>(0);
const [reserves2, setReserves2] = useState<number>(0);

useEffect(() => {
// Function to fetch tokens asynchronously
const fetchTokens = async () => {
setIsLoading(true);
try {
let asset1;
let asset2;

if ("lpevent_attributes" in lp_event) {
asset1 = base64ToUint8Array(
lp_event.lpevent_attributes.tradingPair!.asset1.inner
);
asset2 = base64ToUint8Array(
lp_event.lpevent_attributes.tradingPair!.asset2.inner
);
} else {
asset1 = base64ToUint8Array(
lp_event.execution_event_attributes.tradingPair!.asset1.inner
);
asset2 = base64ToUint8Array(
lp_event.execution_event_attributes.tradingPair!.asset2.inner
);
}
const { asset1, asset2 } = lp_event.lpevent_attributes.tradingPair
?? lp_event.execution_event_attributes.tradingPair;

if (asset1) {
const fetchedAsset1Token = fetchTokenAsset(asset1)
if (!fetchedAsset1Token) {
setAssetError("Asset 1 token not found");
throw new Error("Asset 1 token not found");
}
setAsset1Token(fetchedAsset1Token);
}
const asset1Token = useTokenAsset(asset1 && base64ToUint8Array(asset1.inner));
const asset2Token = useTokenAsset(asset2 && base64ToUint8Array(asset2.inner));

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

fetchTokens();
}, [lp_event]);

if (!isLoading && (!asset1Token || !asset2Token)) {
return <div>{`LP exists, but ${assetError}.`}</div>;
}
const [reserves1, setReserves1] = useState<number>(0);
const [reserves2, setReserves2] = useState<number>(0);

useEffect(() => {
if (!asset1Token) return;
// number to bigint
// if undefined, default to 0
let reserves1;
Expand Down
Loading