diff --git a/packages/apps/src/hooks/use-relayer.ts b/packages/apps/src/hooks/use-relayer.ts new file mode 100644 index 000000000..034ae9707 --- /dev/null +++ b/packages/apps/src/hooks/use-relayer.ts @@ -0,0 +1,4 @@ +import { RelayerContext } from "@/providers/relayer-provider"; +import { useContext } from "react"; + +export const useRelayer = () => useContext(RelayerContext); diff --git a/packages/apps/src/providers/relayer-provider.tsx b/packages/apps/src/providers/relayer-provider.tsx new file mode 100644 index 000000000..d672c73d9 --- /dev/null +++ b/packages/apps/src/providers/relayer-provider.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { LnBridgeDefault } from "@/bridges/lnbridge-default"; +import { LnBridgeOpposite } from "@/bridges/lnbridge-opposite"; +import { BridgeCategory } from "@/types/bridge"; +import { ChainConfig } from "@/types/chain"; +import { Token } from "@/types/token"; +import { + Dispatch, + PropsWithChildren, + SetStateAction, + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { useAccount, usePublicClient, useWalletClient } from "wagmi"; +import { Subscription, forkJoin } from "rxjs"; + +interface RelayerCtx { + margin: { formatted: bigint; value: string }; + baseFee: { formatted: bigint; value: string }; + feeRate: { formatted: number; value: string }; + sourceAllowance: { value: bigint; token: Token } | undefined; + targetAllowance: { value: bigint; token: Token } | undefined; + sourceBalance: { value: bigint; token: Token } | undefined; + targetBalance: { value: bigint; token: Token } | undefined; + bridgeCategory: BridgeCategory | undefined; + sourceChain: ChainConfig | undefined; + targetChain: ChainConfig | undefined; + transferToken: Token | undefined; + defaultBridge: LnBridgeDefault | undefined; + oppositeBridge: LnBridgeOpposite | undefined; + + setMargin: Dispatch>; + setBaseFee: Dispatch>; + setFeeRate: Dispatch>; + setBridgeCategory: Dispatch>; + setSourceChain: Dispatch>; + setTargetChain: Dispatch>; + setTransferToken: Dispatch>; + sourceApprove: (amount: bigint) => Promise; + targetApprove: (amount: bigint) => Promise; + depositMargin: (margin: bigint) => Promise; + updateFeeAndMargin: (margin: bigint, baseFee: bigint, feeRate: number) => Promise; +} + +const defaultValue: RelayerCtx = { + margin: { formatted: 0n, value: "" }, + baseFee: { formatted: 0n, value: "" }, + feeRate: { formatted: 0, value: "" }, + sourceAllowance: undefined, + targetAllowance: undefined, + sourceBalance: undefined, + targetBalance: undefined, + bridgeCategory: undefined, + sourceChain: undefined, + targetChain: undefined, + transferToken: undefined, + defaultBridge: undefined, + oppositeBridge: undefined, + setMargin: () => undefined, + setBaseFee: () => undefined, + setFeeRate: () => undefined, + setBridgeCategory: () => undefined, + setSourceChain: () => undefined, + setTargetChain: () => undefined, + setTransferToken: () => undefined, + sourceApprove: async () => undefined, + targetApprove: async () => undefined, + depositMargin: async () => undefined, + updateFeeAndMargin: async () => undefined, +}; + +export const RelayerContext = createContext(defaultValue); + +export default function RelayerProvider({ children }: PropsWithChildren) { + const { data: walletClient } = useWalletClient(); + const publicClient = usePublicClient(); + const { address } = useAccount(); + + const [margin, setMargin] = useState(defaultValue.margin); + const [baseFee, setBaseFee] = useState(defaultValue.baseFee); + const [feeRate, setFeeRate] = useState(defaultValue.feeRate); + + const [sourceAllowance, setSourceAllowance] = useState(defaultValue.sourceAllowance); + const [targetAllowance, setTargetAllowance] = useState(defaultValue.targetAllowance); + const [sourceBalance, setSourceBalance] = useState(defaultValue.sourceBalance); + const [targetBalance, setTargetBalance] = useState(defaultValue.targetBalance); + + const [bridgeCategory, setBridgeCategory] = useState(defaultValue.bridgeCategory); + const [sourceChain, setSourceChain] = useState(defaultValue.sourceChain); + const [targetChain, setTargetChain] = useState(defaultValue.targetChain); + const [transferToken, setTransferToken] = useState(defaultValue.transferToken); + + const { defaultBridge, oppositeBridge, bridgeClient } = useMemo(() => { + let defaultBridge: LnBridgeDefault | undefined; + let oppositeBridge: LnBridgeOpposite | undefined; + + if (bridgeCategory === "lnbridgev20-default") { + defaultBridge = new LnBridgeDefault({ + category: bridgeCategory, + sourceChain, + targetChain, + walletClient, + publicClient, + }); + } else if (bridgeCategory === "lnbridgev20-opposite") { + oppositeBridge = new LnBridgeOpposite({ + category: bridgeCategory, + sourceChain, + targetChain, + walletClient, + publicClient, + }); + } + + return { defaultBridge, oppositeBridge, bridgeClient: defaultBridge ?? oppositeBridge }; + }, [sourceChain, targetChain, bridgeCategory, walletClient, publicClient]); + + const sourceApprove = useCallback( + async (amount: bigint) => { + if (address && bridgeClient) { + try { + await bridgeClient.sourceApprove(amount, address); + setSourceAllowance(await bridgeClient.getSourceAllowance(address)); + } catch (err) { + console.error(err); + } + } + }, + [address, bridgeClient], + ); + + const targetApprove = useCallback( + async (amount: bigint) => { + if (address && bridgeClient) { + try { + await bridgeClient.targetApprove(amount, address); + setTargetAllowance(await bridgeClient.getTargetAllowance(address)); + } catch (err) { + console.error(err); + } + } + }, + [address, bridgeClient], + ); + + const depositMargin = useCallback( + async (margin: bigint) => { + if (address && defaultBridge) { + try { + await defaultBridge.depositMargin(margin); + const a = await defaultBridge.getTargetAllowance(address); + const b = await defaultBridge.getTargetBalance(address); + setTargetAllowance(a); + setTargetBalance(b); + } catch (err) { + console.error(err); + } + } + }, + [address, defaultBridge], + ); + + const updateFeeAndMargin = useCallback( + async (margin: bigint, baseFee: bigint, feeRate: number) => { + if (address && oppositeBridge) { + try { + await oppositeBridge.updateFeeAndMargin(margin, baseFee, feeRate); + const a = await oppositeBridge.getSourceAllowance(address); + const b = await oppositeBridge.getSourceBalance(address); + setSourceAllowance(a); + setSourceBalance(b); + } catch (err) { + console.error(err); + } + } + }, + [address, oppositeBridge], + ); + + useEffect(() => { + let sub$$: Subscription | undefined; + + if (address && bridgeClient) { + sub$$ = forkJoin([ + bridgeClient.getSourceAllowance(address), + bridgeClient.getTargetAllowance(address), + bridgeClient.getSourceBalance(address), + bridgeClient.getTargetBalance(address), + ]).subscribe({ + next: ([as, at, bs, bt]) => { + setSourceAllowance(as); + setTargetAllowance(at); + setSourceBalance(bs); + setTargetBalance(bt); + }, + error: (err) => { + console.error(err); + setSourceAllowance(defaultValue.sourceAllowance); + setTargetAllowance(defaultValue.targetAllowance); + setSourceBalance(defaultValue.sourceBalance); + setTargetBalance(defaultValue.targetBalance); + }, + }); + } else { + setSourceAllowance(defaultValue.sourceAllowance); + setTargetAllowance(defaultValue.targetAllowance); + setSourceBalance(defaultValue.sourceBalance); + setTargetBalance(defaultValue.targetBalance); + } + + return () => sub$$?.unsubscribe(); + }, [address, bridgeClient]); + + return ( + + {children} + + ); +} diff --git a/packages/apps/src/providers/transfer-provider.tsx b/packages/apps/src/providers/transfer-provider.tsx index 51bc6ba25..6b2221ad4 100644 --- a/packages/apps/src/providers/transfer-provider.tsx +++ b/packages/apps/src/providers/transfer-provider.tsx @@ -1,25 +1,157 @@ "use client"; +import { BaseBridge } from "@/bridges/base"; import { TransferValue } from "@/components/transfer-input"; -import { Dispatch, PropsWithChildren, SetStateAction, createContext, useState } from "react"; +import { BridgeCategory } from "@/types/bridge"; +import { ChainConfig } from "@/types/chain"; +import { Token } from "@/types/token"; +import { bridgeFactory } from "@/utils/bridge"; +import { + Dispatch, + PropsWithChildren, + SetStateAction, + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { Address, useAccount, usePublicClient, useWalletClient } from "wagmi"; +import { Subscription, forkJoin } from "rxjs"; interface TransferCtx { + bridgeCategory: BridgeCategory | undefined; transferValue: TransferValue; + sourceChain: ChainConfig | undefined; + targetChain: ChainConfig | undefined; + sourceToken: Token | undefined; + targetToken: Token | undefined; + bridgeClient: BaseBridge | undefined; + sourceAllowance: { value: bigint; token: Token } | undefined; + sourceBalance: { value: bigint; token: Token } | undefined; + setTransferValue: Dispatch>; + setBridgeCategory: Dispatch>; + setSourceChain: Dispatch>; + setTargetChain: Dispatch>; + setSourceToken: Dispatch>; + setTargetToken: Dispatch>; + transfer: (sender: Address, recipient: Address, amount: bigint, options?: Object) => Promise; } const defaultValue: TransferCtx = { transferValue: { value: "", formatted: 0n }, + bridgeCategory: undefined, + sourceChain: undefined, + targetChain: undefined, + sourceToken: undefined, + targetToken: undefined, + bridgeClient: undefined, + sourceAllowance: undefined, + sourceBalance: undefined, + setBridgeCategory: () => undefined, setTransferValue: () => undefined, + setSourceChain: () => undefined, + setTargetChain: () => undefined, + setSourceToken: () => undefined, + setTargetToken: () => undefined, + transfer: async () => undefined, }; export const TransferContext = createContext(defaultValue); export default function TransferProvider({ children }: PropsWithChildren) { - /** - * Why we define here: https://react.dev/reference/react/useDeferredValue#caveats - */ + const { data: walletClient } = useWalletClient(); + const publicClient = usePublicClient(); + const { address } = useAccount(); + + const [sourceAllowance, setSourceAllowance] = useState(defaultValue.sourceAllowance); + const [sourceBalance, setSourceBalance] = useState(defaultValue.sourceBalance); + const [transferValue, setTransferValue] = useState(defaultValue.transferValue); + const [bridgeCategory, setBridgeCategory] = useState(defaultValue.bridgeCategory); + const [sourceChain, setSourceChain] = useState(defaultValue.sourceChain); + const [targetChain, setTargetChain] = useState(defaultValue.targetChain); + const [sourceToken, setSourceToken] = useState(defaultValue.sourceToken); + const [targetToken, setTargetToken] = useState(defaultValue.targetToken); + + const { bridgeClient } = useMemo(() => { + const bridgeClient = + bridgeCategory && + bridgeFactory({ + category: bridgeCategory, + sourceChain, + targetChain, + sourceToken, + targetToken, + walletClient, + publicClient, + }); + return { bridgeClient }; + }, [sourceChain, targetChain, sourceToken, targetToken, bridgeCategory, walletClient, publicClient]); + + const transfer = useCallback( + async (sender: Address, recipient: Address, amount: bigint, options?: Object) => { + if (address && bridgeClient) { + try { + await bridgeClient.transfer(sender, recipient, amount, options); + const a = await bridgeClient.getSourceAllowance(address); + const b = await bridgeClient.getSourceBalance(address); + setSourceAllowance(a); + setSourceBalance(b); + } catch (err) { + console.error(err); + } + } + }, + [address, bridgeClient], + ); + + useEffect(() => { + let sub$$: Subscription | undefined; + + if (address && bridgeClient) { + sub$$ = forkJoin([bridgeClient.getSourceAllowance(address), bridgeClient.getSourceBalance(address)]).subscribe({ + next: ([a, b]) => { + setSourceAllowance(a); + setSourceBalance(b); + }, + error: (err) => { + console.error(err); + setSourceAllowance(defaultValue.sourceAllowance); + setSourceBalance(defaultValue.sourceBalance); + }, + }); + } else { + setSourceAllowance(defaultValue.sourceAllowance); + setSourceBalance(defaultValue.sourceBalance); + } + + return () => sub$$?.unsubscribe(); + }, [address, bridgeClient]); - return {children}; + return ( + + {children} + + ); }