Skip to content

Commit

Permalink
Feat/ethers (#11)
Browse files Browse the repository at this point in the history
* create test page for using ethers in browser

* small fixes

* correctly export all types, errors, utils and actions

* small fix
gsteenkamp89 authored Sep 18, 2024
1 parent be7f5ab commit c1ba18e
Showing 21 changed files with 1,245 additions and 265 deletions.
181 changes: 181 additions & 0 deletions apps/example/app/ethers/components/Bridge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"use client";
import { Button } from "@/components/ui/button";
import {
AcrossClient,
DepositStatus,
FillStatus,
Quote,
} from "@across-toolkit/sdk";
import { useEthers } from "@usedapp/core";
import { useEffect, useState } from "react";
import { Address, createWalletClient, custom, Hash, parseEther } from "viem";
import { toAccount } from "viem/accounts";
import { arbitrum, mainnet } from "viem/chains";

const chains = [mainnet, arbitrum];

const sdk = AcrossClient.create({
chains,
useTestnet: false,
integratorId: "TEST",
logLevel: "DEBUG",
});

async function getQuote(account: Address) {
const routes = await sdk.actions.getAvailableRoutes({
originChainId: mainnet.id,
destinationChainId: arbitrum.id,
})!;

const route = routes.find((r) => r.inputTokenSymbol === "ETH")!;

// 1. get quote
const bridgeQuoteRes = await sdk.actions.getQuote({
route,
inputAmount: parseEther("0.01"),
recipient: account,
});

return bridgeQuoteRes;
}

async function bridge(
quote: Awaited<ReturnType<typeof getQuote>>,
library: any,
account: string | undefined,
) {
if (!account) return;

const walletClient = createWalletClient({
account: toAccount(account as Address),
chain: mainnet,
transport: custom(library.provider),
});

const { request } = await sdk.actions.simulateDepositTx({
walletClient,
deposit: quote.deposit,
});

const transactionHash = await walletClient.writeContract(request);

return transactionHash;
}

export function Bridge() {
const { account, library } = useEthers();
const [quote, setQuote] = useState<Quote>();
const [txHash, setTxHash] = useState<Hash>();
const [destinationBlock, setDestinationBlock] = useState<bigint>();
const [depositData, setDepositData] = useState<DepositStatus>();
const [fillData, setFillData] = useState<FillStatus>();

const [loadingDeposit, setLoadingDeposit] = useState(false);
const [loadingFill, setLoadingFill] = useState(false);

async function handleQuote() {
if (!account) return;
const quote = await getQuote(account as Address);
setQuote(quote);
}

async function handleBridge() {
if (!quote || !account) return;
const destinationBlock = await sdk
.getPublicClient(quote.deposit.destinationChainId)
.getBlockNumber();

const hash = await bridge(quote, library as any, account);
setTxHash(hash);

setDestinationBlock(destinationBlock);
}

const waitForDeposit = async (txHash: Hash, quote: Quote) => {
setLoadingDeposit(true);
// wait for tx to be mined
const data = await sdk.waitForDepositTx({
transactionHash: txHash,
chainId: quote.deposit.originChainId,
});
setLoadingDeposit(false);
setDepositData(data);
};

useEffect(() => {
if (txHash && quote) {
waitForDeposit(txHash, quote);
}
}, [txHash, quote]);

const waitForFill = async (
deposit: DepositStatus,
quote: Quote,
destinationBlock: bigint,
) => {
setLoadingFill(true);
// wait for tx to be filled
const data = await sdk.actions.waitForFillTx({
depositId: deposit.depositId,
deposit: quote.deposit,
fromBlock: destinationBlock,
});
setLoadingFill(false);
setFillData(data);
};

useEffect(() => {
if (depositData && quote && destinationBlock) {
waitForFill(depositData, quote, destinationBlock);
}
}, [depositData, quote, destinationBlock]);

return (
<>
<Button variant="filled" onClick={handleQuote}>
get Quote
</Button>
{quote && (
<details>
<summary>Quote</summary>
<pre>
{JSON.stringify(
quote,
(_, v) => (typeof v === "bigint" ? v.toString() : v),
2,
)}
</pre>
</details>
)}
<Button variant="filled" onClick={handleBridge}>
Bridge
</Button>
{!depositData && loadingDeposit && <h3>Waiting for deposit...</h3>}
{depositData && (
<details className="break-words max-w-full">
<summary>Deposit Data</summary>
<pre>
{JSON.stringify(
depositData,
(_, v) => (typeof v === "bigint" ? v.toString() : v),
2,
)}
</pre>
</details>
)}
{!fillData && loadingFill && <h3>Waiting for fill...</h3>}
{fillData && (
<details>
<summary>Fill Data</summary>
<pre>
{JSON.stringify(
fillData,
(_, v) => (typeof v === "bigint" ? v.toString() : v),
2,
)}
</pre>
</details>
)}
</>
);
}
57 changes: 57 additions & 0 deletions apps/example/app/ethers/components/ConnectButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";
import { Button, ButtonProps } from "@/components/ui/button";
import { Icon } from "@/components/Icon";
import { cn } from "@/lib/utils";
import { shortenIfAddress, useEthers } from "@usedapp/core";

type ConnectButton = {
className?: string;
};

export const ConnectButton = ({ className }: ConnectButton) => {
const { account, active, activateBrowserWallet, deactivate } = useEthers();
const connected = !!account && !!active;

return (
<div className={cn(className)}>
{(() => {
if (!connected) {
return <HamburgerButton onClick={activateBrowserWallet} />;
}
// if (connected) {
// return (
// <Button variant="bordered" onClick={openChainModal} type="button">
// Wrong network
// </Button>
// );
// }
return (
<div className="flex items-center gap-2">
<Button
className="flex gap-1"
variant="bordered"
onClick={deactivate}
>
<span className="max-w-[120px] truncate">
{shortenIfAddress(account)}
</span>
</Button>
</div>
);
})()}
</div>
);
};

const HamburgerButton = (props: ButtonProps) => {
return (
<Button
size="icon"
variant="bordered"
className="h-[40px] w-[40px]"
{...props}
>
<Icon name="hamburger" className="h-[20px] w-[20px] text-text/75" />
</Button>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils";
import { Icon } from "@/components/Icon";
import { ConnectButton } from "@/components/ConnectButton";
import { ConnectButton } from "./ConnectButton";

export const Header = ({
className,
@@ -16,7 +16,7 @@ export const Header = ({
>
<div className="relative flex items-center justify-start gap-2">
<Icon name="across-logo" className="h-8 w-8 text-accent" />
<h2 className="text-gradient-oval text-2xl">SDK Example</h2>
<h2 className="text-gradient-oval text-2xl">SDK Example - Ethers</h2>
</div>

<ConnectButton className="relative" />
14 changes: 14 additions & 0 deletions apps/example/app/ethers/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Providers } from "./providers";
import { Header } from "./components/Header";
import { Bridge } from "./components/Bridge";

export default function Ethers() {
return (
<Providers>
<Header />
<main className="flex gap-2 text-sm min-h-screen max-w-[800px] min-w-[600px] mx-auto flex-col items-center justify-start p-24">
<Bridge />
</main>
</Providers>
);
}
27 changes: 27 additions & 0 deletions apps/example/app/ethers/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";
import {
useEthers,
useEtherBalance,
DAppProvider,
Arbitrum,
Config,
Mainnet,
} from "@usedapp/core";
import * as React from "react";
import { ThemeProvider } from "next-themes";

export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={true}>
<DAppProvider config={config}>{children}</DAppProvider>
</ThemeProvider>
);
}

const config: Config = {
readOnlyChainId: Mainnet.chainId,
readOnlyUrls: {
[Mainnet.chainId]: "https://eth.llamarpc.com",
[Arbitrum.chainId]: "https://arbitrum.llamarpc.com",
},
};
9 changes: 1 addition & 8 deletions apps/example/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -2,8 +2,6 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "@rainbow-me/rainbowkit/styles.css";
import "./globals.css";
import { Providers } from "./providers";
import { Header } from "@/components/Header";
import { cn } from "@/lib/utils";

const inter = Inter({ subsets: ["latin"] });
@@ -20,12 +18,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={cn(inter.className, "bg-background")}>
<Providers>
<Header />
{children}
</Providers>
</body>
<body className={cn(inter.className, "bg-background")}>{children}</body>
</html>
);
}
12 changes: 9 additions & 3 deletions apps/example/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Header } from "./viem/components";
import { Providers } from "./viem/providers";

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div></div>
</main>
<Providers>
<Header />
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div></div>
</main>
</Providers>
);
}
File renamed without changes.
25 changes: 25 additions & 0 deletions apps/example/app/viem/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { cn } from "@/lib/utils";
import { Icon } from "@/components/Icon";
import { ConnectButton } from "./ConnectButton";

export const Header = ({
className,
...props
}: React.ComponentPropsWithoutRef<"div">) => {
return (
<div
className={cn(
"fixed top-0 z-20 flex w-full items-center justify-between bg-transparent p-4 md:p-5",
className,
)}
{...props}
>
<div className="relative flex items-center justify-start gap-2">
<Icon name="across-logo" className="h-8 w-8 text-accent" />
<h2 className="text-gradient-oval text-2xl">SDK Example - Viem</h2>
</div>

<ConnectButton className="relative" />
</div>
);
};
1 change: 1 addition & 0 deletions apps/example/app/viem/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Header";
File renamed without changes.
9 changes: 9 additions & 0 deletions apps/example/lib/ethers/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import { useEtherBalance, useEthers } from "@usedapp/core";

export function useGetBalance() {
const { account } = useEthers();
const userBalance = useEtherBalance(account);
return userBalance;
}
3 changes: 3 additions & 0 deletions apps/example/package.json
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
"start": "next start",
"lint": "next lint",
"sdk": "tsx ./scripts/sdk.ts",
"sdk-ethers": "tsx ./scripts/sdk-ethers.ts",
"ci": "pnpm run build && pnpm run lint"
},
"dependencies": {
@@ -18,8 +19,10 @@
"@radix-ui/react-tooltip": "^1.1.2",
"@rainbow-me/rainbowkit": "^2.1.5",
"@tanstack/react-query": "^5.52.2",
"@usedapp/core": "^1.2.16",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"ethers": "v5",
"lucide-react": "^0.436.0",
"next": "14.2.7",
"next-themes": "^0.3.0",
17 changes: 16 additions & 1 deletion apps/example/scripts/sdk.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,22 @@ loadEnvConfig(projectDir);
async function main() {
const chains = [mainnet, arbitrum, optimism];

const account = privateKeyToAccount(process.env.DEV_PK as Hex);
const PRIVATE_KEY = process.env.DEV_PK
? (process.env.DEV_PK as Hex)
: undefined;

if (!PRIVATE_KEY) {
throw new Error("No Private key in ENV");
}

// if in Node environment, running a script, we can just create a viem wallet client using the private key
const account = privateKeyToAccount(PRIVATE_KEY);

// for non-local accounts (eg. JsonRpcProvider in the browser) we can use this example

// const convertedEthersAccount = toAccount(
// "0xD25f7e77386F9f797b64E878A3D060956de99163",
// );

const walletClient = createWalletClient({
account,
4 changes: 2 additions & 2 deletions packages/sdk/src/actions/getFillByDepositTx.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import {
PublicClient,
TransactionReceipt,
} from "viem";
import { QuoteResponse } from "./getQuote";
import { Quote } from "./getQuote";
import { spokePoolAbi } from "../abis/SpokePool";
import { MAINNET_INDEXER_API } from "../constants";
import { HttpError, IndexerError, NoFillLogError } from "../errors";
@@ -18,7 +18,7 @@ type DepositStatusQueryParams = {
originChainId: number;
};

export type GetFillByDepositTxParams = Pick<QuoteResponse, "deposit"> & {
export type GetFillByDepositTxParams = Pick<Quote, "deposit"> & {
depositId: DepositStatus["depositId"];
depositTransactionHash: Hash;
fromBlock: bigint;
2 changes: 0 additions & 2 deletions packages/sdk/src/actions/getQuote.ts
Original file line number Diff line number Diff line change
@@ -132,5 +132,3 @@ export async function getQuote(params: GetQuoteParams) {
estimatedFillTimeSec,
};
}

export type QuoteResponse = Awaited<ReturnType<typeof getQuote>>;
4 changes: 2 additions & 2 deletions packages/sdk/src/actions/waitForFillTx.ts
Original file line number Diff line number Diff line change
@@ -2,9 +2,9 @@ import { parseAbiItem, parseEventLogs } from "viem";
import { ConfiguredPublicClient } from "../types";
import { spokePoolAbi } from "../abis/SpokePool";
import { FillStatus } from "./getFillByDepositTx";
import { QuoteResponse } from "./getQuote";
import { Quote } from "./getQuote";

export type WaitForFillTxParams = Pick<QuoteResponse, "deposit"> & {
export type WaitForFillTxParams = Pick<Quote, "deposit"> & {
depositId: number;
destinationPublicClient: ConfiguredPublicClient; // destination client
fromBlock: bigint;
8 changes: 4 additions & 4 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ import { ConfigError } from "./errors";
import { ConfiguredPublicClient, ConfiguredPublicClientMap } from "./types";

const CLIENT_DEFAULTS = {
pollingIntervalSec: 2,
pollingInterval: 3_000,
logLevel: "ERROR",
} as const;

@@ -53,7 +53,7 @@ export type AcrossClientOptions = {
logLevel?: LogLevel; // for default logger
useTestnet?: boolean;
logger?: LoggerT;
pollingIntervalSec?: number; // seconds
pollingInterval?: number; // milliseconds seconds
// tenderlyApiKey?: string
};

@@ -90,7 +90,7 @@ export class AcrossClient {
this.walletClient = args.walletClient;
this.publicClients = configurePublicClients(
args.chains,
args.pollingIntervalSec ?? CLIENT_DEFAULTS.pollingIntervalSec,
args.pollingInterval ?? CLIENT_DEFAULTS.pollingInterval,
args?.rpcUrls,
);
this.indexerUrl =
@@ -207,7 +207,7 @@ export class AcrossClient {
return getLimits({ ...params, apiUrl: this.apiUrl, logger: this.logger });
}

async getQuote(params: Omit<GetQuoteParams, "logger">) {
async getQuote(params: Omit<GetQuoteParams, "logger" | "apiUrl">) {
return getQuote({ ...params, logger: this.logger, apiUrl: this.apiUrl });
}

3 changes: 3 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from "./client";
export * from "./types";
export * from "./actions";
export * from "./errors";
export * from "./utils";
2 changes: 1 addition & 1 deletion packages/sdk/src/utils/configurePublicClients.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { ConfiguredPublicClientMap } from "../types";
// creates a mapping chainId => publicClient
export function configurePublicClients(
chains: Chain[],
pollingInterval: number,
pollingInterval: number, // milliseconds
rpcUrls?: {
[key: number]: string;
},
1,128 changes: 888 additions & 240 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

0 comments on commit c1ba18e

Please sign in to comment.