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

Release 11/7/24-1 #59

Merged
merged 13 commits into from
Nov 8, 2024
Merged
251 changes: 193 additions & 58 deletions web/src/components/DepositCard/DepositCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {
sendIbcTransfer,
useIbcChainSelection,
} from "features/KeplrWallet";
import { useNotifications, NotificationType } from "features/Notifications";
import { NotificationType, useNotifications } from "features/Notifications";

export default function DepositCard(): React.ReactElement {
const { evmChains, ibcChains } = useConfig();
const { addNotification } = useNotifications();

const {
evmAccountAddress: recipientAddress,
evmAccountAddress,
selectEvmChain,
evmChainsOptions,
selectedEvmChain,
Expand All @@ -29,6 +29,7 @@ export default function DepositCard(): React.ReactElement {
evmBalance,
isLoadingEvmBalance,
connectEVMWallet,
resetState: resetEvmWalletState,
} = useEvmChainSelection(evmChains);

const {
Expand Down Expand Up @@ -73,14 +74,38 @@ export default function DepositCard(): React.ReactElement {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isAnimating, setIsAnimating] = useState<boolean>(false);

// recipientAddressOverride is used to allow manual entry of an address
const [recipientAddressOverride, setRecipientAddressOverride] =
useState<string>("");
const [isRecipientAddressEditable, setIsRecipientAddressEditable] =
useState<boolean>(false);
const handleEditRecipientClick = () => {
setIsRecipientAddressEditable(!isRecipientAddressEditable);
};
const handleEditRecipientSave = () => {
setIsRecipientAddressEditable(false);
// reset evmWalletState when user manually enters address
resetEvmWalletState();
};
const handleEditRecipientClear = () => {
setIsRecipientAddressEditable(false);
setRecipientAddressOverride("");
};
const updateRecipientAddressOverride = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setRecipientAddressOverride(event.target.value);
};

// check if form is valid whenever values change
useEffect(() => {
if (recipientAddress || amount) {
// have touched form when recipientAddress or amount change
if (evmAccountAddress || amount || recipientAddressOverride) {
// have touched form when evmAccountAddress or amount change
setHasTouchedForm(true);
}
const recipientAddress = recipientAddressOverride || evmAccountAddress;
checkIsFormValid(recipientAddress, amount);
}, [recipientAddress, amount]);
}, [evmAccountAddress, amount, recipientAddressOverride]);

const updateAmount = (event: React.ChangeEvent<HTMLInputElement>) => {
setAmount(event.target.value);
Expand All @@ -94,6 +119,11 @@ export default function DepositCard(): React.ReactElement {
setIsRecipientAddressValid(false);
return;
}
// check that address is correct evm address format
if (!addressInput.startsWith("0x")) {
setIsRecipientAddressValid(false);
return;
}

const amount = Number.parseFloat(amountInput);
const amountValid = amount > 0;
Expand All @@ -103,26 +133,43 @@ export default function DepositCard(): React.ReactElement {
setIsRecipientAddressValid(addressValid);
};

const handleConnectEVMWallet = async () => {
setIsRecipientAddressEditable(false);
setRecipientAddressOverride("");
await connectEVMWallet();
};

// ensure evm wallet connection when selected EVM chain changes
/* biome-ignore lint/correctness/useExhaustiveDependencies: */
useEffect(() => {
if (!selectedEvmChain) {
return;
}
connectEVMWallet().then((_) => {});
handleConnectEVMWallet().then((_) => {});
}, [selectedEvmChain]);

const sendBalance = async () => {
// ensure keplr wallet connection when selected ibc chain changes
/* biome-ignore lint/correctness/useExhaustiveDependencies: */
useEffect(() => {
if (!selectedIbcChain) {
return;
}
connectKeplrWallet().then((_) => {});
}, [selectedIbcChain]);

const handleDeposit = async () => {
if (!selectedIbcChain || !selectedIbcCurrency) {
addNotification({
toastOpts: {
toastType: NotificationType.WARNING,
message: "Please select a chain and token first.",
message: "Please select a chain and token to bridge first.",
onAcknowledge: () => {},
},
});
return;
}

const recipientAddress = recipientAddressOverride || evmAccountAddress;
if (!fromAddress || !recipientAddress) {
addNotification({
toastOpts: {
Expand Down Expand Up @@ -155,35 +202,71 @@ export default function DepositCard(): React.ReactElement {
formattedAmount,
selectedIbcCurrency,
);
addNotification({
toastOpts: {
toastType: NotificationType.SUCCESS,
message: "Deposit successful!",
onAcknowledge: () => {},
},
});
} catch (e) {
if (e instanceof Error) {
if (/failed to get account from keplr wallet/i.test(e.message)) {
addNotification({
toastOpts: {
toastType: NotificationType.DANGER,
message:
"Failed to get account from Keplr wallet. Does this address have funds for the selected chain?",
onAcknowledge: () => {},
},
});
} else {
console.error(e.message);
addNotification({
toastOpts: {
toastType: NotificationType.DANGER,
message: "Failed to send IBC transfer",
onAcknowledge: () => {},
},
});
}
setIsAnimating(false);
console.error("IBC transfer failed", e);
const message = e instanceof Error ? e.message : "Unknown error.";
if (/failed to get account from keplr wallet/i.test(message)) {
addNotification({
toastOpts: {
toastType: NotificationType.DANGER,
message:
"Failed to get account from Keplr wallet. Does this address have funds for the selected chain?",
onAcknowledge: () => {},
},
});
} else {
addNotification({
toastOpts: {
toastType: NotificationType.DANGER,
component: (
<>
<p className="mb-1">Failed to send IBC transfer.</p>
<p className="message-body-inner">{message}</p>
</>
),
onAcknowledge: () => {},
},
});
}
} finally {
setIsLoading(false);
setTimeout(() => setIsAnimating(false), 2000);
setTimeout(() => setIsAnimating(false), 1000);
}
};

// this additional option is the "Connect Keplr Wallet" button
// disable deposit button if form is invalid
const isDepositDisabled = useMemo<boolean>((): boolean => {
if (recipientAddressOverride) {
// there won't be a selected evm chain and currency if user manually
// enters a recipient address
return !(isAmountValid && isRecipientAddressValid && fromAddress);
}
return !(
evmAccountAddress &&
isAmountValid &&
isRecipientAddressValid &&
fromAddress &&
selectedIbcCurrency?.coinDenom ===
selectedEvmCurrencyOption?.value?.coinDenom
);
}, [
recipientAddressOverride,
evmAccountAddress,
isAmountValid,
isRecipientAddressValid,
fromAddress,
selectedIbcCurrency,
selectedEvmCurrencyOption,
]);

const additionalIbcOptions = useMemo(
() => [
{
Expand All @@ -197,17 +280,22 @@ export default function DepositCard(): React.ReactElement {
[connectKeplrWallet],
);

// this additional option is the "Connect EVM Wallet" button
const additionalEvmOptions = useMemo(() => {
return [
{
label: "Connect EVM Wallet",
action: connectEVMWallet,
action: handleConnectEVMWallet,
className: "has-text-primary",
rightIconClass: "fas fa-plus",
},
{
label: "Enter address manually",
action: handleEditRecipientClick,
className: "has-text-primary",
rightIconClass: "fas fa-pen-to-square",
},
];
}, [connectEVMWallet]);
}, [handleConnectEVMWallet, handleEditRecipientClick]);

return (
<div>
Expand Down Expand Up @@ -276,7 +364,7 @@ export default function DepositCard(): React.ReactElement {
<div className="label-left">To</div>
<div className="is-flex-grow-1">
<Dropdown
placeholder="Connect EVM Wallet"
placeholder="Connect EVM Wallet or enter address"
options={evmChainsOptions}
onSelect={selectEvmChain}
leftIconClass={"i-wallet"}
Expand All @@ -299,23 +387,77 @@ export default function DepositCard(): React.ReactElement {
</div>
)}
</div>
{recipientAddress && (
{evmAccountAddress &&
!isRecipientAddressEditable &&
!recipientAddressOverride && (
<div className="field-info-box mt-3 py-2 px-3">
{evmAccountAddress && (
<p
className="has-text-grey-light has-text-weight-semibold is-clickable"
onKeyDown={handleEditRecipientClick}
onClick={handleEditRecipientClick}
>
<span className="mr-2">Address: {evmAccountAddress}</span>
<i className="fas fa-pen-to-square" />
</p>
)}
{evmAccountAddress && !isLoadingEvmBalance && (
<p className="mt-2 has-text-grey-lighter has-text-weight-semibold">
Balance: {evmBalance}
</p>
)}
{evmAccountAddress && isLoadingEvmBalance && (
<p className="mt-2 has-text-grey-lighter has-text-weight-semibold">
Balance: <i className="fas fa-spinner fa-pulse" />
</p>
)}
</div>
)}
{recipientAddressOverride && !isRecipientAddressEditable && (
<div className="field-info-box mt-3 py-2 px-3">
{recipientAddress && (
<p className="has-text-grey-light has-text-weight-semibold">
Address: {recipientAddress}
</p>
)}
{recipientAddress && !isLoadingEvmBalance && (
<p className="mt-2 has-text-grey-lighter has-text-weight-semibold">
Balance: {evmBalance}
</p>
)}
{recipientAddress && isLoadingEvmBalance && (
<p className="mt-2 has-text-grey-lighter has-text-weight-semibold">
Balance: <i className="fas fa-spinner fa-pulse" />
</p>
<p
className="has-text-grey-light has-text-weight-semibold is-clickable"
onKeyDown={handleEditRecipientClick}
onClick={handleEditRecipientClick}
>
<span className="mr-2">Address: {recipientAddressOverride}</span>
<i className="fas fa-pen-to-square" />
</p>
{!isRecipientAddressValid && hasTouchedForm && (
<div className="help is-danger mt-2">
Recipient address must be a valid EVM address
</div>
)}
<p className="mt-2 has-text-grey-lighter has-text-weight-semibold is-size-7">
Connect via wallet to show balance
</p>
</div>
)}
{isRecipientAddressEditable && (
<div className="field-info-box mt-3 py-2 px-3">
<div className="has-text-grey-light has-text-weight-semibold">
<input
className="input is-medium is-outlined-white"
type="text"
placeholder="0x..."
onChange={updateRecipientAddressOverride}
value={recipientAddressOverride}
/>
<button
type="button"
className="button is-ghost is-outlined-white mr-2 mt-2"
onClick={handleEditRecipientSave}
>
Save
</button>
<button
type="button"
className="button is-ghost is-outlined-white mt-2"
onClick={handleEditRecipientClear}
>
Clear
</button>
</div>
</div>
)}
</div>
Expand Down Expand Up @@ -348,15 +490,8 @@ export default function DepositCard(): React.ReactElement {
<button
type="button"
className="button is-tall is-wide has-gradient-to-right-orange has-text-weight-bold has-text-white"
onClick={() => sendBalance()}
disabled={
!isAmountValid ||
!isRecipientAddressValid ||
!fromAddress ||
!recipientAddress ||
selectedIbcCurrency?.coinDenom !==
selectedEvmCurrencyOption?.value?.coinDenom
}
onClick={() => handleDeposit()}
disabled={isDepositDisabled}
>
{isLoading ? "Processing..." : "Deposit"}
</button>
Expand Down
Loading