Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add depositTo and withdrawTo functionality for smart contract wallets #9

Merged
merged 5 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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