Skip to content

Commit

Permalink
Feat/execute quote (#16)
Browse files Browse the repository at this point in the history
* get quote

* small refactor

* execute

* execute, track progress

* show explorer links

* narrower Progress type

* clean up

* link
  • Loading branch information
gsteenkamp89 authored Sep 26, 2024
1 parent 769f7f7 commit a73f39c
Show file tree
Hide file tree
Showing 16 changed files with 641 additions and 234 deletions.
19 changes: 14 additions & 5 deletions apps/example/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
.dark {
--background: 230 6% 19%;
--foreground: 231.43 6.31% 21.76%;

--muted: 231.43 6.31% 21.76%;

--popover: 240 5.88% 13.33%;

--text-color: 204 30% 83%;

/* across teal */
--accent: 166 92% 70%;

/* gray */
--border: 226.67 5.52% 31.96%;
--border-secondary: 226.67 6.77% 26.08%;
Expand All @@ -44,10 +46,6 @@
--destructive: 0 95% 66%;
}

* {
font-weight: 200;
}

/* remove arrows from number input */
@layer base {
input[type="number"]::-webkit-outer-spin-button,
Expand All @@ -56,4 +54,15 @@
-webkit-appearance: none;
-moz-appearance: textfield !important;
}

h1,
h2,
h3,
h4,
h5,
p,
span,
div {
@apply text-text font-light;
}
}
136 changes: 107 additions & 29 deletions apps/example/app/viem/components/Bridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
import { ChainSelect } from "@/components/ChainSelect";
import { Divider } from "@/components/Divider";
import { TokenSelect } from "@/components/TokenSelect";
import { Button, Input, Label, Skeleton } from "@/components/ui";
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 { cn, reduceAcrossChains } from "@/lib/utils";
import { getExplorerLink, reduceAcrossChains } from "@/lib/utils";
import { TokenInfo } from "@across-toolkit/sdk";
import { useEffect, useState } from "react";
import { Address, formatUnits, parseUnits } from "viem";
import { useAccount, useChains } from "wagmi";
import { useDebounce } from "@uidotdev/usehooks";
import { formatUnits, parseUnits } from "viem";
import { useAccount, useBalance, useChains } from "wagmi";
import { useDebounceValue } from "usehooks-ts";
import { useExecuteQuote } from "@/lib/hooks/useExecuteQuote";
import { Progress } from "./Progress";
import { TokenInput } from "@/components/TokenInput";
import { ExternalLink } from "@/components/ExternalLink";

export function Bridge() {
const { address } = useAccount();
Expand All @@ -34,11 +38,16 @@ export function Bridge() {

// FROM TOKEN
const { inputTokens } = useInputTokens(originChainId);
const [fromTokenAddress, setFromTokenAddress] = useState<Address | undefined>(
inputTokens?.[2]?.address,
const [fromToken, setFromToken] = useState<TokenInfo | undefined>(
inputTokens?.[0],
);
const { data: balance } = useBalance({
address,
token: fromToken?.address,
});
const inputToken = inputTokens?.find(
(token) => token.address.toLowerCase() === fromTokenAddress?.toLowerCase(),
(token) =>
token.address.toLowerCase() === fromToken?.address?.toLowerCase(),
);

const [destinationChainId, setDestinationChainId] = useState<
Expand All @@ -48,7 +57,7 @@ export function Bridge() {
const { availableRoutes } = useAvailableRoutes({
originChainId,
destinationChainId,
originToken: fromTokenAddress,
originToken: fromToken?.address,
});

const outputTokensForRoute = availableRoutes?.map((route) =>
Expand All @@ -67,25 +76,28 @@ export function Bridge() {
setOutputTokens(_outputTokens);
}, [availableRoutes]);

const [toTokenAddress, setToTokenAddress] = useState<Address | undefined>(
outputTokens?.[0]?.address,
);
const toToken = outputTokens?.find(
(token) => token.address.toLowerCase() === toTokenAddress?.toLowerCase(),
const [toToken, setToToken] = useState<TokenInfo | undefined>(
outputTokens?.[0],
);

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

const [inputAmount, setInputAmount] = useState<string>();
const debouncedInputAmount = useDebounce(inputAmount, 300);
const route = availableRoutes?.find(
(route) =>
route.outputToken.toLocaleLowerCase() === toTokenAddress?.toLowerCase(),
);
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 && inputToken
Expand All @@ -96,7 +108,50 @@ export function Bridge() {
}
: undefined;

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

const {
executeQuote,
progress,
error,
isPending,
depositReceipt,
fillReceipt,
} = useExecuteQuote(quote);
const inputBalance = balance?.value
? parseFloat(formatUnits(balance?.value, balance?.decimals)).toFixed(4)
: undefined;

function onMax() {
if (!balance?.value) return;
setInputAmount(formatUnits(balance?.value, balance?.decimals));
}
const originChain = chains.find((chain) => chain.id === originChainId);
const destinationChain = chains.find(
(chain) => chain.id === destinationChainId,
);

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 (
<>
Expand All @@ -119,8 +174,8 @@ export function Bridge() {
<TokenSelect
className="flex-[3]"
tokens={inputTokens}
onTokenChange={setFromTokenAddress}
token={fromTokenAddress}
onTokenChange={setFromToken}
token={fromToken}
/>
</div>

Expand All @@ -138,23 +193,25 @@ export function Bridge() {
/>

<TokenSelect
className={cn("flex-[3]")}
className="flex-[3]"
disabled={outputTokens ? !(outputTokens?.length > 1) : true}
tokens={outputTokens}
onTokenChange={setToTokenAddress}
token={toTokenAddress}
onTokenChange={setToToken}
token={toToken}
/>
</div>

<Divider className="my-4" />

<Label htmlFor="input-amount">Send</Label>
<Input
<TokenInput
className="flex-[5]"
balance={inputBalance}
id="input-amount"
placeholder="Enter amount"
type="number"
value={inputAmount}
onMax={onMax}
onChange={(e) => setInputAmount(e.currentTarget.value)}
/>
</div>
Expand All @@ -175,12 +232,33 @@ export function Bridge() {
</p>
)}
<Button
disabled={!(quote && toToken)}
onClick={() => executeQuote()}
disabled={!(quote && toToken) || isRefetching || isPending}
className="mt-2"
variant="accent"
>
Confirm Transaction
{isPending
? "Executing..."
: isRefetching
? "Updating quote..."
: "Confirm Transaction"}
</Button>

{progress && (
<Progress className="mt-8" error={error} progress={progress} />
)}
<div className="flex gap-2 mt-4">
{depositTxLink && (
<ExternalLink icon href={depositTxLink}>
Deposit Tx
</ExternalLink>
)}
{fillTxLink && (
<ExternalLink icon href={fillTxLink}>
Fill Tx
</ExternalLink>
)}
</div>
</div>
</>
);
Expand Down
62 changes: 62 additions & 0 deletions apps/example/app/viem/components/Progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { LoadingIndicator, Status } from "@/components/LoadingIndicator";
import { cn } from "@/lib/utils";
import { ExecutionProgress } from "@across-toolkit/sdk";

export type ProgressProps = {
progress: ExecutionProgress;
error?: Error | null;
className?: string;
};

// TODO: make more fully featured
export function Progress({ progress, error, className }: ProgressProps) {
if (progress.status === "idle") {
return;
}

const status = (() => {
if (
progress.status === "txError" ||
progress.status === "simulationError" ||
progress.status === "error"
) {
return Status.ERROR;
}
if (progress.status === "txSuccess" && progress.step === "fill") {
return Status.SUCCESS;
}
return Status.PENDING;
})();

const label = (() => {
if (
progress.status === "txError" ||
progress.status === "simulationError" ||
progress.status === "error"
) {
return progress.error.name;
}
if (progress.step === "approve") {
return "Approving ERC20 spend...";
}
if (progress.step === "deposit") {
return "Depositing on origin chain...";
}
if (progress.step === "fill" && progress.status === "txSuccess") {
return "Bridge complete!";
}

if (progress.step === "fill" && progress.status === "txPending") {
return "Filling on destination chain...";
}
})();

return (
<div
className={cn("px-2 w-full flex flex-col items-center gap-2", className)}
>
<p className="text-text/75 text-sm">{label}</p>
<LoadingIndicator status={status} />
</div>
);
}
39 changes: 39 additions & 0 deletions apps/example/components/ExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { cn } from "@/lib/utils";
import { Button, ButtonProps } from "./ui";
import Link from "next/link";
import { Icon } from "./Icon";

export type ExternalLinkProps = {
href: string;
variant?: ButtonProps["variant"];
icon?: boolean;
className?: string;
children?: React.ReactNode;
};

export function ExternalLink({
href,
icon = false,
className,
variant,
children,
...props
}: ExternalLinkProps) {
return (
<Button
variant={variant}
asChild
className={cn(
"text-text/75 hover:text-text hover:border-text border border-border-secondary rounded-md px-3 py-2 flex gap-2 items-center",
className,
)}
>
<Link target="_blank" href={href} {...props}>
{children}
{icon && (
<Icon className="w-[1em] h-[1em] text-inherit" name="link-external" />
)}
</Link>
</Button>
);
}
Loading

0 comments on commit a73f39c

Please sign in to comment.