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

feat(spaceward): technical debt #868

Merged
merged 6 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions spaceward/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ if [ "$1" = 'nginx-fe' ]; then
replace_var WARDEN_COSMOSKIT_CHAIN_NAME "$filename"
replace_var WARDEN_MAINTENANCE "$filename"
replace_var WARDEN_SNAP_ORIGIN "$filename"
replace_var WARDEN_SNAP_VERSION "$filename"
replace_var WARDEN_ENVIRONMENT "$filename"
replace_var WARDEN_STORYBLOK_TOKEN "$filename"
replace_var WARDEN_ETHEREUM_ANALYZER_CONTRACT "$filename"
Expand Down
11 changes: 4 additions & 7 deletions spaceward/src/config/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,19 +216,16 @@ export const COSMOS_PRICES: Record<string, bigint | undefined> = {
OSMO: BigInt(0.4446 * 10 ** 8),
};

/* TODO networks
Astar native ASTR
Avalanche native AVAX
Binance Coin native BNB
Ethereum Classic native ETC
Polygon native MATIC
*/
const _ENABLED_ETH_CHAINS: { chainName: ChainName; testnet?: boolean }[] = [
{ chainName: "arbitrum" },
{ chainName: "astar" },
{ chainName: "avalanche" },
{ chainName: "base" },
{ chainName: "bsc" },
{ chainName: "ethereumClassic" },
{ chainName: "mainnet" },
{ chainName: "optimism" },
{ chainName: "polygon" },
{ chainName: "sepolia", testnet: true },
];

Expand Down
3 changes: 3 additions & 0 deletions spaceward/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const chainId = import.meta.env.VITE_WARDEN_CHAIN_ID || "warden";
const maintenance = import.meta.env.VITE_WARDEN_MAINTENANCE || false;
const snapOrigin =
import.meta.env.VITE_WARDEN_SNAP_ORIGIN || "local:http://localhost:8123";
const snapVersion =
import.meta.env.VITE_WARDEN_SNAP_VERSION || "0.1.5";
const spacewardEnv = import.meta.env.VITE_WARDEN_ENVIRONMENT || "development"; // development, production
const storyblokToken =
import.meta.env.VITE_WARDEN_STORYBLOK_TOKEN || "LTh76K2yz5nU6jUThhFG3Qtt";
Expand All @@ -34,6 +36,7 @@ export const env = {
chainId,
maintenance,
snapOrigin,
snapVersion,
spacewardEnv,
storyblokToken,
cosmoskitChainName,
Expand Down
6 changes: 4 additions & 2 deletions spaceward/src/features/actions/StatusSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { QueuedAction, QueuedActionStatus, useActionsState } from "./hooks";
import { getActionHandler, GetStatus, handleCosmos, handleEth, handleEthRaw } from "./util";
import { TEMP_KEY, useKeySettingsState } from "../keys/state";
import Assets from "../keys/assets";
import { useQueryClient } from "@tanstack/react-query";

interface ItemProps extends QueuedAction {
single?: boolean;
Expand All @@ -43,6 +44,7 @@ const waitForVisibility = () => {
};

function ActionItem({ single, ...item }: ItemProps) {
const queryClient = useQueryClient();
const { walletManager } = useContext(walletContext);
const { data: ks, setData: setKeySettings } = useKeySettingsState();
const { toast } = useToast()
Expand Down Expand Up @@ -307,9 +309,9 @@ function ActionItem({ single, ...item }: ItemProps) {
if (item.networkType === "eth-raw") {
res = await handleEthRaw({ action: item, w });
} else if (item.networkType === "eth") {
res = await handleEth({ action: item, w });
res = await handleEth({ action: item, w, queryClient });
} else if (item.networkType === "cosmos") {
res = await handleCosmos({ action: item, w, walletManager });
res = await handleCosmos({ action: item, w, walletManager, queryClient });
}
} catch (e) {
console.error("broadcast failed", e);
Expand Down
58 changes: 43 additions & 15 deletions spaceward/src/features/actions/util.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { hexlify, Transaction } from "ethers";
import { WalletManager } from "@cosmos-kit/core";
import { isDeliverTxSuccess, StargateClient } from "@cosmjs/stargate";
import { KeyringSnapRpcClient } from "@metamask/keyring-api";
import type { QueryClient } from "@tanstack/react-query";
import { IWeb3Wallet } from "@walletconnect/web3wallet";
import { cosmos, warden } from "@wardenprotocol/wardenjs";
import { base64FromBytes } from "@wardenprotocol/wardenjs/codegen/helpers";
import { env } from "@/env";
import { COSMOS_CHAINS } from "@/config/tokens";
import type { getClient } from "@/hooks/useClient";
import type { QueuedAction } from "./hooks";
import { IWeb3Wallet } from "@walletconnect/web3wallet";
import { hexlify, Transaction } from "ethers";
import { KeyringSnapRpcClient } from "@metamask/keyring-api";
import { getBalanceQueryKey } from "@/features/assets/queries";
import { getProvider, isSupportedNetwork } from "@/lib/eth";
import { COSMOS_CHAINS } from "@/config/tokens";
import { isUint8Array } from "@/lib/utils";
import { base64FromBytes } from "@wardenprotocol/wardenjs/codegen/helpers";
import type { QueuedAction } from "./hooks";
import { prepareTx } from "../modals/util";
import { WalletManager } from "@cosmos-kit/core";
import { isDeliverTxSuccess, StargateClient } from "@cosmjs/stargate";

export type GetStatus = (
client: Awaited<ReturnType<typeof getClient>>,
Expand Down Expand Up @@ -130,6 +132,15 @@ export const getActionHandler = ({
};
break;
}
case warden.warden.v1beta3.MsgUpdateSpaceResponse.typeUrl: {
getStatus = async () => ({
pending: false,
error: false,
done: true,
});

break;
}
default:
throw new Error(`action type not implemented: ${typeUrl}`);
}
Expand Down Expand Up @@ -203,9 +214,11 @@ export const handleEthRaw = async ({
export const handleEth = async ({
action,
w,
queryClient,
}: {
action: QueuedAction;
w: IWeb3Wallet | null;
queryClient: QueryClient;
}) => {
const {
chainName,
Expand Down Expand Up @@ -269,22 +282,25 @@ export const handleEth = async ({
.then(() => true);
}

return (
provider
.waitForTransaction(res.hash)
// fixme
.then(() => true)
);
return provider.waitForTransaction(res.hash).then(() => {
queryClient.invalidateQueries({
queryKey: getBalanceQueryKey("eip155", chainName, "").slice(0, -1),
});

return true;
});
Comment on lines +285 to +291
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling for transaction confirmation and query invalidation.

Currently, if provider.waitForTransaction(res.hash) or queryClient.invalidateQueries() throws an error, it might cause unhandled promise rejections. Consider adding error handling to manage potential issues during transaction confirmation or query invalidation.

You can modify the code as follows to include error handling:

 return provider.waitForTransaction(res.hash)
   .then(() => {
     queryClient.invalidateQueries({
       queryKey: getBalanceQueryKey("eip155", chainName, "").slice(0, -1),
     });
     return true;
+  })
+  .catch((error) => {
+    // Handle the error appropriately
+    console.error('Error during transaction confirmation or query invalidation:', error);
+    return false; // or rethrow the error as needed
   });
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return provider.waitForTransaction(res.hash).then(() => {
queryClient.invalidateQueries({
queryKey: getBalanceQueryKey("eip155", chainName, "").slice(0, -1),
});
return true;
});
return provider.waitForTransaction(res.hash)
.then(() => {
queryClient.invalidateQueries({
queryKey: getBalanceQueryKey("eip155", chainName, "").slice(0, -1),
});
return true;
})
.catch((error) => {
// Handle the error appropriately
console.error('Error during transaction confirmation or query invalidation:', error);
return false; // or rethrow the error as needed
});

};

export const handleCosmos = async ({
action,
w,
walletManager,
queryClient,
}: {
action: QueuedAction;
w: IWeb3Wallet | null;
walletManager: WalletManager;
queryClient: QueryClient;
}) => {
const {
chainName,
Expand Down Expand Up @@ -343,7 +359,19 @@ export const handleCosmos = async ({
},
})
// fixme
.then(() => true);
.then(() => {
// todo
// possibly add a timeout, as walletconnect with cosmos does not wait for tx result

queryClient.invalidateQueries({
queryKey: getBalanceQueryKey("cosmos", chainName, "").slice(
0,
-1,
),
});

return true;
});
}

const { signedTxBodyBytes, signedAuthInfoBytes } = prepareTx(
Expand Down
104 changes: 65 additions & 39 deletions spaceward/src/features/assets/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,85 @@
import { useQueryHooks } from "@/hooks/useClient";
import { walletContext } from "@cosmos-kit/react-lite";
import type { ExtendedHttpEndpoint, WalletManager } from "@cosmos-kit/core";
import { AddressType } from "@wardenprotocol/wardenjs/codegen/warden/warden/v1beta3/key";
import { cosmos } from "@wardenprotocol/wardenjs";
import { useQueries } from "@tanstack/react-query";
import { useQueries, useQuery } from "@tanstack/react-query";
import { useContext, useEffect, useMemo, useState } from "react";
import { COSMOS_CHAINS } from "@/config/tokens";
import { useQueryHooks } from "@/hooks/useClient";
import {
balancesQueryCosmos,
balancesQueryEth,
fiatPricesQuery,
} from "./queries";
import type { CosmosQueryClient, PriceMapSlinky } from "./types";
import { COSMOS_CHAINS } from "@/config/tokens";
import { walletContext } from "@cosmos-kit/react-lite";
import { ExtendedHttpEndpoint } from "@cosmos-kit/core";

const DERIVE_ADDRESSES = [
AddressType.ADDRESS_TYPE_ETHEREUM,
AddressType.ADDRESS_TYPE_OSMOSIS,
];

const queryCosmosClients = (walletManager: WalletManager) => {
const rpcClients: Record<string, CosmosQueryClient | undefined> = {};
const rpcRetry: Record<string, number> = {};

return {
queryKey: ["cosmos", "rpcClients"],
queryFn: async () => {
const clients: [CosmosQueryClient, string][] = [];

for (let i = 0; i < COSMOS_CHAINS.length; i++) {
const { chainName, rpc } = COSMOS_CHAINS[i];
let client = rpcClients[chainName];

if (client) {
// todo implement client health check
clients.push([client, chainName]);
continue;
}

let endpoint: ExtendedHttpEndpoint | string;

if (rpc) {
const retry = rpcRetry[chainName] ?? 0;
endpoint = rpc[retry % rpc.length];
rpcRetry[chainName] = retry + 1;
} else {
const repo = walletManager.getWalletRepo(chainName);
repo.activate();

try {
endpoint = await repo.getRpcEndpoint();
} catch (e) {
console.error(e);
endpoint = `https://rpc.cosmos.directory/${chainName}`;
}
}

try {
const client =
await cosmos.ClientFactory.createRPCQueryClient({
rpcEndpoint:
typeof endpoint === "string"
? endpoint
: endpoint.url,
});

clients.push([client, chainName]);
} catch (e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Store created clients in rpcClients to enable caching.

Currently, the newly created clients are not stored in rpcClients, which means the caching mechanism is ineffective. Storing the clients will allow subsequent iterations to reuse existing clients, improving performance.

Apply this diff to store the clients in rpcClients:

	clients.push([client, chainName]);
+	rpcClients[chainName] = client;
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
clients.push([client, chainName]);
clients.push([client, chainName]);
rpcClients[chainName] = client;

console.error(e);
continue;
}
}

return clients;
},
} as const;
};

export const useAssetQueries = (spaceId?: string | null) => {
const { walletManager } = useContext(walletContext);
const { isReady, useKeysBySpaceId, slinky } = useQueryHooks();
const [clients, setClients] = useState<[CosmosQueryClient, string][]>([]);
const clients = useQuery(queryCosmosClients(walletManager)).data;

const pairs = slinky.oracle.v1.useGetAllCurrencyPairs({
options: { enabled: isReady, refetchInterval: Infinity },
Expand Down Expand Up @@ -83,39 +142,6 @@ export const useAssetQueries = (spaceId?: string | null) => {
return priceMap;
}, [prices.data, currencyPairs]);

useEffect(() => {
Promise.all(
COSMOS_CHAINS.map(({ chainName, rpc }) => {
let promise: Promise<ExtendedHttpEndpoint | string>;

if (!rpc) {
const repo = walletManager.getWalletRepo(chainName);
repo.activate();
promise = repo.getRpcEndpoint();
} else {
promise = Promise.resolve(rpc[0]);
}

return promise
.then((endpoint) =>
cosmos.ClientFactory.createRPCQueryClient({
rpcEndpoint: endpoint
? typeof endpoint === "string"
? endpoint
: endpoint.url
: `https://rpc.cosmos.directory/${chainName}`,
}),
)
.then(
(client) =>
[client, chainName] as [CosmosQueryClient, string],
);
}),
).then((clients) => {
setClients(clients);
});
}, []);

const queryKeys = useKeysBySpaceId({
request: {
spaceId: spaceId ? BigInt(spaceId) : BigInt(0),
Expand Down
32 changes: 25 additions & 7 deletions spaceward/src/features/assets/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ const getAsset = (chainAssets: AssetList, denom: string) => {
return asset;
};

export const getBalanceQueryKey = (
chainType: "cosmos" | "eip155",
chainName: string,
address: string,
) => {
return ["balance", chainType, chainName, address];
};

const cosmosBalancesQuery = (params: {
address?: string;
enabled: boolean;
Expand All @@ -60,7 +68,11 @@ const cosmosBalancesQuery = (params: {
prices?: PriceMapSlinky;
}) => ({
enabled: params.enabled,
queryKey: ["cosmos", params.chainName, "balance", params.address],
queryKey: getBalanceQueryKey(
"cosmos",
params.chainName,
params.address ?? "",
),
queryFn: async () => {
if (!params.address) {
throw new Error("Address is required");
Expand Down Expand Up @@ -174,7 +186,10 @@ const eip155NativeBalanceQuery = ({
address?: `0x${string}`;
prices?: PriceMapSlinky;
}) => ({
queryKey: ["eip155", chainName, "native", address],
queryKey: [
...getBalanceQueryKey("eip155", chainName, address ?? ""),
"native",
],
queryFn: async (): Promise<BalanceEntry> => {
if (!address) {
throw new Error("Address is required");
Expand All @@ -199,10 +214,10 @@ const eip155NativeBalanceQuery = ({

const price: bigint = slinkyPrice
? BigInt(slinkyPrice.price?.price ?? 0)
: (priceFeedContract
: ((priceFeedContract
? await priceFeedContract.latestRoundData()
: undefined
)?.answer ?? BigInt(0);
)?.answer ?? BigInt(0));

const priceDecimals = slinkyPrice ? Number(slinkyPrice.decimals) : 8;

Expand Down Expand Up @@ -255,7 +270,10 @@ const eip155ERC20BalanceQuery = ({
prices?: PriceMapSlinky;
}) => ({
enabled: enabled && Boolean(address && token),
queryKey: ["eip155", chainName, "erc20", address, token],
queryKey: [
...getBalanceQueryKey("eip155", chainName, address ?? ""),
`erc20:${token}`,
],
queryFn: async (): Promise<BalanceEntry> => {
if (!address || !token) {
throw new Error("Address and token are required");
Expand Down Expand Up @@ -353,10 +371,10 @@ const eip155ERC20BalanceQuery = ({
? BigInt("100000000")
: slinkyPrice
? BigInt(slinkyPrice.price?.price ?? 0)
: (priceFeedContract
: ((priceFeedContract
? await priceFeedContract.latestRoundData()
: undefined
)?.answer ?? BigInt(0);
)?.answer ?? BigInt(0));

const priceDecimals = stablecoin
? 8
Expand Down
Loading
Loading