Skip to content
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
81 changes: 27 additions & 54 deletions apps/example/app/viem/components/Bridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,26 @@ import { ChainSelect } from "@/components/ChainSelect";
import { Divider } from "@/components/Divider";
import { TokenSelect } from "@/components/TokenSelect";
import { Button, Label, Skeleton } from "@/components/ui";
import { useAvailableRoutes } from "@/lib/hooks/useAvailableRoutes";
import { useInputTokens } from "@/lib/hooks/useInputTokens";
import { useOutputTokens } from "@/lib/hooks/useOutputTokens";
import { useQuote } from "@/lib/hooks/useQuote";
import { useSupportedAcrossChains } from "@/lib/hooks/useSupportedAcrossChains";
import { useSwapQuote } from "@/lib/hooks/useSwapQuote";
import { getExplorerLink, isNativeToken } from "@/lib/utils";
import { TokenInfo } from "@across-protocol/app-sdk";
import { useEffect, useState } from "react";
import { useState } from "react";
import { formatUnits, parseUnits } from "viem";
import { useAccount, useBalance, useChains } from "wagmi";
import { useDebounceValue } from "usehooks-ts";
import { useExecuteQuote } from "@/lib/hooks/useExecuteQuote";
import { useExecuteSwapQuote } from "@/lib/hooks/useExecuteSwapQuote";
import { Progress } from "./Progress";
import { TokenInput } from "@/components/TokenInput";
import { ExternalLink } from "@/components/ExternalLink";
import { useAcrossChains } from "@/lib/hooks/useAcrossChains";
import { useSwapTokens } from "@/lib/hooks/useSwapTokens";
import { useSwapChains } from "@/lib/hooks/useSwapChains";

export function Bridge() {
const { address } = useAccount();
const chains = useChains();
// CHAINS
const { supportedChains } = useSupportedAcrossChains({});
const { swapChains: supportedChains } = useSwapChains({});

// use only token data for chains we support
const acrossChains = useAcrossChains();
Expand All @@ -37,7 +35,7 @@ export function Bridge() {
);

// FROM TOKEN
const { inputTokens } = useInputTokens(originChainId);
const { tokens: inputTokens } = useSwapTokens(originChainId);

const [fromToken, setFromToken] = useState<TokenInfo | undefined>(
inputTokens?.[0],
Expand All @@ -55,74 +53,49 @@ export function Bridge() {
number | undefined
>(chains.find((chain) => chain.id !== originChainId)?.id);

const { availableRoutes } = useAvailableRoutes({
originChainId,
destinationChainId,
originToken: fromToken?.address,
});

const outputTokensForRoute = availableRoutes?.map((route) =>
route.outputToken.toLowerCase(),
);

const { outputTokens: outputTokensForChain } =
useOutputTokens(destinationChainId);

const [outputTokens, setOutputTokens] = useState<TokenInfo[] | undefined>();

useEffect(() => {
const _outputTokens = outputTokensForChain?.filter((token) =>
outputTokensForRoute?.includes(token.address.toLowerCase()),
);
setOutputTokens(_outputTokens);
}, [availableRoutes]);
const { tokens: outputTokens } = useSwapTokens(destinationChainId);

const [toToken, setToToken] = useState<TokenInfo | undefined>(
outputTokens?.[0],
);

useEffect(() => {
if (outputTokens) {
setToToken(
outputTokens.find((token) => token.symbol === fromToken?.symbol) ??
outputTokens?.[0],
);
}
}, [outputTokens]);

const [inputAmount, setInputAmount] = useState<string>("");
const [debouncedInputAmount] = useDebounceValue(inputAmount, 300);
const route = availableRoutes?.find((route) => {
return (
route.outputToken.toLocaleLowerCase() ===
toToken?.address?.toLowerCase() &&
route.outputTokenSymbol === toToken.symbol
);
});

const quoteConfig =
route && debouncedInputAmount && fromToken
debouncedInputAmount &&
fromToken &&
toToken &&
destinationChainId &&
originChainId &&
address
? {
route,
route: {
originChainId: originChainId,
destinationChainId: destinationChainId,
inputToken: fromToken.address,
outputToken: toToken.address,
},
amount: parseUnits(debouncedInputAmount, fromToken?.decimals),
depositor: address,
recipient: address,
inputAmount: parseUnits(debouncedInputAmount, fromToken?.decimals),
}
: undefined;

const {
quote,
isLoading: quoteLoading,
isRefetching,
} = useQuote(quoteConfig);
} = useSwapQuote(quoteConfig);

const {
executeQuote,
executeSwapQuote,
progress,
error,
isPending,
depositReceipt,
fillReceipt,
} = useExecuteQuote(quote);
} = useExecuteSwapQuote(quote ? { swapQuote: quote } : undefined);
const inputBalance = fromTokenBalance
? parseFloat(
formatUnits(fromTokenBalance?.value, fromTokenBalance?.decimals),
Expand Down Expand Up @@ -232,12 +205,12 @@ export function Bridge() {
{quote && toToken && (
<p className="text-md font-normal text-text">
{parseFloat(
formatUnits(quote.deposit.outputAmount, toToken.decimals),
formatUnits(BigInt(quote.minOutputAmount), toToken.decimals),
).toFixed(3)}
</p>
)}
<Button
onClick={() => executeQuote()}
onClick={() => executeSwapQuote()}
disabled={!(quote && toToken) || isRefetching || isPending}
className="mt-2"
variant="accent"
Expand Down
4 changes: 2 additions & 2 deletions apps/example/app/viem/components/Progress.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { LoadingIndicator, Status } from "@/components/LoadingIndicator";
import { cn } from "@/lib/utils";
import { ExecutionProgress } from "@across-protocol/app-sdk";
import { ExecutionProgress, SwapExecutionProgress } from "@across-protocol/app-sdk";

export type ProgressProps = {
progress: ExecutionProgress;
progress: ExecutionProgress | SwapExecutionProgress;
error?: Error | null;
className?: string;
};
Expand Down
35 changes: 15 additions & 20 deletions apps/example/app/viem/components/Stake.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { optimism } from "viem/chains";
import { useAccount, useBalance } from "wagmi";
import { useStakeQuote } from "@/lib/hooks/useStakeQuote";
import { useQueryClient } from "@tanstack/react-query";
import { useExecuteSwapQuote } from "@/lib/hooks/useExecuteSwapQuote";

export function Stake() {
const queryClient = useQueryClient();
Expand All @@ -44,8 +45,6 @@ export function Stake() {
originChainIds.has(c.chainId),
);

const toToken = stakeToken;

const [originChainId, setOriginChainId] = useState<number | undefined>(
originChains?.[0]?.chainId,
);
Expand Down Expand Up @@ -94,14 +93,6 @@ export function Stake() {

const [inputAmount, setInputAmount] = useState<string>("");
const [debouncedInputAmount] = useDebounceValue(inputAmount, 300);
const route = availableRoutes?.find((route) => {
return (
route.outputToken.toLowerCase() === toToken?.address?.toLowerCase() &&
route.inputToken.toLowerCase() === fromToken?.address.toLowerCase() &&
route.originChainId === originChainId &&
route.outputTokenSymbol === toToken.symbol
);
});

const { userStakeFormatted, userStakeQueryKey } = useUserStake();
const {
Expand All @@ -114,24 +105,25 @@ export function Stake() {

const {
stakeQuote,
error: stakeQuoteError,
isLoading: stakeQuotePending,
isRefetching: stakeQuoteRefetching,
} = useStakeQuote(
{
route,
inputAmount: parseUnits(
debouncedInputAmount,
STAKE_CONTRACT.token.decimals,
),
route: {
originChainId: originChainId!,
destinationChainId: routeConfig.destinationChainId,
inputToken: fromToken?.address!,
outputToken: stakeToken.address,
},
amount: parseUnits(debouncedInputAmount, STAKE_CONTRACT.token.decimals),
},
{
enabled: Boolean(debouncedInputAmount),
},
);

const { executeQuote, progress, isPending, fillTxLink, depositTxLink } =
useExecuteQuote(stakeQuote);
const { executeSwapQuote, progress, isPending, fillTxLink, depositTxLink } =
useExecuteSwapQuote(stakeQuote ? { swapQuote: stakeQuote } : undefined);

useEffect(() => {
if (withdrawConfirming) {
Expand Down Expand Up @@ -250,7 +242,7 @@ export function Stake() {
onChange={(e) => setInputAmount(e.currentTarget.value)}
/>
<Button
onClick={() => executeQuote()}
onClick={() => executeSwapQuote()}
disabled={!stakeQuote || stakeQuoteRefetching || isPending}
className="mt-2"
variant="accent"
Expand All @@ -270,7 +262,10 @@ export function Stake() {
{stakeQuote && fromToken && (
<p className="text-md font-normal text-text">
{parseFloat(
formatUnits(stakeQuote.deposit.outputAmount, toToken.decimals),
formatUnits(
BigInt(stakeQuote.minOutputAmount),
stakeToken.decimals,
),
).toFixed(6)}
</p>
)}
Expand Down
111 changes: 111 additions & 0 deletions apps/example/lib/hooks/useExecuteSwapQuote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useMutation } from "@tanstack/react-query";
import { useAcross } from "../across";
import { buildQueryKey, getExplorerLink } from "../utils";
import { AcrossClient, SwapExecutionProgress } from "@across-protocol/app-sdk";
import { useChainId, useChains, useConfig, useSwitchChain } from "wagmi";
import { useState } from "react";
import { TransactionReceipt } from "viem";
import { getWalletClient } from "wagmi/actions";

export type useExecuteSwapQuoteParams =
| Omit<Parameters<AcrossClient["executeSwapQuote"]>[0], "walletClient">
| undefined;

export function useExecuteSwapQuote(params: useExecuteSwapQuoteParams) {
const sdk = useAcross();
const config = useConfig();
const chains = useChains();
const { switchChainAsync } = useSwitchChain();
const chainId = useChainId();
const mutationKey = buildQueryKey("executeSwapQuote", params);

const [progress, setProgress] = useState<SwapExecutionProgress>({
status: "idle",
step: "approve",
});
const [depositReceipt, setDepositReceipt] = useState<TransactionReceipt>();
const [fillReceipt, setFillReceipt] = useState<TransactionReceipt>();

function resetProgress() {
setProgress({
status: "idle",
step: "approve",
});
}

const { mutate: executeSwapQuote, ...rest } = useMutation({
mutationKey,
mutationFn: async () => {
resetProgress();

if (!params) {
return;
}

const inputChainId = params.swapQuote.inputToken.chainId;

if (chainId !== inputChainId) {
await switchChainAsync({ chainId: inputChainId });
}

const walletClient = await getWalletClient(config);
if (!walletClient) {
return;
}

return sdk.executeSwapQuote({
...params,
walletClient,
onProgress: (progress) => {
console.log(progress);
if (progress.status === "txSuccess" && progress.step === "swap") {
setDepositReceipt(progress.txReceipt);
}
if (progress.status === "txSuccess" && progress.step === "fill") {
setFillReceipt(progress.txReceipt);
}
setProgress(progress);
},
});
},
onError: (error) => {
console.log("ERROR", error);
},
});

const originChain = chains.find(
(chain) => chain.id === params?.swapQuote.inputToken.chainId,
);

const destinationChain = chains.find(
(chain) => chain.id === params?.swapQuote.outputToken.chainId,
);

const depositTxLink =
depositReceipt &&
originChain &&
getExplorerLink({
chain: originChain,
type: "transaction",
txHash: depositReceipt.transactionHash,
});

const fillTxLink =
fillReceipt &&
destinationChain &&
getExplorerLink({
chain: destinationChain,
type: "transaction",
txHash: fillReceipt.transactionHash,
});

return {
progress,
executeSwapQuote,
depositReceipt,
fillReceipt,
depositTxLink,
fillTxLink,
...rest,
};
}
Loading