Skip to content

Commit

Permalink
add depositTo and withdrawTo functionality for smart contract wallets (
Browse files Browse the repository at this point in the history
…#9)

* add depositTo and withdrawTo functionality for smart contract wallets

* tx list updates

* compliance check

* fix prettier

* fix prettier & address nits
  • Loading branch information
lukasrosario authored Sep 25, 2023
1 parent 54be786 commit 71b9ec3
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 73 deletions.
1 change: 1 addition & 0 deletions apps/bridge/public/icons/question-mark-circled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions apps/bridge/src/components/BridgeToInput/BridgeToInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Dispatch, SetStateAction } from 'react';
import Image from 'next/image';

type BridgeToInputProps = {
bridgeTo: string;
setBridgeTo: Dispatch<SetStateAction<string>>;
action: 'deposit' | 'withdraw';
};

export function BridgeToInput({ bridgeTo, setBridgeTo, action }: BridgeToInputProps) {
function handleChangeBridgeTo(e: { target: { value: SetStateAction<string> } }) {
setBridgeTo(e.target.value);
}

return (
<div className="flex flex-col p-6">
<div className="flex flex-row items-center space-x-2 mb-1">
<span className="font-sans text-sm font-medium text-white">{action === 'deposit' ? 'Deposit to' : 'Withdraw to'}</span>
<div className="has-tooltip">
<span className="tooltip -mt-10 ml-6 rounded-lg bg-cds-background-gray-90 p-2 text-black shadow-lg">
Only send funds on networks supported by your wallet provider if it is a smart contract
wallet or there may be permanent loss of funds.
</span>
<Image alt="tooltip" src="/icons/question-mark-circled.svg" width={16} height={16} />
</div>
</div>
<input
className="bg-transparent font-sans text-md text-white outline-none max-[640px]:grow sm:text-xl border border-cds-background-gray-60 p-4 rounded min-[640px]:w-96"
placeholder="Wallet address"
value={bridgeTo}
onChange={handleChangeBridgeTo}
/>
</div>
);
}
75 changes: 61 additions & 14 deletions apps/bridge/src/components/DepositContainer/DepositContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { BridgeButton } from 'apps/bridge/src/components/BridgeButton/BridgeButton';
import { BridgeInput } from 'apps/bridge/src/components/BridgeInput/BridgeInput';
import { BridgeToInput } from 'apps/bridge/src/components/BridgeToInput/BridgeToInput';
import { ConnectWalletButton } from 'apps/bridge/src/components/ConnectWalletButton/ConnectWalletButton';
import { DepositModal } from 'apps/bridge/src/components/DepositModal/DepositModal';
import { FaqSidebar } from 'apps/bridge/src/components/Faq/FaqSidebar';
Expand All @@ -11,14 +12,18 @@ import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetLi
import { useApproveContract } from 'apps/bridge/src/utils/hooks/useApproveContract';
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
import { useGetCode } from 'apps/bridge/src/utils/hooks/useGetCode';
import { useIsContractApproved } from 'apps/bridge/src/utils/hooks/useIsContractApproved';
import { useIsPermittedToBridge } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridge';
import { useIsWalletConnected } from 'apps/bridge/src/utils/hooks/useIsWalletConnected';
import { usePrepareERC20Deposit } from 'apps/bridge/src/utils/hooks/usePrepareERC20Deposit';
import { usePrepareERC20DepositTo } from 'apps/bridge/src/utils/hooks/usePrepareERC20DepositTo';
import { usePrepareETHDeposit } from 'apps/bridge/src/utils/hooks/usePrepareETHDeposit';
import { utils } from 'ethers';
import { parseUnits } from 'ethers/lib/utils.js';
import getConfig from 'next/config';
import { useAccount, useBalance, useContractWrite } from 'wagmi';
import { useIsPermittedToBridgeTo } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridgeTo';

const assetList = getAssetListForChainEnv();

Expand All @@ -29,13 +34,16 @@ export function DepositContainer() {
const [depositAmount, setDepositAmount] = useState('0');
const [L1ApproveTxHash, setL1ApproveTxHash] = useState<`0x${string}` | undefined>(undefined);
const [L1DepositTxHash, setL1DepositTxHash] = useState<`0x${string}` | undefined>(undefined);
const [depositTo, setDepositTo] = useState('');
const [isApprovalTx, setIsApprovalTx] = useState(false);
const isWalletConnected = useIsWalletConnected();
const [selectedAsset, setSelectedAsset] = useState<Asset>(assetList[0]);
const activeAssets = assetList.filter((asset) =>
publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
);
const { address } = useAccount();
const codeAtAddress = useGetCode(address);
const isSmartContractWallet = !!codeAtAddress && codeAtAddress !== '0x';

const { data: L1Balance } = useBalance({
address,
Expand Down Expand Up @@ -79,11 +87,15 @@ export function DepositContainer() {
const chainEnv = useChainEnv();
const isMainnet = chainEnv === 'mainnet';
const includeTosVersionByte = isMainnet;
const isPermittedToBridge = useIsPermittedToBridge();
const isUserPermittedToBridge = useIsPermittedToBridge();
const isPermittedToBridgeTo = useIsPermittedToBridgeTo(depositTo as `0x${string}`);
const isPermittedToBridge = isSmartContractWallet
? isUserPermittedToBridge && isPermittedToBridgeTo
: isUserPermittedToBridge;

// deposit eth
const depositETHConfig = usePrepareETHDeposit({
userAddress: address,
userAddress: isSmartContractWallet ? (depositTo as `0x${string}`) : address,
depositAmount,
isPermittedToBridge,
includeTosVersionByte,
Expand All @@ -99,8 +111,17 @@ export function DepositContainer() {
isPermittedToBridge,
includeTosVersionByte,
});
const depositERC20ToConfig = usePrepareERC20DepositTo({
asset: selectedAsset,
to: depositTo as `0x${string}`,
depositAmount,
readApprovalResult,
isPermittedToBridge,
includeTosVersionByte,
});

const { writeAsync: depositERC20Write } = useContractWrite(depositERC20Config);
const { writeAsync: depositERC20ToWrite } = useContractWrite(depositERC20ToConfig);

const initiateApproval = useCallback(() => {
void (async () => {
Expand All @@ -118,7 +139,9 @@ export function DepositContainer() {

// next, call the transfer function
setIsApprovalTx(false);
const depositResult = await depositERC20Write?.();
const depositResult = await (isSmartContractWallet
? depositERC20ToWrite?.()
: depositERC20Write?.());
if (depositResult?.hash) {
const depositTxHash = depositResult.hash;
setL1DepositTxHash(depositTxHash);
Expand All @@ -128,17 +151,28 @@ export function DepositContainer() {
onCloseDepositModal();
}
})();
}, [approveWrite, depositERC20Write, onCloseDepositModal, onOpenDepositModal, setIsApprovalTx]);
}, [
approveWrite,
depositERC20ToWrite,
depositERC20Write,
isSmartContractWallet,
onCloseDepositModal,
onOpenDepositModal,
]);

const initiateDeposit = useCallback(() => {
void (async () => {
onOpenDepositModal();
try {
// Only bridge on mainnet if user has accepted ToS. Always allow bridging on testnet.
if (isPermittedToBridge) {
const depositResult = await (selectedAsset.L1contract
? depositERC20Write?.()
: depositETHWrite?.());
let depositMethod;
if (selectedAsset.L1contract) {
depositMethod = isSmartContractWallet ? depositERC20ToWrite : depositERC20Write;
} else {
depositMethod = depositETHWrite;
}
const depositResult = await depositMethod?.();
if (depositResult?.hash) {
const depositTxHash = depositResult.hash;
setL1DepositTxHash(depositTxHash);
Expand All @@ -155,36 +189,45 @@ export function DepositContainer() {
onOpenDepositModal,
isPermittedToBridge,
selectedAsset.L1contract,
isSmartContractWallet,
depositERC20ToWrite,
depositERC20Write,
depositETHWrite,
onCloseDepositModal,
]);

let button;
let depositDisabled;
if (!isWalletConnected) {
button = (
<ConnectWalletButton className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto" />
);
} else if (readApprovalResult || selectedAsset.L1symbol === 'ETH') {
depositDisabled =
parseFloat(depositAmount) <= 0 ||
parseFloat(depositAmount) >= parseFloat(L1Balance?.formatted ?? '0') ||
depositAmount === '' ||
(isSmartContractWallet && !utils.isAddress(depositTo ?? '')) ||
!isPermittedToBridge;

button = (
<BaseButton
onClick={initiateDeposit}
disabled={
parseFloat(depositAmount) <= 0 ||
parseFloat(depositAmount) >= parseFloat(L1Balance?.formatted ?? '0') ||
depositAmount === ''
}
disabled={depositDisabled}
toChainId={chainId}
className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto"
>
{`Deposit ${selectedAsset.L1symbol}`}
</BaseButton>
);
} else {
depositDisabled =
(isSmartContractWallet && !utils.isAddress(depositTo ?? '')) || !isPermittedToBridge;

button = (
<BridgeButton
onClick={initiateApproval}
disabled={false}
disabled={depositDisabled}
className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto"
>
Approval
Expand Down Expand Up @@ -216,6 +259,10 @@ export function DepositContainer() {
{button}
</BridgeInput>

{isSmartContractWallet && (
<BridgeToInput bridgeTo={depositTo} setBridgeTo={setDepositTo} action="deposit" />
)}

<div className="border-t border-sidebar-border">
<TransactionSummary
header="TRANSACTION SUMMARY"
Expand All @@ -225,7 +272,7 @@ export function DepositContainer() {
chainId={publicRuntimeConfig.l1ChainID}
isDeposit
/>
<div className="w-full py-12 px-6 sm:hidden">{button}</div>
<div className="w-full px-6 py-12 sm:hidden">{button}</div>
</div>
</div>
<FaqSidebar />
Expand Down
60 changes: 49 additions & 11 deletions apps/bridge/src/components/WithdrawContainer/WithdrawContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import { BridgeInput } from 'apps/bridge/src/components/BridgeInput/BridgeInput';
import { BridgeToInput } from 'apps/bridge/src/components/BridgeToInput/BridgeToInput';
import { ConnectWalletButton } from 'apps/bridge/src/components/ConnectWalletButton/ConnectWalletButton';
import { FaqSidebar } from 'apps/bridge/src/components/Faq/FaqSidebar';
import { BaseButton } from 'apps/bridge/src/components/SwitchNetworkButton/SwitchNetworkButton';
Expand All @@ -9,12 +10,16 @@ import { Asset } from 'apps/bridge/src/types/Asset';
import { getAssetListForChainEnv } from 'apps/bridge/src/utils/assets/getAssetListForChainEnv';
import { useChainEnv } from 'apps/bridge/src/utils/hooks/useChainEnv';
import { useDisclosure } from 'apps/bridge/src/utils/hooks/useDisclosure';
import { useGetCode } from 'apps/bridge/src/utils/hooks/useGetCode';
import { useIsPermittedToBridge } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridge';
import { useIsWalletConnected } from 'apps/bridge/src/utils/hooks/useIsWalletConnected';
import { usePrepareERC20Withdrawal } from 'apps/bridge/src/utils/hooks/usePrepareERC20Withdrawal';
import { usePrepareERC20WithdrawalTo } from 'apps/bridge/src/utils/hooks/usePrepareERC20WithdrawalTo';
import { usePrepareETHWithdrawal } from 'apps/bridge/src/utils/hooks/usePrepareETHWithdrawal';
import { utils } from 'ethers';
import getConfig from 'next/config';
import { useAccount, useBalance, useContractWrite } from 'wagmi';
import { useIsPermittedToBridgeTo } from 'apps/bridge/src/utils/hooks/useIsPermittedToBridgeTo';

const assetList = getAssetListForChainEnv();

Expand All @@ -24,13 +29,17 @@ const chainId = parseInt(publicRuntimeConfig.l2ChainID);
export function WithdrawContainer() {
const [withdrawAmount, setWithdrawAmount] = useState('');
const [L2TxHash, setL2TxHash] = useState('');
const [withdrawTo, setWithdrawTo] = useState('');
const isWalletConnected = useIsWalletConnected();
const activeAssets = assetList.filter((asset) =>
publicRuntimeConfig.assets.split(',').includes(asset.L1symbol.toLowerCase()),
);
const [selectedAsset, setSelectedAsset] = useState<Asset>(assetList[0]);

const { address } = useAccount();
const codeAtAddress = useGetCode(address);
const isSmartContractWallet = !!codeAtAddress && codeAtAddress !== '0x';

const { data: L2Balance } = useBalance({
address,
token: selectedAsset.L2contract,
Expand All @@ -40,18 +49,31 @@ export function WithdrawContainer() {
const chainEnv = useChainEnv();
const isMainnet = chainEnv === 'mainnet';
const includeTosVersionByte = isMainnet;
const isPermittedToBridge = useIsPermittedToBridge();
const isUserPermittedToBridge = useIsPermittedToBridge();
const isPermittedToBridgeTo = useIsPermittedToBridgeTo(withdrawTo as `0x${string}`);
const isPermittedToBridge = isSmartContractWallet
? isUserPermittedToBridge && isPermittedToBridgeTo
: isUserPermittedToBridge;

const erc20WithdrawalConfig = usePrepareERC20Withdrawal({
asset: selectedAsset,
withdrawAmount,
isPermittedToBridge,
includeTosVersionByte,
});
const erc20WithdrawalToConfig = usePrepareERC20WithdrawalTo({
asset: selectedAsset,
to: withdrawTo as `0x${string}`,
withdrawAmount,
isPermittedToBridge,
includeTosVersionByte,
});

const { writeAsync: withdrawERC20 } = useContractWrite(erc20WithdrawalConfig);
const { writeAsync: withdrawERC20To } = useContractWrite(erc20WithdrawalToConfig);

const withdrawConfig = usePrepareETHWithdrawal({
userAddress: address,
userAddress: isSmartContractWallet ? (withdrawTo as `0x${string}`) : address,
withdrawAmount,
isPermittedToBridge,
includeTosVersionByte,
Expand All @@ -75,9 +97,13 @@ export function WithdrawContainer() {
try {
// Only bridge on mainnet if user has accepted ToS. Always allow bridging on testnet.
if (isPermittedToBridge) {
const withdrawalResult = await (selectedAsset.L1contract
? withdrawERC20?.()
: withdraw?.());
let withdrawMethod;
if (selectedAsset.L1contract) {
withdrawMethod = isSmartContractWallet ? withdrawERC20To : withdrawERC20;
} else {
withdrawMethod = withdraw;
}
const withdrawalResult = await withdrawMethod?.();
if (withdrawalResult?.hash) {
const withdrawalTxHsh = withdrawalResult.hash;
setL2TxHash(withdrawalTxHsh);
Expand All @@ -94,25 +120,32 @@ export function WithdrawContainer() {
onOpenWithdrawModal,
isPermittedToBridge,
selectedAsset.L1contract,
isSmartContractWallet,
withdrawERC20To,
withdrawERC20,
withdraw,
onCloseWithdrawModal,
]);

let button;
let withdrawDisabled;

if (!isWalletConnected) {
button = (
<ConnectWalletButton className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto" />
);
} else {
withdrawDisabled =
parseFloat(withdrawAmount) <= 0 ||
parseFloat(withdrawAmount) >= parseFloat(L2Balance?.formatted ?? '0') ||
withdrawAmount === '' ||
(isSmartContractWallet && !utils.isAddress(withdrawTo ?? '')) ||
!isPermittedToBridge;

button = (
<BaseButton
onClick={initiateWithdrawal}
disabled={
parseFloat(withdrawAmount) <= 0 ||
parseFloat(withdrawAmount) >= parseFloat(L2Balance?.formatted ?? '0') ||
withdrawAmount === ''
}
disabled={withdrawDisabled}
toChainId={chainId}
className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto"
>
Expand Down Expand Up @@ -142,6 +175,11 @@ export function WithdrawContainer() {
>
{button}
</BridgeInput>

{isSmartContractWallet && (
<BridgeToInput bridgeTo={withdrawTo} setBridgeTo={setWithdrawTo} action="withdraw" />
)}

<div className="border-t border-sidebar-border">
<TransactionSummary
selectedAsset={selectedAsset}
Expand All @@ -151,7 +189,7 @@ export function WithdrawContainer() {
chainId={publicRuntimeConfig.l2ChainID}
isDeposit={false}
/>
<div className="w-full py-12 px-6 sm:hidden">{button}</div>
<div className="w-full px-6 py-12 sm:hidden">{button}</div>
</div>
</div>
<FaqSidebar />
Expand Down
2 changes: 1 addition & 1 deletion apps/bridge/src/contexts/OFACContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const OFACContext = createContext<OFACContextType>({
isOFACAllowedLoading: false,
});

async function fetchIsAllowed(address?: `0x${string}`): Promise<{ result: boolean }> {
export async function fetchIsAllowed(address?: `0x${string}`): Promise<{ result: boolean }> {
if (!address) {
return { result: false };
}
Expand Down
Loading

0 comments on commit 71b9ec3

Please sign in to comment.