From 51fa21ea8c80bfef6d5affac15f76af17345a651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dragi=C5=A1a=20Spasojevi=C4=87?= Date: Tue, 26 Sep 2023 18:37:11 +0200 Subject: [PATCH 1/3] stuff --- .../src/__experiments__/BridgeTransfer.ts | 18 ++ .../__experiments__/BridgeTransferStarter.ts | 32 +++ .../BridgeTransferStarterFactory.ts | 21 ++ .../src/__experiments__/Erc20Withdrawal.ts | 73 ++++++ .../__experiments__/Erc20WithdrawalStarter.ts | 23 ++ .../src/__experiments__/EthDeposit.test.ts | 33 +++ .../src/__experiments__/EthDeposit.ts | 69 +++++ .../src/__experiments__/EthDepositStarter.ts | 20 ++ .../__experiments__/EthWithdrawalStarter.ts | 23 ++ .../arb-token-bridge-ui/src/pages/_app.tsx | 2 + .../src/pages/experiment.tsx | 239 ++++++++++++++++++ .../arb-token-bridge-ui/src/pages/index.tsx | 10 +- 12 files changed, 555 insertions(+), 8 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/Erc20Withdrawal.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/Erc20WithdrawalStarter.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.test.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/EthDepositStarter.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/EthWithdrawalStarter.ts create mode 100644 packages/arb-token-bridge-ui/src/pages/experiment.tsx diff --git a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts new file mode 100644 index 0000000000..5b6860d7d5 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts @@ -0,0 +1,18 @@ +import { Provider } from '@ethersproject/providers' + +type Chain = 'from' | 'to' +type TxStatus = 'pending' | 'success' | 'error' + +export type BridgeTransferStatus = `${Chain}_chain_tx_${TxStatus}` +export type BridgeTransferStatusFunctionProps = { toChainProvider: Provider } +export type BridgeTransferStatusFunctionResult = Promise + +export abstract class BridgeTransfer { + public abstract status( + props: BridgeTransferStatusFunctionProps + ): Promise + + public abstract getEstimatedTimeForDestinationChainTxReady(): Promise + + public abstract isDestinationChainTxReady(): Promise +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts new file mode 100644 index 0000000000..64efb4ee17 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts @@ -0,0 +1,32 @@ +import { Provider } from '@ethersproject/providers' +import { BigNumber, Signer, ContractTransaction } from 'ethers' + +export type BridgeTransferStarterConstructorProps = { + fromChainProvider: Provider + fromChainErc20ContractAddress?: string + toChainProvider: Provider +} + +export type BridgeTransferStarterStartProps = { + fromChainSigner: Signer + amount: BigNumber + destinationAddress?: string +} + +export type BridgeTransferStarterStartResult = Promise + +export abstract class BridgeTransferStarter { + protected fromChainProvider: Provider + protected fromChainErc20ContractAddress?: string + protected toChainProvider: Provider + + constructor(props: BridgeTransferStarterConstructorProps) { + this.fromChainProvider = props.fromChainProvider + this.fromChainErc20ContractAddress = props.fromChainErc20ContractAddress + this.toChainProvider = props.toChainProvider + } + + public abstract start( + props: BridgeTransferStarterStartProps + ): BridgeTransferStarterStartResult +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts new file mode 100644 index 0000000000..778e74daff --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts @@ -0,0 +1,21 @@ +import { getL1Network } from '@arbitrum/sdk' +import { Provider } from '@ethersproject/providers' +import { BridgeTransferStarter } from './BridgeTransferStarter' +import { EthDepositStarter } from './EthDepositStarter' +import { EthWithdrawalStarter } from './EthWithdrawalStarter' + +export class BridgeTransferStarterFactory { + public static async create(props: { + fromChainProvider: Provider + fromChainErc20ContractAddress?: string + toChainProvider: Provider + }): Promise { + try { + await getL1Network(props.fromChainProvider) + + return new EthDepositStarter(props) + } catch (error) { + return new EthWithdrawalStarter(props) + } + } +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/Erc20Withdrawal.ts b/packages/arb-token-bridge-ui/src/__experiments__/Erc20Withdrawal.ts new file mode 100644 index 0000000000..fd7d685d45 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/Erc20Withdrawal.ts @@ -0,0 +1,73 @@ +import { Provider, TransactionReceipt } from '@ethersproject/providers' + +import { L2TransactionReceipt } from '@arbitrum/sdk/dist/lib/message/L2Transaction' + +import { + BridgeTransfer, + BridgeTransferStatusFunctionProps, + BridgeTransferStatusFunctionResult +} from './BridgeTransfer' +import { Signer } from 'ethers' +import { L2ToL1Message } from '@arbitrum/sdk' + +export class EthDeposit extends BridgeTransfer { + protected fromChainProvider: Provider + protected fromChainTxReceipt: L2TransactionReceipt + + private constructor(props: { + fromChainProvider: Provider + fromChainTxReceipt: L2TransactionReceipt + }) { + super() + + this.fromChainProvider = props.fromChainProvider + this.fromChainTxReceipt = props.fromChainTxReceipt + } + + public static async create(props: { + fromChainProvider: Provider + fromChainTxHash: string + }) { + const txReceipt = await props.fromChainProvider.getTransactionReceipt( + props.fromChainTxHash + ) + + return new EthDeposit({ + fromChainProvider: props.fromChainProvider, + fromChainTxReceipt: txReceipt as L2TransactionReceipt + }) + } + + public async status( + props: BridgeTransferStatusFunctionProps + ): Promise { + // return 'from_chain_tx_error' + // const [message] = await this.fromChainTxReceipt.getEthDeposits( + // props.toChainProvider + // ) + + // not yet created + // if (typeof message === 'undefined') { + // return 'from_chain_tx_success' + // } + + return 'to_chain_tx_success' + } + + public async complete(props: { + toChainSigner: Signer + toChainProvider: Provider + }) { + const [event] = this.fromChainTxReceipt.getL2ToL1Events() + + if (typeof event === 'undefined') { + throw new Error('event not found') + } + + return L2ToL1Message.fromEvent( + props.toChainSigner, + event, + props.toChainProvider + ).execute(this.fromChainProvider) + } +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/Erc20WithdrawalStarter.ts b/packages/arb-token-bridge-ui/src/__experiments__/Erc20WithdrawalStarter.ts new file mode 100644 index 0000000000..8101c93382 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/Erc20WithdrawalStarter.ts @@ -0,0 +1,23 @@ +import { Erc20Bridger } from '@arbitrum/sdk' +import { + BridgeTransferStarter, + BridgeTransferStarterStartProps, + BridgeTransferStarterStartResult +} from './BridgeTransferStarter' + +export class Erc20WithdrawalStarter extends BridgeTransferStarter { + public async start( + props: BridgeTransferStarterStartProps + ): BridgeTransferStarterStartResult { + const erc20Bridger = await Erc20Bridger.fromProvider(this.toChainProvider) + const address = await props.fromChainSigner.getAddress() + + return erc20Bridger.withdraw({ + amount: props.amount, + l2Signer: props.fromChainSigner, + destinationAddress: props.destinationAddress ?? address, + // todo: get parent chain erc20 address from this one + erc20l1Address: this.fromChainErc20ContractAddress ?? '' + }) + } +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.test.ts b/packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.test.ts new file mode 100644 index 0000000000..d239bdce67 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.test.ts @@ -0,0 +1,33 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers' +import { Wallet, utils } from 'ethers' + +import { EthDeposit } from './EthDeposit' +import { EthDepositStarter } from './EthDepositStarter' + +const fromChainProvider = new StaticJsonRpcProvider('http://localhost:8545') +const toChainProvider = new StaticJsonRpcProvider('http://localhost:8547') + +const fromChainSigner = new Wallet('').connect(fromChainProvider) + +describe('EthDepositStarter', () => { + it('initiates a deposit', async () => { + const ethDepositStarter = new EthDepositStarter({ + fromChainProvider, + toChainProvider + }) + + const tx = await ethDepositStarter.start({ + fromChainSigner, + amount: utils.parseEther('0.1') + }) + + const txReceipt = await tx.wait() + + const transfer = await EthDeposit.create({ + fromChainProvider, + fromChainTxHash: txReceipt.transactionHash + }) + + await transfer.status({ toChainProvider }) + }) +}) diff --git a/packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.ts b/packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.ts new file mode 100644 index 0000000000..fa9f8078e0 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/EthDeposit.ts @@ -0,0 +1,69 @@ +import { Provider } from '@ethersproject/providers' +import { L1EthDepositTransactionReceipt } from '@arbitrum/sdk/dist/lib/message/L1Transaction' + +import { + BridgeTransfer, + BridgeTransferStatusFunctionProps, + BridgeTransferStatusFunctionResult +} from './BridgeTransfer' + +type WithOptionalInterval = T & { interval?: number } + +export class EthDeposit extends BridgeTransfer { + protected fromChainProvider: Provider + protected fromChainTxReceipt: L1EthDepositTransactionReceipt + + private constructor(props: { + fromChainProvider: Provider + fromChainTxReceipt: L1EthDepositTransactionReceipt + }) { + super() + + this.fromChainProvider = props.fromChainProvider + this.fromChainTxReceipt = props.fromChainTxReceipt + } + + public static async create(props: { + fromChainProvider: Provider + fromChainTxHash: string + }) { + const txReceipt = await props.fromChainProvider.getTransactionReceipt( + props.fromChainTxHash + ) + + return new EthDeposit({ + fromChainProvider: props.fromChainProvider, + fromChainTxReceipt: txReceipt as L1EthDepositTransactionReceipt + }) + } + + public async status( + props: BridgeTransferStatusFunctionProps + ): Promise { + // return 'from_chain_tx_error' + const [message] = await this.fromChainTxReceipt.getEthDeposits( + props.toChainProvider + ) + + // not yet created + if (typeof message === 'undefined') { + return 'from_chain_tx_success' + } + + return 'to_chain_tx_success' + } + + public async watchForStatus( + props: WithOptionalInterval + ) { + const intervalId = setInterval(async () => { + const status = await this.status(props) + + if (status === 'to_chain_tx_success') { + clearInterval(intervalId) + } + + return status + }, props?.interval ?? 10_000) + } +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/EthDepositStarter.ts b/packages/arb-token-bridge-ui/src/__experiments__/EthDepositStarter.ts new file mode 100644 index 0000000000..252ec3c868 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/EthDepositStarter.ts @@ -0,0 +1,20 @@ +import { EthBridger } from '@arbitrum/sdk' + +import { + BridgeTransferStarter, + BridgeTransferStarterStartProps, + BridgeTransferStarterStartResult +} from './BridgeTransferStarter' + +export class EthDepositStarter extends BridgeTransferStarter { + public async start( + props: BridgeTransferStarterStartProps + ): BridgeTransferStarterStartResult { + const ethBridger = await EthBridger.fromProvider(this.toChainProvider) + + return ethBridger.deposit({ + amount: props.amount, + l1Signer: props.fromChainSigner + }) + } +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/EthWithdrawalStarter.ts b/packages/arb-token-bridge-ui/src/__experiments__/EthWithdrawalStarter.ts new file mode 100644 index 0000000000..764194a4c5 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/EthWithdrawalStarter.ts @@ -0,0 +1,23 @@ +import { EthBridger } from '@arbitrum/sdk' + +import { + BridgeTransferStarter, + BridgeTransferStarterStartProps, + BridgeTransferStarterStartResult +} from './BridgeTransferStarter' + +export class EthWithdrawalStarter extends BridgeTransferStarter { + public async start( + props: BridgeTransferStarterStartProps + ): BridgeTransferStarterStartResult { + const ethBridger = await EthBridger.fromProvider(this.fromChainProvider) + const address = await props.fromChainSigner.getAddress() + + return ethBridger.withdraw({ + amount: props.amount, + l2Signer: props.fromChainSigner, + destinationAddress: props.destinationAddress ?? address, + from: address + }) + } +} diff --git a/packages/arb-token-bridge-ui/src/pages/_app.tsx b/packages/arb-token-bridge-ui/src/pages/_app.tsx index 35c06d4909..61fe8f914e 100644 --- a/packages/arb-token-bridge-ui/src/pages/_app.tsx +++ b/packages/arb-token-bridge-ui/src/pages/_app.tsx @@ -72,6 +72,8 @@ if ( } export default function App({ Component, pageProps }: AppProps) { + return + return ( <> diff --git a/packages/arb-token-bridge-ui/src/pages/experiment.tsx b/packages/arb-token-bridge-ui/src/pages/experiment.tsx new file mode 100644 index 0000000000..162a6d112b --- /dev/null +++ b/packages/arb-token-bridge-ui/src/pages/experiment.tsx @@ -0,0 +1,239 @@ +import { useAccount, useNetwork, useSigner, WagmiConfig } from 'wagmi' +import { BigNumber, utils } from 'ethers' +import { + lightTheme, + RainbowKitProvider, + ConnectButton +} from '@rainbow-me/rainbowkit' + +import { getProps } from '../util/wagmi/setup' +import { PropsWithChildren, useEffect, useState } from 'react' +import { + StaticJsonRpcProvider, + TransactionReceipt +} from '@ethersproject/providers' + +import { EthDepositStarter } from '../__experiments__/EthDepositStarter' +import { Loader } from '../components/common/atoms/Loader' +import { util } from 'zod' +import { BridgeTransferStarterFactory } from '../__experiments__/BridgeTransferStarterFactory' + +const { wagmiConfigProps, rainbowKitProviderProps } = getProps(null) +console.log(wagmiConfigProps) + +function Connected(props: PropsWithChildren<{ fallback: React.ReactNode }>) { + const { isConnected } = useAccount() + + if (!isConnected) { + return props.fallback + } + + return props.children +} + +type SmolChain = { + name: string + provider: StaticJsonRpcProvider +} + +const goerli: SmolChain = { + name: 'Goerli', + provider: new StaticJsonRpcProvider( + `https://goerli.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_KEY}` + ) +} + +const arbitrumGoerli = { + name: 'Arbitrum Goerli', + provider: new StaticJsonRpcProvider(`https://goerli-rollup.arbitrum.io/rpc `) +} + +const ethDepositsHistory = [ + '0xa03d288446a8aa077df378ca11b4adebc9bca84cdf522d13358bd9beaaa3bcec' +] + +type Balance = BigNumber | null + +function Balance({ provider }: { provider: StaticJsonRpcProvider }) { + const { address } = useAccount() + const [balance, setBalance] = useState(null) + + useEffect(() => { + async function updateBalance() { + setBalance(null) + setBalance(await provider.getBalance(address!)) + } + + updateBalance() + }, [address, provider]) + + if (!balance) { + return + } + + return {utils.formatEther(balance)} ETH +} + +class BridgeTransfer { + public fromChainTxHash: string + public fromChainTxStatus: 'pending' | 'confirmed' | 'failed' + + constructor(props: { fromChainTxHash: string }) { + this.fromChainTxHash = props.fromChainTxHash + this.fromChainTxStatus = 'pending' + } + + public updateWithFromChainTxReceipt(receipt: TransactionReceipt) { + if (receipt.status === 0) { + this.fromChainTxStatus = 'failed' + } else { + this.fromChainTxStatus = 'confirmed' + } + } +} + +async function fetchFromHistory(props: { + txHash: string + provider: StaticJsonRpcProvider +}) { + return props.provider.getTransactionReceipt(props.txHash) +} + +function App() { + const { address } = useAccount() + const { data: signer } = useSigner() + + const [amount, setAmount] = useState('') + const [fromChain, setFromChain] = useState(goerli) + const [toChain, setToChain] = useState(arbitrumGoerli) + + const [bridgeTransfers, setBridgeTransfers] = useState([]) + + useEffect(() => { + async function fetchTxHistory() { + setBridgeTransfers([]) + // setBridgeTransfers( + // await Promise.all( + // ethDepositsHistory.map(async txHash => { + // const bridgeTransfer = new BridgeTransfer({ + // fromChainTxHash: txHash + // }) + // const txReceipt = await fromChain.provider.getTransactionReceipt( + // txHash + // ) + + // bridgeTransfer.updateWithFromChainTxReceipt(txReceipt) + + // return bridgeTransfer + // }) + // ) + // ) + } + + fetchTxHistory() + }, [fromChain.provider]) + + function swap() { + setFromChain(toChain) + setToChain(fromChain) + } + + async function bridge() { + if (!signer) { + throw new Error('signer not found') + } + + const starter = await BridgeTransferStarterFactory.create({ + fromChainProvider: fromChain.provider, + toChainProvider: toChain.provider + }) + + const tx = await starter.start({ + fromChainSigner: signer, + amount: utils.parseEther(amount) + }) + + const bridgeTransfer = new BridgeTransfer({ + fromChainTxHash: tx.hash + }) + + setBridgeTransfers([...bridgeTransfers, bridgeTransfer]) + + // const txReceipt = await tx.wait() + + // bridgeTransfer.updateWithFromChainTxReceipt(txReceipt) + } + + return ( +
+

Experimental Bridge UI

+ + }> + Connected: {address} + + +
+
+ + From: {fromChain.name} + + +
+ +
+ + To: {toChain.name} + + +
+
+ +
+ setAmount(event.target.value)} + /> + + +
+ +
+

Transaction History

+ +
    + {bridgeTransfers.map(bridgeTransfer => ( +
  1. +
    + + fromChainTxHash: {bridgeTransfer.fromChainTxHash} + + + fromChainTxStatus: {bridgeTransfer.fromChainTxStatus} + +
    +
  2. + ))} +
+
+
+ ) +} + +export default function Experiment() { + return ( + + + + + + ) +} + +Experiment.displayName = 'Experiment' diff --git a/packages/arb-token-bridge-ui/src/pages/index.tsx b/packages/arb-token-bridge-ui/src/pages/index.tsx index 78b3274e79..03db95fcb2 100644 --- a/packages/arb-token-bridge-ui/src/pages/index.tsx +++ b/packages/arb-token-bridge-ui/src/pages/index.tsx @@ -11,15 +11,9 @@ import { } from '../util/networks' import { mapCustomChainToNetworkData } from '../util/networks' -const App = dynamic(() => import('../components/App/App'), { +const App = dynamic(() => import('./experiment'), { ssr: false, - loading: () => ( - -
- -
-
- ) + loading: () => null }) export default function Index() { From 4f9c24ec698f78b56d82b5ac17dd53d2afaa8172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dragi=C5=A1a=20Spasojevi=C4=87?= Date: Fri, 6 Oct 2023 15:21:39 +0200 Subject: [PATCH 2/3] lots of stuff --- .../src/__experiments__/BridgeTransfer.ts | 69 +++++++-- .../__experiments__/BridgeTransferStarter.ts | 47 ++++-- .../BridgeTransferStarterFactory.ts | 27 ++-- .../src/__experiments__/Erc20Deposit.ts | 97 ++++++++++++ .../__experiments__/Erc20DepositStarter.ts | 75 +++++++++ .../src/pages/experiment.tsx | 145 ++++++++---------- 6 files changed, 339 insertions(+), 121 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts create mode 100644 packages/arb-token-bridge-ui/src/__experiments__/Erc20DepositStarter.ts diff --git a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts index 5b6860d7d5..41655e9a46 100644 --- a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts +++ b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransfer.ts @@ -1,18 +1,69 @@ import { Provider } from '@ethersproject/providers' +import { ContractReceipt, ContractTransaction } from 'ethers' -type Chain = 'from' | 'to' +type Chain = 'source_chain' | 'destination_chain' type TxStatus = 'pending' | 'success' | 'error' -export type BridgeTransferStatus = `${Chain}_chain_tx_${TxStatus}` -export type BridgeTransferStatusFunctionProps = { toChainProvider: Provider } -export type BridgeTransferStatusFunctionResult = Promise +export type BridgeTransferStatus = `${Chain}_tx_${TxStatus}` +export type BridgeTransferFetchStatusFunctionResult = + Promise export abstract class BridgeTransfer { - public abstract status( - props: BridgeTransferStatusFunctionProps - ): Promise + // status + public status: BridgeTransferStatus - public abstract getEstimatedTimeForDestinationChainTxReady(): Promise + // source chain + public sourceChainProvider: Provider + public sourceChainTx: ContractTransaction + public sourceChainTxReceipt?: ContractReceipt - public abstract isDestinationChainTxReady(): Promise + // destination chain + public destinationChainProvider: Provider + public destinationChainTx?: ContractTransaction + public destinationChainTxReceipt?: ContractReceipt + + protected constructor(props: { + status: BridgeTransferStatus + sourceChainTx: ContractTransaction + sourceChainTxReceipt?: ContractReceipt + sourceChainProvider: Provider + destinationChainProvider: Provider + }) { + this.status = props.status + this.sourceChainTx = props.sourceChainTx + this.sourceChainTxReceipt = props.sourceChainTxReceipt + this.sourceChainProvider = props.sourceChainProvider + this.destinationChainProvider = props.destinationChainProvider + } + + /** + * Checks if the bridge transfer status provided is final. + * + * @param status Status to be checked. + */ + protected abstract isStatusFinal(status: BridgeTransferStatus): boolean + + /** + * Fetches the current status of the bridge transfer. + */ + public abstract fetchStatus(): BridgeTransferFetchStatusFunctionResult + + public pollForStatus(props: { + intervalMs?: number + onChange: (bridgeTransfer: BridgeTransfer) => void + }): void { + const intervalId = setInterval(async () => { + const status = await this.fetchStatus() + const statusChanged = this.status !== status + this.status = status + + if (statusChanged) { + props.onChange(this) + } + + if (this.isStatusFinal(this.status)) { + clearInterval(intervalId) + } + }, props.intervalMs ?? 15_000) + } } diff --git a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts index 64efb4ee17..b946adf4a8 100644 --- a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts +++ b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarter.ts @@ -1,32 +1,45 @@ import { Provider } from '@ethersproject/providers' -import { BigNumber, Signer, ContractTransaction } from 'ethers' +import { BigNumber, ContractTransaction, Signer } from 'ethers' + +import { BridgeTransfer } from './BridgeTransfer' export type BridgeTransferStarterConstructorProps = { - fromChainProvider: Provider - fromChainErc20ContractAddress?: string - toChainProvider: Provider + sourceChainProvider: Provider + sourceChainErc20ContractAddress?: string + destinationChainProvider: Provider } -export type BridgeTransferStarterStartProps = { - fromChainSigner: Signer - amount: BigNumber - destinationAddress?: string +export type BridgeTransferStarterApproveFunctionProps = { + amount?: BigNumber + sourceChainSigner: Signer } -export type BridgeTransferStarterStartResult = Promise +export type BridgeTransferStarterStartFunctionProps = { + amount: BigNumber + sourceChainSigner: Signer + destinationChainAddress?: string +} export abstract class BridgeTransferStarter { - protected fromChainProvider: Provider - protected fromChainErc20ContractAddress?: string - protected toChainProvider: Provider + protected sourceChainProvider: Provider + protected sourceChainErc20ContractAddress?: string + protected destinationChainProvider: Provider constructor(props: BridgeTransferStarterConstructorProps) { - this.fromChainProvider = props.fromChainProvider - this.fromChainErc20ContractAddress = props.fromChainErc20ContractAddress - this.toChainProvider = props.toChainProvider + this.sourceChainProvider = props.sourceChainProvider + this.sourceChainErc20ContractAddress = props.sourceChainErc20ContractAddress + this.destinationChainProvider = props.destinationChainProvider } + public abstract requiresApproval( + props: BridgeTransferStarterStartFunctionProps + ): Promise + + public abstract approve( + props: BridgeTransferStarterApproveFunctionProps + ): Promise + public abstract start( - props: BridgeTransferStarterStartProps - ): BridgeTransferStarterStartResult + props: BridgeTransferStarterStartFunctionProps + ): Promise } diff --git a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts index 778e74daff..660a059e68 100644 --- a/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts +++ b/packages/arb-token-bridge-ui/src/__experiments__/BridgeTransferStarterFactory.ts @@ -1,21 +1,14 @@ -import { getL1Network } from '@arbitrum/sdk' -import { Provider } from '@ethersproject/providers' -import { BridgeTransferStarter } from './BridgeTransferStarter' -import { EthDepositStarter } from './EthDepositStarter' -import { EthWithdrawalStarter } from './EthWithdrawalStarter' +import { + BridgeTransferStarter, + BridgeTransferStarterConstructorProps +} from './BridgeTransferStarter' -export class BridgeTransferStarterFactory { - public static async create(props: { - fromChainProvider: Provider - fromChainErc20ContractAddress?: string - toChainProvider: Provider - }): Promise { - try { - await getL1Network(props.fromChainProvider) +import { Erc20DepositStarter } from './Erc20DepositStarter' - return new EthDepositStarter(props) - } catch (error) { - return new EthWithdrawalStarter(props) - } +export class BridgeTransferStarterFactory { + public static async create( + props: BridgeTransferStarterConstructorProps + ): Promise { + return new Erc20DepositStarter(props) } } diff --git a/packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts b/packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts new file mode 100644 index 0000000000..cbeb9d2c8c --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts @@ -0,0 +1,97 @@ +import { ContractTransaction, ContractReceipt } from 'ethers' +import { Provider } from '@ethersproject/providers' +import { L1ContractCallTransactionReceipt } from '@arbitrum/sdk/dist/lib/message/L1Transaction' + +import { + BridgeTransfer, + BridgeTransferStatus, + BridgeTransferFetchStatusFunctionResult +} from './BridgeTransfer' +import { L1ToL2MessageStatus } from '@arbitrum/sdk' + +export class Erc20Deposit extends BridgeTransfer { + private constructor(props: { + status: BridgeTransferStatus + sourceChainTx: ContractTransaction + sourceChainTxReceipt?: ContractReceipt + sourceChainProvider: Provider + destinationChainProvider: Provider + }) { + super(props) + } + + public static async fromSourceChainTx(props: { + sourceChainTx: ContractTransaction + sourceChainProvider: Provider + destinationChainProvider: Provider + }) { + const sourceChainTxReceipt = + await props.sourceChainProvider.getTransactionReceipt( + props.sourceChainTx.hash + ) + + let status: BridgeTransferStatus + + if (sourceChainTxReceipt) { + status = + sourceChainTxReceipt.status === 0 + ? 'source_chain_tx_error' + : 'source_chain_tx_success' + } else { + status = 'source_chain_tx_pending' + } + + return new Erc20Deposit({ ...props, status, sourceChainTxReceipt }) + } + + protected isStatusFinal(status: BridgeTransferStatus): boolean { + if (status === 'source_chain_tx_error') { + return true + } + + return false + } + + public async fetchStatus(): BridgeTransferFetchStatusFunctionResult { + // we don't have a source chain tx receipt yet + if (!this.sourceChainTxReceipt) { + // let's fetch it + this.sourceChainTxReceipt = + await this.sourceChainProvider.getTransactionReceipt( + this.sourceChainTx.hash + ) + + // still nothing + if (!this.sourceChainTxReceipt) { + return 'source_chain_tx_pending' + } + } + + // okay now we have tx receipt + const [message] = await new L1ContractCallTransactionReceipt( + this.sourceChainTxReceipt + ).getL1ToL2Messages(this.destinationChainProvider) + + // message not yet created + if (typeof message === 'undefined') { + return this.sourceChainTxReceipt.status === 1 + ? 'source_chain_tx_success' + : 'source_chain_tx_error' + } + + return this.mapStatus(await message.status()) + } + + private mapStatus(status: L1ToL2MessageStatus): BridgeTransferStatus { + switch (status) { + case L1ToL2MessageStatus.NOT_YET_CREATED: + return 'source_chain_tx_success' + + case L1ToL2MessageStatus.REDEEMED: + return 'destination_chain_tx_success' + + default: + return 'destination_chain_tx_pending' + } + } +} diff --git a/packages/arb-token-bridge-ui/src/__experiments__/Erc20DepositStarter.ts b/packages/arb-token-bridge-ui/src/__experiments__/Erc20DepositStarter.ts new file mode 100644 index 0000000000..2936aa17d9 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/__experiments__/Erc20DepositStarter.ts @@ -0,0 +1,75 @@ +import { Erc20Bridger } from '@arbitrum/sdk' + +import { + BridgeTransferStarter, + BridgeTransferStarterApproveFunctionProps, + BridgeTransferStarterStartFunctionProps +} from './BridgeTransferStarter' +import { Erc20Deposit } from './Erc20Deposit' +import { ContractTransaction } from 'ethers' +import { BridgeTransfer } from './BridgeTransfer' +import { getL1TokenAllowance } from '../util/TokenUtils' + +export class Erc20DepositStarter extends BridgeTransferStarter { + public async requiresApproval( + props: BridgeTransferStarterStartFunctionProps + ): Promise { + const account = await props.sourceChainSigner.getAddress() + + if (typeof this.sourceChainErc20ContractAddress === 'undefined') { + throw new Error('unexpected') + } + + const allowance = await getL1TokenAllowance({ + account, + erc20L1Address: this.sourceChainErc20ContractAddress, + l1Provider: this.sourceChainProvider, + l2Provider: this.destinationChainProvider + }) + + return allowance.lt(props.amount) + } + + public async approve( + props: BridgeTransferStarterApproveFunctionProps + ): Promise { + const erc20Bridger = await Erc20Bridger.fromProvider( + this.destinationChainProvider + ) + + if (typeof this.sourceChainErc20ContractAddress === 'undefined') { + throw new Error('unexpected') + } + + return erc20Bridger.approveToken({ + amount: props.amount, + l1Signer: props.sourceChainSigner, + erc20L1Address: this.sourceChainErc20ContractAddress + }) + } + + public async start( + props: BridgeTransferStarterStartFunctionProps + ): Promise { + const erc20Bridger = await Erc20Bridger.fromProvider( + this.destinationChainProvider + ) + + if (typeof this.sourceChainErc20ContractAddress === 'undefined') { + throw new Error('unexpected') + } + + const tx = await erc20Bridger.deposit({ + amount: props.amount, + l1Signer: props.sourceChainSigner, + erc20L1Address: this.sourceChainErc20ContractAddress, + l2Provider: this.destinationChainProvider + }) + + return Erc20Deposit.fromSourceChainTx({ + sourceChainTx: tx, + sourceChainProvider: this.sourceChainProvider, + destinationChainProvider: this.destinationChainProvider + }) + } +} diff --git a/packages/arb-token-bridge-ui/src/pages/experiment.tsx b/packages/arb-token-bridge-ui/src/pages/experiment.tsx index 162a6d112b..0f4633b688 100644 --- a/packages/arb-token-bridge-ui/src/pages/experiment.tsx +++ b/packages/arb-token-bridge-ui/src/pages/experiment.tsx @@ -1,4 +1,4 @@ -import { useAccount, useNetwork, useSigner, WagmiConfig } from 'wagmi' +import { useAccount, useSigner, WagmiConfig } from 'wagmi' import { BigNumber, utils } from 'ethers' import { lightTheme, @@ -8,18 +8,13 @@ import { import { getProps } from '../util/wagmi/setup' import { PropsWithChildren, useEffect, useState } from 'react' -import { - StaticJsonRpcProvider, - TransactionReceipt -} from '@ethersproject/providers' +import { StaticJsonRpcProvider } from '@ethersproject/providers' -import { EthDepositStarter } from '../__experiments__/EthDepositStarter' import { Loader } from '../components/common/atoms/Loader' -import { util } from 'zod' +import { BridgeTransfer } from '../__experiments__/BridgeTransfer' import { BridgeTransferStarterFactory } from '../__experiments__/BridgeTransferStarterFactory' const { wagmiConfigProps, rainbowKitProviderProps } = getProps(null) -console.log(wagmiConfigProps) function Connected(props: PropsWithChildren<{ fallback: React.ReactNode }>) { const { isConnected } = useAccount() @@ -74,29 +69,30 @@ function Balance({ provider }: { provider: StaticJsonRpcProvider }) { return {utils.formatEther(balance)} ETH } -class BridgeTransfer { - public fromChainTxHash: string - public fromChainTxStatus: 'pending' | 'confirmed' | 'failed' - - constructor(props: { fromChainTxHash: string }) { - this.fromChainTxHash = props.fromChainTxHash - this.fromChainTxStatus = 'pending' - } - - public updateWithFromChainTxReceipt(receipt: TransactionReceipt) { - if (receipt.status === 0) { - this.fromChainTxStatus = 'failed' - } else { - this.fromChainTxStatus = 'confirmed' - } - } +type TxHistoryEntry = { + type: `${'eth' | 'erc-20'}-${'deposit' | 'withdrawal'}` + txHash: string } -async function fetchFromHistory(props: { - txHash: string - provider: StaticJsonRpcProvider +function BridgeTransferListItem({ + bridgeTransfer +}: { + bridgeTransfer: BridgeTransfer }) { - return props.provider.getTransactionReceipt(props.txHash) + return ( +
  • +
    + + sourceChainTxHash: {bridgeTransfer.sourceChainTx.hash} + + + destinationChainTxHash:{' '} + {bridgeTransfer.destinationChainTxReceipt?.transactionHash ?? '?'} + + status: {bridgeTransfer.status} +
    +
  • + ) } function App() { @@ -104,34 +100,13 @@ function App() { const { data: signer } = useSigner() const [amount, setAmount] = useState('') + const [erc20, setErc20] = useState('') const [fromChain, setFromChain] = useState(goerli) const [toChain, setToChain] = useState(arbitrumGoerli) - const [bridgeTransfers, setBridgeTransfers] = useState([]) - - useEffect(() => { - async function fetchTxHistory() { - setBridgeTransfers([]) - // setBridgeTransfers( - // await Promise.all( - // ethDepositsHistory.map(async txHash => { - // const bridgeTransfer = new BridgeTransfer({ - // fromChainTxHash: txHash - // }) - // const txReceipt = await fromChain.provider.getTransactionReceipt( - // txHash - // ) - - // bridgeTransfer.updateWithFromChainTxReceipt(txReceipt) - - // return bridgeTransfer - // }) - // ) - // ) - } - - fetchTxHistory() - }, [fromChain.provider]) + const [bridgeTransferMap, setBridgeTransferMap] = useState< + Record + >({}) function swap() { setFromChain(toChain) @@ -143,25 +118,38 @@ function App() { throw new Error('signer not found') } - const starter = await BridgeTransferStarterFactory.create({ - fromChainProvider: fromChain.provider, - toChainProvider: toChain.provider + const sourceChainErc20ContractAddress = erc20 !== '' ? erc20 : undefined + + const bridgeTransferStarter = await BridgeTransferStarterFactory.create({ + sourceChainProvider: fromChain.provider, + destinationChainProvider: toChain.provider, + sourceChainErc20ContractAddress }) - const tx = await starter.start({ - fromChainSigner: signer, + const startProps = { + sourceChainSigner: signer, amount: utils.parseEther(amount) - }) + } - const bridgeTransfer = new BridgeTransfer({ - fromChainTxHash: tx.hash - }) + if (await bridgeTransferStarter.requiresApproval(startProps)) { + const approvalTx = await bridgeTransferStarter.approve(startProps) + await approvalTx.wait() + } - setBridgeTransfers([...bridgeTransfers, bridgeTransfer]) + const bridgeTransfer = await bridgeTransferStarter.start(startProps) - // const txReceipt = await tx.wait() + setBridgeTransferMap(prevBridgeTransferMap => ({ + ...prevBridgeTransferMap, + [bridgeTransfer.sourceChainTx.hash]: bridgeTransfer + })) - // bridgeTransfer.updateWithFromChainTxReceipt(txReceipt) + bridgeTransfer.pollForStatus({ + onChange: _bridgeTranfer => + setBridgeTransferMap(prevBridgeTransferMap => ({ + ...prevBridgeTransferMap, + [_bridgeTranfer.sourceChainTx.hash]: _bridgeTranfer + })) + }) } return ( @@ -188,7 +176,14 @@ function App() { -
    +
    + setErc20(event.target.value)} + /> +
    From bc887e3e43fd22ec3485e0fbc64766c99c0ce332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dragi=C5=A1a=20Spasojevi=C4=87?= Date: Fri, 6 Oct 2023 15:56:30 +0200 Subject: [PATCH 3/3] progress --- .../src/__experiments__/Erc20Deposit.ts | 43 +++++-- .../src/pages/experiment.tsx | 118 ++++++++++++++---- 2 files changed, 126 insertions(+), 35 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts b/packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts index cbeb9d2c8c..a0183eb0d1 100644 --- a/packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts +++ b/packages/arb-token-bridge-ui/src/__experiments__/Erc20Deposit.ts @@ -44,6 +44,25 @@ export class Erc20Deposit extends BridgeTransfer { return new Erc20Deposit({ ...props, status, sourceChainTxReceipt }) } + public static async fromSourceChainTxHash(props: { + sourceChainTxHash: string + sourceChainProvider: Provider + destinationChainProvider: Provider + }) { + const sourceChainTx = await props.sourceChainProvider.getTransaction( + props.sourceChainTxHash + ) + + const erc20Deposit = await Erc20Deposit.fromSourceChainTx({ + ...props, + sourceChainTx + }) + + await erc20Deposit.updateStatus() + + return erc20Deposit + } + protected isStatusFinal(status: BridgeTransferStatus): boolean { if (status === 'source_chain_tx_error') { return true @@ -52,6 +71,10 @@ export class Erc20Deposit extends BridgeTransfer { return false } + public async updateStatus(): Promise { + this.status = await this.fetchStatus() + } + public async fetchStatus(): BridgeTransferFetchStatusFunctionResult { // we don't have a source chain tx receipt yet if (!this.sourceChainTxReceipt) { @@ -79,19 +102,17 @@ export class Erc20Deposit extends BridgeTransfer { : 'source_chain_tx_error' } - return this.mapStatus(await message.status()) - } + const successfulRedeem = await message.getSuccessfulRedeem() - private mapStatus(status: L1ToL2MessageStatus): BridgeTransferStatus { - switch (status) { - case L1ToL2MessageStatus.NOT_YET_CREATED: - return 'source_chain_tx_success' - - case L1ToL2MessageStatus.REDEEMED: - return 'destination_chain_tx_success' + if (successfulRedeem.status === L1ToL2MessageStatus.REDEEMED) { + this.destinationChainTxReceipt = successfulRedeem.l2TxReceipt + return 'destination_chain_tx_success' + } - default: - return 'destination_chain_tx_pending' + if (successfulRedeem.status === L1ToL2MessageStatus.NOT_YET_CREATED) { + return 'source_chain_tx_success' } + + return 'destination_chain_tx_error' } } diff --git a/packages/arb-token-bridge-ui/src/pages/experiment.tsx b/packages/arb-token-bridge-ui/src/pages/experiment.tsx index 0f4633b688..a13e571ac7 100644 --- a/packages/arb-token-bridge-ui/src/pages/experiment.tsx +++ b/packages/arb-token-bridge-ui/src/pages/experiment.tsx @@ -8,11 +8,12 @@ import { import { getProps } from '../util/wagmi/setup' import { PropsWithChildren, useEffect, useState } from 'react' -import { StaticJsonRpcProvider } from '@ethersproject/providers' +import { Provider, StaticJsonRpcProvider } from '@ethersproject/providers' import { Loader } from '../components/common/atoms/Loader' import { BridgeTransfer } from '../__experiments__/BridgeTransfer' import { BridgeTransferStarterFactory } from '../__experiments__/BridgeTransferStarterFactory' +import { Erc20Deposit } from '../__experiments__/Erc20Deposit' const { wagmiConfigProps, rainbowKitProviderProps } = getProps(null) @@ -29,24 +30,23 @@ function Connected(props: PropsWithChildren<{ fallback: React.ReactNode }>) { type SmolChain = { name: string provider: StaticJsonRpcProvider + blockExplorer: string } const goerli: SmolChain = { name: 'Goerli', provider: new StaticJsonRpcProvider( `https://goerli.infura.io/v3/${process.env.NEXT_PUBLIC_INFURA_KEY}` - ) + ), + blockExplorer: 'https://goerli.etherscan.io' } const arbitrumGoerli = { name: 'Arbitrum Goerli', - provider: new StaticJsonRpcProvider(`https://goerli-rollup.arbitrum.io/rpc `) + provider: new StaticJsonRpcProvider(`https://goerli-rollup.arbitrum.io/rpc`), + blockExplorer: 'https://goerli.arbiscan.io' } -const ethDepositsHistory = [ - '0xa03d288446a8aa077df378ca11b4adebc9bca84cdf522d13358bd9beaaa3bcec' -] - type Balance = BigNumber | null function Balance({ provider }: { provider: StaticJsonRpcProvider }) { @@ -71,26 +71,81 @@ function Balance({ provider }: { provider: StaticJsonRpcProvider }) { type TxHistoryEntry = { type: `${'eth' | 'erc-20'}-${'deposit' | 'withdrawal'}` - txHash: string + sourceChainTxHash: string +} + +const txHistory: TxHistoryEntry[] = [ + { + type: 'erc-20-deposit', + sourceChainTxHash: + '0xffd303a28eb156fd95ae4b424c00944d672a5737ec93c2b93f772e490ba7a6b8' + }, + { + type: 'erc-20-deposit', + sourceChainTxHash: + '0x9228d80ee1da15301755de4a577a4baa21614250d6c78d97ad4ca72dbfccdb06' + }, + { + type: 'erc-20-deposit', + sourceChainTxHash: + '0x79136479c42ca7f96e6eed6018e4ca015f76270fcf904598eae4e97081a8e509' + } +] + +async function loadTxHistory(props: { + sourceChainProvider: Provider + destinationChainProvider: Provider +}): Promise { + return Promise.all( + Object.values(txHistory).map(async tx => + Erc20Deposit.fromSourceChainTxHash({ + sourceChainTxHash: tx.sourceChainTxHash, + sourceChainProvider: props.sourceChainProvider, + destinationChainProvider: props.destinationChainProvider + }) + ) + ) } function BridgeTransferListItem({ - bridgeTransfer + bridgeTransfer, + sourceChainBlockExplorer, + destinationChainBlockExplorer }: { bridgeTransfer: BridgeTransfer + sourceChainBlockExplorer: string + destinationChainBlockExplorer: string }) { return ( -
  • -
    - - sourceChainTxHash: {bridgeTransfer.sourceChainTx.hash} - - - destinationChainTxHash:{' '} - {bridgeTransfer.destinationChainTxReceipt?.transactionHash ?? '?'} - - status: {bridgeTransfer.status} -
    +
  • + + sourceChainTxHash:{' '} + + {bridgeTransfer.sourceChainTx.hash} + + + + destinationChainTxHash:{' '} + {bridgeTransfer.destinationChainTxReceipt ? ( + + {bridgeTransfer.destinationChainTxReceipt.transactionHash} + + ) : ( + '??' + )} + + status: {bridgeTransfer.status}
  • ) } @@ -104,9 +159,22 @@ function App() { const [fromChain, setFromChain] = useState(goerli) const [toChain, setToChain] = useState(arbitrumGoerli) - const [bridgeTransferMap, setBridgeTransferMap] = useState< - Record - >({}) + const [bridgeTransferMap, setBridgeTransferMap] = useState( + [] + ) + + useEffect(() => { + async function update() { + const result = await loadTxHistory({ + sourceChainProvider: fromChain.provider, + destinationChainProvider: toChain.provider + }) + + setBridgeTransferMap(result) + } + + update() + }, [fromChain.provider, toChain.provider]) function swap() { setFromChain(toChain) @@ -202,11 +270,13 @@ function App() {

    Transaction History

    -
      +
        {Object.values(bridgeTransferMap).map((bridgeTransfer, index) => ( ))}