From 4b639d7a5315c20a6766fb2b59d0ce5d3b973453 Mon Sep 17 00:00:00 2001 From: Francisco Ramos Date: Thu, 20 Jul 2023 08:22:05 +0200 Subject: [PATCH] feat(bridge-ui-v2): amount input validation (#14213) --- packages/bridge-ui-v2/src/app.config.ts | 5 + .../Bridge/AmountInput/AmountInput.svelte | 136 +++++++++++++++++- .../Bridge/AmountInput/Balance.svelte | 11 +- .../Bridge/ProcessingFee/NoneOption.svelte | 2 +- .../Bridge/ProcessingFee/ProcessingFee.svelte | 20 +-- .../src/components/Bridge/state.ts | 7 +- .../ChainSelector/ChainSelector.svelte | 6 +- .../src/components/Faucet/Faucet.svelte | 33 ++--- .../src/components/InputBox/InputBox.svelte | 14 +- packages/bridge-ui-v2/src/i18n/en.json | 6 +- .../src/libs/bridge/ERC1155Bridge.ts | 7 + .../src/libs/bridge/ERC20Bridge.ts | 67 +++++++++ .../src/libs/bridge/ERC721Bridge.ts | 7 + .../bridge-ui-v2/src/libs/bridge/ETHBridge.ts | 69 +++++++++ .../bridge-ui-v2/src/libs/bridge/bridges.ts | 12 ++ .../src/libs/bridge/estimateCostOfBridging.ts | 12 ++ .../src/libs/bridge/getMaxToBridge.ts | 60 ++++++++ .../bridge-ui-v2/src/libs/bridge/index.ts | 3 + .../bridge-ui-v2/src/libs/bridge/types.ts | 97 +++++++++++++ .../bridge-ui-v2/src/libs/chain/chains.ts | 3 +- .../src/libs/token/checkMintable.test.ts | 21 +-- .../src/libs/token/checkMintable.ts | 25 ++-- .../src/libs/token/getBalance.test.ts | 1 - .../bridge-ui-v2/src/libs/token/getBalance.ts | 6 +- .../bridge-ui-v2/src/libs/token/mint.test.ts | 2 +- packages/bridge-ui-v2/src/libs/token/mint.ts | 7 +- packages/bridge-ui-v2/src/libs/token/types.ts | 3 +- .../bridge-ui-v2/src/libs/util/debounce.ts | 14 ++ .../bridge-ui-v2/src/libs/util/getWallet.ts | 11 ++ .../src/libs/util/truncateDecimal.ts | 4 + .../bridge-ui-v2/src/styles/components.css | 6 +- 31 files changed, 584 insertions(+), 93 deletions(-) create mode 100644 packages/bridge-ui-v2/src/libs/bridge/ERC1155Bridge.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/ERC20Bridge.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/ERC721Bridge.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/ETHBridge.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/bridges.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/estimateCostOfBridging.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/getMaxToBridge.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/index.ts create mode 100644 packages/bridge-ui-v2/src/libs/bridge/types.ts create mode 100644 packages/bridge-ui-v2/src/libs/util/debounce.ts create mode 100644 packages/bridge-ui-v2/src/libs/util/getWallet.ts create mode 100644 packages/bridge-ui-v2/src/libs/util/truncateDecimal.ts diff --git a/packages/bridge-ui-v2/src/app.config.ts b/packages/bridge-ui-v2/src/app.config.ts index 5584560c844..0c49606a95c 100644 --- a/packages/bridge-ui-v2/src/app.config.ts +++ b/packages/bridge-ui-v2/src/app.config.ts @@ -8,3 +8,8 @@ export const processingFeeComponent = { closingDelayOptionClick: 300, intervalComputeRecommendedFee: 20000, }; + +export const bridge = { + noOwnerGasLimit: BigInt(140000), + noTokenDeployedGasLimit: BigInt(3000000), +}; diff --git a/packages/bridge-ui-v2/src/components/Bridge/AmountInput/AmountInput.svelte b/packages/bridge-ui-v2/src/components/Bridge/AmountInput/AmountInput.svelte index d2cd50a02cc..02e08753648 100644 --- a/packages/bridge-ui-v2/src/components/Bridge/AmountInput/AmountInput.svelte +++ b/packages/bridge-ui-v2/src/components/Bridge/AmountInput/AmountInput.svelte @@ -1,26 +1,158 @@
- +
+
- +
+ + {#if errorAmount} + +
+ +
+ {$t('amount_input.error.insufficient_balance')} +
+
+ {/if}
diff --git a/packages/bridge-ui-v2/src/components/Bridge/AmountInput/Balance.svelte b/packages/bridge-ui-v2/src/components/Bridge/AmountInput/Balance.svelte index 18000188fa6..a82bc0d8fa2 100644 --- a/packages/bridge-ui-v2/src/components/Bridge/AmountInput/Balance.svelte +++ b/packages/bridge-ui-v2/src/components/Bridge/AmountInput/Balance.svelte @@ -10,7 +10,8 @@ import { destNetwork, selectedToken } from '../state'; - let tokenBalance: Maybe; + export let value: Maybe; + let computingTokenBalance = false; let errorComputingTokenBalance = false; @@ -21,14 +22,14 @@ errorComputingTokenBalance = false; try { - tokenBalance = await getTokenBalance({ + value = await getTokenBalance({ token, destChainId, userAddress: account.address, chainId: srcChainId, }); - } catch (error) { - console.error(error); + } catch (err) { + console.error(err); errorComputingTokenBalance = true; } finally { computingTokenBalance = false; @@ -50,7 +51,7 @@ {:else} - {renderTokenBalance(tokenBalance)} + {renderTokenBalance(value)} {/if} diff --git a/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/NoneOption.svelte b/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/NoneOption.svelte index fb5dbd614f8..99af71c167a 100644 --- a/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/NoneOption.svelte +++ b/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/NoneOption.svelte @@ -1,5 +1,5 @@ - + diff --git a/packages/bridge-ui-v2/src/i18n/en.json b/packages/bridge-ui-v2/src/i18n/en.json index cc9e3ca9999..56ab85e6681 100644 --- a/packages/bridge-ui-v2/src/i18n/en.json +++ b/packages/bridge-ui-v2/src/i18n/en.json @@ -84,7 +84,11 @@ "amount_input": { "label": "Amount", "balance": "Balance", - "button.max": "Max" + "button": { + "max": "Max", + "failed_max": "Coult not estimate max amount to bridge." + }, + "error.insufficient_balance": "Insufficient balance" }, "chain_selector": { diff --git a/packages/bridge-ui-v2/src/libs/bridge/ERC1155Bridge.ts b/packages/bridge-ui-v2/src/libs/bridge/ERC1155Bridge.ts new file mode 100644 index 00000000000..ddad81ecab1 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/ERC1155Bridge.ts @@ -0,0 +1,7 @@ +import type { Bridge } from './types'; + +export class ERC1155Bridge implements Bridge { + async estimateGas(): Promise { + return Promise.resolve(BigInt(0)); + } +} diff --git a/packages/bridge-ui-v2/src/libs/bridge/ERC20Bridge.ts b/packages/bridge-ui-v2/src/libs/bridge/ERC20Bridge.ts new file mode 100644 index 00000000000..074bc062a27 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/ERC20Bridge.ts @@ -0,0 +1,67 @@ +import { getContract } from '@wagmi/core'; + +import { tokenVaultABI } from '$abi'; +import { bridge } from '$config'; +import { getConnectedWallet } from '$libs/util/getWallet'; +import { getLogger } from '$libs/util/logger'; + +import type { Bridge, ERC20BridgeArgs, SendERC20Args } from './types'; + +const log = getLogger('ERC20Bridge'); + +export class ERC20Bridge implements Bridge { + private static async _prepareTransaction(args: ERC20BridgeArgs) { + const walletClient = await getConnectedWallet(); + + const { + to, + memo = '', + amount, + destChainId, + tokenAddress, + processingFee, + tokenVaultAddress, + isTokenAlreadyDeployed, + } = args; + + const tokenVaultContract = getContract({ + walletClient, + abi: tokenVaultABI, + address: tokenVaultAddress, + }); + + const refundAddress = walletClient.account.address; + + const gasLimit = !isTokenAlreadyDeployed + ? BigInt(bridge.noTokenDeployedGasLimit) + : processingFee > 0 + ? bridge.noOwnerGasLimit + : BigInt(0); + + const sendERC20Args: SendERC20Args = [ + BigInt(destChainId), + to, + tokenAddress, + amount, + gasLimit, + processingFee, + refundAddress, + memo, + ]; + + log('Preparing transaction with args', sendERC20Args); + + return { tokenVaultContract, sendERC20Args }; + } + + async estimateGas(args: ERC20BridgeArgs) { + const { tokenVaultContract, sendERC20Args } = await ERC20Bridge._prepareTransaction(args); + const [, , , , , processingFee] = sendERC20Args; + + const value = processingFee; + + log('Estimating gas for sendERC20 call. Sending value', value); + + return tokenVaultContract.estimateGas.sendERC20([...sendERC20Args], { value }); + } +} diff --git a/packages/bridge-ui-v2/src/libs/bridge/ERC721Bridge.ts b/packages/bridge-ui-v2/src/libs/bridge/ERC721Bridge.ts new file mode 100644 index 00000000000..a9d03418e55 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/ERC721Bridge.ts @@ -0,0 +1,7 @@ +import type { Bridge } from './types'; + +export class ERC721Bridge implements Bridge { + async estimateGas(): Promise { + return Promise.resolve(BigInt(0)); + } +} diff --git a/packages/bridge-ui-v2/src/libs/bridge/ETHBridge.ts b/packages/bridge-ui-v2/src/libs/bridge/ETHBridge.ts new file mode 100644 index 00000000000..45c54f813a9 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/ETHBridge.ts @@ -0,0 +1,69 @@ +import { getContract } from '@wagmi/core'; + +import { bridgeABI } from '$abi'; +import { bridge } from '$config'; +import { getConnectedWallet } from '$libs/util/getWallet'; +import { getLogger } from '$libs/util/logger'; + +import type { Bridge, ETHBridgeArgs, Message } from './types'; + +const log = getLogger('ETHBridge'); + +export class ETHBridge implements Bridge { + private static async _prepareTransaction(args: ETHBridgeArgs) { + const walletClient = await getConnectedWallet(); + + const { to, memo = '', amount, srcChainId, destChainId, bridgeAddress, processingFee } = args; + + const bridgeContract = getContract({ + walletClient, + abi: bridgeABI, + address: bridgeAddress, + }); + + const owner = walletClient.account.address; + + // TODO: contract actually supports bridging to ourselves as well as + // to another address at the same time + const [depositValue, callValue] = + to.toLowerCase() === owner.toLowerCase() ? [amount, BigInt(0)] : [BigInt(0), amount]; + + // If there is a processing fee, use the specified message gas limit + // if not called by the owner + const gasLimit = processingFee > 0 ? bridge.noOwnerGasLimit : BigInt(0); + + const message: Message = { + to, + owner, + sender: owner, + refundAddress: owner, + + srcChainId: BigInt(srcChainId), + destChainId: BigInt(destChainId), + + gasLimit, + callValue, + depositValue, + processingFee, + + memo, + data: '0x', + id: BigInt(0), // will be set in contract + }; + + log('Preparing transaction with message', message); + + return { bridgeContract, message }; + } + + async estimateGas(args: ETHBridgeArgs) { + const { bridgeContract, message } = await ETHBridge._prepareTransaction(args); + const { depositValue, callValue, processingFee } = message; + + const value = depositValue + callValue + processingFee; + + log('Estimating gas for sendMessage call. Sending value', value); + + return bridgeContract.estimateGas.sendMessage([message], { value }); + } +} diff --git a/packages/bridge-ui-v2/src/libs/bridge/bridges.ts b/packages/bridge-ui-v2/src/libs/bridge/bridges.ts new file mode 100644 index 00000000000..8eefde0a6dd --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/bridges.ts @@ -0,0 +1,12 @@ +import { ERC20Bridge } from './ERC20Bridge'; +import { ERC721Bridge } from './ERC721Bridge'; +import { ERC1155Bridge } from './ERC1155Bridge'; +import { ETHBridge } from './ETHBridge'; +import type { Bridge, BridgeType } from './types'; + +export const bridges: Record = { + ETH: new ETHBridge(), + ERC20: new ERC20Bridge(), + ERC721: new ERC721Bridge(), + ERC1155: new ERC1155Bridge(), +}; diff --git a/packages/bridge-ui-v2/src/libs/bridge/estimateCostOfBridging.ts b/packages/bridge-ui-v2/src/libs/bridge/estimateCostOfBridging.ts new file mode 100644 index 00000000000..adcb8c81d74 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/estimateCostOfBridging.ts @@ -0,0 +1,12 @@ +import { getPublicClient } from '@wagmi/core'; + +import type { Bridge, BridgeArgs } from './types'; + +export async function estimateCostOfBridging(bridge: Bridge, bridgeArgs: BridgeArgs) { + const publicClient = getPublicClient(); + + // Calculate the estimated cost of bridging + const estimatedGas = await bridge.estimateGas(bridgeArgs); + const gasPrice = await publicClient.getGasPrice(); + return estimatedGas * gasPrice; +} diff --git a/packages/bridge-ui-v2/src/libs/bridge/getMaxToBridge.ts b/packages/bridge-ui-v2/src/libs/bridge/getMaxToBridge.ts new file mode 100644 index 00000000000..041567139a4 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/getMaxToBridge.ts @@ -0,0 +1,60 @@ +import type { Address } from 'viem'; + +import { chainContractsMap, chains } from '$libs/chain'; +import { isETH, type Token } from '$libs/token'; +import { getLogger } from '$libs/util/logger'; + +import { bridges } from './bridges'; +import { estimateCostOfBridging } from './estimateCostOfBridging'; +import type { ETHBridgeArgs } from './types'; + +type GetMaxToBridgeArgs = { + token: Token; + balance: bigint; + srcChainId: number; + userAddress: Address; + processingFee: bigint; + destChainId?: number; + amount?: bigint; +}; + +const log = getLogger('getMaxToBridge'); + +export async function getMaxToBridge({ + token, + balance, + srcChainId, + userAddress, + processingFee, + destChainId, + amount, +}: GetMaxToBridgeArgs) { + if (isETH(token)) { + const to = userAddress; + const { bridgeAddress } = chainContractsMap[srcChainId.toString()]; + + const bridgeArgs = { + to, + srcChainId, + bridgeAddress, + processingFee, + + // If no amount passed in, use whatever just to get an estimation + amount: amount ?? BigInt(1), + + // If no destination chain is selected, find another chain to estimate + // TODO: we might want to really find a compatible chain to bridge to + // if we have multiple layers + destChainId: destChainId ?? chains.find((chain) => chain.id !== srcChainId)?.id, + } as ETHBridgeArgs; + + const estimatedCost = await estimateCostOfBridging(bridges.ETH, bridgeArgs); + + log('Estimated cost of bridging', estimatedCost, 'with argument', bridgeArgs); + + return balance - processingFee - estimatedCost; + } + + // For ERC20 tokens, we can bridge the whole balance + return balance; +} diff --git a/packages/bridge-ui-v2/src/libs/bridge/index.ts b/packages/bridge-ui-v2/src/libs/bridge/index.ts new file mode 100644 index 00000000000..5187917dc4b --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/index.ts @@ -0,0 +1,3 @@ +export { bridges } from './bridges'; +export { estimateCostOfBridging } from './estimateCostOfBridging'; +export * from './types'; diff --git a/packages/bridge-ui-v2/src/libs/bridge/types.ts b/packages/bridge-ui-v2/src/libs/bridge/types.ts new file mode 100644 index 00000000000..7dd0f52172c --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/bridge/types.ts @@ -0,0 +1,97 @@ +import type { Address, Hex } from 'viem'; + +export enum BridgeType { + ETH = 'ETH', + + // https://ethereum.org/en/developers/docs/standards/tokens/erc-20/ + ERC20 = 'ERC20', + + // https://ethereum.org/en/developers/docs/standards/tokens/erc-721/ + ERC721 = 'ERC721', + + // https://ethereum.org/en/developers/docs/standards/tokens/erc-1155/ + ERC1155 = 'ERC1155', +} + +// Bridge sendMessage(message: Message) +export type Message = { + // Message ID. Will be set in contract + id: bigint; + // Message sender address (auto filled) + sender: Address; + // Source chain ID (auto filled) + srcChainId: bigint; + // Destination chain ID where the `to` address lives (auto filled) + destChainId: bigint; + // Owner address of the bridged asset. + owner: Address; + // Destination owner address + to: Address; + // Alternate address to send any refund. If blank, defaults to owner. + refundAddress: Address; + // Deposited Ether minus the processingFee. + depositValue: bigint; + // callValue to invoke on the destination chain, for ERC20 transfers. + callValue: bigint; + // Processing fee for the relayer. Zero if owner will process themself. + processingFee: bigint; + // gasLimit to invoke on the destination chain, for ERC20 transfers. + gasLimit: bigint; + // callData to invoke on the destination chain, for ERC20 transfers. + data: Hex; + // Optional memo. + memo: string; +}; + +// TokenVault sendERC20(...args) +export type SendERC20Args = [ + bigint, // destChainId + Address, // to + Address, // token + bigint, // amount + bigint, // gasLimit + bigint, // processingFee + Address, // refundAddress + string, // memo +]; + +// TODO: future sendToken(op: BridgeTransferOp) +export type BridgeTransferOp = { + destChainId: bigint; + to: Address; + token: Address; + amount: bigint; + gasLimit: bigint; + processingFee: bigint; + refundAddress: Address; + memo: string; +}; + +export type ApproveArgs = { + amount: bigint; + tokenAddress: Address; + spenderAddress: Address; +}; + +export type BridgeArgs = { + to: Address; + srcChainId: number; + destChainId: number; + amount: bigint; + processingFee: bigint; + memo?: string; +}; + +export type ETHBridgeArgs = BridgeArgs & { + bridgeAddress: Address; +}; + +export type ERC20BridgeArgs = BridgeArgs & { + tokenAddress: Address; + tokenVaultAddress: Address; + isTokenAlreadyDeployed?: boolean; +}; + +export interface Bridge { + estimateGas(args: BridgeArgs): Promise; +} diff --git a/packages/bridge-ui-v2/src/libs/chain/chains.ts b/packages/bridge-ui-v2/src/libs/chain/chains.ts index 2779bff7880..496137b14ba 100644 --- a/packages/bridge-ui-v2/src/libs/chain/chains.ts +++ b/packages/bridge-ui-v2/src/libs/chain/chains.ts @@ -1,5 +1,4 @@ -import type { Chain } from '@wagmi/core'; -import type { Address } from 'abitype'; +import type { Address, Chain } from '@wagmi/core'; import { PUBLIC_L1_BRIDGE_ADDRESS, diff --git a/packages/bridge-ui-v2/src/libs/token/checkMintable.test.ts b/packages/bridge-ui-v2/src/libs/token/checkMintable.test.ts index 3066604fa6c..503542c036e 100644 --- a/packages/bridge-ui-v2/src/libs/token/checkMintable.test.ts +++ b/packages/bridge-ui-v2/src/libs/token/checkMintable.test.ts @@ -45,24 +45,15 @@ describe('checkMintable', () => { vi.mocked(getPublicClient).mockReturnValue(mockPublicClient); }); - it('should throw when wallet is not connected', async () => { - vi.mocked(getWalletClient).mockResolvedValueOnce(null); - - try { - await checkMintable(BLLToken, mainnetChain); - expect.fail('should have thrown'); - } catch (error) { - const { cause } = error as Error; - expect(cause).toBe(MintableError.NOT_CONNECTED); - expect(getWalletClient).toHaveBeenCalledWith({ chainId: mainnetChain.id }); - } + beforeEach(() => { + vi.clearAllMocks(); }); it('should throw when user has already minted', async () => { vi.mocked(mockTokenContract.read.minters).mockResolvedValueOnce(true); try { - await checkMintable(BLLToken, mainnetChain); + await checkMintable(BLLToken, mainnetChain.id); expect.fail('should have thrown'); } catch (error) { const { cause } = error as Error; @@ -91,12 +82,12 @@ describe('checkMintable', () => { vi.mocked(mockPublicClient.getBalance).mockResolvedValueOnce(BigInt(100)); try { - await checkMintable(BLLToken, mainnetChain); + await checkMintable(BLLToken, mainnetChain.id); expect.fail('should have thrown'); } catch (error) { const { cause } = error as Error; expect(cause).toBe(MintableError.INSUFFICIENT_BALANCE); - expect(getPublicClient).toHaveBeenCalledWith({ chainId: mainnetChain.id }); + expect(getPublicClient).toHaveBeenCalled(); expect(mockTokenContract.estimateGas.mint).toHaveBeenCalledWith([mockWalletClient.account.address]); expect(mockPublicClient.getBalance).toHaveBeenCalledWith({ address: mockWalletClient.account.address }); } @@ -117,7 +108,7 @@ describe('checkMintable', () => { vi.mocked(mockPublicClient.getBalance).mockResolvedValueOnce(BigInt(300)); try { - await checkMintable(BLLToken, mainnetChain); + await checkMintable(BLLToken, mainnetChain.id); } catch (error) { expect.fail('should not have thrown'); } diff --git a/packages/bridge-ui-v2/src/libs/token/checkMintable.ts b/packages/bridge-ui-v2/src/libs/token/checkMintable.ts index 848de485316..2c91d47a4eb 100644 --- a/packages/bridge-ui-v2/src/libs/token/checkMintable.ts +++ b/packages/bridge-ui-v2/src/libs/token/checkMintable.ts @@ -1,21 +1,15 @@ -import { type Chain, getContract, getPublicClient, getWalletClient } from '@wagmi/core'; -import { formatEther } from 'viem'; +import { getContract, getPublicClient } from '@wagmi/core'; import { freeMintErc20ABI } from '$abi'; +import { getConnectedWallet } from '$libs/util/getWallet'; import { MintableError, type Token } from './types'; // Throws an error if: -// 1. User is not connected to the network -// 2. User has already minted this token -// 3. User has insufficient balance to mint this token -export async function checkMintable(token: Token, network: Chain) { - const chainId = network.id; - const walletClient = await getWalletClient({ chainId }); - - if (!walletClient) { - throw Error(`user is not connected to ${network.name}`, { cause: MintableError.NOT_CONNECTED }); - } +// 1. User has already minted this token +// 2. User has insufficient balance to mint this token +export async function checkMintable(token: Token, chainId: number) { + const walletClient = await getConnectedWallet(); const tokenContract = getContract({ walletClient, @@ -28,12 +22,13 @@ export async function checkMintable(token: Token, network: Chain) { const hasMinted = await tokenContract.read.minters([userAddress]); if (hasMinted) { - throw Error(`token ${token.symbol} has already been minted`, { cause: MintableError.TOKEN_MINTED }); + throw Error(`token already minted`, { cause: MintableError.TOKEN_MINTED }); } // Check whether the user has enough balance to mint. // Compute the cost of the transaction: - const publicClient = getPublicClient({ chainId }); + const publicClient = getPublicClient(); + const estimatedGas = await tokenContract.estimateGas.mint([userAddress]); const gasPrice = await publicClient.getGasPrice(); const estimatedCost = estimatedGas * gasPrice; @@ -41,7 +36,7 @@ export async function checkMintable(token: Token, network: Chain) { const userBalance = await publicClient.getBalance({ address: userAddress }); if (estimatedCost > userBalance) { - throw Error(`user has insufficient balance to mint ${token.symbol}: ${formatEther(userBalance)}`, { + throw Error('user has insufficient balance', { cause: MintableError.INSUFFICIENT_BALANCE, }); } diff --git a/packages/bridge-ui-v2/src/libs/token/getBalance.test.ts b/packages/bridge-ui-v2/src/libs/token/getBalance.test.ts index a6846bb2be6..7b9d00d2e70 100644 --- a/packages/bridge-ui-v2/src/libs/token/getBalance.test.ts +++ b/packages/bridge-ui-v2/src/libs/token/getBalance.test.ts @@ -68,7 +68,6 @@ describe('getBalance', () => { }); expect(fetchBalance).toHaveBeenCalledWith({ address: mockWalletClient.account.address, - chainId: Number(PUBLIC_L1_CHAIN_ID), token: BLLToken.addresses[PUBLIC_L1_CHAIN_ID], }); }); diff --git a/packages/bridge-ui-v2/src/libs/token/getBalance.ts b/packages/bridge-ui-v2/src/libs/token/getBalance.ts index ebb974455e1..f4715362bd5 100644 --- a/packages/bridge-ui-v2/src/libs/token/getBalance.ts +++ b/packages/bridge-ui-v2/src/libs/token/getBalance.ts @@ -1,6 +1,5 @@ import { fetchBalance, type FetchBalanceResult } from '@wagmi/core'; -import type { Address } from 'abitype'; -import { zeroAddress } from 'viem'; +import { type Address, zeroAddress } from 'viem'; import { getLogger } from '$libs/util/logger'; @@ -21,7 +20,7 @@ export async function getBalance({ token, userAddress, chainId, destChainId }: G let tokenBalance: FetchBalanceResult | null = null; if (isETH(token)) { - tokenBalance = await fetchBalance({ address: userAddress, chainId }); + tokenBalance = await fetchBalance({ address: userAddress }); } else { // We are dealing with an ERC20 token. We need to first find out its address // on the current chain in order to fetch the balance. @@ -32,7 +31,6 @@ export async function getBalance({ token, userAddress, chainId, destChainId }: G // Wagmi is such an amazing library. We had to do this // more manually before. tokenBalance = await fetchBalance({ - chainId, address: userAddress, token: tokenAddress, }); diff --git a/packages/bridge-ui-v2/src/libs/token/mint.test.ts b/packages/bridge-ui-v2/src/libs/token/mint.test.ts index 379c5798d88..2be21d8334d 100644 --- a/packages/bridge-ui-v2/src/libs/token/mint.test.ts +++ b/packages/bridge-ui-v2/src/libs/token/mint.test.ts @@ -28,7 +28,7 @@ describe('mint', () => { vi.mocked(getContract).mockReturnValue(mockTokenContract); vi.mocked(mockTokenContract.write.mint).mockResolvedValue('0x123456'); - await expect(mint(BLLToken, mockWalletClient)).resolves.toEqual('0x123456'); + await expect(mint(BLLToken)).resolves.toEqual('0x123456'); expect(mockTokenContract.write.mint).toHaveBeenCalledWith([mockWalletClient.account.address]); }); }); diff --git a/packages/bridge-ui-v2/src/libs/token/mint.ts b/packages/bridge-ui-v2/src/libs/token/mint.ts index cfd35b3b331..c9585583711 100644 --- a/packages/bridge-ui-v2/src/libs/token/mint.ts +++ b/packages/bridge-ui-v2/src/libs/token/mint.ts @@ -1,13 +1,16 @@ -import { getContract, type WalletClient } from '@wagmi/core'; +import { getContract } from '@wagmi/core'; import { freeMintErc20ABI } from '$abi'; +import { getConnectedWallet } from '$libs/util/getWallet'; import { getLogger } from '../util/logger'; import type { Token } from './types'; const log = getLogger('token:mint'); -export async function mint(token: Token, walletClient: WalletClient) { +export async function mint(token: Token) { + const walletClient = await getConnectedWallet(); + const tokenSymbol = token.symbol; const userAddress = walletClient.account.address; const chainId = walletClient.chain.id; diff --git a/packages/bridge-ui-v2/src/libs/token/types.ts b/packages/bridge-ui-v2/src/libs/token/types.ts index c47b27f7fa3..230e5814d1d 100644 --- a/packages/bridge-ui-v2/src/libs/token/types.ts +++ b/packages/bridge-ui-v2/src/libs/token/types.ts @@ -1,4 +1,4 @@ -import type { Address } from 'abitype'; +import type { Address } from 'viem'; export type Token = { name: string; @@ -14,7 +14,6 @@ export type TokenEnv = { }; export enum MintableError { - NOT_CONNECTED = 'NOT_CONNECTED', TOKEN_MINTED = 'TOKEN_MINTED', INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE', } diff --git a/packages/bridge-ui-v2/src/libs/util/debounce.ts b/packages/bridge-ui-v2/src/libs/util/debounce.ts new file mode 100644 index 00000000000..75cdc862732 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/util/debounce.ts @@ -0,0 +1,14 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function debounce ReturnType>( + callback: T, + timeout: number, +): (...args: Parameters) => void { + let timer: ReturnType; + + return (...args: Parameters) => { + clearTimeout(timer); + timer = setTimeout(() => { + callback(...args); + }, timeout); + }; +} diff --git a/packages/bridge-ui-v2/src/libs/util/getWallet.ts b/packages/bridge-ui-v2/src/libs/util/getWallet.ts new file mode 100644 index 00000000000..d609ef62344 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/util/getWallet.ts @@ -0,0 +1,11 @@ +import { getWalletClient } from '@wagmi/core'; + +export async function getConnectedWallet() { + const walletClient = await getWalletClient(); + + if (!walletClient) { + throw Error('wallet is not connected'); + } + + return walletClient; +} diff --git a/packages/bridge-ui-v2/src/libs/util/truncateDecimal.ts b/packages/bridge-ui-v2/src/libs/util/truncateDecimal.ts new file mode 100644 index 00000000000..bd3391864cb --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/util/truncateDecimal.ts @@ -0,0 +1,4 @@ +export function truncateDecimal(num: number, decimalPlaces: number) { + const factor = 10 ** decimalPlaces; + return Math.floor(num * factor) / factor; +} diff --git a/packages/bridge-ui-v2/src/styles/components.css b/packages/bridge-ui-v2/src/styles/components.css index 7b82bbfa2c6..839066bb4d9 100644 --- a/packages/bridge-ui-v2/src/styles/components.css +++ b/packages/bridge-ui-v2/src/styles/components.css @@ -83,7 +83,11 @@ /* focus:border-[3px] */ /* focus:border-primary-border-accent */ - focus:shadow-[0_0_0_3px_#E81899]; + focus:!shadow-[0_0_0_3px_#E81899]; + } + + .input-box.error { + @apply !shadow-[0_0_0_3px_#F15C5D]; } /* Separatos */