From adca4600c2f19b720640b5001f9a8b89385185ca Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Tue, 7 Mar 2023 00:17:02 -0800 Subject: [PATCH] feat(deposits): deposit flow (#3062) --- .../trading-e2e/src/integration/capsule.cy.ts | 11 +- .../trading-e2e/src/integration/deposit.cy.ts | 21 +- .../trading-e2e/src/integration/wallets.cy.ts | 2 +- .../hooks/use-ethereum-transaction-toasts.tsx | 14 +- .../deal-ticket/expiry-selector.tsx | 6 +- .../components/deal-ticket/side-selector.tsx | 6 +- .../deal-ticket/time-in-force-selector.tsx | 6 +- .../components/deal-ticket/type-selector.tsx | 2 +- .../deposits/src/lib/approve-notification.tsx | 215 ++++++++++++ libs/deposits/src/lib/deposit-container.tsx | 10 +- libs/deposits/src/lib/deposit-dialog.tsx | 34 +- libs/deposits/src/lib/deposit-form.spec.tsx | 63 ++-- libs/deposits/src/lib/deposit-form.tsx | 305 +++++++++--------- libs/deposits/src/lib/deposit-limits.tsx | 2 +- libs/deposits/src/lib/deposit-manager.tsx | 73 ++--- libs/deposits/src/lib/faucet-notification.tsx | 108 +++++++ libs/deposits/src/lib/use-deposit-balances.ts | 38 +-- libs/deposits/src/lib/use-submit-approval.ts | 50 +-- libs/deposits/src/lib/use-submit-faucet.ts | 45 ++- .../src/components/form-group/form-group.tsx | 11 +- .../lib/use-ethereum-transaction-store.tsx | 2 +- 21 files changed, 677 insertions(+), 347 deletions(-) create mode 100644 libs/deposits/src/lib/approve-notification.tsx create mode 100644 libs/deposits/src/lib/faucet-notification.tsx diff --git a/apps/trading-e2e/src/integration/capsule.cy.ts b/apps/trading-e2e/src/integration/capsule.cy.ts index e6bba018a..6ab51194b 100644 --- a/apps/trading-e2e/src/integration/capsule.cy.ts +++ b/apps/trading-e2e/src/integration/capsule.cy.ts @@ -41,6 +41,7 @@ const completeWithdrawalBtn = 'complete-withdrawal'; const submitTransferBtn = '[type="submit"]'; const transferForm = 'transfer-form'; const depositSubmit = 'deposit-submit'; +const approveSubmit = 'approve-submit'; // Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order. describe('capsule - without MultiSign', { tags: '@slow' }, () => { @@ -73,13 +74,13 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => { cy.getByTestId('deposit-button').click(); connectEthereumWallet('Unknown'); cy.get(assetSelectField, txTimeout).select(btcName, { force: true }); - cy.getByTestId('approve-warning').should( + cy.getByTestId('approve-default').should( 'contain.text', - `Deposits of ${btcSymbol} not approved` + `Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet` ); - cy.getByTestId(depositSubmit).click(); - cy.getByTestId('dialog-title').should('contain.text', 'Approve complete'); - cy.get('[data-testid="Return to deposit"]').click(); + cy.getByTestId(approveSubmit).click(); + cy.getByTestId('approve-pending').should('exist'); + cy.getByTestId('approve-confirmed').should('exist'); cy.get(amountField).clear().type('10'); cy.getByTestId(depositSubmit).click(); cy.getByTestId(toastContent, txTimeout).should( diff --git a/apps/trading-e2e/src/integration/deposit.cy.ts b/apps/trading-e2e/src/integration/deposit.cy.ts index ec53bfe9f..dad973e56 100644 --- a/apps/trading-e2e/src/integration/deposit.cy.ts +++ b/apps/trading-e2e/src/integration/deposit.cy.ts @@ -31,7 +31,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => { it('handles empty fields', () => { cy.getByTestId('deposit-submit').click(); cy.getByTestId(formFieldError).should('contain.text', 'Required'); - cy.getByTestId(formFieldError).should('have.length', 2); + // once Ethereum wallet is connected and key selected the only field that will + // error is the asset select + cy.getByTestId(formFieldError).should('have.length', 1); + cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should( + 'have.length', + 1 + ); }); it('unable to select assets not enabled', () => { @@ -41,12 +47,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => { cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist'); }); - it('invalid public key', () => { - cy.get(toAddressField) - .clear() - .type('INVALID_DEPOSIT_TO_ADDRESS') - .next(`[data-testid="${formFieldError}"]`) - .should('have.text', 'Invalid Vega key'); + it('invalid public key when entering address manually', () => { + cy.getByTestId('enter-pubkey-manually').click(); + cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS'); + cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should( + 'have.text', + 'Invalid Vega key' + ); }); it('invalid amount', () => { diff --git a/apps/trading-e2e/src/integration/wallets.cy.ts b/apps/trading-e2e/src/integration/wallets.cy.ts index ee02b8873..9711ce579 100644 --- a/apps/trading-e2e/src/integration/wallets.cy.ts +++ b/apps/trading-e2e/src/integration/wallets.cy.ts @@ -151,7 +151,7 @@ describe('ethereum wallet', { tags: '@smoke' }, () => { cy.getByTestId('Deposits').click(); cy.getByTestId('deposit-button').click(); connectEthereumWallet('MetaMask'); - cy.get('#ethereum-address').should('have.value', ethWalletAddress); + cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress); cy.getByTestId('disconnect-ethereum-wallet') .should('have.text', 'Disconnect') .click(); diff --git a/apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx b/apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx index c41cfcfeb..43a973685 100644 --- a/apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx +++ b/apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx @@ -177,22 +177,14 @@ export const useEthereumTransactionToasts = () => { store.remove, ]); - const [dismissTx, deleteTx] = useEthTransactionStore((state) => [ - state.dismiss, - state.delete, - ]); + const dismissTx = useEthTransactionStore((state) => state.dismiss); const onClose = useCallback( (tx: EthStoredTxState) => () => { - const safeToDelete = isFinal(tx); - if (safeToDelete) { - deleteTx(tx.id); - } else { - dismissTx(tx.id); - } + dismissTx(tx.id); removeToast(`eth-${tx.id}`); }, - [deleteTx, dismissTx, removeToast] + [dismissTx, removeToast] ); const fromEthTransaction = useCallback( diff --git a/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx index 0ab9972dd..b4abb8975 100644 --- a/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx @@ -22,7 +22,11 @@ export const ExpirySelector = ({ const dateFormatted = formatForInput(date); const minDate = formatForInput(date); return ( - + { }; return ( - + + +
+

+ {account} +

{ setValue('from', ''); // clear from value so required ethereum connection validation works + onDisconnect(); }} /> - +
); } return ( )}
+ - setValue('to', '')} + select={ + + } + input={ + + } /> {errors.to?.message && ( {errors.to.message} )} - {pubKey && ( - { - setValue('to', pubKey); - clearErrors('to'); - }} - > - {t('Use connected')} - - )} - {selectedAsset && max && deposited && ( + {selectedAsset && balances && (
- +
)} - {formState === 'deposit' && ( + {approved && ( minSafe(new BigNumber(min))(value), approved: (v) => { const value = new BigNumber(v); - if (value.isGreaterThan(maxAmount.approved)) { + if (value.isGreaterThan(balances?.allowance || 0)) { return t('Amount is above approved amount'); } return true; }, limit: (v) => { const value = new BigNumber(v); - if (value.isGreaterThan(maxAmount.limit)) { - return t('Amount is above deposit limit'); + if (!balances) { + return t('Could not verify balances of account'); // this should never happen + } + + let lifetimeLimit = new BigNumber(Infinity); + if (balances.max.isGreaterThan(0)) { + lifetimeLimit = balances.max.minus(balances.deposited); + } + + if (value.isGreaterThan(lifetimeLimit)) { + return t('Amount is above lifetime deposit limit'); } return true; }, balance: (v) => { const value = new BigNumber(v); - if (value.isGreaterThan(maxAmount.available)) { + if (value.isGreaterThan(balances?.balance || 0)) { return t('Insufficient amount in Ethereum wallet'); } return true; }, maxSafe: (v) => { - return maxSafe(maxAmount.amount)(v); + return maxSafe(balances?.balance || new BigNumber(0))(v); }, }, })} /> {errors.amount?.message && ( - + + {errors.amount.message} + )} - {selectedAsset && balance && ( + {selectedAsset && balances && ( { - setValue('amount', balance.toFixed(selectedAsset.decimals)); + setValue( + 'amount', + balances.balance.toFixed(selectedAsset.decimals) + ); clearErrors('amount'); }} > @@ -339,59 +351,31 @@ export const DepositForm = ({ )} )} - + + ); }; -const AmountError = ({ - error, - submitApprove, -}: { - error: FieldError; - submitApprove: () => void; -}) => { - if (error.type === 'approved') { - return ( - - {error.message}. - - - ); - } - return ( - - {error.message} - - ); -}; - interface FormButtonProps { - selectedAsset?: Asset; - formState: ReturnType; + approved: boolean; + selectedAsset: AssetFieldsFragment | undefined; } -const FormButton = ({ selectedAsset, formState }: FormButtonProps) => { +const FormButton = ({ approved, selectedAsset }: FormButtonProps) => { const { isActive, chainId } = useWeb3React(); const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId); - const submitText = - formState === 'approve' - ? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`) - : t('Deposit'); const invalidChain = isActive && chainId !== desiredChainId; return ( <> - {formState === 'approve' && ( -
- -
- )} {invalidChain && (
{ data-testid="deposit-submit" variant={isActive ? 'primary' : 'default'} fill={true} - disabled={invalidChain} + disabled={invalidChain || (selectedAsset && !approved)} > - {submitText} + {t('Deposit')} ); @@ -437,7 +421,7 @@ const DisconnectEthereumButton = ({ const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT); return ( - { connector.deactivate(); removeEagerConnector(); @@ -446,17 +430,46 @@ const DisconnectEthereumButton = ({ data-testid="disconnect-ethereum-wallet" > {t('Disconnect')} - + ); }; -const getFormState = ( - selectedAsset: Asset | undefined, - isActive: boolean, - approved: boolean -) => { - if (!selectedAsset) return 'deposit'; - if (!isActive) return 'deposit'; - if (approved) return 'deposit'; - return 'approve'; +interface AddressInputProps { + pubKeys: string[] | null; + select: ReactNode; + input: ReactNode; + onChange: () => void; +} + +export const AddressField = ({ + pubKeys, + select, + input, + onChange, +}: AddressInputProps) => { + const [isInput, setIsInput] = useState(() => { + if (pubKeys && pubKeys.length <= 1) { + return true; + } + return false; + }); + + return ( + <> + {isInput ? input : select} + {pubKeys && pubKeys.length > 1 && ( + + )} + + ); }; diff --git a/libs/deposits/src/lib/deposit-limits.tsx b/libs/deposits/src/lib/deposit-limits.tsx index 8cf74c020..dc08b492a 100644 --- a/libs/deposits/src/lib/deposit-limits.tsx +++ b/libs/deposits/src/lib/deposit-limits.tsx @@ -35,7 +35,7 @@ export const DepositLimits = ({ }, { key: 'MAX_LIMIT', - label: t('Maximum total deposit amount'), + label: t('Lifetime deposit allowance'), rawValue: max, value: , }, diff --git a/libs/deposits/src/lib/deposit-manager.tsx b/libs/deposits/src/lib/deposit-manager.tsx index b57f9c6ca..1086d75ea 100644 --- a/libs/deposits/src/lib/deposit-manager.tsx +++ b/libs/deposits/src/lib/deposit-manager.tsx @@ -5,36 +5,26 @@ import { prepend0x } from '@vegaprotocol/smart-contracts'; import sortBy from 'lodash/sortBy'; import { useSubmitApproval } from './use-submit-approval'; import { useSubmitFaucet } from './use-submit-faucet'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useDepositBalances } from './use-deposit-balances'; import { useDepositDialog } from './deposit-dialog'; import type { Asset } from '@vegaprotocol/assets'; -import type { DepositDialogStylePropsSetter } from './deposit-dialog'; -import pick from 'lodash/pick'; -import type { EthTransaction } from '@vegaprotocol/web3'; import { - EthTxStatus, useEthTransactionStore, useBridgeContract, useEthereumConfig, } from '@vegaprotocol/web3'; -import { t } from '@vegaprotocol/i18n'; interface DepositManagerProps { assetId?: string; assets: Asset[]; isFaucetable: boolean; - setDialogStyleProps?: DepositDialogStylePropsSetter; } -const getProps = (txContent?: EthTransaction['TxContent']) => - txContent ? pick(txContent, ['title', 'icon', 'intent']) : undefined; - export const DepositManager = ({ assetId: initialAssetId, assets, isFaucetable, - setDialogStyleProps, }: DepositManagerProps) => { const createEthTransaction = useEthTransactionStore((state) => state.create); const { config } = useEthereumConfig(); @@ -43,26 +33,16 @@ export const DepositManager = ({ const bridgeContract = useBridgeContract(); const closeDepositDialog = useDepositDialog((state) => state.close); - const { balance, allowance, deposited, max, refresh } = useDepositBalances( + const { getBalances, reset, balances } = useDepositBalances( asset, isFaucetable ); // Set up approve transaction - const approve = useSubmitApproval(asset); + const approve = useSubmitApproval(asset, getBalances); // Set up faucet transaction - const faucet = useSubmitFaucet(asset); - - const transactionInProgress = [approve.TxContent, faucet.TxContent].filter( - (t) => t.status !== EthTxStatus.Default - )[0]; - - useEffect(() => { - setDialogStyleProps?.(getProps(transactionInProgress)); - }, [setDialogStyleProps, transactionInProgress]); - - const returnLabel = t('Return to deposit'); + const faucet = useSubmitFaucet(asset, getBalances); const submitDeposit = ( args: Parameters['0'] @@ -86,31 +66,24 @@ export const DepositManager = ({ }; return ( - <> - {!transactionInProgress && ( - { - await approve.perform(); - refresh(); - }} - submitDeposit={submitDeposit} - requestFaucet={async () => { - await faucet.perform(); - refresh(); - }} - deposited={deposited} - max={max} - allowance={allowance} - isFaucetable={isFaucetable} - /> - )} - - - - + { + setAssetId(id); + // When we change asset, also clear the tracked faucet/approve transactions so + // we dont render stale UI + approve.reset(); + faucet.reset(); + }} + assets={sortBy(assets, 'name')} + submitApprove={approve.perform} + submitDeposit={submitDeposit} + submitFaucet={faucet.perform} + faucetTxId={faucet.id} + approveTxId={approve.id} + balances={balances} + isFaucetable={isFaucetable} + /> ); }; diff --git a/libs/deposits/src/lib/faucet-notification.tsx b/libs/deposits/src/lib/faucet-notification.tsx new file mode 100644 index 000000000..9be1e4641 --- /dev/null +++ b/libs/deposits/src/lib/faucet-notification.tsx @@ -0,0 +1,108 @@ +import type { Asset } from '@vegaprotocol/assets'; +import { useEnvironment } from '@vegaprotocol/environment'; +import { t } from '@vegaprotocol/i18n'; +import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit'; +import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3'; + +interface FaucetNotificationProps { + isActive: boolean; + selectedAsset?: Asset; + faucetTxId: number | null; +} + +/** + * Render a notification for the faucet transaction + */ +export const FaucetNotification = ({ + isActive, + selectedAsset, + faucetTxId, +}: FaucetNotificationProps) => { + const { ETHERSCAN_URL } = useEnvironment(); + const tx = useEthTransactionStore((state) => { + return state.transactions.find((t) => t?.id === faucetTxId); + }); + + if (!isActive) { + return null; + } + + if (!selectedAsset) { + return null; + } + + if (!tx) { + return null; + } + + if (tx.status === EthTxStatus.Error) { + return ( +
+ +
+ ); + } + + if (tx.status === EthTxStatus.Requested) { + return ( +
+ +
+ ); + } + + if (tx.status === EthTxStatus.Pending) { + return ( +
+ + {t('Faucet pending...')}{' '} + {tx.txHash && ( + + {t('View on Etherscan')} + + )} +

+ } + /> +
+ ); + } + + if (tx.status === EthTxStatus.Confirmed) { + return ( +
+ + {t('Faucet successful')}{' '} + {tx.txHash && ( + + {t('View on Etherscan')} + + )} +

+ } + /> +
+ ); + } + + return null; +}; diff --git a/libs/deposits/src/lib/use-deposit-balances.ts b/libs/deposits/src/lib/use-deposit-balances.ts index f5880d414..ade220c4f 100644 --- a/libs/deposits/src/lib/use-deposit-balances.ts +++ b/libs/deposits/src/lib/use-deposit-balances.ts @@ -7,21 +7,17 @@ import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token'; import { useGetDepositMaximum } from './use-get-deposit-maximum'; import { useGetDepositedAmount } from './use-get-deposited-amount'; import { isAssetTypeERC20 } from '@vegaprotocol/utils'; -import { usePrevious } from '@vegaprotocol/react-helpers'; import { useAccountBalance } from '@vegaprotocol/accounts'; import type { Asset } from '@vegaprotocol/assets'; -type DepositBalances = { - balance: BigNumber; - allowance: BigNumber; - deposited: BigNumber; - max: BigNumber; - refresh: () => void; -}; +export interface DepositBalances { + balance: BigNumber; // amount in Ethereum wallet + allowance: BigNumber; // amount approved + deposited: BigNumber; // total amounted deposited over lifetime + max: BigNumber; // life time deposit cap +} -type DepositBalancesState = Omit; - -const initialState: DepositBalancesState = { +const initialState: DepositBalances = { balance: new BigNumber(0), allowance: new BigNumber(0), deposited: new BigNumber(0), @@ -35,7 +31,7 @@ const initialState: DepositBalancesState = { export const useDepositBalances = ( asset: Asset | undefined, isFaucetable: boolean -): DepositBalances => { +) => { const tokenContract = useTokenContract( isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined, isFaucetable @@ -45,21 +41,14 @@ export const useDepositBalances = ( const getBalance = useGetBalanceOfERC20Token(tokenContract, asset); const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset); const getDepositedAmount = useGetDepositedAmount(asset); - const prevAsset = usePrevious(asset); - const [state, setState] = useState(initialState); - - useEffect(() => { - if (asset?.id !== prevAsset?.id) { - // reset values to initial state when asset changes - setState(initialState); - } - }, [asset?.id, prevAsset?.id]); + const [state, setState] = useState(null); const { accountBalance } = useAccountBalance(asset?.id); const getBalances = useCallback(async () => { if (!asset) return; try { + setState(null); const [max, deposited, balance, allowance] = await Promise.all([ getDepositMaximum(), getDepositedAmount(), @@ -75,12 +64,17 @@ export const useDepositBalances = ( }); } catch (err) { Sentry.captureException(err); + setState(null); } }, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]); + const reset = useCallback(() => { + setState(null); + }, []); + useEffect(() => { getBalances(); }, [asset, getBalances, accountBalance]); - return { ...state, refresh: getBalances }; + return { balances: state, getBalances, reset }; }; diff --git a/libs/deposits/src/lib/use-submit-approval.ts b/libs/deposits/src/lib/use-submit-approval.ts index e5904dea3..8eb8dfa73 100644 --- a/libs/deposits/src/lib/use-submit-approval.ts +++ b/libs/deposits/src/lib/use-submit-approval.ts @@ -1,36 +1,48 @@ import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils'; -import * as Sentry from '@sentry/react'; -import type { Token } from '@vegaprotocol/smart-contracts'; import { + EthTxStatus, useEthereumConfig, - useEthereumTransaction, + useEthTransactionStore, useTokenContract, } from '@vegaprotocol/web3'; import type { Asset } from '@vegaprotocol/assets'; +import { useEffect, useState } from 'react'; -export const useSubmitApproval = (asset?: Asset) => { +export const useSubmitApproval = ( + asset: Asset | undefined, + getBalances: () => void +) => { + const [id, setId] = useState(null); + const createEthTransaction = useEthTransactionStore((state) => state.create); + const tx = useEthTransactionStore((state) => { + return state.transactions.find((t) => t?.id === id); + }); const { config } = useEthereumConfig(); const contract = useTokenContract( isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined, true ); - const transaction = useEthereumTransaction( - contract, - 'approve' - ); + + // When tx is confirmed refresh balances + useEffect(() => { + if (tx?.status === EthTxStatus.Confirmed) { + getBalances(); + } + }, [tx?.status, getBalances]); + return { - ...transaction, - perform: async () => { + id, + reset: () => { + setId(null); + }, + perform: () => { if (!asset || !config) return; - try { - const amount = removeDecimal('1000000', asset.decimals); - await transaction.perform( - config.collateral_bridge_contract.address, - amount - ); - } catch (err) { - Sentry.captureException(err); - } + const amount = removeDecimal('1000000', asset.decimals); + const id = createEthTransaction(contract, 'approve', [ + config?.collateral_bridge_contract.address, + amount, + ]); + setId(id); }, }; }; diff --git a/libs/deposits/src/lib/use-submit-faucet.ts b/libs/deposits/src/lib/use-submit-faucet.ts index b16df2f7b..05ef3e5b8 100644 --- a/libs/deposits/src/lib/use-submit-faucet.ts +++ b/libs/deposits/src/lib/use-submit-faucet.ts @@ -1,26 +1,41 @@ -import type { TokenFaucetable } from '@vegaprotocol/smart-contracts'; -import * as Sentry from '@sentry/react'; -import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3'; +import { + EthTxStatus, + useEthTransactionStore, + useTokenContract, +} from '@vegaprotocol/web3'; import { isAssetTypeERC20 } from '@vegaprotocol/utils'; import type { Asset } from '@vegaprotocol/assets'; +import { useEffect, useState } from 'react'; -export const useSubmitFaucet = (asset?: Asset) => { +export const useSubmitFaucet = ( + asset: Asset | undefined, + getBalances: () => void +) => { + const [id, setId] = useState(null); + const createEthTransaction = useEthTransactionStore((state) => state.create); + const tx = useEthTransactionStore((state) => { + return state.transactions.find((t) => t?.id === id); + }); const contract = useTokenContract( isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined, true ); - const transaction = useEthereumTransaction( - contract, - 'faucet' - ); + + // When tx is confirmed refresh balances + useEffect(() => { + if (tx?.status === EthTxStatus.Confirmed) { + getBalances(); + } + }, [tx?.status, getBalances]); + return { - ...transaction, - perform: async () => { - try { - await transaction.perform(); - } catch (err) { - Sentry.captureException(err); - } + id, + reset: () => { + setId(null); + }, + perform: () => { + const id = createEthTransaction(contract, 'faucet', []); + setId(id); }, }; }; diff --git a/libs/ui-toolkit/src/components/form-group/form-group.tsx b/libs/ui-toolkit/src/components/form-group/form-group.tsx index 1242aea9f..91f9d6389 100644 --- a/libs/ui-toolkit/src/components/form-group/form-group.tsx +++ b/libs/ui-toolkit/src/components/form-group/form-group.tsx @@ -9,6 +9,7 @@ export interface FormGroupProps { hideLabel?: boolean; labelDescription?: string; labelAlign?: 'left' | 'right'; + compact?: boolean; } export const FormGroup = ({ @@ -19,8 +20,16 @@ export const FormGroup = ({ labelDescription, labelAlign = 'left', hideLabel = false, + compact = false, }: FormGroupProps) => { - const wrapperClasses = classNames('relative mb-2', className); + const wrapperClasses = classNames( + 'relative', + { + 'mb-2': compact, + 'mb-4': !compact, + }, + className + ); const labelClasses = classNames('block mb-2 text-sm', { 'text-right': labelAlign === 'right', 'sr-only': hideLabel, diff --git a/libs/web3/src/lib/use-ethereum-transaction-store.tsx b/libs/web3/src/lib/use-ethereum-transaction-store.tsx index b5f1b2da4..177ad7ab2 100644 --- a/libs/web3/src/lib/use-ethereum-transaction-store.tsx +++ b/libs/web3/src/lib/use-ethereum-transaction-store.tsx @@ -26,7 +26,7 @@ export interface EthStoredTxState extends EthTxState { methodName: ContractMethod; args: string[]; requiredConfirmations: number; - requiresConfirmation: boolean; + requiresConfirmation: boolean; // whether or not the tx needs external confirmation (IE from a subscription even) assetId?: string; deposit?: DepositBusEventFieldsFragment; }