Skip to content

feat: support svm useBalance hooks #1558

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

Draft
wants to merge 1 commit into
base: evm-svm-bridge-action-hooks
Choose a base branch
from
Draft
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
145 changes: 145 additions & 0 deletions src/hooks/useBalance_new/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useQuery, useQueries, UseQueryOptions } from "@tanstack/react-query";
import { BigNumber } from "ethers";
import { BalanceStrategy } from "./strategies/types";
import {
getEcosystem,
balanceQueryKey,
TOKEN_SYMBOLS_MAP,
getNativeTokenSymbol,
} from "utils";

type StrategyPerEcosystem = {
evm: BalanceStrategy;
svm: BalanceStrategy;
};

type BalanceQueryKey = ReturnType<typeof balanceQueryKey>;

type MultiBalanceQuery = UseQueryOptions<
BigNumber | undefined,
Error,
BigNumber | undefined,
BalanceQueryKey
>;

async function fetchBalance(
queryKey: BalanceQueryKey,
strategies: StrategyPerEcosystem
) {
const [, chainId, tokenSymbol, account] = queryKey;

if (!chainId || !tokenSymbol) {
return BigNumber.from(0);
}

const ecosystem = getEcosystem(chainId);
const strategy = strategies[ecosystem];

const accountToQuery = account ?? strategy.getAccount();
console.log("accountToQuery", queryKey, { accountToQuery });

if (!accountToQuery) {
return BigNumber.from(0);
}

return strategy.getBalance(chainId, tokenSymbol, accountToQuery);
}

export function createBalanceHook(strategies: StrategyPerEcosystem) {
return function useBalance(
tokenSymbol?: string,
chainId?: number,
account?: string
) {
const { data: balance, ...delegated } = useQuery({
queryKey: balanceQueryKey(account, chainId, tokenSymbol, "balance"),
queryFn: ({ queryKey }) => fetchBalance(queryKey, strategies),
enabled: Boolean(chainId && tokenSymbol),
refetchInterval: 10_000,
});

return {
balance,
...delegated,
};
};
}

export function createBalancesBySymbolsHook(strategies: StrategyPerEcosystem) {
return function useBalancesBySymbols({
tokenSymbols,
chainId,
account,
}: {
tokenSymbols: string[];
chainId?: number;
account?: string;
}) {
const result = useQueries({
queries: tokenSymbols.map<MultiBalanceQuery>((tokenSymbol) => ({
queryKey: balanceQueryKey(
account,
chainId,
tokenSymbol,
"balances-by-symbols"
),
queryFn: ({ queryKey }) => fetchBalance(queryKey, strategies),
enabled: Boolean(chainId && tokenSymbols.length),
refetchInterval: 10_000,
})),
});
return {
balances: result.map((result) => result.data),
isLoading: result.some((s) => s.isLoading),
};
};
}

export function createBalanceBySymbolPerChainHook(
strategies: StrategyPerEcosystem
) {
return function useBalanceBySymbolPerChain({
tokenSymbol,
chainIds,
account,
}: {
tokenSymbol?: string;
chainIds: number[];
account?: string;
}) {
const result = useQueries({
queries: chainIds.map<MultiBalanceQuery>((chainId) => ({
queryKey: balanceQueryKey(
account,
chainId,
tokenSymbol,
"balance-by-symbol-per-chain"
),
queryFn: async ({ queryKey }) => {
const [, chainIdToQuery, tokenSymbolToQuery] = queryKey;
if (
tokenSymbolToQuery === TOKEN_SYMBOLS_MAP.ETH.symbol &&
getNativeTokenSymbol(chainIdToQuery!) !==
TOKEN_SYMBOLS_MAP.ETH.symbol
) {
return Promise.resolve(BigNumber.from(0));
}
return fetchBalance(queryKey, strategies);
},
enabled: Boolean(chainId && tokenSymbol),
refetchInterval: 10_000,
})),
});

return {
balances: result.reduce(
(acc, { data }, idx) => ({
...acc,
[chainIds[idx]]: (data ?? BigNumber.from(0)) as BigNumber,
}),
{} as Record<number, BigNumber>
),
isLoading: result.some((s) => s.isLoading),
};
};
}
60 changes: 60 additions & 0 deletions src/hooks/useBalance_new/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useConnectionEVM } from "../useConnectionEVM";
import { useConnectionSVM } from "../useConnectionSVM";
import {
createBalanceHook,
createBalancesBySymbolsHook,
createBalanceBySymbolPerChainHook,
} from "./factory";
import { EVMBalanceStrategy } from "./strategies/evm";
import { SVMBalanceStrategy } from "./strategies/svm";

function useBalanceStrategies() {
const connectionEVM = useConnectionEVM();
const connectionSVM = useConnectionSVM();
return {
evm: new EVMBalanceStrategy(connectionEVM),
svm: new SVMBalanceStrategy(connectionSVM),
};
}
export function useBalance(
tokenSymbol?: string,
chainId?: number,
account?: string
) {
const strategies = useBalanceStrategies();
return createBalanceHook(strategies)(tokenSymbol, chainId, account);
}

export function useBalancesBySymbols({
tokenSymbols,
chainId,
account,
}: {
tokenSymbols: string[];
chainId?: number;
account?: string;
}) {
const strategies = useBalanceStrategies();
return createBalancesBySymbolsHook(strategies)({
tokenSymbols,
chainId,
account,
});
}

export function useBalanceBySymbolPerChain({
tokenSymbol,
chainIds,
account,
}: {
tokenSymbol?: string;
chainIds: number[];
account?: string;
}) {
const strategies = useBalanceStrategies();
return createBalanceBySymbolPerChainHook(strategies)({
tokenSymbol,
chainIds,
account,
});
}
43 changes: 43 additions & 0 deletions src/hooks/useBalance_new/strategies/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BigNumber } from "ethers";
import { BalanceStrategy } from "./types";
import { getBalance, getNativeBalance, getConfig, getProvider } from "utils";
import { useConnectionEVM } from "hooks/useConnectionEVM";

export class EVMBalanceStrategy implements BalanceStrategy {
constructor(
private readonly connection: ReturnType<typeof useConnectionEVM>
) {}

getAccount() {
return this.connection.account;
}

async getBalance(
chainId: number,
tokenSymbol: string,
account: string
): Promise<BigNumber> {
const config = getConfig();
const tokenInfo = config.getTokenInfoBySymbolSafe(chainId, tokenSymbol);
const provider =
this.connection.chainId === chainId
? this.connection.provider
: getProvider(chainId);

if (!tokenInfo || !tokenInfo.addresses?.[chainId]) {
return BigNumber.from(0);
}

if (tokenInfo.isNative) {
return getNativeBalance(chainId, account, "latest", provider);
} else {
return getBalance(
chainId,
account,
tokenInfo.addresses[chainId],
"latest",
provider
);
}
}
}
62 changes: 62 additions & 0 deletions src/hooks/useBalance_new/strategies/svm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BigNumber } from "ethers";
import { PublicKey } from "@solana/web3.js";
import {
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { BalanceStrategy } from "./types";
import { getConfig } from "utils";
import { useConnectionSVM } from "hooks/useConnectionSVM";

export class SVMBalanceStrategy implements BalanceStrategy {
constructor(
private readonly connection: ReturnType<typeof useConnectionSVM>
) {}

getAccount() {
return this.connection.account?.toString();
}

async getBalance(
chainId: number,
tokenSymbol: string,
account: string
): Promise<BigNumber> {
const config = getConfig();
const tokenInfo = config.getTokenInfoBySymbolSafe(chainId, tokenSymbol);

if (!tokenInfo || !tokenInfo.addresses?.[chainId]) {
return BigNumber.from(0);
}

if (tokenInfo.isNative) {
// Get native SOL balance
const balance = await this.connection.provider.getBalance(
new PublicKey(account)
);
return BigNumber.from(balance.toString());
} else {
// Get SPL token balance
const tokenMint = new PublicKey(tokenInfo.addresses[chainId]);
const owner = new PublicKey(account);

// Get the associated token account address
const tokenAccount = getAssociatedTokenAddressSync(
tokenMint,
owner,
true, // allowOwnerOffCurve
TOKEN_PROGRAM_ID
);

try {
// Get token account info
const tokenAccountInfo =
await this.connection.provider.getTokenAccountBalance(tokenAccount);
return BigNumber.from(tokenAccountInfo.value.amount);
} catch (error) {
// If token account doesn't exist or other error, return 0 balance
return BigNumber.from(0);
}
}
}
}
10 changes: 10 additions & 0 deletions src/hooks/useBalance_new/strategies/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { BigNumber } from "ethers";

export interface BalanceStrategy {
getAccount(): string | undefined;
getBalance(
chainId: number,
tokenSymbol: string,
account: string
): Promise<BigNumber>;
}
5 changes: 3 additions & 2 deletions src/utils/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export function latestBlockQueryKey(chainId: ChainId) {
export function balanceQueryKey(
account?: string,
chainId?: ChainId,
token?: string
token?: string,
prefix = "balance"
) {
return ["balance", chainId, token, account] as const;
return [prefix, chainId, token, account] as const;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/views/Bridge/components/ChainSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
shortenAddress,
} from "utils";

import { useBalanceBySymbolPerChain, useConnection } from "hooks";
import { useConnection } from "hooks";
import { useBalanceBySymbolPerChain } from "hooks/useBalance_new";
import { useMemo } from "react";
import { BigNumber } from "ethers";
import { getSupportedChains } from "../utils";
Expand Down Expand Up @@ -41,11 +42,10 @@ export function ChainSelector({
// Get supported chains and filter based on external projects
const availableChains = filterAvailableChains(fromOrTo, selectedRoute);

const { account, isConnected } = useConnection();
const { isConnected } = useConnection();
const { balances } = useBalanceBySymbolPerChain({
tokenSymbol: tokenInfo.symbol,
chainIds: availableChains.map((c) => c.chainId),
account,
});

const sortedChains = useMemo(
Expand Down
5 changes: 1 addition & 4 deletions src/views/Bridge/components/TokenSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
getToken,
tokenList,
} from "utils";
import { useBalancesBySymbols, useConnection } from "hooks";
import { useBalancesBySymbols } from "hooks/useBalance_new";

import { RouteNotSupportedTooltipText } from "./RouteNotSupportedTooltipText";
import {
Expand Down Expand Up @@ -58,8 +58,6 @@ export function TokenSelector({
? getToken(receiveTokenSymbol)
: selectedToken;

const { account } = useConnection();

const orderedTokens: Array<
TokenInfo & {
disabled?: boolean;
Expand Down Expand Up @@ -102,7 +100,6 @@ export function TokenSelector({
const { balances } = useBalancesBySymbols({
tokenSymbols: orderedTokens.filter((t) => !t.disabled).map((t) => t.symbol),
chainId: isInputTokenSelector ? fromChain : toChain,
account,
});

return (
Expand Down
Loading
Loading